mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
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:
@@ -24,7 +24,12 @@ import {
|
|||||||
useCodexLensLspStatus,
|
useCodexLensLspStatus,
|
||||||
useCodexLensSemanticSearch,
|
useCodexLensSemanticSearch,
|
||||||
} from '@/hooks/useCodexLens';
|
} 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';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
type SearchType = 'search' | 'search_files' | 'symbol' | 'semantic';
|
type SearchType = 'search' | 'search_files' | 'symbol' | 'semantic';
|
||||||
@@ -40,6 +45,7 @@ export function SearchTab({ enabled }: SearchTabProps) {
|
|||||||
const [searchMode, setSearchMode] = useState<SearchMode>('dense_rerank');
|
const [searchMode, setSearchMode] = useState<SearchMode>('dense_rerank');
|
||||||
const [semanticMode, setSemanticMode] = useState<CodexLensSemanticSearchMode>('fusion');
|
const [semanticMode, setSemanticMode] = useState<CodexLensSemanticSearchMode>('fusion');
|
||||||
const [fusionStrategy, setFusionStrategy] = useState<CodexLensFusionStrategy>('rrf');
|
const [fusionStrategy, setFusionStrategy] = useState<CodexLensFusionStrategy>('rrf');
|
||||||
|
const [stagedStage2Mode, setStagedStage2Mode] = useState<CodexLensStagedStage2Mode>('precomputed');
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [hasSearched, setHasSearched] = useState(false);
|
const [hasSearched, setHasSearched] = useState(false);
|
||||||
|
|
||||||
@@ -76,6 +82,7 @@ export function SearchTab({ enabled }: SearchTabProps) {
|
|||||||
query,
|
query,
|
||||||
mode: semanticMode,
|
mode: semanticMode,
|
||||||
fusion_strategy: semanticMode === 'fusion' ? fusionStrategy : undefined,
|
fusion_strategy: semanticMode === 'fusion' ? fusionStrategy : undefined,
|
||||||
|
staged_stage2_mode: semanticMode === 'fusion' && fusionStrategy === 'staged' ? stagedStage2Mode : undefined,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
include_match_reason: true,
|
include_match_reason: true,
|
||||||
},
|
},
|
||||||
@@ -123,6 +130,11 @@ export function SearchTab({ enabled }: SearchTabProps) {
|
|||||||
setHasSearched(false);
|
setHasSearched(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStagedStage2ModeChange = (value: CodexLensStagedStage2Mode) => {
|
||||||
|
setStagedStage2Mode(value);
|
||||||
|
setHasSearched(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleQueryChange = (value: string) => {
|
const handleQueryChange = (value: string) => {
|
||||||
setQuery(value);
|
setQuery(value);
|
||||||
setHasSearched(false);
|
setHasSearched(false);
|
||||||
@@ -308,6 +320,29 @@ export function SearchTab({ enabled }: SearchTabProps) {
|
|||||||
</div>
|
</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 */}
|
{/* Query Input */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="search-query">{formatMessage({ id: 'codexlens.search.query' })}</Label>
|
<Label htmlFor="search-query">{formatMessage({ id: 'codexlens.search.query' })}</Label>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// CodexLens Environment Variable Schema
|
// CodexLens Environment Variable Schema
|
||||||
// ========================================
|
// ========================================
|
||||||
// TypeScript port of ENV_VAR_GROUPS from codexlens-manager.js
|
// 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';
|
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: {
|
chunking: {
|
||||||
id: 'chunking',
|
id: 'chunking',
|
||||||
labelKey: 'codexlens.envGroup.chunking',
|
labelKey: 'codexlens.envGroup.chunking',
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
Settings,
|
Settings,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Folder,
|
||||||
|
Maximize2,
|
||||||
|
Minimize2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
@@ -57,6 +60,14 @@ interface DashboardToolbarProps {
|
|||||||
isFileSidebarOpen?: boolean;
|
isFileSidebarOpen?: boolean;
|
||||||
/** Callback to toggle file sidebar */
|
/** Callback to toggle file sidebar */
|
||||||
onToggleFileSidebar?: () => void;
|
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 ==========
|
// ========== Layout Presets ==========
|
||||||
@@ -83,7 +94,7 @@ const LAUNCH_COMMANDS: Record<CliTool, Record<LaunchMode, string>> = {
|
|||||||
|
|
||||||
// ========== Component ==========
|
// ========== Component ==========
|
||||||
|
|
||||||
export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen, onToggleFileSidebar }: DashboardToolbarProps) {
|
export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen, onToggleFileSidebar, isSessionSidebarOpen, onToggleSessionSidebar, isFullscreen, onToggleFullscreen }: DashboardToolbarProps) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
// Issues count
|
// Issues count
|
||||||
@@ -117,17 +128,30 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
|||||||
// Launch CLI handlers
|
// Launch CLI handlers
|
||||||
const projectPath = useWorkflowStore(selectProjectPath);
|
const projectPath = useWorkflowStore(selectProjectPath);
|
||||||
const focusedPaneId = useTerminalGridStore(selectTerminalGridFocusedPaneId);
|
const focusedPaneId = useTerminalGridStore(selectTerminalGridFocusedPaneId);
|
||||||
|
const panes = useTerminalGridStore((s) => s.panes);
|
||||||
const createSessionAndAssign = useTerminalGridStore((s) => s.createSessionAndAssign);
|
const createSessionAndAssign = useTerminalGridStore((s) => s.createSessionAndAssign);
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [selectedTool, setSelectedTool] = useState<CliTool>('gemini');
|
const [selectedTool, setSelectedTool] = useState<CliTool>('gemini');
|
||||||
const [launchMode, setLaunchMode] = useState<LaunchMode>('yolo');
|
const [launchMode, setLaunchMode] = useState<LaunchMode>('yolo');
|
||||||
const [isConfigOpen, setIsConfigOpen] = useState(false);
|
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 () => {
|
const handleQuickCreate = useCallback(async () => {
|
||||||
if (!focusedPaneId || !projectPath) return;
|
if (!projectPath) return;
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
try {
|
try {
|
||||||
const created = await createSessionAndAssign(focusedPaneId, {
|
const targetPaneId = getOrCreateFocusedPane();
|
||||||
|
if (!targetPaneId) return;
|
||||||
|
|
||||||
|
const created = await createSessionAndAssign(targetPaneId, {
|
||||||
workingDir: projectPath,
|
workingDir: projectPath,
|
||||||
preferredShell: 'bash',
|
preferredShell: 'bash',
|
||||||
tool: selectedTool,
|
tool: selectedTool,
|
||||||
@@ -146,18 +170,21 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
|||||||
} finally {
|
} finally {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
}
|
}
|
||||||
}, [focusedPaneId, projectPath, createSessionAndAssign, selectedTool, launchMode]);
|
}, [projectPath, createSessionAndAssign, selectedTool, launchMode, getOrCreateFocusedPane]);
|
||||||
|
|
||||||
const handleConfigure = useCallback(() => {
|
const handleConfigure = useCallback(() => {
|
||||||
setIsConfigOpen(true);
|
setIsConfigOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleCreateConfiguredSession = useCallback(async (config: CliSessionConfig) => {
|
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);
|
setIsCreating(true);
|
||||||
try {
|
try {
|
||||||
|
const targetPaneId = getOrCreateFocusedPane();
|
||||||
|
if (!targetPaneId) throw new Error('Failed to create pane');
|
||||||
|
|
||||||
const created = await createSessionAndAssign(
|
const created = await createSessionAndAssign(
|
||||||
focusedPaneId,
|
targetPaneId,
|
||||||
{
|
{
|
||||||
workingDir: config.workingDir || projectPath,
|
workingDir: config.workingDir || projectPath,
|
||||||
preferredShell: config.preferredShell,
|
preferredShell: config.preferredShell,
|
||||||
@@ -182,7 +209,7 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
|||||||
} finally {
|
} finally {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
}
|
}
|
||||||
}, [focusedPaneId, projectPath, createSessionAndAssign]);
|
}, [projectPath, createSessionAndAssign, getOrCreateFocusedPane]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -254,7 +281,7 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={handleQuickCreate}
|
onClick={handleQuickCreate}
|
||||||
disabled={isCreating || !projectPath || !focusedPaneId}
|
disabled={isCreating || !projectPath}
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<Zap className="w-4 h-4" />
|
<Zap className="w-4 h-4" />
|
||||||
@@ -263,7 +290,7 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={handleConfigure}
|
onClick={handleConfigure}
|
||||||
disabled={isCreating || !projectPath || !focusedPaneId}
|
disabled={isCreating || !projectPath}
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<Settings className="w-4 h-4" />
|
<Settings className="w-4 h-4" />
|
||||||
@@ -275,6 +302,17 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
|||||||
{/* Separator */}
|
{/* Separator */}
|
||||||
<div className="w-px h-5 bg-border mx-1" />
|
<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 */}
|
{/* Panel toggle buttons */}
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={AlertCircle}
|
icon={AlertCircle}
|
||||||
@@ -322,6 +360,30 @@ export function DashboardToolbar({ activePanel, onTogglePanel, isFileSidebarOpen
|
|||||||
</button>
|
</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 */}
|
{/* Right-aligned title */}
|
||||||
<span className="ml-auto text-xs text-muted-foreground font-medium">
|
<span className="ml-auto text-xs text-muted-foreground font-medium">
|
||||||
{formatMessage({ id: 'terminalDashboard.page.title' })}
|
{formatMessage({ id: 'terminalDashboard.page.title' })}
|
||||||
|
|||||||
@@ -23,19 +23,20 @@ import { TerminalPane } from './TerminalPane';
|
|||||||
interface GridGroupRendererProps {
|
interface GridGroupRendererProps {
|
||||||
group: AllotmentLayoutGroup;
|
group: AllotmentLayoutGroup;
|
||||||
minSize: number;
|
minSize: number;
|
||||||
onSizeChange: (sizes: number[]) => void;
|
depth?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Recursive Group Renderer ==========
|
// ========== Recursive Group Renderer ==========
|
||||||
|
|
||||||
function GridGroupRenderer({ group, minSize, onSizeChange }: GridGroupRendererProps) {
|
function GridGroupRenderer({ group, minSize, depth = 0 }: GridGroupRendererProps) {
|
||||||
const panes = useTerminalGridStore(selectTerminalGridPanes);
|
const panes = useTerminalGridStore(selectTerminalGridPanes);
|
||||||
|
const updateLayoutSizes = useTerminalGridStore((s) => s.updateLayoutSizes);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(sizes: number[]) => {
|
(sizes: number[]) => {
|
||||||
onSizeChange(sizes);
|
updateLayoutSizes(sizes);
|
||||||
},
|
},
|
||||||
[onSizeChange]
|
[updateLayoutSizes]
|
||||||
);
|
);
|
||||||
|
|
||||||
const validChildren = useMemo(() => {
|
const validChildren = useMemo(() => {
|
||||||
@@ -51,22 +52,27 @@ function GridGroupRenderer({ group, minSize, onSizeChange }: GridGroupRendererPr
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate stable key based on children
|
||||||
|
const groupKey = useMemo(() => {
|
||||||
|
return validChildren.map(c => isPaneId(c) ? c : 'group').join('-');
|
||||||
|
}, [validChildren]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Allotment
|
<Allotment
|
||||||
|
key={groupKey}
|
||||||
vertical={group.direction === 'vertical'}
|
vertical={group.direction === 'vertical'}
|
||||||
defaultSizes={group.sizes}
|
defaultSizes={group.sizes}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="h-full"
|
|
||||||
>
|
>
|
||||||
{validChildren.map((child, index) => (
|
{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) ? (
|
{isPaneId(child) ? (
|
||||||
<TerminalPane paneId={child} />
|
<TerminalPane paneId={child} />
|
||||||
) : (
|
) : (
|
||||||
<GridGroupRenderer
|
<GridGroupRenderer
|
||||||
group={child}
|
group={child}
|
||||||
minSize={minSize}
|
minSize={minSize}
|
||||||
onSizeChange={onSizeChange}
|
depth={depth + 1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
@@ -80,14 +86,6 @@ function GridGroupRenderer({ group, minSize, onSizeChange }: GridGroupRendererPr
|
|||||||
export function TerminalGrid({ className }: { className?: string }) {
|
export function TerminalGrid({ className }: { className?: string }) {
|
||||||
const layout = useTerminalGridStore(selectTerminalGridLayout);
|
const layout = useTerminalGridStore(selectTerminalGridLayout);
|
||||||
const panes = useTerminalGridStore(selectTerminalGridPanes);
|
const panes = useTerminalGridStore(selectTerminalGridPanes);
|
||||||
const updateLayoutSizes = useTerminalGridStore((s) => s.updateLayoutSizes);
|
|
||||||
|
|
||||||
const handleSizeChange = useCallback(
|
|
||||||
(sizes: number[]) => {
|
|
||||||
updateLayoutSizes(sizes);
|
|
||||||
},
|
|
||||||
[updateLayoutSizes]
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
if (!layout.children || layout.children.length === 0) {
|
if (!layout.children || layout.children.length === 0) {
|
||||||
@@ -105,10 +103,10 @@ export function TerminalGrid({ className }: { className?: string }) {
|
|||||||
<GridGroupRenderer
|
<GridGroupRenderer
|
||||||
group={layout}
|
group={layout}
|
||||||
minSize={150}
|
minSize={150}
|
||||||
onSizeChange={handleSizeChange}
|
depth={0}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [layout, panes, handleSizeChange]);
|
}, [layout, panes]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('h-full w-full overflow-hidden bg-background', className)}>
|
<div className={cn('h-full w-full overflow-hidden bg-background', className)}>
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
// Single terminal pane = PaneToolbar + content area.
|
// Single terminal pane = PaneToolbar + content area.
|
||||||
// Content can be terminal output or file preview based on displayMode.
|
// Content can be terminal output or file preview based on displayMode.
|
||||||
// Renders within the TerminalGrid recursive layout.
|
// Renders within the TerminalGrid recursive layout.
|
||||||
|
// File preview is triggered from right sidebar FileSidebarPanel.
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import {
|
import {
|
||||||
SplitSquareHorizontal,
|
SplitSquareHorizontal,
|
||||||
SplitSquareVertical,
|
SplitSquareVertical,
|
||||||
FolderOpen,
|
|
||||||
Eraser,
|
Eraser,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
X,
|
X,
|
||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { TerminalInstance } from './TerminalInstance';
|
import { TerminalInstance } from './TerminalInstance';
|
||||||
import { FloatingFileBrowser } from './FloatingFileBrowser';
|
|
||||||
import { FilePreview } from '@/components/shared/FilePreview';
|
import { FilePreview } from '@/components/shared/FilePreview';
|
||||||
import {
|
import {
|
||||||
useTerminalGridStore,
|
useTerminalGridStore,
|
||||||
@@ -43,7 +42,6 @@ import {
|
|||||||
} from '@/stores/issueQueueIntegrationStore';
|
} from '@/stores/issueQueueIntegrationStore';
|
||||||
import { useCliSessionStore } from '@/stores/cliSessionStore';
|
import { useCliSessionStore } from '@/stores/cliSessionStore';
|
||||||
import { getAllPaneIds } from '@/lib/layout-utils';
|
import { getAllPaneIds } from '@/lib/layout-utils';
|
||||||
import { sendCliSessionText } from '@/lib/api';
|
|
||||||
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
|
||||||
import { useFileContent } from '@/hooks/useFileExplorer';
|
import { useFileContent } from '@/hooks/useFileExplorer';
|
||||||
import type { PaneId } from '@/stores/viewerStore';
|
import type { PaneId } from '@/stores/viewerStore';
|
||||||
@@ -89,8 +87,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
const isFileMode = displayMode === 'file' && filePath;
|
const isFileMode = displayMode === 'file' && filePath;
|
||||||
|
|
||||||
const projectPath = useWorkflowStore(selectProjectPath);
|
const projectPath = useWorkflowStore(selectProjectPath);
|
||||||
const [isFileBrowserOpen, setIsFileBrowserOpen] = useState(false);
|
|
||||||
const [initialFileBrowserPath, setInitialFileBrowserPath] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Session data
|
// Session data
|
||||||
const groups = useSessionManagerStore(selectGroups);
|
const groups = useSessionManagerStore(selectGroups);
|
||||||
@@ -168,25 +164,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
}
|
}
|
||||||
}, [paneId, sessionId, assignSession]);
|
}, [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 () => {
|
const handleRestart = useCallback(async () => {
|
||||||
if (!sessionId || isRestarting) return;
|
if (!sessionId || isRestarting) return;
|
||||||
setIsRestarting(true);
|
setIsRestarting(true);
|
||||||
@@ -229,9 +206,9 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
onClick={handleFocus}
|
onClick={handleFocus}
|
||||||
>
|
>
|
||||||
{/* PaneToolbar */}
|
{/* 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) */}
|
{/* 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 ? (
|
{isFileMode ? (
|
||||||
// File mode header
|
// File mode header
|
||||||
<>
|
<>
|
||||||
@@ -255,13 +232,13 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
className={cn('w-2 h-2 rounded-full shrink-0', statusDotStyles[status])}
|
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
|
<select
|
||||||
value={sessionId ?? ''}
|
value={sessionId ?? ''}
|
||||||
onChange={handleSessionChange}
|
onChange={handleSessionChange}
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full text-xs bg-transparent border-none outline-none cursor-pointer',
|
'text-xs bg-transparent border-none outline-none cursor-pointer',
|
||||||
'appearance-none pr-5 truncate',
|
'appearance-none pr-4 max-w-[140px] truncate',
|
||||||
!sessionId && 'text-muted-foreground'
|
!sessionId && 'text-muted-foreground'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -282,7 +259,7 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
|
|
||||||
{/* Center: Linked issue badge */}
|
{/* Center: Linked issue badge */}
|
||||||
{linkedIssueId && !isFileMode && (
|
{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}
|
{linkedIssueId}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -357,21 +334,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
</button>
|
</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 && (
|
{alertCount > 0 && !isFileMode && (
|
||||||
<span className="flex items-center gap-0.5 px-1 text-destructive">
|
<span className="flex items-center gap-0.5 px-1 text-destructive">
|
||||||
<AlertTriangle className="w-3 h-3" />
|
<AlertTriangle className="w-3 h-3" />
|
||||||
@@ -406,7 +368,7 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
) : sessionId ? (
|
) : sessionId ? (
|
||||||
// Terminal mode with session
|
// Terminal mode with session
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
<TerminalInstance sessionId={sessionId} onRevealPath={handleRevealPath} />
|
<TerminalInstance sessionId={sessionId} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Empty terminal state
|
// Empty terminal state
|
||||||
@@ -422,17 +384,6 @@ export function TerminalPane({ paneId }: TerminalPaneProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FloatingFileBrowser
|
|
||||||
isOpen={isFileBrowserOpen}
|
|
||||||
onClose={() => {
|
|
||||||
setIsFileBrowserOpen(false);
|
|
||||||
setInitialFileBrowserPath(null);
|
|
||||||
}}
|
|
||||||
rootPath={projectPath ?? '/'}
|
|
||||||
onInsertPath={sessionId ? handleInsertPath : undefined}
|
|
||||||
initialSelectedPath={initialFileBrowserPath}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5015,12 +5015,14 @@ export interface CodexLensLspStatusResponse {
|
|||||||
*/
|
*/
|
||||||
export type CodexLensSemanticSearchMode = 'fusion' | 'vector' | 'structural';
|
export type CodexLensSemanticSearchMode = 'fusion' | 'vector' | 'structural';
|
||||||
export type CodexLensFusionStrategy = 'rrf' | 'staged' | 'binary' | 'hybrid' | 'dense_rerank';
|
export type CodexLensFusionStrategy = 'rrf' | 'staged' | 'binary' | 'hybrid' | 'dense_rerank';
|
||||||
|
export type CodexLensStagedStage2Mode = 'precomputed' | 'realtime' | 'static_global_graph';
|
||||||
|
|
||||||
export interface CodexLensSemanticSearchParams {
|
export interface CodexLensSemanticSearchParams {
|
||||||
query: string;
|
query: string;
|
||||||
path?: string;
|
path?: string;
|
||||||
mode?: CodexLensSemanticSearchMode;
|
mode?: CodexLensSemanticSearchMode;
|
||||||
fusion_strategy?: CodexLensFusionStrategy;
|
fusion_strategy?: CodexLensFusionStrategy;
|
||||||
|
staged_stage2_mode?: CodexLensStagedStage2Mode;
|
||||||
vector_weight?: number;
|
vector_weight?: number;
|
||||||
structural_weight?: number;
|
structural_weight?: number;
|
||||||
keyword_weight?: number;
|
keyword_weight?: number;
|
||||||
|
|||||||
@@ -239,6 +239,10 @@
|
|||||||
"fusionStrategy.binary": "Binary",
|
"fusionStrategy.binary": "Binary",
|
||||||
"fusionStrategy.hybrid": "Hybrid",
|
"fusionStrategy.hybrid": "Hybrid",
|
||||||
"fusionStrategy.staged": "Staged",
|
"fusionStrategy.staged": "Staged",
|
||||||
|
"stagedStage2Mode": "Staged Stage 2",
|
||||||
|
"stagedStage2Mode.precomputed": "Precomputed (graph_neighbors)",
|
||||||
|
"stagedStage2Mode.realtime": "Realtime (LSP)",
|
||||||
|
"stagedStage2Mode.static_global_graph": "Static Global Graph",
|
||||||
"lspStatus": "LSP Status",
|
"lspStatus": "LSP Status",
|
||||||
"lspAvailable": "Semantic search available",
|
"lspAvailable": "Semantic search available",
|
||||||
"lspUnavailable": "Semantic search unavailable",
|
"lspUnavailable": "Semantic search unavailable",
|
||||||
@@ -285,6 +289,7 @@
|
|||||||
"reranker": "Reranker",
|
"reranker": "Reranker",
|
||||||
"concurrency": "Concurrency",
|
"concurrency": "Concurrency",
|
||||||
"cascade": "Cascade Search",
|
"cascade": "Cascade Search",
|
||||||
|
"indexing": "Indexing",
|
||||||
"chunking": "Chunking"
|
"chunking": "Chunking"
|
||||||
},
|
},
|
||||||
"envField": {
|
"envField": {
|
||||||
@@ -305,6 +310,9 @@
|
|||||||
"searchStrategy": "Search Strategy",
|
"searchStrategy": "Search Strategy",
|
||||||
"coarseK": "Coarse K",
|
"coarseK": "Coarse K",
|
||||||
"fineK": "Fine K",
|
"fineK": "Fine K",
|
||||||
|
"useAstGrep": "Use ast-grep",
|
||||||
|
"staticGraphEnabled": "Static Graph",
|
||||||
|
"staticGraphRelationshipTypes": "Relationship Types",
|
||||||
"stripComments": "Strip Comments",
|
"stripComments": "Strip Comments",
|
||||||
"stripDocstrings": "Strip Docstrings",
|
"stripDocstrings": "Strip Docstrings",
|
||||||
"testFilePenalty": "Test File Penalty",
|
"testFilePenalty": "Test File Penalty",
|
||||||
|
|||||||
@@ -239,6 +239,10 @@
|
|||||||
"fusionStrategy.binary": "Binary",
|
"fusionStrategy.binary": "Binary",
|
||||||
"fusionStrategy.hybrid": "Hybrid",
|
"fusionStrategy.hybrid": "Hybrid",
|
||||||
"fusionStrategy.staged": "Staged",
|
"fusionStrategy.staged": "Staged",
|
||||||
|
"stagedStage2Mode": "Stage 2 扩展",
|
||||||
|
"stagedStage2Mode.precomputed": "预计算 (graph_neighbors)",
|
||||||
|
"stagedStage2Mode.realtime": "实时 (LSP)",
|
||||||
|
"stagedStage2Mode.static_global_graph": "静态全局图",
|
||||||
"lspStatus": "LSP 状态",
|
"lspStatus": "LSP 状态",
|
||||||
"lspAvailable": "语义搜索可用",
|
"lspAvailable": "语义搜索可用",
|
||||||
"lspUnavailable": "语义搜索不可用",
|
"lspUnavailable": "语义搜索不可用",
|
||||||
@@ -285,6 +289,7 @@
|
|||||||
"reranker": "重排序",
|
"reranker": "重排序",
|
||||||
"concurrency": "并发",
|
"concurrency": "并发",
|
||||||
"cascade": "级联搜索",
|
"cascade": "级联搜索",
|
||||||
|
"indexing": "索引与解析",
|
||||||
"chunking": "分块"
|
"chunking": "分块"
|
||||||
},
|
},
|
||||||
"envField": {
|
"envField": {
|
||||||
@@ -305,6 +310,9 @@
|
|||||||
"searchStrategy": "搜索策略",
|
"searchStrategy": "搜索策略",
|
||||||
"coarseK": "粗筛 K 值",
|
"coarseK": "粗筛 K 值",
|
||||||
"fineK": "精筛 K 值",
|
"fineK": "精筛 K 值",
|
||||||
|
"useAstGrep": "使用 ast-grep",
|
||||||
|
"staticGraphEnabled": "启用静态图",
|
||||||
|
"staticGraphRelationshipTypes": "关系类型",
|
||||||
"stripComments": "去除注释",
|
"stripComments": "去除注释",
|
||||||
"stripDocstrings": "去除文档字符串",
|
"stripDocstrings": "去除文档字符串",
|
||||||
"testFilePenalty": "测试文件惩罚",
|
"testFilePenalty": "测试文件惩罚",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// Right sidebar: FileSidebarPanel (file tree, resizable)
|
// Right sidebar: FileSidebarPanel (file tree, resizable)
|
||||||
// Top: DashboardToolbar with panel toggles and layout presets
|
// Top: DashboardToolbar with panel toggles and layout presets
|
||||||
// Floating panels: Issues, Queue, Inspector (overlay, mutually exclusive)
|
// Floating panels: Issues, Queue, Inspector (overlay, mutually exclusive)
|
||||||
|
// Fullscreen mode: Hides all sidebars for maximum terminal space
|
||||||
|
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
@@ -30,6 +31,8 @@ export function TerminalDashboardPage() {
|
|||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [activePanel, setActivePanel] = useState<PanelId | null>(null);
|
const [activePanel, setActivePanel] = useState<PanelId | null>(null);
|
||||||
const [isFileSidebarOpen, setIsFileSidebarOpen] = useState(true);
|
const [isFileSidebarOpen, setIsFileSidebarOpen] = useState(true);
|
||||||
|
const [isSessionSidebarOpen, setIsSessionSidebarOpen] = useState(true);
|
||||||
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
|
||||||
const projectPath = useWorkflowStore(selectProjectPath);
|
const projectPath = useWorkflowStore(selectProjectPath);
|
||||||
|
|
||||||
@@ -41,8 +44,17 @@ export function TerminalDashboardPage() {
|
|||||||
setActivePanel(null);
|
setActivePanel(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const toggleFullscreen = useCallback(() => {
|
||||||
|
setIsFullscreen((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// In fullscreen mode, hide all sidebars and panels
|
||||||
|
const showSessionSidebar = isSessionSidebarOpen && !isFullscreen;
|
||||||
|
const showFileSidebar = isFileSidebarOpen && !isFullscreen;
|
||||||
|
const showFloatingPanels = !isFullscreen;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="-m-4 md:-m-6 flex flex-col h-[calc(100vh-56px)] overflow-hidden">
|
<div className={`flex flex-col overflow-hidden ${isFullscreen ? 'h-screen -m-0' : 'h-[calc(100vh-56px)] -m-4 md:-m-6'}`}>
|
||||||
<AssociationHighlightProvider>
|
<AssociationHighlightProvider>
|
||||||
{/* Global toolbar */}
|
{/* Global toolbar */}
|
||||||
<DashboardToolbar
|
<DashboardToolbar
|
||||||
@@ -50,30 +62,36 @@ export function TerminalDashboardPage() {
|
|||||||
onTogglePanel={togglePanel}
|
onTogglePanel={togglePanel}
|
||||||
isFileSidebarOpen={isFileSidebarOpen}
|
isFileSidebarOpen={isFileSidebarOpen}
|
||||||
onToggleFileSidebar={() => setIsFileSidebarOpen((prev) => !prev)}
|
onToggleFileSidebar={() => setIsFileSidebarOpen((prev) => !prev)}
|
||||||
|
isSessionSidebarOpen={isSessionSidebarOpen}
|
||||||
|
onToggleSessionSidebar={() => setIsSessionSidebarOpen((prev) => !prev)}
|
||||||
|
isFullscreen={isFullscreen}
|
||||||
|
onToggleFullscreen={toggleFullscreen}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main content with three-column layout */}
|
{/* Main content with three-column layout */}
|
||||||
<div className="flex-1 min-h-0">
|
<div className="flex-1 min-h-0">
|
||||||
<Allotment>
|
<Allotment className="h-full">
|
||||||
{/* Fixed session sidebar (240px) */}
|
{/* Session sidebar (conditional) */}
|
||||||
<Allotment.Pane preferredSize={240} minSize={180} maxSize={320}>
|
{showSessionSidebar && (
|
||||||
<div className="h-full flex flex-col border-r border-border">
|
<Allotment.Pane preferredSize={240} minSize={180} maxSize={320}>
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
<div className="h-full flex flex-col border-r border-border">
|
||||||
<SessionGroupTree />
|
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||||
|
<SessionGroupTree />
|
||||||
|
</div>
|
||||||
|
<div className="shrink-0">
|
||||||
|
<AgentList />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">
|
</Allotment.Pane>
|
||||||
<AgentList />
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Allotment.Pane>
|
|
||||||
|
|
||||||
{/* Terminal grid (flexible) */}
|
{/* Terminal grid (flexible) */}
|
||||||
<Allotment.Pane minSize={300}>
|
<Allotment.Pane preferredSize={-1} minSize={300}>
|
||||||
<TerminalGrid />
|
<TerminalGrid />
|
||||||
</Allotment.Pane>
|
</Allotment.Pane>
|
||||||
|
|
||||||
{/* File sidebar (conditional, default 280px) */}
|
{/* File sidebar (conditional, default 280px) */}
|
||||||
{isFileSidebarOpen && (
|
{showFileSidebar && (
|
||||||
<Allotment.Pane preferredSize={280} minSize={200} maxSize={400}>
|
<Allotment.Pane preferredSize={280} minSize={200} maxSize={400}>
|
||||||
<FileSidebarPanel
|
<FileSidebarPanel
|
||||||
rootPath={projectPath ?? '/'}
|
rootPath={projectPath ?? '/'}
|
||||||
@@ -86,35 +104,39 @@ export function TerminalDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating panels (conditional, overlay) */}
|
{/* Floating panels (conditional, overlay) */}
|
||||||
<FloatingPanel
|
{showFloatingPanels && (
|
||||||
isOpen={activePanel === 'issues'}
|
<>
|
||||||
onClose={closePanel}
|
<FloatingPanel
|
||||||
title={formatMessage({ id: 'terminalDashboard.toolbar.issues' })}
|
isOpen={activePanel === 'issues'}
|
||||||
side="left"
|
onClose={closePanel}
|
||||||
width={380}
|
title={formatMessage({ id: 'terminalDashboard.toolbar.issues' })}
|
||||||
>
|
side="left"
|
||||||
<IssuePanel />
|
width={380}
|
||||||
</FloatingPanel>
|
>
|
||||||
|
<IssuePanel />
|
||||||
|
</FloatingPanel>
|
||||||
|
|
||||||
<FloatingPanel
|
<FloatingPanel
|
||||||
isOpen={activePanel === 'queue'}
|
isOpen={activePanel === 'queue'}
|
||||||
onClose={closePanel}
|
onClose={closePanel}
|
||||||
title={formatMessage({ id: 'terminalDashboard.toolbar.queue' })}
|
title={formatMessage({ id: 'terminalDashboard.toolbar.queue' })}
|
||||||
side="right"
|
side="right"
|
||||||
width={400}
|
width={400}
|
||||||
>
|
>
|
||||||
<QueuePanel />
|
<QueuePanel />
|
||||||
</FloatingPanel>
|
</FloatingPanel>
|
||||||
|
|
||||||
<FloatingPanel
|
<FloatingPanel
|
||||||
isOpen={activePanel === 'inspector'}
|
isOpen={activePanel === 'inspector'}
|
||||||
onClose={closePanel}
|
onClose={closePanel}
|
||||||
title={formatMessage({ id: 'terminalDashboard.toolbar.inspector' })}
|
title={formatMessage({ id: 'terminalDashboard.toolbar.inspector' })}
|
||||||
side="right"
|
side="right"
|
||||||
width={360}
|
width={360}
|
||||||
>
|
>
|
||||||
<InspectorContent />
|
<InspectorContent />
|
||||||
</FloatingPanel>
|
</FloatingPanel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</AssociationHighlightProvider>
|
</AssociationHighlightProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1187,6 +1187,7 @@ except Exception as e:
|
|||||||
path: projectPath,
|
path: projectPath,
|
||||||
mode = 'fusion',
|
mode = 'fusion',
|
||||||
fusion_strategy = 'rrf',
|
fusion_strategy = 'rrf',
|
||||||
|
staged_stage2_mode,
|
||||||
vector_weight = 0.5,
|
vector_weight = 0.5,
|
||||||
structural_weight = 0.3,
|
structural_weight = 0.3,
|
||||||
keyword_weight = 0.2,
|
keyword_weight = 0.2,
|
||||||
@@ -1198,6 +1199,7 @@ except Exception as e:
|
|||||||
path?: unknown;
|
path?: unknown;
|
||||||
mode?: unknown;
|
mode?: unknown;
|
||||||
fusion_strategy?: unknown;
|
fusion_strategy?: unknown;
|
||||||
|
staged_stage2_mode?: unknown;
|
||||||
vector_weight?: unknown;
|
vector_weight?: unknown;
|
||||||
structural_weight?: unknown;
|
structural_weight?: unknown;
|
||||||
keyword_weight?: unknown;
|
keyword_weight?: unknown;
|
||||||
@@ -1215,6 +1217,23 @@ except Exception as e:
|
|||||||
const resolvedMode = typeof mode === 'string' && ['fusion', 'vector', 'structural'].includes(mode) ? mode : 'fusion';
|
const resolvedMode = typeof mode === 'string' && ['fusion', 'vector', 'structural'].includes(mode) ? mode : 'fusion';
|
||||||
const resolvedStrategy = typeof fusion_strategy === 'string' &&
|
const resolvedStrategy = typeof fusion_strategy === 'string' &&
|
||||||
['rrf', 'staged', 'binary', 'hybrid', 'dense_rerank'].includes(fusion_strategy) ? fusion_strategy : 'rrf';
|
['rrf', 'staged', 'binary', 'hybrid', 'dense_rerank'].includes(fusion_strategy) ? fusion_strategy : 'rrf';
|
||||||
|
let resolvedStage2Mode: 'precomputed' | 'realtime' | 'static_global_graph' | undefined;
|
||||||
|
if (resolvedStrategy === 'staged' && typeof staged_stage2_mode === 'string') {
|
||||||
|
const stage2 = staged_stage2_mode.trim().toLowerCase();
|
||||||
|
if (stage2.length > 0) {
|
||||||
|
if (stage2 === 'live') {
|
||||||
|
resolvedStage2Mode = 'realtime';
|
||||||
|
} else if (stage2 === 'precomputed' || stage2 === 'realtime' || stage2 === 'static_global_graph') {
|
||||||
|
resolvedStage2Mode = stage2;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Invalid staged_stage2_mode: ${stage2}. Must be one of: precomputed, realtime, static_global_graph`,
|
||||||
|
status: 400,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const resolvedVectorWeight = typeof vector_weight === 'number' ? vector_weight : 0.5;
|
const resolvedVectorWeight = typeof vector_weight === 'number' ? vector_weight : 0.5;
|
||||||
const resolvedStructuralWeight = typeof structural_weight === 'number' ? structural_weight : 0.3;
|
const resolvedStructuralWeight = typeof structural_weight === 'number' ? structural_weight : 0.3;
|
||||||
const resolvedKeywordWeight = typeof keyword_weight === 'number' ? keyword_weight : 0.2;
|
const resolvedKeywordWeight = typeof keyword_weight === 'number' ? keyword_weight : 0.2;
|
||||||
@@ -1234,6 +1253,10 @@ except Exception as e:
|
|||||||
include_match_reason: resolvedIncludeReason,
|
include_match_reason: resolvedIncludeReason,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (resolvedStage2Mode) {
|
||||||
|
apiArgs.staged_stage2_mode = resolvedStage2Mode;
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(kind_filter) && kind_filter.length > 0) {
|
if (Array.isArray(kind_filter) && kind_filter.length > 0) {
|
||||||
apiArgs.kind_filter = kind_filter;
|
apiArgs.kind_filter = kind_filter;
|
||||||
}
|
}
|
||||||
|
|||||||
391
codex-lens/benchmarks/compare_staged_stage2_modes.py
Normal file
391
codex-lens/benchmarks/compare_staged_stage2_modes.py
Normal file
@@ -0,0 +1,391 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Compare staged cascade Stage-2 modes (precomputed vs realtime vs static graph).
|
||||||
|
|
||||||
|
This benchmark compares the *same* staged cascade strategy with different Stage-2
|
||||||
|
expansion sources:
|
||||||
|
|
||||||
|
1) precomputed: per-dir `graph_neighbors` expansion (fast, index-local)
|
||||||
|
2) realtime: live LSP graph expansion (contextual, requires LSP availability)
|
||||||
|
3) static_global_graph: global_relationships expansion (project-wide, requires static graph indexing)
|
||||||
|
|
||||||
|
Because most repos do not have ground-truth labels, this script reports:
|
||||||
|
- latency statistics per mode
|
||||||
|
- top-k overlap metrics (Jaccard + RBO) between modes
|
||||||
|
- diversity proxies (unique files/dirs)
|
||||||
|
- staged pipeline stage stats (when present)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python benchmarks/compare_staged_stage2_modes.py --source ./src
|
||||||
|
python benchmarks/compare_staged_stage2_modes.py --queries-file benchmarks/queries.txt
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import gc
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import statistics
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
||||||
|
|
||||||
|
# Add src to path (match other benchmark scripts)
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
||||||
|
|
||||||
|
from codexlens.config import Config
|
||||||
|
from codexlens.search.chain_search import ChainSearchEngine
|
||||||
|
from codexlens.storage.path_mapper import PathMapper
|
||||||
|
from codexlens.storage.registry import RegistryStore
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_QUERIES = [
|
||||||
|
"class Config",
|
||||||
|
"def search",
|
||||||
|
"LspBridge",
|
||||||
|
"graph expansion",
|
||||||
|
"static graph relationships",
|
||||||
|
"clustering strategy",
|
||||||
|
"error handling",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
VALID_STAGE2_MODES = ("precomputed", "realtime", "static_global_graph")
|
||||||
|
|
||||||
|
|
||||||
|
def _now_ms() -> float:
|
||||||
|
return time.perf_counter() * 1000.0
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_path_key(path: str) -> str:
|
||||||
|
"""Normalize file paths for overlap/dedup metrics (Windows-safe)."""
|
||||||
|
try:
|
||||||
|
p = Path(path)
|
||||||
|
if str(p) and (p.is_absolute() or re.match(r"^[A-Za-z]:", str(p))):
|
||||||
|
norm = str(p.resolve())
|
||||||
|
else:
|
||||||
|
norm = str(p)
|
||||||
|
except Exception:
|
||||||
|
norm = path
|
||||||
|
norm = norm.replace("/", "\\")
|
||||||
|
if os.name == "nt":
|
||||||
|
norm = norm.lower()
|
||||||
|
return norm
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_stage_stats(errors: List[str]) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Extract STAGE_STATS JSON blob from SearchStats.errors."""
|
||||||
|
for item in errors or []:
|
||||||
|
if not isinstance(item, str):
|
||||||
|
continue
|
||||||
|
if not item.startswith("STAGE_STATS:"):
|
||||||
|
continue
|
||||||
|
payload = item[len("STAGE_STATS:") :]
|
||||||
|
try:
|
||||||
|
return json.loads(payload)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def jaccard_topk(a: List[str], b: List[str]) -> float:
|
||||||
|
sa, sb = set(a), set(b)
|
||||||
|
if not sa and not sb:
|
||||||
|
return 1.0
|
||||||
|
if not sa or not sb:
|
||||||
|
return 0.0
|
||||||
|
return len(sa & sb) / len(sa | sb)
|
||||||
|
|
||||||
|
|
||||||
|
def rbo(a: List[str], b: List[str], p: float = 0.9) -> float:
|
||||||
|
"""Rank-biased overlap for two ranked lists."""
|
||||||
|
if p <= 0.0 or p >= 1.0:
|
||||||
|
raise ValueError("p must be in (0, 1)")
|
||||||
|
if not a and not b:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
depth = max(len(a), len(b))
|
||||||
|
seen_a: set[str] = set()
|
||||||
|
seen_b: set[str] = set()
|
||||||
|
|
||||||
|
score = 0.0
|
||||||
|
for d in range(1, depth + 1):
|
||||||
|
if d <= len(a):
|
||||||
|
seen_a.add(a[d - 1])
|
||||||
|
if d <= len(b):
|
||||||
|
seen_b.add(b[d - 1])
|
||||||
|
overlap = len(seen_a & seen_b)
|
||||||
|
score += (overlap / d) * ((1.0 - p) * (p ** (d - 1)))
|
||||||
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
def _unique_parent_dirs(paths: Iterable[str]) -> int:
|
||||||
|
dirs = set()
|
||||||
|
for p in paths:
|
||||||
|
try:
|
||||||
|
dirs.add(str(Path(p).parent))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return len(dirs)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_queries(path: Optional[Path], inline: Optional[List[str]]) -> List[str]:
|
||||||
|
if inline:
|
||||||
|
return [q.strip() for q in inline if isinstance(q, str) and q.strip()]
|
||||||
|
if path:
|
||||||
|
if not path.exists():
|
||||||
|
raise SystemExit(f"Queries file does not exist: {path}")
|
||||||
|
raw = path.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
queries = [line.strip() for line in raw.splitlines() if line.strip() and not line.strip().startswith("#")]
|
||||||
|
return queries
|
||||||
|
return list(DEFAULT_QUERIES)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RunDetail:
|
||||||
|
stage2_mode: str
|
||||||
|
query: str
|
||||||
|
latency_ms: float
|
||||||
|
num_results: int
|
||||||
|
topk_paths: List[str]
|
||||||
|
stage_stats: Optional[Dict[str, Any]] = None
|
||||||
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PairwiseCompare:
|
||||||
|
query: str
|
||||||
|
mode_a: str
|
||||||
|
mode_b: str
|
||||||
|
jaccard_topk: float
|
||||||
|
rbo_topk: float
|
||||||
|
a_unique_files_topk: int
|
||||||
|
b_unique_files_topk: int
|
||||||
|
a_unique_dirs_topk: int
|
||||||
|
b_unique_dirs_topk: int
|
||||||
|
|
||||||
|
|
||||||
|
def _run_once(
|
||||||
|
engine: ChainSearchEngine,
|
||||||
|
config: Config,
|
||||||
|
query: str,
|
||||||
|
source_path: Path,
|
||||||
|
*,
|
||||||
|
stage2_mode: str,
|
||||||
|
k: int,
|
||||||
|
coarse_k: int,
|
||||||
|
) -> RunDetail:
|
||||||
|
if stage2_mode not in VALID_STAGE2_MODES:
|
||||||
|
raise ValueError(f"Invalid stage2_mode: {stage2_mode}")
|
||||||
|
|
||||||
|
# Mutate config for this run; ChainSearchEngine reads config fields per-call.
|
||||||
|
config.staged_stage2_mode = stage2_mode
|
||||||
|
|
||||||
|
gc.collect()
|
||||||
|
start_ms = _now_ms()
|
||||||
|
try:
|
||||||
|
result = engine.cascade_search(
|
||||||
|
query=query,
|
||||||
|
source_path=source_path,
|
||||||
|
k=k,
|
||||||
|
coarse_k=coarse_k,
|
||||||
|
strategy="staged",
|
||||||
|
)
|
||||||
|
latency_ms = _now_ms() - start_ms
|
||||||
|
paths_raw = [r.path for r in (result.results or []) if getattr(r, "path", None)]
|
||||||
|
paths = [_normalize_path_key(p) for p in paths_raw]
|
||||||
|
|
||||||
|
topk: List[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for p in paths:
|
||||||
|
if p in seen:
|
||||||
|
continue
|
||||||
|
seen.add(p)
|
||||||
|
topk.append(p)
|
||||||
|
if len(topk) >= k:
|
||||||
|
break
|
||||||
|
|
||||||
|
stage_stats = None
|
||||||
|
try:
|
||||||
|
stage_stats = _extract_stage_stats(getattr(result.stats, "errors", []) or [])
|
||||||
|
except Exception:
|
||||||
|
stage_stats = None
|
||||||
|
|
||||||
|
return RunDetail(
|
||||||
|
stage2_mode=stage2_mode,
|
||||||
|
query=query,
|
||||||
|
latency_ms=latency_ms,
|
||||||
|
num_results=len(result.results or []),
|
||||||
|
topk_paths=topk,
|
||||||
|
stage_stats=stage_stats,
|
||||||
|
error=None,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
return RunDetail(
|
||||||
|
stage2_mode=stage2_mode,
|
||||||
|
query=query,
|
||||||
|
latency_ms=_now_ms() - start_ms,
|
||||||
|
num_results=0,
|
||||||
|
topk_paths=[],
|
||||||
|
stage_stats=None,
|
||||||
|
error=str(exc),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Compare staged Stage-2 expansion modes.")
|
||||||
|
parser.add_argument("--source", type=Path, default=Path.cwd(), help="Project path to search")
|
||||||
|
parser.add_argument("--queries-file", type=Path, default=None, help="Optional newline-delimited queries file")
|
||||||
|
parser.add_argument("--queries", nargs="*", default=None, help="Inline queries (overrides queries-file)")
|
||||||
|
parser.add_argument("--k", type=int, default=20, help="Top-k to evaluate")
|
||||||
|
parser.add_argument("--coarse-k", type=int, default=100, help="Stage-1 coarse_k")
|
||||||
|
parser.add_argument(
|
||||||
|
"--stage2-modes",
|
||||||
|
nargs="*",
|
||||||
|
default=list(VALID_STAGE2_MODES),
|
||||||
|
help="Stage-2 modes to compare",
|
||||||
|
)
|
||||||
|
parser.add_argument("--warmup", type=int, default=0, help="Warmup iterations per mode")
|
||||||
|
parser.add_argument(
|
||||||
|
"--output",
|
||||||
|
type=Path,
|
||||||
|
default=Path(__file__).parent / "results" / "staged_stage2_modes.json",
|
||||||
|
help="Output JSON path",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.source.exists():
|
||||||
|
raise SystemExit(f"Source path does not exist: {args.source}")
|
||||||
|
|
||||||
|
stage2_modes = [str(m).strip().lower() for m in (args.stage2_modes or []) if str(m).strip()]
|
||||||
|
for m in stage2_modes:
|
||||||
|
if m not in VALID_STAGE2_MODES:
|
||||||
|
raise SystemExit(f"Invalid --stage2-modes entry: {m} (valid: {', '.join(VALID_STAGE2_MODES)})")
|
||||||
|
|
||||||
|
queries = _load_queries(args.queries_file, args.queries)
|
||||||
|
if not queries:
|
||||||
|
raise SystemExit("No queries to run")
|
||||||
|
|
||||||
|
# Match CLI behavior: load settings + apply global/workspace .env overrides.
|
||||||
|
config = Config.load()
|
||||||
|
config.cascade_strategy = "staged"
|
||||||
|
config.enable_staged_rerank = True
|
||||||
|
config.embedding_use_gpu = False # stability on some Windows setups
|
||||||
|
|
||||||
|
registry = RegistryStore()
|
||||||
|
registry.initialize()
|
||||||
|
mapper = PathMapper()
|
||||||
|
engine = ChainSearchEngine(registry=registry, mapper=mapper, config=config)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Warmup
|
||||||
|
if args.warmup > 0:
|
||||||
|
warm_query = queries[0]
|
||||||
|
for mode in stage2_modes:
|
||||||
|
for _ in range(args.warmup):
|
||||||
|
try:
|
||||||
|
_run_once(
|
||||||
|
engine,
|
||||||
|
config,
|
||||||
|
warm_query,
|
||||||
|
args.source,
|
||||||
|
stage2_mode=mode,
|
||||||
|
k=min(args.k, 5),
|
||||||
|
coarse_k=min(args.coarse_k, 50),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
per_query: Dict[str, Dict[str, RunDetail]] = {}
|
||||||
|
runs: List[RunDetail] = []
|
||||||
|
comparisons: List[PairwiseCompare] = []
|
||||||
|
|
||||||
|
for i, query in enumerate(queries, start=1):
|
||||||
|
print(f"[{i}/{len(queries)}] {query}")
|
||||||
|
per_query[query] = {}
|
||||||
|
|
||||||
|
for mode in stage2_modes:
|
||||||
|
detail = _run_once(
|
||||||
|
engine,
|
||||||
|
config,
|
||||||
|
query,
|
||||||
|
args.source,
|
||||||
|
stage2_mode=mode,
|
||||||
|
k=args.k,
|
||||||
|
coarse_k=args.coarse_k,
|
||||||
|
)
|
||||||
|
per_query[query][mode] = detail
|
||||||
|
runs.append(detail)
|
||||||
|
|
||||||
|
# Pairwise overlaps for this query
|
||||||
|
for a_idx in range(len(stage2_modes)):
|
||||||
|
for b_idx in range(a_idx + 1, len(stage2_modes)):
|
||||||
|
mode_a = stage2_modes[a_idx]
|
||||||
|
mode_b = stage2_modes[b_idx]
|
||||||
|
a = per_query[query][mode_a]
|
||||||
|
b = per_query[query][mode_b]
|
||||||
|
comparisons.append(
|
||||||
|
PairwiseCompare(
|
||||||
|
query=query,
|
||||||
|
mode_a=mode_a,
|
||||||
|
mode_b=mode_b,
|
||||||
|
jaccard_topk=jaccard_topk(a.topk_paths, b.topk_paths),
|
||||||
|
rbo_topk=rbo(a.topk_paths, b.topk_paths, p=0.9),
|
||||||
|
a_unique_files_topk=len(set(a.topk_paths)),
|
||||||
|
b_unique_files_topk=len(set(b.topk_paths)),
|
||||||
|
a_unique_dirs_topk=_unique_parent_dirs(a.topk_paths),
|
||||||
|
b_unique_dirs_topk=_unique_parent_dirs(b.topk_paths),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _latencies(details: List[RunDetail]) -> List[float]:
|
||||||
|
return [d.latency_ms for d in details if not d.error]
|
||||||
|
|
||||||
|
mode_summaries: Dict[str, Dict[str, Any]] = {}
|
||||||
|
for mode in stage2_modes:
|
||||||
|
mode_runs = [r for r in runs if r.stage2_mode == mode]
|
||||||
|
lat = _latencies(mode_runs)
|
||||||
|
mode_summaries[mode] = {
|
||||||
|
"success": sum(1 for r in mode_runs if not r.error),
|
||||||
|
"avg_latency_ms": statistics.mean(lat) if lat else 0.0,
|
||||||
|
"p50_latency_ms": statistics.median(lat) if lat else 0.0,
|
||||||
|
"p95_latency_ms": statistics.quantiles(lat, n=20)[18] if len(lat) >= 2 else (lat[0] if lat else 0.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"source": str(args.source),
|
||||||
|
"k": args.k,
|
||||||
|
"coarse_k": args.coarse_k,
|
||||||
|
"query_count": len(queries),
|
||||||
|
"stage2_modes": stage2_modes,
|
||||||
|
"modes": mode_summaries,
|
||||||
|
"avg_pairwise_jaccard_topk": statistics.mean([c.jaccard_topk for c in comparisons]) if comparisons else 0.0,
|
||||||
|
"avg_pairwise_rbo_topk": statistics.mean([c.rbo_topk for c in comparisons]) if comparisons else 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
payload = {
|
||||||
|
"summary": summary,
|
||||||
|
"runs": [asdict(r) for r in runs],
|
||||||
|
"comparisons": [asdict(c) for c in comparisons],
|
||||||
|
}
|
||||||
|
args.output.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
||||||
|
print(f"\nSaved: {args.output}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
engine.close()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"WARNING engine.close() failed: {exc!r}", file=sys.stderr)
|
||||||
|
try:
|
||||||
|
registry.close()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"WARNING registry.close() failed: {exc!r}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ def semantic_search(
|
|||||||
structural_weight: float = 0.3,
|
structural_weight: float = 0.3,
|
||||||
keyword_weight: float = 0.2,
|
keyword_weight: float = 0.2,
|
||||||
fusion_strategy: str = "rrf",
|
fusion_strategy: str = "rrf",
|
||||||
|
staged_stage2_mode: Optional[str] = None,
|
||||||
kind_filter: Optional[List[str]] = None,
|
kind_filter: Optional[List[str]] = None,
|
||||||
limit: int = 20,
|
limit: int = 20,
|
||||||
include_match_reason: bool = False,
|
include_match_reason: bool = False,
|
||||||
@@ -50,6 +51,10 @@ def semantic_search(
|
|||||||
- binary: Binary rerank cascade -> binary_cascade_search
|
- binary: Binary rerank cascade -> binary_cascade_search
|
||||||
- hybrid: Binary rerank cascade (backward compat) -> binary_rerank_cascade_search
|
- hybrid: Binary rerank cascade (backward compat) -> binary_rerank_cascade_search
|
||||||
- dense_rerank: Dense rerank cascade -> dense_rerank_cascade_search
|
- dense_rerank: Dense rerank cascade -> dense_rerank_cascade_search
|
||||||
|
staged_stage2_mode: Optional override for staged Stage-2 expansion mode
|
||||||
|
- precomputed: GraphExpander over per-dir graph_neighbors (default)
|
||||||
|
- realtime: Live LSP expansion (requires LSP availability)
|
||||||
|
- static_global_graph: GlobalGraphExpander over global_relationships
|
||||||
kind_filter: Symbol type filter (e.g., ["function", "class"])
|
kind_filter: Symbol type filter (e.g., ["function", "class"])
|
||||||
limit: Max return count (default 20)
|
limit: Max return count (default 20)
|
||||||
include_match_reason: Generate match reason (heuristic, not LLM)
|
include_match_reason: Generate match reason (heuristic, not LLM)
|
||||||
@@ -97,6 +102,17 @@ def semantic_search(
|
|||||||
# Load config
|
# Load config
|
||||||
config = Config.load()
|
config = Config.load()
|
||||||
|
|
||||||
|
# Optional per-call override for staged cascade Stage-2 mode.
|
||||||
|
if staged_stage2_mode:
|
||||||
|
stage2 = str(staged_stage2_mode).strip().lower()
|
||||||
|
if stage2 in {"live"}:
|
||||||
|
stage2 = "realtime"
|
||||||
|
valid_stage2 = {"precomputed", "realtime", "static_global_graph"}
|
||||||
|
if stage2 in valid_stage2:
|
||||||
|
config.staged_stage2_mode = stage2
|
||||||
|
else:
|
||||||
|
logger.debug("Ignoring invalid staged_stage2_mode: %r", staged_stage2_mode)
|
||||||
|
|
||||||
# Get or create registry and mapper
|
# Get or create registry and mapper
|
||||||
try:
|
try:
|
||||||
registry = RegistryStore.default()
|
registry = RegistryStore.default()
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class TestSemanticSearchFunctionSignature:
|
|||||||
"structural_weight",
|
"structural_weight",
|
||||||
"keyword_weight",
|
"keyword_weight",
|
||||||
"fusion_strategy",
|
"fusion_strategy",
|
||||||
|
"staged_stage2_mode",
|
||||||
"kind_filter",
|
"kind_filter",
|
||||||
"limit",
|
"limit",
|
||||||
"include_match_reason",
|
"include_match_reason",
|
||||||
@@ -49,6 +50,7 @@ class TestSemanticSearchFunctionSignature:
|
|||||||
assert sig.parameters["structural_weight"].default == 0.3
|
assert sig.parameters["structural_weight"].default == 0.3
|
||||||
assert sig.parameters["keyword_weight"].default == 0.2
|
assert sig.parameters["keyword_weight"].default == 0.2
|
||||||
assert sig.parameters["fusion_strategy"].default == "rrf"
|
assert sig.parameters["fusion_strategy"].default == "rrf"
|
||||||
|
assert sig.parameters["staged_stage2_mode"].default is None
|
||||||
assert sig.parameters["kind_filter"].default is None
|
assert sig.parameters["kind_filter"].default is None
|
||||||
assert sig.parameters["limit"].default == 20
|
assert sig.parameters["limit"].default == 20
|
||||||
assert sig.parameters["include_match_reason"].default is False
|
assert sig.parameters["include_match_reason"].default is False
|
||||||
|
|||||||
Reference in New Issue
Block a user