mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
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:
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"none": "No sessions",
|
||||
"refresh": "Refresh",
|
||||
"new": "New Session",
|
||||
"close": "Close"
|
||||
"close": "Close",
|
||||
"share": "Share (Read-only)"
|
||||
},
|
||||
"exec": {
|
||||
"tool": "Tool",
|
||||
|
||||
@@ -119,7 +119,8 @@
|
||||
"none": "暂无会话",
|
||||
"refresh": "刷新",
|
||||
"new": "新建会话",
|
||||
"close": "关闭"
|
||||
"close": "关闭",
|
||||
"share": "分享(只读)"
|
||||
},
|
||||
"exec": {
|
||||
"tool": "工具",
|
||||
|
||||
Reference in New Issue
Block a user