Add benchmark results and tests for LSP graph builder and staged search

- Introduced a new benchmark results file for performance comparison on 2026-02-09.
- Added a test for LspGraphBuilder to ensure it does not expand nodes at maximum depth.
- Created a test for the staged search pipeline to validate fallback behavior when stage 1 returns empty results.
This commit is contained in:
catlog22
2026-02-09 21:43:13 +08:00
parent 4344e79e68
commit 362f354f1c
25 changed files with 2613 additions and 51 deletions

View File

@@ -310,14 +310,14 @@ export function IssueBoardPanel() {
preferredShell: 'bash',
tool: autoStart.tool,
resumeKey: issueId,
});
}, projectPath);
await executeInCliSession(created.session.sessionKey, {
tool: autoStart.tool,
prompt: buildIssueAutoPrompt({ ...issue, status: destStatus }),
mode: autoStart.mode,
resumeKey: issueId,
resumeStrategy: autoStart.resumeStrategy,
});
}, projectPath);
} catch (e) {
setOptimisticError(`Auto-start failed: ${e instanceof Error ? e.message : String(e)}`);
}
@@ -328,7 +328,7 @@ export function IssueBoardPanel() {
}
}
},
[issues, idsByStatus, updateIssue]
[autoStart, issues, idsByStatus, projectPath, updateIssue]
);
if (error) {

View File

@@ -5,7 +5,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
import { Plus, RefreshCw, XCircle } from 'lucide-react';
import { Copy, Plus, RefreshCw, Share2, XCircle } from 'lucide-react';
import { Terminal as XTerm } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { Button } from '@/components/ui/Button';
@@ -16,6 +16,7 @@ import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
import {
closeCliSession,
createCliSession,
createCliSessionShareToken,
executeInCliSession,
fetchCliSessionBuffer,
fetchCliSessions,
@@ -53,6 +54,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
const [resumeStrategy, setResumeStrategy] = useState<ResumeStrategy>('nativeResume');
const [prompt, setPrompt] = useState('');
const [isExecuting, setIsExecuting] = useState(false);
const [shareUrl, setShareUrl] = useState<string>('');
const terminalHostRef = useRef<HTMLDivElement | null>(null);
const xtermRef = useRef<XTerm | null>(null);
@@ -69,7 +71,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
pendingInputRef.current = '';
if (!pending) return;
try {
await sendCliSessionText(sessionKey, { text: pending, appendNewline: false });
await sendCliSessionText(sessionKey, { text: pending, appendNewline: false }, projectPath || undefined);
} catch (e) {
// Ignore transient failures (WS output still shows process state)
}
@@ -86,13 +88,13 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
useEffect(() => {
setIsLoadingSessions(true);
setError(null);
fetchCliSessions()
fetchCliSessions(projectPath || undefined)
.then((r) => {
setSessions(r.sessions as unknown as CliSession[]);
})
.catch((e) => setError(e instanceof Error ? e.message : String(e)))
.finally(() => setIsLoadingSessions(false));
}, [setSessions]);
}, [projectPath, setSessions]);
// Auto-select a session if none selected yet
useEffect(() => {
@@ -152,7 +154,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
if (!selectedSessionKey) return;
clearOutput(selectedSessionKey);
fetchCliSessionBuffer(selectedSessionKey)
fetchCliSessionBuffer(selectedSessionKey, projectPath || undefined)
.then(({ buffer }) => {
setBuffer(selectedSessionKey, buffer || '');
})
@@ -162,7 +164,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
.finally(() => {
fitAddon.fit();
});
}, [selectedSessionKey, setBuffer, clearOutput]);
}, [selectedSessionKey, projectPath, setBuffer, clearOutput]);
// Stream new output chunks into xterm
useEffect(() => {
@@ -192,7 +194,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
if (selectedSessionKey) {
void (async () => {
try {
await resizeCliSession(selectedSessionKey, { cols: term.cols, rows: term.rows });
await resizeCliSession(selectedSessionKey, { cols: term.cols, rows: term.rows }, projectPath || undefined);
} catch {
// ignore
}
@@ -203,7 +205,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
const ro = new ResizeObserver(resize);
ro.observe(host);
return () => ro.disconnect();
}, [selectedSessionKey]);
}, [selectedSessionKey, projectPath]);
const handleCreateSession = async () => {
setIsCreating(true);
@@ -217,7 +219,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
tool,
model: undefined,
resumeKey,
});
}, projectPath || undefined);
upsertSession(created.session as unknown as CliSession);
setSelectedSessionKey(created.session.sessionKey);
} catch (e) {
@@ -232,7 +234,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
setIsClosing(true);
setError(null);
try {
await closeCliSession(selectedSessionKey);
await closeCliSession(selectedSessionKey, projectPath || undefined);
setSelectedSessionKey('');
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
@@ -254,7 +256,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
resumeKey: resumeKey.trim() || undefined,
resumeStrategy,
category: 'user',
});
}, projectPath || undefined);
setPrompt('');
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
@@ -267,7 +269,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
setIsLoadingSessions(true);
setError(null);
try {
const r = await fetchCliSessions();
const r = await fetchCliSessions(projectPath || undefined);
setSessions(r.sessions as unknown as CliSession[]);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
@@ -276,6 +278,31 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
}
};
const handleCreateShareLink = async () => {
if (!selectedSessionKey) return;
setError(null);
setShareUrl('');
try {
const r = await createCliSessionShareToken(selectedSessionKey, { mode: 'read' }, projectPath || undefined);
const url = new URL(window.location.href);
const base = (import.meta.env.BASE_URL ?? '/').replace(/\/$/, '');
url.pathname = `${base}/cli-sessions/share`;
url.search = `sessionKey=${encodeURIComponent(selectedSessionKey)}&shareToken=${encodeURIComponent(r.shareToken)}`;
setShareUrl(url.toString());
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
};
const handleCopyShareLink = async () => {
if (!shareUrl) return;
try {
await navigator.clipboard.writeText(shareUrl);
} catch {
// ignore
}
};
return (
<div className="space-y-3">
<div className="flex items-center gap-2 flex-wrap">
@@ -317,8 +344,23 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
<XCircle className="w-4 h-4 mr-2" />
{formatMessage({ id: 'issues.terminal.session.close' })}
</Button>
<Button variant="outline" onClick={handleCreateShareLink} disabled={!selectedSessionKey}>
<Share2 className="w-4 h-4 mr-2" />
{formatMessage({ id: 'issues.terminal.session.share' })}
</Button>
</div>
{shareUrl && (
<div className="flex items-center gap-2">
<Input value={shareUrl} readOnly />
<Button variant="outline" onClick={handleCopyShareLink}>
<Copy className="w-4 h-4 mr-2" />
{formatMessage({ id: 'common.actions.copy' })}
</Button>
</div>
)}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'issues.terminal.exec.tool' })}</div>

View File

@@ -103,7 +103,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
setIsLoading(true);
setError(null);
try {
const r = await fetchCliSessions();
const r = await fetchCliSessions(projectPath || undefined);
setSessions(r.sessions as unknown as CliSession[]);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
@@ -115,7 +115,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
useEffect(() => {
void refreshSessions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [projectPath]);
useEffect(() => {
if (selectedSessionKey) return;
@@ -130,7 +130,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
workingDir: projectPath,
preferredShell: 'bash',
resumeKey: item.issue_id,
});
}, projectPath);
upsertSession(created.session as unknown as CliSession);
setSelectedSessionKey(created.session.sessionKey);
return created.session.sessionKey;
@@ -144,7 +144,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
workingDir: projectPath,
preferredShell: 'bash',
resumeKey: item.issue_id,
});
}, projectPath);
upsertSession(created.session as unknown as CliSession);
setSelectedSessionKey(created.session.sessionKey);
await refreshSessions();
@@ -168,7 +168,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
category: 'user',
resumeKey: item.issue_id,
resumeStrategy,
});
}, projectPath);
setLastExecution({ executionId: result.executionId, command: result.command });
} catch (e) {
setError(e instanceof Error ? e.message : String(e));

View File

@@ -5707,28 +5707,41 @@ export interface CreateCliSessionInput {
resumeKey?: string;
}
export async function fetchCliSessions(): Promise<{ sessions: CliSession[] }> {
return fetchApi<{ sessions: CliSession[] }>('/api/cli-sessions');
function withPath(url: string, projectPath?: string): string {
if (!projectPath) return url;
const sep = url.includes('?') ? '&' : '?';
return `${url}${sep}path=${encodeURIComponent(projectPath)}`;
}
export async function createCliSession(input: CreateCliSessionInput): Promise<{ success: boolean; session: CliSession }> {
return fetchApi<{ success: boolean; session: CliSession }>('/api/cli-sessions', {
export async function fetchCliSessions(projectPath?: string): Promise<{ sessions: CliSession[] }> {
return fetchApi<{ sessions: CliSession[] }>(withPath('/api/cli-sessions', projectPath));
}
export async function createCliSession(
input: CreateCliSessionInput,
projectPath?: string
): Promise<{ success: boolean; session: CliSession }> {
return fetchApi<{ success: boolean; session: CliSession }>(withPath('/api/cli-sessions', projectPath), {
method: 'POST',
body: JSON.stringify(input),
});
}
export async function fetchCliSessionBuffer(sessionKey: string): Promise<{ session: CliSession; buffer: string }> {
export async function fetchCliSessionBuffer(
sessionKey: string,
projectPath?: string
): Promise<{ session: CliSession; buffer: string }> {
return fetchApi<{ session: CliSession; buffer: string }>(
`/api/cli-sessions/${encodeURIComponent(sessionKey)}/buffer`
withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/buffer`, projectPath)
);
}
export async function sendCliSessionText(
sessionKey: string,
input: { text: string; appendNewline?: boolean }
input: { text: string; appendNewline?: boolean },
projectPath?: string
): Promise<{ success: boolean }> {
return fetchApi<{ success: boolean }>(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/send`, {
return fetchApi<{ success: boolean }>(withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/send`, projectPath), {
method: 'POST',
body: JSON.stringify(input),
});
@@ -5747,27 +5760,40 @@ export interface ExecuteInCliSessionInput {
export async function executeInCliSession(
sessionKey: string,
input: ExecuteInCliSessionInput
input: ExecuteInCliSessionInput,
projectPath?: string
): Promise<{ success: boolean; executionId: string; command: string }> {
return fetchApi<{ success: boolean; executionId: string; command: string }>(
`/api/cli-sessions/${encodeURIComponent(sessionKey)}/execute`,
withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/execute`, projectPath),
{ method: 'POST', body: JSON.stringify(input) }
);
}
export async function resizeCliSession(
sessionKey: string,
input: { cols: number; rows: number }
input: { cols: number; rows: number },
projectPath?: string
): Promise<{ success: boolean }> {
return fetchApi<{ success: boolean }>(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/resize`, {
return fetchApi<{ success: boolean }>(withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/resize`, projectPath), {
method: 'POST',
body: JSON.stringify(input),
});
}
export async function closeCliSession(sessionKey: string): Promise<{ success: boolean }> {
return fetchApi<{ success: boolean }>(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/close`, {
export async function closeCliSession(sessionKey: string, projectPath?: string): Promise<{ success: boolean }> {
return fetchApi<{ success: boolean }>(withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/close`, projectPath), {
method: 'POST',
body: JSON.stringify({}),
});
}
export async function createCliSessionShareToken(
sessionKey: string,
input: { mode?: 'read' | 'write'; ttlMs?: number },
projectPath?: string
): Promise<{ success: boolean; shareToken: string; expiresAt: string; mode: 'read' | 'write' }> {
return fetchApi<{ success: boolean; shareToken: string; expiresAt: string; mode: 'read' | 'write' }>(
withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/share`, projectPath),
{ method: 'POST', body: JSON.stringify(input) }
);
}

View File

@@ -119,7 +119,8 @@
"none": "No sessions",
"refresh": "Refresh",
"new": "New Session",
"close": "Close"
"close": "Close",
"share": "Share (Read-only)"
},
"exec": {
"tool": "Tool",

View File

@@ -119,7 +119,8 @@
"none": "暂无会话",
"refresh": "刷新",
"new": "新建会话",
"close": "关闭"
"close": "关闭",
"share": "分享(只读)"
},
"exec": {
"tool": "工具",