feat: Add indexing group to CodexLens environment variable schema

- Introduced a new `indexing` group in the environment variable schema with fields for AST grep usage, static graph enablement, and relationship types.
- Updated the CodexLens configuration to support new indexing features.

feat: Enhance DashboardToolbar with session and fullscreen controls

- Added props for session sidebar visibility and fullscreen mode to the DashboardToolbar component.
- Implemented handlers for toggling session sidebar and fullscreen mode.
- Updated the toolbar layout to include session sidebar toggle and fullscreen button.

refactor: Improve TerminalGrid and TerminalPane components

- Refactored GridGroupRenderer to handle pane size changes directly via store.
- Enhanced TerminalPane to remove unused file browser logic and improve layout handling.
- Updated key generation for child panes to ensure stability.

feat: Extend CodexLens API for staged Stage-2 expansion modes

- Added support for `staged_stage2_mode` in the CodexLens API, allowing for different expansion strategies.
- Updated semantic search handlers to process new stage-2 mode parameter.
- Implemented validation and handling for new stage-2 modes in the backend.

test: Add benchmarks for staged Stage-2 modes comparison

- Created a benchmark script to compare performance and results of different staged Stage-2 modes.
- Included metrics for latency, overlap, and diversity across modes.
This commit is contained in:
catlog22
2026-02-16 12:12:38 +08:00
parent 2202c2ccfd
commit de3dd044b9
13 changed files with 674 additions and 126 deletions

View File

@@ -24,7 +24,12 @@ import {
useCodexLensLspStatus,
useCodexLensSemanticSearch,
} from '@/hooks/useCodexLens';
import type { CodexLensSearchParams, CodexLensSemanticSearchMode, CodexLensFusionStrategy } from '@/lib/api';
import type {
CodexLensSearchParams,
CodexLensSemanticSearchMode,
CodexLensFusionStrategy,
CodexLensStagedStage2Mode,
} from '@/lib/api';
import { cn } from '@/lib/utils';
type SearchType = 'search' | 'search_files' | 'symbol' | 'semantic';
@@ -40,6 +45,7 @@ export function SearchTab({ enabled }: SearchTabProps) {
const [searchMode, setSearchMode] = useState<SearchMode>('dense_rerank');
const [semanticMode, setSemanticMode] = useState<CodexLensSemanticSearchMode>('fusion');
const [fusionStrategy, setFusionStrategy] = useState<CodexLensFusionStrategy>('rrf');
const [stagedStage2Mode, setStagedStage2Mode] = useState<CodexLensStagedStage2Mode>('precomputed');
const [query, setQuery] = useState('');
const [hasSearched, setHasSearched] = useState(false);
@@ -76,6 +82,7 @@ export function SearchTab({ enabled }: SearchTabProps) {
query,
mode: semanticMode,
fusion_strategy: semanticMode === 'fusion' ? fusionStrategy : undefined,
staged_stage2_mode: semanticMode === 'fusion' && fusionStrategy === 'staged' ? stagedStage2Mode : undefined,
limit: 20,
include_match_reason: true,
},
@@ -123,6 +130,11 @@ export function SearchTab({ enabled }: SearchTabProps) {
setHasSearched(false);
};
const handleStagedStage2ModeChange = (value: CodexLensStagedStage2Mode) => {
setStagedStage2Mode(value);
setHasSearched(false);
};
const handleQueryChange = (value: string) => {
setQuery(value);
setHasSearched(false);
@@ -308,6 +320,29 @@ export function SearchTab({ enabled }: SearchTabProps) {
</div>
)}
{/* Staged Stage-2 Mode - only when semantic + fusion + staged */}
{searchType === 'semantic' && semanticMode === 'fusion' && fusionStrategy === 'staged' && (
<div className="space-y-2">
<Label>{formatMessage({ id: 'codexlens.search.stagedStage2Mode' })}</Label>
<Select value={stagedStage2Mode} onValueChange={handleStagedStage2ModeChange}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="precomputed">
{formatMessage({ id: 'codexlens.search.stagedStage2Mode.precomputed' })}
</SelectItem>
<SelectItem value="realtime">
{formatMessage({ id: 'codexlens.search.stagedStage2Mode.realtime' })}
</SelectItem>
<SelectItem value="static_global_graph">
{formatMessage({ id: 'codexlens.search.stagedStage2Mode.static_global_graph' })}
</SelectItem>
</SelectContent>
</Select>
</div>
)}
{/* Query Input */}
<div className="space-y-2">
<Label htmlFor="search-query">{formatMessage({ id: 'codexlens.search.query' })}</Label>

View File

@@ -2,7 +2,7 @@
// CodexLens Environment Variable Schema
// ========================================
// TypeScript port of ENV_VAR_GROUPS from codexlens-manager.js
// Defines the 5 structured groups: embedding, reranker, concurrency, cascade, chunking
// Defines structured groups for CodexLens configuration
import type { EnvVarGroupsSchema } from '@/types/codexlens';
@@ -306,6 +306,36 @@ export const envVarGroupsSchema: EnvVarGroupsSchema = {
},
},
},
indexing: {
id: 'indexing',
labelKey: 'codexlens.envGroup.indexing',
icon: 'git-branch',
vars: {
CODEXLENS_USE_ASTGREP: {
key: 'CODEXLENS_USE_ASTGREP',
labelKey: 'codexlens.envField.useAstGrep',
type: 'checkbox',
default: 'false',
settingsPath: 'parsing.use_astgrep',
},
CODEXLENS_STATIC_GRAPH_ENABLED: {
key: 'CODEXLENS_STATIC_GRAPH_ENABLED',
labelKey: 'codexlens.envField.staticGraphEnabled',
type: 'checkbox',
default: 'false',
settingsPath: 'indexing.static_graph_enabled',
},
CODEXLENS_STATIC_GRAPH_RELATIONSHIP_TYPES: {
key: 'CODEXLENS_STATIC_GRAPH_RELATIONSHIP_TYPES',
labelKey: 'codexlens.envField.staticGraphRelationshipTypes',
type: 'text',
placeholder: 'imports,inherits,calls',
default: 'imports,inherits',
settingsPath: 'indexing.static_graph_relationship_types',
showWhen: (env) => env['CODEXLENS_STATIC_GRAPH_ENABLED'] === 'true',
},
},
},
chunking: {
id: 'chunking',
labelKey: 'codexlens.envGroup.chunking',

View File

@@ -21,6 +21,9 @@ import {
Zap,
Settings,
Loader2,
Folder,
Maximize2,
Minimize2,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/Badge';
@@ -57,6 +60,14 @@ interface DashboardToolbarProps {
isFileSidebarOpen?: boolean;
/** Callback to toggle file sidebar */
onToggleFileSidebar?: () => void;
/** Whether the session sidebar is open */
isSessionSidebarOpen?: boolean;
/** Callback to toggle session sidebar */
onToggleSessionSidebar?: () => void;
/** Whether fullscreen mode is active */
isFullscreen?: boolean;
/** Callback to toggle fullscreen mode */
onToggleFullscreen?: () => void;
}
// ========== Layout Presets ==========
@@ -83,7 +94,7 @@ const LAUNCH_COMMANDS: Record<CliTool, Record<LaunchMode, string>> = {
// ========== Component ==========
export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen, onToggleFileSidebar }: DashboardToolbarProps) {
export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen, onToggleFileSidebar, isSessionSidebarOpen, onToggleSessionSidebar, isFullscreen, onToggleFullscreen }: DashboardToolbarProps) {
const { formatMessage } = useIntl();
// Issues count
@@ -117,17 +128,30 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
// Launch CLI handlers
const projectPath = useWorkflowStore(selectProjectPath);
const focusedPaneId = useTerminalGridStore(selectTerminalGridFocusedPaneId);
const panes = useTerminalGridStore((s) => s.panes);
const createSessionAndAssign = useTerminalGridStore((s) => s.createSessionAndAssign);
const [isCreating, setIsCreating] = useState(false);
const [selectedTool, setSelectedTool] = useState<CliTool>('gemini');
const [launchMode, setLaunchMode] = useState<LaunchMode>('yolo');
const [isConfigOpen, setIsConfigOpen] = useState(false);
// Helper to get or create a focused pane
const getOrCreateFocusedPane = useCallback(() => {
if (focusedPaneId) return focusedPaneId;
// No focused pane - reset layout to create a single pane
resetLayout('single');
// Get the new focused pane id from store
return useTerminalGridStore.getState().focusedPaneId;
}, [focusedPaneId]);
const handleQuickCreate = useCallback(async () => {
if (!focusedPaneId || !projectPath) return;
if (!projectPath) return;
setIsCreating(true);
try {
const created = await createSessionAndAssign(focusedPaneId, {
const targetPaneId = getOrCreateFocusedPane();
if (!targetPaneId) return;
const created = await createSessionAndAssign(targetPaneId, {
workingDir: projectPath,
preferredShell: 'bash',
tool: selectedTool,
@@ -146,18 +170,21 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
} finally {
setIsCreating(false);
}
}, [focusedPaneId, projectPath, createSessionAndAssign, selectedTool, launchMode]);
}, [projectPath, createSessionAndAssign, selectedTool, launchMode, getOrCreateFocusedPane]);
const handleConfigure = useCallback(() => {
setIsConfigOpen(true);
}, []);
const handleCreateConfiguredSession = useCallback(async (config: CliSessionConfig) => {
if (!focusedPaneId || !projectPath) throw new Error('No focused pane or project path');
if (!projectPath) throw new Error('No project path');
setIsCreating(true);
try {
const targetPaneId = getOrCreateFocusedPane();
if (!targetPaneId) throw new Error('Failed to create pane');
const created = await createSessionAndAssign(
focusedPaneId,
targetPaneId,
{
workingDir: config.workingDir || projectPath,
preferredShell: config.preferredShell,
@@ -182,7 +209,7 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
} finally {
setIsCreating(false);
}
}, [focusedPaneId, projectPath, createSessionAndAssign]);
}, [projectPath, createSessionAndAssign, getOrCreateFocusedPane]);
return (
<>
@@ -254,7 +281,7 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleQuickCreate}
disabled={isCreating || !projectPath || !focusedPaneId}
disabled={isCreating || !projectPath}
className="gap-2"
>
<Zap className="w-4 h-4" />
@@ -263,7 +290,7 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleConfigure}
disabled={isCreating || !projectPath || !focusedPaneId}
disabled={isCreating || !projectPath}
className="gap-2"
>
<Settings className="w-4 h-4" />
@@ -275,6 +302,17 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
{/* Separator */}
<div className="w-px h-5 bg-border mx-1" />
{/* Session sidebar toggle */}
<ToolbarButton
icon={Folder}
label={formatMessage({ id: 'terminalDashboard.toolbar.sessions', defaultMessage: 'Sessions' })}
isActive={isSessionSidebarOpen ?? true}
onClick={() => onToggleSessionSidebar?.()}
/>
{/* Separator */}
<div className="w-px h-5 bg-border mx-1" />
{/* Panel toggle buttons */}
<ToolbarButton
icon={AlertCircle}
@@ -322,6 +360,30 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
</button>
))}
{/* Separator */}
<div className="w-px h-5 bg-border mx-1" />
{/* Fullscreen toggle */}
<button
onClick={onToggleFullscreen}
className={cn(
'p-1.5 rounded transition-colors',
isFullscreen
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
)}
title={isFullscreen
? formatMessage({ id: 'terminalDashboard.toolbar.exitFullscreen', defaultMessage: 'Exit Fullscreen' })
: formatMessage({ id: 'terminalDashboard.toolbar.fullscreen', defaultMessage: 'Fullscreen' })
}
>
{isFullscreen ? (
<Minimize2 className="w-3.5 h-3.5" />
) : (
<Maximize2 className="w-3.5 h-3.5" />
)}
</button>
{/* Right-aligned title */}
<span className="ml-auto text-xs text-muted-foreground font-medium">
{formatMessage({ id: 'terminalDashboard.page.title' })}

View File

@@ -23,19 +23,20 @@ import { TerminalPane } from './TerminalPane';
interface GridGroupRendererProps {
group: AllotmentLayoutGroup;
minSize: number;
onSizeChange: (sizes: number[]) => void;
depth?: number;
}
// ========== Recursive Group Renderer ==========
function GridGroupRenderer({ group, minSize, onSizeChange }: GridGroupRendererProps) {
function GridGroupRenderer({ group, minSize, depth = 0 }: GridGroupRendererProps) {
const panes = useTerminalGridStore(selectTerminalGridPanes);
const updateLayoutSizes = useTerminalGridStore((s) => s.updateLayoutSizes);
const handleChange = useCallback(
(sizes: number[]) => {
onSizeChange(sizes);
updateLayoutSizes(sizes);
},
[onSizeChange]
[updateLayoutSizes]
);
const validChildren = useMemo(() => {
@@ -51,22 +52,27 @@ function GridGroupRenderer({ group, minSize, onSizeChange }: GridGroupRendererPr
return null;
}
// Generate stable key based on children
const groupKey = useMemo(() => {
return validChildren.map(c => isPaneId(c) ? c : 'group').join('-');
}, [validChildren]);
return (
<Allotment
key={groupKey}
vertical={group.direction === 'vertical'}
defaultSizes={group.sizes}
onChange={handleChange}
className="h-full"
>
{validChildren.map((child, index) => (
<Allotment.Pane key={isPaneId(child) ? child : `group-${index}`} minSize={minSize}>
<Allotment.Pane key={isPaneId(child) ? child : `group-${depth}-${index}`} minSize={minSize}>
{isPaneId(child) ? (
<TerminalPane paneId={child} />
) : (
<GridGroupRenderer
group={child}
minSize={minSize}
onSizeChange={onSizeChange}
depth={depth + 1}
/>
)}
</Allotment.Pane>
@@ -80,14 +86,6 @@ function GridGroupRenderer({ group, minSize, onSizeChange }: GridGroupRendererPr
export function TerminalGrid({ className }: { className?: string }) {
const layout = useTerminalGridStore(selectTerminalGridLayout);
const panes = useTerminalGridStore(selectTerminalGridPanes);
const updateLayoutSizes = useTerminalGridStore((s) => s.updateLayoutSizes);
const handleSizeChange = useCallback(
(sizes: number[]) => {
updateLayoutSizes(sizes);
},
[updateLayoutSizes]
);
const content = useMemo(() => {
if (!layout.children || layout.children.length === 0) {
@@ -105,10 +103,10 @@ export function TerminalGrid({ className }: { className?: string }) {
<GridGroupRenderer
group={layout}
minSize={150}
onSizeChange={handleSizeChange}
depth={0}
/>
);
}, [layout, panes, handleSizeChange]);
}, [layout, panes]);
return (
<div className={cn('h-full w-full overflow-hidden bg-background', className)}>

View File

@@ -4,13 +4,13 @@
// Single terminal pane = PaneToolbar + content area.
// Content can be terminal output or file preview based on displayMode.
// Renders within the TerminalGrid recursive layout.
// File preview is triggered from right sidebar FileSidebarPanel.
import { useCallback, useMemo, useState } from 'react';
import { useIntl } from 'react-intl';
import {
SplitSquareHorizontal,
SplitSquareVertical,
FolderOpen,
Eraser,
AlertTriangle,
X,
@@ -25,7 +25,6 @@ import {
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { TerminalInstance } from './TerminalInstance';
import { FloatingFileBrowser } from './FloatingFileBrowser';
import { FilePreview } from '@/components/shared/FilePreview';
import {
useTerminalGridStore,
@@ -43,7 +42,6 @@ import {
} from '@/stores/issueQueueIntegrationStore';
import { useCliSessionStore } from '@/stores/cliSessionStore';
import { getAllPaneIds } from '@/lib/layout-utils';
import { sendCliSessionText } from '@/lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import { useFileContent } from '@/hooks/useFileExplorer';
import type { PaneId } from '@/stores/viewerStore';
@@ -89,8 +87,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
const isFileMode = displayMode === 'file' && filePath;
const projectPath = useWorkflowStore(selectProjectPath);
const [isFileBrowserOpen, setIsFileBrowserOpen] = useState(false);
const [initialFileBrowserPath, setInitialFileBrowserPath] = useState<string | null>(null);
// Session data
const groups = useSessionManagerStore(selectGroups);
@@ -168,25 +164,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
}
}, [paneId, sessionId, assignSession]);
const handleOpenFileBrowser = useCallback(() => {
setInitialFileBrowserPath(null);
setIsFileBrowserOpen(true);
}, []);
const handleRevealPath = useCallback((path: string) => {
setInitialFileBrowserPath(path);
setIsFileBrowserOpen(true);
}, []);
const handleInsertPath = useCallback((path: string) => {
if (!sessionId) return;
sendCliSessionText(
sessionId,
{ text: path, appendNewline: false },
projectPath ?? undefined
).catch((err) => console.error('[TerminalPane] insert path failed:', err));
}, [sessionId, projectPath]);
const handleRestart = useCallback(async () => {
if (!sessionId || isRestarting) return;
setIsRestarting(true);
@@ -229,9 +206,9 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
onClick={handleFocus}
>
{/* PaneToolbar */}
<div className="flex items-center gap-1 px-2 py-1 border-b border-border bg-muted/30 shrink-0">
<div className="flex items-center justify-between gap-1 px-2 py-1 border-b border-border bg-muted/30 shrink-0 overflow-hidden">
{/* Left: Session selector + status (or file path in file mode) */}
<div className="flex items-center gap-1.5 min-w-0 flex-1">
<div className="flex items-center gap-1.5 min-w-0 overflow-hidden">
{isFileMode ? (
// File mode header
<>
@@ -255,13 +232,13 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
className={cn('w-2 h-2 rounded-full shrink-0', statusDotStyles[status])}
/>
)}
<div className="relative min-w-0 flex-1 max-w-[180px]">
<div className="relative min-w-0 overflow-hidden">
<select
value={sessionId ?? ''}
onChange={handleSessionChange}
className={cn(
'w-full text-xs bg-transparent border-none outline-none cursor-pointer',
'appearance-none pr-5 truncate',
'text-xs bg-transparent border-none outline-none cursor-pointer',
'appearance-none pr-4 max-w-[140px] truncate',
!sessionId && 'text-muted-foreground'
)}
>
@@ -282,7 +259,7 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
{/* Center: Linked issue badge */}
{linkedIssueId && !isFileMode && (
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-primary/10 text-primary shrink-0">
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-primary/10 text-primary shrink-0 hidden sm:inline-flex">
{linkedIssueId}
</span>
)}
@@ -357,21 +334,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
</button>
</>
)}
{!isFileMode && (
<button
onClick={handleOpenFileBrowser}
disabled={!projectPath}
className={cn(
'p-1 rounded hover:bg-muted transition-colors',
projectPath
? 'text-muted-foreground hover:text-foreground'
: 'text-muted-foreground/40 cursor-not-allowed'
)}
title={formatMessage({ id: 'terminalDashboard.fileBrowser.open' })}
>
<FolderOpen className="w-3.5 h-3.5" />
</button>
)}
{alertCount > 0 && !isFileMode && (
<span className="flex items-center gap-0.5 px-1 text-destructive">
<AlertTriangle className="w-3 h-3" />
@@ -406,7 +368,7 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
) : sessionId ? (
// Terminal mode with session
<div className="flex-1 min-h-0">
<TerminalInstance sessionId={sessionId} onRevealPath={handleRevealPath} />
<TerminalInstance sessionId={sessionId} />
</div>
) : (
// Empty terminal state
@@ -422,17 +384,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
</div>
</div>
)}
<FloatingFileBrowser
isOpen={isFileBrowserOpen}
onClose={() => {
setIsFileBrowserOpen(false);
setInitialFileBrowserPath(null);
}}
rootPath={projectPath ?? '/'}
onInsertPath={sessionId ? handleInsertPath : undefined}
initialSelectedPath={initialFileBrowserPath}
/>
</div>
);
}