UI improvements and prompt improvements

This commit is contained in:
abhigyanpatwari 2026-01-10 00:38:15 +05:30
parent 0a42fe46ac
commit 9ee4972aa6
5 changed files with 321 additions and 16 deletions

View file

@ -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"

View file

@ -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>
);
}

View file

@ -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)}`;

View file

@ -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}` : ''}`;
}
},
{

View file

@ -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 (