- item.title.toLowerCase().includes(q) ||
- item.description.toLowerCase().includes(q) ||
- item.keywords.some((kw) => kw.toLowerCase().includes(q)),
- );
+ const words = query
+ .toLowerCase()
+ .split(/\s+/)
+ .filter((w) => w.length > 0);
+ if (words.length === 0) return SETTINGS_ITEMS;
+
+ return SETTINGS_ITEMS.filter((item) => {
+ const haystack = [
+ item.title.toLowerCase(),
+ item.description.toLowerCase(),
+ ...item.keywords.map((kw) => kw.toLowerCase()),
+ ].join(" ");
+ return words.every((word) => haystack.includes(word));
+ });
}
export function getMatchCountBySection(
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/PersistentTabRenderer.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/PersistentTabRenderer.tsx
index f2984005f49..438076b9167 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/PersistentTabRenderer.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/PersistentTabRenderer.tsx
@@ -32,7 +32,11 @@ export function PersistentTabRenderer({
if (
paneIds.some((id) => {
const type = panes[id]?.type;
- return type === "webview" || type === "vscode-extension";
+ return (
+ type === "webview" ||
+ type === "vscode-extension" ||
+ type === "reference-graph"
+ );
})
) {
ids.add(tab.id);
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/components/ChatMessageList/components/ChatSearch/ChatSearch.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/components/ChatMessageList/components/ChatSearch/ChatSearch.tsx
index 6a3f0155f4e..f4bda52a7b7 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/components/ChatMessageList/components/ChatSearch/ChatSearch.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ChatPane/ChatPaneInterface/components/ChatMessageList/components/ChatSearch/ChatSearch.tsx
@@ -103,6 +103,7 @@ export function ChatSearch({
style={{ width: `min(${width}px, calc(100% - 4rem))` }}
>
{/* Left resize handle */}
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: resize handle */}
;
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/DiffViewerContextMenu/DiffViewerContextMenu.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/DiffViewerContextMenu/DiffViewerContextMenu.tsx
index 9e0fb72a8bc..14b86f30871 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/DiffViewerContextMenu/DiffViewerContextMenu.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/DiffViewerContextMenu/DiffViewerContextMenu.tsx
@@ -87,6 +87,10 @@ export function DiffViewerContextMenu({
setValue(_value: string) {},
revealPosition(_line: number, _column?: number) {},
getSelectionLines,
+ getCursorPosition() {
+ const sel = getSelectionLines();
+ return sel ? { line: sel.startLine, column: 1 } : null;
+ },
selectAll() {
const selection = window.getSelection();
if (!selection) return;
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/MarkdownSearch/MarkdownSearch.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/MarkdownSearch/MarkdownSearch.tsx
index ac6a72c1b9f..7449bc8177e 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/MarkdownSearch/MarkdownSearch.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileViewerPane/components/MarkdownSearch/MarkdownSearch.tsx
@@ -101,6 +101,7 @@ export function MarkdownSearch({
style={{ width: `min(${width}px, calc(100% - 0.5rem))` }}
>
{/* Left resize handle */}
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: resize handle */}
(null);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ async function highlight() {
+ try {
+ const html = await highlightCode(
+ code,
+ language || "typescript",
+ shikiTheme,
+ );
+ if (!cancelled) {
+ setHighlightedHtml(html);
+ }
+ } catch {
+ if (!cancelled) {
+ setHighlightedHtml(null);
+ }
+ }
+ }
+
+ highlight();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [code, language, shikiTheme]);
+
+ const lines = code.split("\n");
+
+ if (highlightedHtml) {
+ return (
+
+ {/* biome-ignore lint/security/noDangerouslySetInnerHtml: shiki output */}
+
+
+ );
+ }
+
+ // Fallback: plain text with line numbers
+ return (
+
+
+
+ {lines.map((line, index) => {
+ const lineNum = startLine + index;
+ return (
+
+ {lineNum}
+
+ {line || " "}
+
+
+ );
+ })}
+
+
+
+ );
+});
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ReferenceGraphPane/ReferenceGraphPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ReferenceGraphPane/ReferenceGraphPane.tsx
index fc26816b6e5..08ba26fa985 100644
--- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ReferenceGraphPane/ReferenceGraphPane.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/ReferenceGraphPane/ReferenceGraphPane.tsx
@@ -1,7 +1,10 @@
import {
Background,
+ BackgroundVariant,
Controls,
type Edge,
+ getNodesBounds,
+ getViewportForBounds,
type Node,
ReactFlow,
ReactFlowProvider,
@@ -11,12 +14,16 @@ import {
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import ELK from "elkjs/lib/elk.bundled.js";
+import { toPng } from "html-to-image";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { MosaicBranch } from "react-mosaic-component";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { useTabsStore } from "renderer/stores/tabs/store";
+import { useTheme } from "renderer/stores/theme";
+import { createShikiTheme } from "../../../../utils/code-theme/shiki-theme";
import { BasePaneWindow, PaneToolbarActions } from "../components";
import { ReferenceNode } from "./ReferenceNode";
+import "./reference-graph.css";
const elk = new ELK();
@@ -36,27 +43,47 @@ interface ReferenceGraphPaneProps {
onPopOut?: () => void;
}
-const NODE_WIDTH = 350;
-const NODE_HEIGHT = 200;
+const NODE_MIN_WIDTH = 280;
+const NODE_MAX_WIDTH = 500;
+const NODE_HEIGHT = 180;
+const CHAR_WIDTH = 7.5;
+const NODE_PADDING = 40;
+
+const ELK_OPTIONS = {
+ "elk.algorithm": "layered",
+ "elk.direction": "DOWN",
+ "elk.layered.cycleBreaking.strategy": "DEPTH_FIRST",
+ "elk.spacing.nodeNode": "60",
+ "elk.layered.spacing.nodeNodeBetweenLayers": "100",
+ "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
+ "elk.layered.nodePlacement.favorStraightEdges": "true",
+ "elk.edgeRouting": "ORTHOGONAL",
+ "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
+ "elk.separateConnectedComponents": "true",
+ "elk.spacing.componentComponent": "80",
+};
const nodeTypes = { referenceNode: ReferenceNode };
+function estimateNodeWidth(codeSnippet: string): number {
+ const lines = codeSnippet.split("\n");
+ const maxLineLength = Math.max(...lines.map((line) => line.length));
+ const estimatedWidth = maxLineLength * CHAR_WIDTH + NODE_PADDING;
+ return Math.min(NODE_MAX_WIDTH, Math.max(NODE_MIN_WIDTH, estimatedWidth));
+}
+
async function layoutGraph(
nodes: Node[],
edges: Edge[],
): Promise<{ nodes: Node[]; edges: Edge[] }> {
const graph = {
id: "root",
- layoutOptions: {
- "elk.algorithm": "layered",
- "elk.direction": "DOWN",
- "elk.layered.spacing.nodeNodeBetweenLayers": "80",
- "elk.spacing.nodeNode": "40",
- "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
- },
+ layoutOptions: ELK_OPTIONS,
children: nodes.map((n) => ({
id: n.id,
- width: NODE_WIDTH,
+ width: estimateNodeWidth(
+ (n.data as { codeSnippet?: string })?.codeSnippet ?? "",
+ ),
height: NODE_HEIGHT,
})),
edges: edges.map((e) => ({
@@ -95,6 +122,11 @@ function ReferenceGraphInner({
const pane = useTabsStore((s) => s.panes[paneId]);
const refGraphState = pane?.referenceGraph;
const { fitView } = useReactFlow();
+ const activeTheme = useTheme();
+ const shikiTheme = useMemo(
+ () => (activeTheme ? createShikiTheme(activeTheme) : undefined),
+ [activeTheme],
+ );
const [nodes, setNodes, onNodesChange] = useNodesState
([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
@@ -107,6 +139,9 @@ function ReferenceGraphInner({
const mutateAsyncRef = useRef(buildGraphMutation.mutateAsync);
mutateAsyncRef.current = buildGraphMutation.mutateAsync;
const requestGenerationRef = useRef(0);
+ const [isExporting, setIsExporting] = useState(false);
+ const { getNodes } = useReactFlow();
+ const graphContainerRef = useRef(null);
const addFileViewerPane = useTabsStore((s) => s.addFileViewerPane);
@@ -148,6 +183,7 @@ function ReferenceGraphInner({
data: {
...n,
onDoubleClick: handleNodeDoubleClick,
+ shikiTheme,
},
}));
@@ -155,8 +191,8 @@ function ReferenceGraphInner({
id: e.id,
source: e.source,
target: e.target,
- animated: true,
- style: { stroke: "var(--muted-foreground)", strokeWidth: 1.5 },
+ type: "smoothstep",
+ animated: false,
}));
if (flowNodes.length > 0) {
@@ -185,6 +221,7 @@ function ReferenceGraphInner({
workspaceId,
maxDepth,
handleNodeDoubleClick,
+ shikiTheme,
setNodes,
setEdges,
fitView,
@@ -195,6 +232,66 @@ function ReferenceGraphInner({
void loadGraph();
}, [loadGraph]);
+ const handleExportPng = useCallback(async () => {
+ if (isExporting || nodes.length === 0) return;
+ setIsExporting(true);
+
+ const container = graphContainerRef.current;
+ const controls = container?.querySelector(
+ ".react-flow__controls",
+ ) as HTMLElement | null;
+ const background = container?.querySelector(
+ ".react-flow__background",
+ ) as HTMLElement | null;
+
+ try {
+ const nodesList = getNodes();
+ const nodesBounds = getNodesBounds(nodesList);
+ const padding = 100;
+ const imageWidth = nodesBounds.width + padding * 2;
+ const imageHeight = nodesBounds.height + padding * 2;
+ const viewport = getViewportForBounds(
+ nodesBounds,
+ imageWidth,
+ imageHeight,
+ 0.5,
+ 2,
+ 0,
+ );
+
+ const viewportEl = container?.querySelector(
+ ".react-flow__viewport",
+ ) as HTMLElement | null;
+ if (!viewportEl) return;
+
+ if (controls) controls.style.display = "none";
+ if (background) background.style.display = "none";
+
+ const dataUrl = await toPng(viewportEl, {
+ backgroundColor: "transparent",
+ width: imageWidth,
+ height: imageHeight,
+ style: {
+ width: `${imageWidth}px`,
+ height: `${imageHeight}px`,
+ transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
+ },
+ });
+
+ // Trigger download
+ const link = document.createElement("a");
+ link.download = `reference-graph-${Date.now()}.png`;
+ link.href = dataUrl;
+ link.click();
+ } catch (err) {
+ console.error("[reference-graph] Export PNG failed:", err);
+ } finally {
+ if (controls) controls.style.display = "";
+ if (background) background.style.display = "";
+ setIsExporting(false);
+ }
+ }, [isExporting, nodes.length, getNodes]);
+
const depthOptions = useMemo(
() => [1, 2, 3, 4, 5].map((d) => ({ value: d, label: `Depth: ${d}` })),
[],
@@ -240,6 +337,14 @@ function ReferenceGraphInner({
>
{isLoading ? "Loading..." : "Refresh"}
+