{isLoading && repos.length === 0 ? (
- |
+ |
Loading repositories...
|
) : repos.length === 0 ? (
- |
+ |
No Git repositories registered. Click "Add Repository" to index a remote repo.
|
) : (
repos.map((r) => {
const isAutoSync = r.auto_sync !== false && r.auto_sync !== 0;
+ const isKeepShallow = r.keep_shallow === true || r.keep_shallow === 1;
const job = syncStates?.[r.id];
const isSyncing = r.status === 'syncing' || job?.status === 'syncing';
@@ -197,6 +201,30 @@ export function RepoListTable({
Auto-Sync: {isAutoSync ? 'ON' : 'OFF'}
+
+
+ |
{(r.file_count || 0).toLocaleString()} files |
{r.last_synced || 'Never'}
@@ -257,6 +285,7 @@ export function RepoListTable({
) : (
repos.map((r) => {
const isAutoSync = r.auto_sync !== false && r.auto_sync !== 0;
+ const isKeepShallow = r.keep_shallow === true || r.keep_shallow === 1;
const job = syncStates?.[r.id];
const isSyncing = r.status === 'syncing' || job?.status === 'syncing';
@@ -291,13 +320,13 @@ export function RepoListTable({
)}
-
- URL:
+
+ URL:
{r.url}
@@ -315,7 +344,7 @@ export function RepoListTable({
+
+ Shallow Copy:
+
+
Files:
{(r.file_count || 0).toLocaleString()} files
diff --git a/frontend/src/components/git/RepoSyncDrawer.tsx b/frontend/src/components/git/RepoSyncDrawer.tsx
index 42f08d2..12de089 100644
--- a/frontend/src/components/git/RepoSyncDrawer.tsx
+++ b/frontend/src/components/git/RepoSyncDrawer.tsx
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useMemo } from 'react';
+import { createPortal } from 'react-dom';
import type { GitSyncJob } from '../../types';
export interface RepoSyncDrawerProps {
@@ -121,7 +122,7 @@ export function RepoSyncDrawer({
const percent = job?.percent ?? (status === 'synced' ? 100 : 0);
const currentStep = job?.step ?? (status === 'synced' ? 5 : 1);
- return (
+ const drawerNode = (
{
@@ -373,4 +374,6 @@ export function RepoSyncDrawer({
);
+
+ return typeof document !== 'undefined' ? createPortal(drawerNode, document.body) : drawerNode;
}
diff --git a/frontend/src/components/git/WebhookModal.tsx b/frontend/src/components/git/WebhookModal.tsx
index 82ec141..5941494 100644
--- a/frontend/src/components/git/WebhookModal.tsx
+++ b/frontend/src/components/git/WebhookModal.tsx
@@ -1,4 +1,5 @@
import type { Repo } from '../../types';
+import { createPortal } from 'react-dom';
interface WebhookModalProps {
repo: Repo | null;
@@ -13,7 +14,7 @@ export function WebhookModal({ repo, onClose, onCopyUrl, copiedUrl }: WebhookMod
const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
const webhookEndpoint = `${currentOrigin}/api/webhooks/git`;
- return (
+ const modalNode = (
{
@@ -131,4 +132,6 @@ export function WebhookModal({ repo, onClose, onCopyUrl, copiedUrl }: WebhookMod
);
+
+ return typeof document !== 'undefined' ? createPortal(modalNode, document.body) : modalNode;
}
diff --git a/frontend/src/components/navigator/NavigatorBreadcrumbs.tsx b/frontend/src/components/navigator/NavigatorBreadcrumbs.tsx
new file mode 100644
index 0000000..e67207c
--- /dev/null
+++ b/frontend/src/components/navigator/NavigatorBreadcrumbs.tsx
@@ -0,0 +1,169 @@
+import React, { useState } from 'react';
+
+export interface NavigatorBreadcrumbsProps {
+ repo: string;
+ path: string | null;
+ symbol: string | null;
+ onNavigatePath: (path: string) => void;
+ canGoBack: boolean;
+ canGoForward: boolean;
+ onGoBack: () => void;
+ onGoForward: () => void;
+ onToggleSidebar: () => void;
+ isSidebarOpen: boolean;
+}
+
+export const NavigatorBreadcrumbs: React.FC = ({
+ repo,
+ path,
+ symbol,
+ onNavigatePath,
+ canGoBack,
+ canGoForward,
+ onGoBack,
+ onGoForward,
+ onToggleSidebar,
+ isSidebarOpen,
+}) => {
+ const [copied, setCopied] = useState(false);
+
+ const pathSegments = React.useMemo(() => {
+ if (!path) return [];
+ // Strip repo:// prefix for display
+ const clean = path.replace(/\\/g, '/').replace(/^[^/]+:\/\//, '').replace(/^\/+/, '');
+ return clean.split('/').filter(Boolean);
+ }, [path]);
+
+ const handleCopyPermalink = async () => {
+ if (!path) return;
+ const lineRef = symbol ? `#${symbol}` : '';
+ const permalink = `${path}${lineRef}`;
+ try {
+ if (navigator?.clipboard?.writeText) {
+ await navigator.clipboard.writeText(permalink);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ }
+ } catch {
+ // Fallback: ignore
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ {/* Right side: Copy Permalink — always visible when a file is open */}
+ {path && (
+
+
+
+ )}
+
+ );
+};
diff --git a/frontend/src/components/navigator/NavigatorCodeViewer.tsx b/frontend/src/components/navigator/NavigatorCodeViewer.tsx
index 22d74f6..9fd6ba4 100644
--- a/frontend/src/components/navigator/NavigatorCodeViewer.tsx
+++ b/frontend/src/components/navigator/NavigatorCodeViewer.tsx
@@ -1,7 +1,199 @@
import React, { useState, useEffect, useRef, useMemo } from 'react';
+import { Highlight } from 'prism-react-renderer';
+import Prism from 'prismjs';
+// Programming Language Grammars
+import 'prismjs/components/prism-csharp';
+import 'prismjs/components/prism-c';
+import 'prismjs/components/prism-cpp';
+import 'prismjs/components/prism-python';
+import 'prismjs/components/prism-javascript';
+import 'prismjs/components/prism-jsx';
+import 'prismjs/components/prism-typescript';
+import 'prismjs/components/prism-tsx';
+import 'prismjs/components/prism-go';
+import 'prismjs/components/prism-rust';
+import 'prismjs/components/prism-bash';
+import 'prismjs/components/prism-docker';
+import 'prismjs/components/prism-sql';
+import 'prismjs/components/prism-plsql';
+import 'prismjs/components/prism-json';
+import 'prismjs/components/prism-yaml';
+import 'prismjs/components/prism-cobol';
import type { SymbolImpact, DensityMode } from './types';
import { getMethodBadgeClass, getKindBadgeClass } from './NavigatorOutline';
+// Comprehensive VS/Rider/JetBrains dark theme with rich token mappings for C#, Python, TS/JS, C++, Go, Rust, COBOL, SQL, etc.
+const universalIDETheme = {
+ plain: {
+ color: '#d4d4d4',
+ backgroundColor: '#1e1e1e',
+ },
+ styles: [
+ {
+ types: ['comment', 'prolog', 'doctype', 'cdata'],
+ style: { color: '#6a9955', fontStyle: 'italic' as const },
+ },
+ {
+ types: ['level'],
+ style: { color: '#38bdf8', fontWeight: 'bold' as const },
+ },
+ {
+ types: ['keyword', 'builtin-keyword'],
+ style: { color: '#569cd6' },
+ },
+ {
+ types: [
+ 'class-name',
+ 'return-type',
+ 'type-list',
+ 'namespace',
+ 'base-clause',
+ 'builtin',
+ ],
+ style: { color: '#4ec9b0' },
+ },
+ {
+ types: ['interface'],
+ style: { color: '#b8d7a3' },
+ },
+ {
+ types: ['function', 'generic-function', 'function-variable', 'generic-method'],
+ style: { color: '#dcdcaa' },
+ },
+ {
+ types: ['decorator', 'attribute'],
+ style: { color: '#dcdcaa' },
+ },
+ {
+ types: [
+ 'string',
+ 'char',
+ 'attr-value',
+ 'triple-quoted-string',
+ 'raw-string',
+ 'template-string',
+ 'interpolation-string',
+ 'string-interpolation',
+ ],
+ style: { color: '#ce9178' },
+ },
+ {
+ types: ['number', 'boolean'],
+ style: { color: '#b5cea8' },
+ },
+ {
+ types: [
+ 'variable',
+ 'parameter',
+ 'named-parameter',
+ 'property',
+ 'string-property',
+ 'attr-name',
+ 'identifier',
+ ],
+ style: { color: '#9cdcfe' },
+ },
+ {
+ types: ['constant', 'symbol'],
+ style: { color: '#4fc1ff' },
+ },
+ {
+ types: [
+ 'macro',
+ 'directive',
+ 'preprocessor',
+ 'preprocessor-directive',
+ ],
+ style: { color: '#c586c0' },
+ },
+ {
+ types: ['directive-hash'],
+ style: { color: '#9b9b9b' },
+ },
+ {
+ types: ['tag'],
+ style: { color: '#569cd6' },
+ },
+ {
+ types: ['regex'],
+ style: { color: '#d16969' },
+ },
+ {
+ types: ['punctuation', 'operator', 'double-colon'],
+ style: { color: '#d4d4d4' },
+ },
+ ],
+};
+
+function getPrismLanguage(filepath: string): string {
+ const clean = filepath.split('?')[0].split('#')[0];
+ const ext = (clean.split('.').pop() || '').toLowerCase();
+ const map: Record = {
+ // C#
+ cs: 'csharp',
+ csharp: 'csharp',
+ // Python
+ py: 'python',
+ pyw: 'python',
+ python: 'python',
+ // TypeScript & JavaScript
+ ts: 'typescript',
+ tsx: 'tsx',
+ js: 'javascript',
+ jsx: 'jsx',
+ mjs: 'javascript',
+ cjs: 'javascript',
+ // C & C++
+ c: 'c',
+ h: 'cpp',
+ cpp: 'cpp',
+ cc: 'cpp',
+ cxx: 'cpp',
+ hpp: 'cpp',
+ hh: 'cpp',
+ hxx: 'cpp',
+ // Go & Rust
+ go: 'go',
+ rs: 'rust',
+ rust: 'rust',
+ // Web, Config & Data
+ json: 'json',
+ yml: 'yaml',
+ yaml: 'yaml',
+ md: 'markdown',
+ markdown: 'markdown',
+ css: 'css',
+ scss: 'css',
+ html: 'markup',
+ xml: 'markup',
+ svg: 'markup',
+ // SQL, Databases & Procedural Dialects
+ sql: 'sql',
+ psql: 'sql',
+ pls: 'plsql',
+ plsql: 'plsql',
+ pkb: 'plsql',
+ pks: 'plsql',
+ ddl: 'sql',
+ dml: 'sql',
+ cql: 'sql',
+ hql: 'sql',
+ // Shell & Infra
+ sh: 'bash',
+ bash: 'bash',
+ zsh: 'bash',
+ dockerfile: 'docker',
+ // COBOL
+ cbl: 'cobol',
+ cob: 'cobol',
+ cpy: 'cobol',
+ cobol: 'cobol',
+ pco: 'cobol',
+ };
+ const candidate = map[ext] || 'plain';
+ return (Prism.languages as Record)[candidate] ? candidate : 'plain';
+}
+
export interface NavigatorCodeViewerProps {
filepath: string;
content: string;
@@ -205,33 +397,43 @@ export const NavigatorCodeViewer: React.FC = ({
File is empty.
) : (
-
- {lines.map((line, idx) => {
- const lineNum = idx + 1;
- const isTarget =
- targetStartLine !== undefined &&
- lineNum >= targetStartLine &&
- lineNum <= (targetEndLine || targetStartLine);
+
+ {({ tokens, getTokenProps }) => (
+
+ {tokens.map((lineTokens, idx) => {
+ const lineNum = idx + 1;
+ const isTarget =
+ targetStartLine !== undefined &&
+ lineNum >= targetStartLine &&
+ lineNum <= (targetEndLine || targetStartLine);
- const isFirstTarget = targetStartLine !== undefined && lineNum === targetStartLine;
+ const isFirstTarget = targetStartLine !== undefined && lineNum === targetStartLine;
- return (
-
-
- {lineNum}
-
-
- {line || ' '}
-
+ return (
+
+
+ {lineNum}
+
+
+ {lineTokens.length === 0 || (lineTokens.length === 1 && lineTokens[0].content === '') ? (
+ ' '
+ ) : (
+ lineTokens.map((token, key) => (
+
+ ))
+ )}
+
+
+ );
+ })}
- );
- })}
-
+ )}
+
)}
diff --git a/frontend/src/components/navigator/NavigatorDocReader.tsx b/frontend/src/components/navigator/NavigatorDocReader.tsx
index bb74ebd..7b8dde6 100644
--- a/frontend/src/components/navigator/NavigatorDocReader.tsx
+++ b/frontend/src/components/navigator/NavigatorDocReader.tsx
@@ -1,4 +1,6 @@
-import React, { useState } from 'react';
+import React, { useState, useEffect, useRef, useMemo } from 'react';
+import { Marked } from 'marked';
+import mermaid from 'mermaid';
interface NavigatorDocReaderProps {
filepath: string;
@@ -10,6 +12,95 @@ interface NavigatorDocReaderProps {
onRefresh?: () => void;
}
+// Configured GFM markdown parser with Mermaid code block interception
+const markdownParser = new Marked({
+ gfm: true,
+ breaks: true,
+ renderer: {
+ code({ text, lang }: { text: string; lang?: string }) {
+ if (lang === 'mermaid') {
+ return `\n`;
+ }
+ return false; // use standard code block rendering
+ },
+ },
+});
+
+/**
+ * Normalizes loose or non-standard markdown tables:
+ * 1. Automatically inserts missing separator rows (| --- | --- |) after header rows
+ * 2. Bridges blank lines between consecutive table rows (common in notes and changelogs)
+ * 3. Preserves code blocks (``` and ~~~) without modification
+ */
+function normalizeMarkdownTables(md: string): string {
+ const lines = md.split('\n');
+ const result: string[] = [];
+ let inCodeBlock = false;
+ let inTable = false;
+ let tableColCount = 0;
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i];
+ const trimmed = line.trim();
+
+ // Preserve code blocks verbatim
+ if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
+ inCodeBlock = !inCodeBlock;
+ inTable = false;
+ result.push(line);
+ continue;
+ }
+
+ if (inCodeBlock) {
+ result.push(line);
+ continue;
+ }
+
+ const isTableRow = trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.includes('|');
+
+ if (isTableRow) {
+ const colCount = Math.max(1, trimmed.split('|').length - 2);
+
+ if (!inTable) {
+ inTable = true;
+ tableColCount = colCount;
+ result.push(line);
+
+ // Peek next non-empty line
+ let nextIdx = i + 1;
+ while (nextIdx < lines.length && lines[nextIdx].trim() === '') nextIdx++;
+
+ if (nextIdx < lines.length) {
+ const nextTrimmed = lines[nextIdx].trim();
+ const isSeparator = /^\|?(\s*:?-+:?\s*\|)+\s*:?-+:?\s*\|?$/.test(nextTrimmed);
+ if (!isSeparator && nextTrimmed.startsWith('|') && nextTrimmed.endsWith('|')) {
+ // Missing separator row under header! Auto-insert matching columns
+ const separator = '|' + Array(tableColCount).fill(' --- ').join('|') + '|';
+ result.push(separator);
+ }
+ }
+ } else {
+ result.push(line);
+ }
+ } else if (trimmed === '' && inTable) {
+ // Check if next non-empty line is also a table row (bridging human blank lines in tables)
+ let nextIdx = i + 1;
+ while (nextIdx < lines.length && lines[nextIdx].trim() === '') nextIdx++;
+ if (nextIdx < lines.length && lines[nextIdx].trim().startsWith('|') && lines[nextIdx].trim().endsWith('|')) {
+ continue;
+ } else {
+ inTable = false;
+ result.push(line);
+ }
+ } else {
+ inTable = false;
+ result.push(line);
+ }
+ }
+
+ return result.join('\n');
+}
+
export const NavigatorDocReader: React.FC = ({
filepath,
content,
@@ -22,6 +113,7 @@ export const NavigatorDocReader: React.FC = ({
const isMarkdown = filepath.toLowerCase().endsWith('.md') || filepath.toLowerCase().endsWith('.markdown');
const [viewMode, setViewMode] = useState<'rendered' | 'raw'>(isMarkdown ? 'rendered' : 'raw');
const [copied, setCopied] = useState(false);
+ const containerRef = useRef(null);
const handleCopy = async () => {
if (!content) return;
@@ -44,123 +136,83 @@ export const NavigatorDocReader: React.FC = ({
const basename = filepath ? filepath.split('/').pop() || filepath : '';
- // Simple safe Markdown parser for formatted preview
- const renderMarkdown = (text: string) => {
- const lines = text.split('\n');
- const elements: React.ReactNode[] = [];
- let inCodeBlock = false;
- let codeBlockLang = '';
- let codeBlockLines: string[] = [];
-
- lines.forEach((line, index) => {
- // Code block toggle
- if (line.trim().startsWith('```')) {
- if (inCodeBlock) {
- elements.push(
-
- {codeBlockLines.join('\n')}
-
- );
- codeBlockLines = [];
- inCodeBlock = false;
- } else {
- inCodeBlock = true;
- codeBlockLang = line.trim().slice(3);
- }
- return;
- }
+ // Render HTML via Marked parser with table normalization
+ const renderedHtml = useMemo(() => {
+ if (!content) return '';
+ try {
+ const normalized = normalizeMarkdownTables(content);
+ return markdownParser.parse(normalized) as string;
+ } catch (e) {
+ console.error('Failed to parse markdown:', e);
+ return `${content} `;
+ }
+ }, [content]);
- if (inCodeBlock) {
- codeBlockLines.push(line);
- return;
- }
+ // Render Mermaid diagrams whenever renderedHtml or viewMode updates
+ useEffect(() => {
+ if (viewMode !== 'rendered' || !containerRef.current) return;
- // Headings
- if (line.startsWith('# ')) {
- elements.push({line.slice(2)});
- } else if (line.startsWith('## ')) {
- elements.push({line.slice(3)});
- } else if (line.startsWith('### ')) {
- elements.push({line.slice(4)});
- } else if (line.startsWith('#### ')) {
- elements.push({line.slice(5)});
- } else if (line.trim() === '---' || line.trim() === '***') {
- elements.push( );
- } else if (line.startsWith('> ')) {
- elements.push({line.slice(2)} );
- } else if (line.trim().startsWith('- ') || line.trim().startsWith('* ')) {
- elements.push(
-
- {formatInline(line.trim().slice(2))}
-
- );
- } else if (/^\d+\.\s/.test(line.trim())) {
- const itemText = line.trim().replace(/^\d+\.\s/, '');
- elements.push(
-
- {formatInline(itemText)}
-
- );
- } else if (line.trim() === '') {
- elements.push();
- } else {
- elements.push({formatInline(line)} );
- }
- });
-
- if (inCodeBlock && codeBlockLines.length > 0) {
- elements.push(
-
- {codeBlockLines.join('\n')}
-
- );
- }
+ const mermaidNodes = containerRef.current.querySelectorAll('.mermaid');
+ if (mermaidNodes.length === 0) return;
- return elements;
- };
+ try {
+ mermaid.initialize({
+ startOnLoad: false,
+ theme: 'dark',
+ securityLevel: 'loose',
+ fontFamily: 'monospace, sans-serif',
+ themeVariables: {
+ darkMode: true,
+ background: '#07181b',
+ primaryColor: '#0891b2',
+ primaryTextColor: '#f8fafc',
+ primaryBorderColor: '#15474d',
+ lineColor: '#2dd4bf',
+ secondaryColor: '#164e63',
+ tertiaryColor: '#0d2c2f',
+ },
+ });
- // Helper for inline bold, inline code, links
- const formatInline = (text: string): React.ReactNode => {
- // Process inline code `foo`
- const parts = text.split(/(`[^`]+`)/);
- return parts.map((part, i) => {
- if (part.startsWith('`') && part.endsWith('`') && part.length >= 2) {
- return {part.slice(1, -1)};
+ // Filter unrendered nodes
+ const unrendered = Array.from(mermaidNodes).filter((n) => !n.getAttribute('data-processed'));
+ if (unrendered.length > 0) {
+ mermaid.run({
+ nodes: unrendered,
+ }).catch((err) => {
+ console.warn('Mermaid rendering warning:', err);
+ });
}
- // Process bold **text**
- const boldParts = part.split(/(\*\*[^*]+\*\*)/);
- return boldParts.map((bPart, j) => {
- if (bPart.startsWith('**') && bPart.endsWith('**') && bPart.length >= 4) {
- return {bPart.slice(2, -2)};
- }
- return bPart;
- });
- });
- };
+ } catch (e) {
+ console.warn('Failed to initialize mermaid:', e);
+ }
+ }, [renderedHtml, viewMode]);
return (
- {/* Doc Reader Header */}
+ {/* Header Toolbar */}
-
- {isMarkdown ? '📝' : '📄'}
-
- {basename}
-
- {filepath}
- {totalLines} lines
- {formatSize(sizeBytes)}
-
-
+
+
+
+ {basename}
+
+ {totalLines} lines
+ {sizeBytes > 0 && {formatSize(sizeBytes)}}
-
+
{isMarkdown && (
-
+
@@ -168,6 +220,7 @@ export const NavigatorDocReader: React.FC = ({
type="button"
className={`toggle-btn ${viewMode === 'raw' ? 'active' : ''}`}
onClick={() => setViewMode('raw')}
+ aria-pressed={viewMode === 'raw'}
>
Raw Source
@@ -204,9 +257,12 @@ export const NavigatorDocReader: React.FC = ({
)}
) : viewMode === 'rendered' && isMarkdown ? (
-
- {renderMarkdown(content)}
-
+
) : (
diff --git a/frontend/src/components/navigator/NavigatorInspector.tsx b/frontend/src/components/navigator/NavigatorInspector.tsx
index 68d837c..f5ef56d 100644
--- a/frontend/src/components/navigator/NavigatorInspector.tsx
+++ b/frontend/src/components/navigator/NavigatorInspector.tsx
@@ -35,16 +35,27 @@ export const NavigatorInspector: React.FC = ({
loading = false,
onRefreshContent,
}) => {
- const [internalTab, setInternalTab] = useState<'intelligence' | 'reader'>('intelligence');
+ const [internalTab, setInternalTab] = useState<'intelligence' | 'reader'>(
+ impact?.symbol || loading ? 'intelligence' : (fileContent ? 'reader' : 'intelligence')
+ );
const [copied, setCopied] = useState(false);
const currentTab = activeInspectorTab !== undefined ? activeInspectorTab : internalTab;
const setTab = onChangeInspectorTab || setInternalTab;
+ const symbol = impact?.symbol;
+ const route = impact?.route;
+ const callers = impact?.callers || [];
+ const callees = impact?.callees || [];
+ const imports = impact?.imports || [];
+
+ const hasFile = !!(fileContent || loadingContent || contentError);
+ const showDocReader = currentTab === 'reader';
+
const handleCopyPermalink = async () => {
if (!impact?.symbol) return;
const { filepath, start_line, end_line } = impact.symbol;
- const permalink = `${filepath}#L${start_line}${end_line && end_line !== start_line ? `-L${end_line}` : ''}`;
+ const permalink = `${window.location.origin}${window.location.pathname}?repo=${encodeURIComponent(impact.symbol.repo || '')}&file=${filepath}#L${start_line}-L${end_line}`;
try {
if (navigator?.clipboard?.writeText) {
await navigator.clipboard.writeText(permalink);
@@ -56,50 +67,48 @@ export const NavigatorInspector: React.FC = ({
}
};
- const symbol = impact?.symbol;
- const route = impact?.route;
- const callers = impact?.callers || [];
- const callees = impact?.callees || [];
- const imports = impact?.imports || [];
-
- const showDocReader = (!symbol && (fileContent || loadingContent || contentError)) || (symbol && currentTab === 'reader');
-
return (
-
+ {/* Stable tabs — always rendered when a file is open or impact is present */}
- {symbol && fileContent && (
+ {(hasFile || symbol) && (
)}
@@ -134,6 +143,7 @@ export const NavigatorInspector: React.FC = ({
+ {/* File/Reader tab */}
{showDocReader && fileContent ? (
(fileContent.filepath.toLowerCase().endsWith('.md') || fileContent.filepath.toLowerCase().endsWith('.markdown')) ? (
= ({
+ ) : showDocReader ? (
+ /* reader tab but no file yet */
+
+
+ No File or Symbol Selected
+ Select a file from the tree to read its documentation or select a symbol to inspect callers and dependencies.
+
) : loading ? (
+ /* intelligence tab loading */
- ) : !impact || !symbol ? (
+ ) : !symbol ? (
+ /* intelligence tab but no symbol picked yet */
- 🔍
+
No File or Symbol Selected
Select a file from the tree to read its documentation or select a symbol to inspect callers and dependencies.
diff --git a/frontend/src/components/navigator/NavigatorOmniSearch.tsx b/frontend/src/components/navigator/NavigatorOmniSearch.tsx
index efb9c3d..009b5ea 100644
--- a/frontend/src/components/navigator/NavigatorOmniSearch.tsx
+++ b/frontend/src/components/navigator/NavigatorOmniSearch.tsx
@@ -172,7 +172,24 @@ export const NavigatorOmniSearch: React.FC = ({
aria-expanded={isOpen}
/>
- {loading && }
+ {loading && (
+
+
+
+
+ )}
{query && (
) : !outline ? (
- 📂
+
No File Selected
Select a file from the tree to inspect its symbols and routes.
) : symbols.length === 0 ? (
- 📝
- Document File Selected
-
- This file contains documentation or text content with no code AST symbols.
+
+ Document File Active
+
+ Full document is loaded in the main viewer.
{onReadDoc && (
)}
diff --git a/frontend/src/components/navigator/NavigatorToolbar.tsx b/frontend/src/components/navigator/NavigatorToolbar.tsx
index 70fb463..5cf13d8 100644
--- a/frontend/src/components/navigator/NavigatorToolbar.tsx
+++ b/frontend/src/components/navigator/NavigatorToolbar.tsx
@@ -32,7 +32,7 @@ export const NavigatorToolbar: React.FC = ({
loading = false,
}) => {
return (
-
+
|