mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
UI improvements and prompt improvements
This commit is contained in:
parent
0a42fe46ac
commit
9ee4972aa6
5 changed files with 321 additions and 16 deletions
|
|
@ -36,11 +36,24 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =
|
|||
removeCodeReference,
|
||||
clearCodeReferences,
|
||||
setSelectedNode,
|
||||
codeReferenceFocus,
|
||||
} = useAppState();
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [glowRefId, setGlowRefId] = useState<string | null>(null);
|
||||
const panelRef = useRef<HTMLElement | null>(null);
|
||||
const resizeRef = useRef<{ startX: number; startWidth: number } | null>(null);
|
||||
const refCardEls = useRef<Map<string, HTMLDivElement | null>>(new Map());
|
||||
const glowTimerRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (glowTimerRef.current) {
|
||||
window.clearTimeout(glowTimerRef.current);
|
||||
glowTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [panelWidth, setPanelWidth] = useState<number>(() => {
|
||||
try {
|
||||
|
|
@ -90,6 +103,47 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =
|
|||
|
||||
const aiReferences = useMemo(() => codeReferences.filter(r => r.source === 'ai'), [codeReferences]);
|
||||
|
||||
// When the user clicks a citation badge in chat, focus the corresponding snippet card:
|
||||
// - expand the panel if collapsed
|
||||
// - smooth-scroll the card into view
|
||||
// - briefly glow it for discoverability
|
||||
useEffect(() => {
|
||||
if (!codeReferenceFocus) return;
|
||||
|
||||
// Ensure panel is expanded
|
||||
setIsCollapsed(false);
|
||||
|
||||
const { filePath, startLine, endLine } = codeReferenceFocus;
|
||||
const target =
|
||||
aiReferences.find(r =>
|
||||
r.filePath === filePath &&
|
||||
r.startLine === startLine &&
|
||||
r.endLine === endLine
|
||||
) ??
|
||||
aiReferences.find(r => r.filePath === filePath);
|
||||
|
||||
if (!target) return;
|
||||
|
||||
// Double rAF: wait for collapse state + list DOM to render.
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const el = refCardEls.current.get(target.id);
|
||||
if (!el) return;
|
||||
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
setGlowRefId(target.id);
|
||||
|
||||
if (glowTimerRef.current) {
|
||||
window.clearTimeout(glowTimerRef.current);
|
||||
}
|
||||
glowTimerRef.current = window.setTimeout(() => {
|
||||
setGlowRefId((prev) => (prev === target.id ? null : prev));
|
||||
glowTimerRef.current = null;
|
||||
}, 1200);
|
||||
});
|
||||
});
|
||||
}, [codeReferenceFocus?.ts, aiReferences]);
|
||||
|
||||
const refsWithSnippets = useMemo(() => {
|
||||
return aiReferences.map((ref) => {
|
||||
const content = fileContents.get(ref.filePath);
|
||||
|
|
@ -291,8 +345,17 @@ export const CodeReferencesPanel = ({ onFocusNode }: CodeReferencesPanelProps) =
|
|||
ref.filePath.endsWith('.ts') || ref.filePath.endsWith('.tsx') ? 'typescript' :
|
||||
'text';
|
||||
|
||||
const isGlowing = glowRefId === ref.id;
|
||||
|
||||
return (
|
||||
<div key={ref.id} className="bg-elevated border border-border-subtle rounded-xl overflow-hidden">
|
||||
<div
|
||||
key={ref.id}
|
||||
ref={(el) => { refCardEls.current.set(ref.id, el); }}
|
||||
className={[
|
||||
'bg-elevated border border-border-subtle rounded-xl overflow-hidden transition-all',
|
||||
isGlowing ? 'ring-2 ring-cyan-300/70 shadow-[0_0_0_6px_rgba(34,211,238,0.14)] animate-pulse' : '',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="px-3 py-2 border-b border-border-subtle bg-surface/40 flex items-start gap-2">
|
||||
<span
|
||||
className="mt-0.5 px-2 py-0.5 rounded text-[10px] font-semibold uppercase tracking-wide flex-shrink-0"
|
||||
|
|
|
|||
|
|
@ -113,17 +113,70 @@ export const RightPanel = () => {
|
|||
}, [addCodeReference, findFileNodeIdForUI, resolveFilePathForUI]);
|
||||
|
||||
const formatMarkdownForDisplay = useCallback((md: string) => {
|
||||
// Avoid rewriting inside fenced code blocks. Also avoid rewriting when immediately preceded by a backtick.
|
||||
// Avoid rewriting inside fenced code blocks.
|
||||
const parts = md.split('```');
|
||||
for (let i = 0; i < parts.length; i += 2) {
|
||||
parts[i] = parts[i].replace(/(^|[^`])\[\[([^\]\n]+?)\]\]/g, (_m, prefix: string, inner: string) => {
|
||||
parts[i] = parts[i].replace(/\[\[([^\]\n]+?)\]\]/g, (_m, inner: string) => {
|
||||
const trimmed = inner.trim();
|
||||
const href = `code-ref:${encodeURIComponent(trimmed)}`;
|
||||
return `${prefix}[${trimmed}](${href})`;
|
||||
return `[${trimmed}](${href})`;
|
||||
});
|
||||
}
|
||||
return parts.join('```');
|
||||
}, []);
|
||||
|
||||
const formatRefChipLabel = useCallback((ref: string): string => {
|
||||
const raw = ref.trim();
|
||||
if (!raw) return '';
|
||||
|
||||
// Drop any scheme prefix we might have accidentally passed through
|
||||
const withoutScheme = raw.startsWith('code-ref:') ? raw.slice('code-ref:'.length) : raw;
|
||||
|
||||
// Strip query/hash
|
||||
const cleaned = withoutScheme.split('#')[0].split('?')[0];
|
||||
|
||||
const m = cleaned.match(/^(.*):(\d+)(?:-(\d+))?$/);
|
||||
const path = (m ? m[1] : cleaned).replace(/\\/g, '/');
|
||||
const base = path.split('/').pop() ?? path;
|
||||
|
||||
if (!m) return base;
|
||||
|
||||
const start = m[2];
|
||||
const end = m[3] ?? m[2];
|
||||
return `${base} ${start}–${end}`;
|
||||
}, []);
|
||||
|
||||
const isLikelyFileRefHref = useCallback((href: string): boolean => {
|
||||
const h = href.trim();
|
||||
if (!h) return false;
|
||||
if (h.startsWith('code-ref:')) return true;
|
||||
if (/^(https?:|mailto:|tel:|#)/i.test(h)) return false;
|
||||
if (h.includes('://')) return false;
|
||||
|
||||
// Strip query/hash
|
||||
const cleaned = h.split('#')[0].split('?')[0];
|
||||
|
||||
// Looks like: path/to/file.ext or path\to\file.ext:12-34
|
||||
return /[A-Za-z0-9_\-./\\]+\.[A-Za-z0-9]+(?::\d+(?:-\d+)?)?$/.test(cleaned);
|
||||
}, []);
|
||||
|
||||
const extractTextFromChildren = useCallback((children: any): string => {
|
||||
if (children == null) return '';
|
||||
if (typeof children === 'string' || typeof children === 'number') return String(children);
|
||||
if (Array.isArray(children)) return children.map(extractTextFromChildren).join('');
|
||||
// React element or other objects
|
||||
return '';
|
||||
}, []);
|
||||
|
||||
const getInternalRefFromLink = useCallback((href: string | undefined, children: any): string | null => {
|
||||
const hrefStr = (href ?? '').trim();
|
||||
const textStr = extractTextFromChildren(children).trim();
|
||||
|
||||
if (hrefStr && isLikelyFileRefHref(hrefStr)) return hrefStr;
|
||||
if (textStr && isLikelyFileRefHref(textStr)) return textStr;
|
||||
|
||||
return null;
|
||||
}, [extractTextFromChildren, isLikelyFileRefHref]);
|
||||
|
||||
// Auto-resize textarea as user types
|
||||
const adjustTextareaHeight = useCallback(() => {
|
||||
|
|
@ -298,8 +351,91 @@ export const RightPanel = () => {
|
|||
{message.steps.map((step) => (
|
||||
<div key={step.id}>
|
||||
{step.type === 'reasoning' && step.content && (
|
||||
<div className="text-text-secondary text-[13px] italic border-l-2 border-accent/40 pl-3 py-0.5 bg-accent/5 rounded-r">
|
||||
{step.content}
|
||||
<div className="text-text-primary text-sm chat-prose">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: ({ href, children, ...props }) => {
|
||||
if (href && href.startsWith('code-ref:')) {
|
||||
const inner = decodeURIComponent(href.slice('code-ref:'.length));
|
||||
const label = formatRefChipLabel(inner);
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleGroundingClick(inner);
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
|
||||
title={`Open in Code panel • ${inner}`}
|
||||
{...props}
|
||||
>
|
||||
<span className="text-inherit">{label || children}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const internalRef = getInternalRefFromLink(href, children);
|
||||
if (internalRef) {
|
||||
const label = formatRefChipLabel(internalRef);
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleGroundingClick(internalRef);
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
|
||||
title={`Open in Code panel • ${internalRef}`}
|
||||
{...props}
|
||||
>
|
||||
<span className="text-inherit">{label || children}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
className="text-accent underline underline-offset-2 hover:text-purple-300"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
code: ({ className, children, ...props }) => {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const isInline = !className && !match;
|
||||
const codeContent = String(children).replace(/\n$/, '');
|
||||
|
||||
if (isInline) {
|
||||
return <code {...props}>{children}</code>;
|
||||
}
|
||||
|
||||
const language = match ? match[1] : 'text';
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
style={customTheme}
|
||||
language={language}
|
||||
PreTag="div"
|
||||
customStyle={{
|
||||
margin: 0,
|
||||
padding: '14px 16px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '13px',
|
||||
background: '#0a0a10',
|
||||
border: '1px solid #1e1e2a',
|
||||
}}
|
||||
>
|
||||
{codeContent}
|
||||
</SyntaxHighlighter>
|
||||
);
|
||||
},
|
||||
pre: ({ children }) => <>{children}</>,
|
||||
}}
|
||||
>
|
||||
{formatMarkdownForDisplay(step.content)}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)}
|
||||
{step.type === 'tool_call' && step.toolCall && (
|
||||
|
|
@ -311,6 +447,7 @@ export const RightPanel = () => {
|
|||
a: ({ href, children, ...props }) => {
|
||||
if (href && href.startsWith('code-ref:')) {
|
||||
const inner = decodeURIComponent(href.slice('code-ref:'.length));
|
||||
const label = formatRefChipLabel(inner);
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
|
|
@ -318,11 +455,29 @@ export const RightPanel = () => {
|
|||
e.preventDefault();
|
||||
handleGroundingClick(inner);
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-400/40 bg-cyan-500/10 text-cyan-200 font-mono text-[12px] hover:bg-cyan-500/15 hover:border-cyan-300/60 transition-colors"
|
||||
title="Open in Code panel"
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
|
||||
title={`Open in Code panel • ${inner}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<span className="text-inherit">{label || children}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const internalRef = getInternalRefFromLink(href, children);
|
||||
if (internalRef) {
|
||||
const label = formatRefChipLabel(internalRef);
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleGroundingClick(internalRef);
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
|
||||
title={`Open in Code panel • ${internalRef}`}
|
||||
{...props}
|
||||
>
|
||||
<span className="text-inherit">{label || children}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
@ -383,6 +538,7 @@ export const RightPanel = () => {
|
|||
a: ({ href, children, ...props }) => {
|
||||
if (href && href.startsWith('code-ref:')) {
|
||||
const inner = decodeURIComponent(href.slice('code-ref:'.length));
|
||||
const label = formatRefChipLabel(inner);
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
|
|
@ -390,11 +546,29 @@ export const RightPanel = () => {
|
|||
e.preventDefault();
|
||||
handleGroundingClick(inner);
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-400/40 bg-cyan-500/10 text-cyan-200 font-mono text-[12px] hover:bg-cyan-500/15 hover:border-cyan-300/60 transition-colors"
|
||||
title="Open in Code panel"
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
|
||||
title={`Open in Code panel • ${inner}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<span className="text-inherit">{label || children}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
const internalRef = getInternalRefFromLink(href, children);
|
||||
if (internalRef) {
|
||||
const label = formatRefChipLabel(internalRef);
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleGroundingClick(internalRef);
|
||||
}}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded-md border border-cyan-300/55 bg-cyan-400/10 !text-cyan-200 visited:!text-cyan-200 font-mono text-[12px] !no-underline hover:!no-underline hover:bg-cyan-400/15 hover:border-cyan-200/70 transition-colors"
|
||||
title={`Open in Code panel • ${internalRef}`}
|
||||
{...props}
|
||||
>
|
||||
<span className="text-inherit">{label || children}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,8 +108,19 @@ Single polymorphic table: \`CodeNode\` with \`label\` property (File, Function,
|
|||
|
||||
Relationships: \`CodeRelation\` with \`type\` (CALLS, IMPORTS, CONTAINS, DEFINES)
|
||||
|
||||
**IMPORTANT:** There is NO relationship label/table named \`CALLS\` / \`IMPORTS\` / etc.
|
||||
Always use \`CodeRelation\` and filter on \`r.type\`, e.g.:
|
||||
- ✅ \`MATCH (a:CodeNode)-[r:CodeRelation]->(b:CodeNode) WHERE r.type = 'CALLS'\`
|
||||
- ❌ \`MATCH (a)-[:CALLS]->(b)\` -- WRONG, will fail with "Table CALLS does not exist"
|
||||
|
||||
Vector search requires JOIN: \`CALL QUERY_VECTOR_INDEX(...) YIELD node AS emb, distance WITH emb, distance WHERE ... MATCH (n:CodeNode {id: emb.nodeId})\`
|
||||
|
||||
## ERROR RECOVERY (BE AGENTIC)
|
||||
|
||||
If a tool call returns an error (e.g., Cypher binder/syntax errors), do NOT stop.
|
||||
- Correct the query and retry at least once.
|
||||
- If unsure, call \`get_graph_schema\` to ground the correct schema, then retry.
|
||||
|
||||
## USE HIGHLIGHTING
|
||||
|
||||
The user sees a visual knowledge graph alongside this chat. Use \`highlight_in_graph\` liberally to:
|
||||
|
|
@ -255,7 +266,11 @@ export async function* streamAgentResponse(
|
|||
// Use BOTH modes: 'values' for structure, 'messages' for token streaming
|
||||
const stream = await agent.stream(
|
||||
{ messages: formattedMessages },
|
||||
{ streamMode: ['values', 'messages'] as any }
|
||||
{
|
||||
streamMode: ['values', 'messages'] as any,
|
||||
// Allow longer tool/reasoning loops (more Cursor-like persistence)
|
||||
recursionLimit: 50,
|
||||
} as any
|
||||
);
|
||||
|
||||
// Track what we've yielded to avoid duplicates
|
||||
|
|
@ -264,6 +279,10 @@ export async function* streamAgentResponse(
|
|||
let lastProcessedMsgCount = formattedMessages.length;
|
||||
// Track if all tools are done (for distinguishing reasoning vs final content)
|
||||
let allToolsDone = true;
|
||||
// Track if we've seen any tool calls in this response turn.
|
||||
// Anything before the first tool call should be treated as "reasoning/narration"
|
||||
// so the UI can show the Cursor-like loop: plan → tool → update → tool → answer.
|
||||
let hasSeenToolCallThisTurn = false;
|
||||
|
||||
for await (const event of stream) {
|
||||
// Events come as [streamMode, data] tuples when using multiple modes
|
||||
|
|
@ -297,8 +316,14 @@ export async function* streamAgentResponse(
|
|||
|
||||
// If chunk has content, stream it
|
||||
if (content && typeof content === 'string' && content.length > 0) {
|
||||
// Determine if this is reasoning (before/between tools) or final content
|
||||
const isReasoning = toolCalls.length > 0 || !allToolsDone;
|
||||
// Determine if this is reasoning/narration vs final answer content.
|
||||
// - Before the first tool call: treat as reasoning (narration)
|
||||
// - Between tool calls/results: treat as reasoning
|
||||
// - After all tools are done: treat as final content
|
||||
const isReasoning =
|
||||
!hasSeenToolCallThisTurn ||
|
||||
toolCalls.length > 0 ||
|
||||
!allToolsDone;
|
||||
yield {
|
||||
type: isReasoning ? 'reasoning' : 'content',
|
||||
[isReasoning ? 'reasoning' : 'content']: content,
|
||||
|
|
@ -307,6 +332,7 @@ export async function* streamAgentResponse(
|
|||
|
||||
// Track tool calls from message chunks
|
||||
if (toolCalls.length > 0) {
|
||||
hasSeenToolCallThisTurn = true;
|
||||
allToolsDone = false;
|
||||
for (const tc of toolCalls) {
|
||||
const toolId = tc.id || `tool-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,26 @@ export const createGraphRAGTools = (
|
|||
isBM25Ready: () => boolean,
|
||||
fileContents: Map<string, string>
|
||||
) => {
|
||||
const buildCypherSchemaHint = (message: string): string => {
|
||||
const m = message.match(/Table\s+([A-Za-z_][A-Za-z0-9_]*)\s+does\s+not\s+exist/i);
|
||||
const missing = (m?.[1] ?? '').toUpperCase();
|
||||
const knownRelTypes = new Set(['CALLS', 'IMPORTS', 'CONTAINS', 'DEFINES']);
|
||||
if (!knownRelTypes.has(missing)) return '';
|
||||
|
||||
return [
|
||||
'',
|
||||
'Schema hint:',
|
||||
`- There is NO relationship label/table named "${missing}".`,
|
||||
"- All relationships use the single relationship label `CodeRelation` with a `type` property.",
|
||||
`- Rewrite patterns like \`-[:${missing}]->\` to \`-[r:CodeRelation]->\` and add \`WHERE r.type = '${missing}'\`.`,
|
||||
'',
|
||||
'Example:',
|
||||
"MATCH (a:CodeNode)-[r:CodeRelation]->(b:CodeNode)",
|
||||
`WHERE r.type = '${missing}'`,
|
||||
'RETURN a.id, b.id LIMIT 25',
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool: Execute Cypher Query
|
||||
* Allows the agent to run arbitrary Cypher queries against the graph
|
||||
|
|
@ -51,7 +71,8 @@ export const createGraphRAGTools = (
|
|||
return `Query returned ${results.length} results:\n${resultText}${truncated}`;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return `Cypher query error: ${message}\n\nPlease check your query syntax and try again.`;
|
||||
const hint = buildCypherSchemaHint(message);
|
||||
return `Cypher query error: ${message}\n\nPlease check your query syntax and try again.${hint ? `\n\n${hint}` : ''}`;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ export interface CodeReference {
|
|||
source: 'ai' | 'user'; // How it was added
|
||||
}
|
||||
|
||||
export interface CodeReferenceFocus {
|
||||
filePath: string;
|
||||
startLine?: number;
|
||||
endLine?: number;
|
||||
ts: number;
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
// View state
|
||||
viewMode: ViewMode;
|
||||
|
|
@ -131,6 +138,7 @@ interface AppState {
|
|||
removeCodeReference: (id: string) => void;
|
||||
clearAICodeReferences: () => void;
|
||||
clearCodeReferences: () => void;
|
||||
codeReferenceFocus: CodeReferenceFocus | null;
|
||||
}
|
||||
|
||||
const AppStateContext = createContext<AppState | null>(null);
|
||||
|
|
@ -215,6 +223,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
// Code References Panel state
|
||||
const [codeReferences, setCodeReferences] = useState<CodeReference[]>([]);
|
||||
const [isCodePanelOpen, setCodePanelOpen] = useState(false);
|
||||
const [codeReferenceFocus, setCodeReferenceFocus] = useState<CodeReferenceFocus | null>(null);
|
||||
|
||||
const normalizePath = useCallback((p: string) => {
|
||||
return p.replace(/\\/g, '/').replace(/^\.?\//, '');
|
||||
|
|
@ -283,6 +292,16 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
|
||||
// Auto-open panel when references are added
|
||||
setCodePanelOpen(true);
|
||||
|
||||
// Signal the Code Inspector to focus (scroll + glow) this reference.
|
||||
// This should happen even if the reference already exists (duplicates are ignored),
|
||||
// so it must be separate from the add-to-list behavior.
|
||||
setCodeReferenceFocus({
|
||||
filePath: ref.filePath,
|
||||
startLine: ref.startLine,
|
||||
endLine: ref.endLine,
|
||||
ts: Date.now(),
|
||||
});
|
||||
|
||||
// Track AI highlights separately so they can be toggled off in the UI
|
||||
if (ref.nodeId && ref.source === 'ai') {
|
||||
|
|
@ -832,6 +851,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
const clearCodeReferences = useCallback(() => {
|
||||
setCodeReferences([]);
|
||||
setCodePanelOpen(false);
|
||||
setCodeReferenceFocus(null);
|
||||
}, []);
|
||||
|
||||
const toggleLabelVisibility = useCallback((label: NodeLabel) => {
|
||||
|
|
@ -914,6 +934,7 @@ export const AppStateProvider = ({ children }: { children: ReactNode }) => {
|
|||
removeCodeReference,
|
||||
clearAICodeReferences,
|
||||
clearCodeReferences,
|
||||
codeReferenceFocus,
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue