Skip to content

Commit 362f354

Browse files
author
catlog22
committed
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.
1 parent 4344e79 commit 362f354

25 files changed

Lines changed: 2613 additions & 51 deletions

ccw/frontend/src/components/issue/hub/IssueBoardPanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -310,14 +310,14 @@ export function IssueBoardPanel() {
310310
preferredShell: 'bash',
311311
tool: autoStart.tool,
312312
resumeKey: issueId,
313-
});
313+
}, projectPath);
314314
await executeInCliSession(created.session.sessionKey, {
315315
tool: autoStart.tool,
316316
prompt: buildIssueAutoPrompt({ ...issue, status: destStatus }),
317317
mode: autoStart.mode,
318318
resumeKey: issueId,
319319
resumeStrategy: autoStart.resumeStrategy,
320-
});
320+
}, projectPath);
321321
} catch (e) {
322322
setOptimisticError(`Auto-start failed: ${e instanceof Error ? e.message : String(e)}`);
323323
}
@@ -328,7 +328,7 @@ export function IssueBoardPanel() {
328328
}
329329
}
330330
},
331-
[issues, idsByStatus, updateIssue]
331+
[autoStart, issues, idsByStatus, projectPath, updateIssue]
332332
);
333333

334334
if (error) {

ccw/frontend/src/components/issue/hub/IssueTerminalTab.tsx

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { useEffect, useMemo, useRef, useState } from 'react';
77
import { useIntl } from 'react-intl';
8-
import { Plus, RefreshCw, XCircle } from 'lucide-react';
8+
import { Copy, Plus, RefreshCw, Share2, XCircle } from 'lucide-react';
99
import { Terminal as XTerm } from 'xterm';
1010
import { FitAddon } from 'xterm-addon-fit';
1111
import { Button } from '@/components/ui/Button';
@@ -16,6 +16,7 @@ import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
1616
import {
1717
closeCliSession,
1818
createCliSession,
19+
createCliSessionShareToken,
1920
executeInCliSession,
2021
fetchCliSessionBuffer,
2122
fetchCliSessions,
@@ -53,6 +54,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
5354
const [resumeStrategy, setResumeStrategy] = useState<ResumeStrategy>('nativeResume');
5455
const [prompt, setPrompt] = useState('');
5556
const [isExecuting, setIsExecuting] = useState(false);
57+
const [shareUrl, setShareUrl] = useState<string>('');
5658

5759
const terminalHostRef = useRef<HTMLDivElement | null>(null);
5860
const xtermRef = useRef<XTerm | null>(null);
@@ -69,7 +71,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
6971
pendingInputRef.current = '';
7072
if (!pending) return;
7173
try {
72-
await sendCliSessionText(sessionKey, { text: pending, appendNewline: false });
74+
await sendCliSessionText(sessionKey, { text: pending, appendNewline: false }, projectPath || undefined);
7375
} catch (e) {
7476
// Ignore transient failures (WS output still shows process state)
7577
}
@@ -86,13 +88,13 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
8688
useEffect(() => {
8789
setIsLoadingSessions(true);
8890
setError(null);
89-
fetchCliSessions()
91+
fetchCliSessions(projectPath || undefined)
9092
.then((r) => {
9193
setSessions(r.sessions as unknown as CliSession[]);
9294
})
9395
.catch((e) => setError(e instanceof Error ? e.message : String(e)))
9496
.finally(() => setIsLoadingSessions(false));
95-
}, [setSessions]);
97+
}, [projectPath, setSessions]);
9698

9799
// Auto-select a session if none selected yet
98100
useEffect(() => {
@@ -152,7 +154,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
152154
if (!selectedSessionKey) return;
153155
clearOutput(selectedSessionKey);
154156

155-
fetchCliSessionBuffer(selectedSessionKey)
157+
fetchCliSessionBuffer(selectedSessionKey, projectPath || undefined)
156158
.then(({ buffer }) => {
157159
setBuffer(selectedSessionKey, buffer || '');
158160
})
@@ -162,7 +164,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
162164
.finally(() => {
163165
fitAddon.fit();
164166
});
165-
}, [selectedSessionKey, setBuffer, clearOutput]);
167+
}, [selectedSessionKey, projectPath, setBuffer, clearOutput]);
166168

167169
// Stream new output chunks into xterm
168170
useEffect(() => {
@@ -192,7 +194,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
192194
if (selectedSessionKey) {
193195
void (async () => {
194196
try {
195-
await resizeCliSession(selectedSessionKey, { cols: term.cols, rows: term.rows });
197+
await resizeCliSession(selectedSessionKey, { cols: term.cols, rows: term.rows }, projectPath || undefined);
196198
} catch {
197199
// ignore
198200
}
@@ -203,7 +205,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
203205
const ro = new ResizeObserver(resize);
204206
ro.observe(host);
205207
return () => ro.disconnect();
206-
}, [selectedSessionKey]);
208+
}, [selectedSessionKey, projectPath]);
207209

208210
const handleCreateSession = async () => {
209211
setIsCreating(true);
@@ -217,7 +219,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
217219
tool,
218220
model: undefined,
219221
resumeKey,
220-
});
222+
}, projectPath || undefined);
221223
upsertSession(created.session as unknown as CliSession);
222224
setSelectedSessionKey(created.session.sessionKey);
223225
} catch (e) {
@@ -232,7 +234,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
232234
setIsClosing(true);
233235
setError(null);
234236
try {
235-
await closeCliSession(selectedSessionKey);
237+
await closeCliSession(selectedSessionKey, projectPath || undefined);
236238
setSelectedSessionKey('');
237239
} catch (e) {
238240
setError(e instanceof Error ? e.message : String(e));
@@ -254,7 +256,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
254256
resumeKey: resumeKey.trim() || undefined,
255257
resumeStrategy,
256258
category: 'user',
257-
});
259+
}, projectPath || undefined);
258260
setPrompt('');
259261
} catch (e) {
260262
setError(e instanceof Error ? e.message : String(e));
@@ -267,7 +269,7 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
267269
setIsLoadingSessions(true);
268270
setError(null);
269271
try {
270-
const r = await fetchCliSessions();
272+
const r = await fetchCliSessions(projectPath || undefined);
271273
setSessions(r.sessions as unknown as CliSession[]);
272274
} catch (e) {
273275
setError(e instanceof Error ? e.message : String(e));
@@ -276,6 +278,31 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
276278
}
277279
};
278280

281+
const handleCreateShareLink = async () => {
282+
if (!selectedSessionKey) return;
283+
setError(null);
284+
setShareUrl('');
285+
try {
286+
const r = await createCliSessionShareToken(selectedSessionKey, { mode: 'read' }, projectPath || undefined);
287+
const url = new URL(window.location.href);
288+
const base = (import.meta.env.BASE_URL ?? '/').replace(/\/$/, '');
289+
url.pathname = `${base}/cli-sessions/share`;
290+
url.search = `sessionKey=${encodeURIComponent(selectedSessionKey)}&shareToken=${encodeURIComponent(r.shareToken)}`;
291+
setShareUrl(url.toString());
292+
} catch (e) {
293+
setError(e instanceof Error ? e.message : String(e));
294+
}
295+
};
296+
297+
const handleCopyShareLink = async () => {
298+
if (!shareUrl) return;
299+
try {
300+
await navigator.clipboard.writeText(shareUrl);
301+
} catch {
302+
// ignore
303+
}
304+
};
305+
279306
return (
280307
<div className="space-y-3">
281308
<div className="flex items-center gap-2 flex-wrap">
@@ -317,8 +344,23 @@ export function IssueTerminalTab({ issueId }: { issueId: string }) {
317344
<XCircle className="w-4 h-4 mr-2" />
318345
{formatMessage({ id: 'issues.terminal.session.close' })}
319346
</Button>
347+
348+
<Button variant="outline" onClick={handleCreateShareLink} disabled={!selectedSessionKey}>
349+
<Share2 className="w-4 h-4 mr-2" />
350+
{formatMessage({ id: 'issues.terminal.session.share' })}
351+
</Button>
320352
</div>
321353

354+
{shareUrl && (
355+
<div className="flex items-center gap-2">
356+
<Input value={shareUrl} readOnly />
357+
<Button variant="outline" onClick={handleCopyShareLink}>
358+
<Copy className="w-4 h-4 mr-2" />
359+
{formatMessage({ id: 'common.actions.copy' })}
360+
</Button>
361+
</div>
362+
)}
363+
322364
<div className="grid grid-cols-2 gap-2">
323365
<div className="space-y-1">
324366
<div className="text-xs text-muted-foreground">{formatMessage({ id: 'issues.terminal.exec.tool' })}</div>

ccw/frontend/src/components/issue/queue/QueueExecuteInSession.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
103103
setIsLoading(true);
104104
setError(null);
105105
try {
106-
const r = await fetchCliSessions();
106+
const r = await fetchCliSessions(projectPath || undefined);
107107
setSessions(r.sessions as unknown as CliSession[]);
108108
} catch (e) {
109109
setError(e instanceof Error ? e.message : String(e));
@@ -115,7 +115,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
115115
useEffect(() => {
116116
void refreshSessions();
117117
// eslint-disable-next-line react-hooks/exhaustive-deps
118-
}, []);
118+
}, [projectPath]);
119119

120120
useEffect(() => {
121121
if (selectedSessionKey) return;
@@ -130,7 +130,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
130130
workingDir: projectPath,
131131
preferredShell: 'bash',
132132
resumeKey: item.issue_id,
133-
});
133+
}, projectPath);
134134
upsertSession(created.session as unknown as CliSession);
135135
setSelectedSessionKey(created.session.sessionKey);
136136
return created.session.sessionKey;
@@ -144,7 +144,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
144144
workingDir: projectPath,
145145
preferredShell: 'bash',
146146
resumeKey: item.issue_id,
147-
});
147+
}, projectPath);
148148
upsertSession(created.session as unknown as CliSession);
149149
setSelectedSessionKey(created.session.sessionKey);
150150
await refreshSessions();
@@ -168,7 +168,7 @@ export function QueueExecuteInSession({ item, className }: { item: QueueItem; cl
168168
category: 'user',
169169
resumeKey: item.issue_id,
170170
resumeStrategy,
171-
});
171+
}, projectPath);
172172
setLastExecution({ executionId: result.executionId, command: result.command });
173173
} catch (e) {
174174
setError(e instanceof Error ? e.message : String(e));

ccw/frontend/src/lib/api.ts

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5707,28 +5707,41 @@ export interface CreateCliSessionInput {
57075707
resumeKey?: string;
57085708
}
57095709

5710-
export async function fetchCliSessions(): Promise<{ sessions: CliSession[] }> {
5711-
return fetchApi<{ sessions: CliSession[] }>('/api/cli-sessions');
5710+
function withPath(url: string, projectPath?: string): string {
5711+
if (!projectPath) return url;
5712+
const sep = url.includes('?') ? '&' : '?';
5713+
return `${url}${sep}path=${encodeURIComponent(projectPath)}`;
57125714
}
57135715

5714-
export async function createCliSession(input: CreateCliSessionInput): Promise<{ success: boolean; session: CliSession }> {
5715-
return fetchApi<{ success: boolean; session: CliSession }>('/api/cli-sessions', {
5716+
export async function fetchCliSessions(projectPath?: string): Promise<{ sessions: CliSession[] }> {
5717+
return fetchApi<{ sessions: CliSession[] }>(withPath('/api/cli-sessions', projectPath));
5718+
}
5719+
5720+
export async function createCliSession(
5721+
input: CreateCliSessionInput,
5722+
projectPath?: string
5723+
): Promise<{ success: boolean; session: CliSession }> {
5724+
return fetchApi<{ success: boolean; session: CliSession }>(withPath('/api/cli-sessions', projectPath), {
57165725
method: 'POST',
57175726
body: JSON.stringify(input),
57185727
});
57195728
}
57205729

5721-
export async function fetchCliSessionBuffer(sessionKey: string): Promise<{ session: CliSession; buffer: string }> {
5730+
export async function fetchCliSessionBuffer(
5731+
sessionKey: string,
5732+
projectPath?: string
5733+
): Promise<{ session: CliSession; buffer: string }> {
57225734
return fetchApi<{ session: CliSession; buffer: string }>(
5723-
`/api/cli-sessions/${encodeURIComponent(sessionKey)}/buffer`
5735+
withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/buffer`, projectPath)
57245736
);
57255737
}
57265738

57275739
export async function sendCliSessionText(
57285740
sessionKey: string,
5729-
input: { text: string; appendNewline?: boolean }
5741+
input: { text: string; appendNewline?: boolean },
5742+
projectPath?: string
57305743
): Promise<{ success: boolean }> {
5731-
return fetchApi<{ success: boolean }>(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/send`, {
5744+
return fetchApi<{ success: boolean }>(withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/send`, projectPath), {
57325745
method: 'POST',
57335746
body: JSON.stringify(input),
57345747
});
@@ -5747,27 +5760,40 @@ export interface ExecuteInCliSessionInput {
57475760

57485761
export async function executeInCliSession(
57495762
sessionKey: string,
5750-
input: ExecuteInCliSessionInput
5763+
input: ExecuteInCliSessionInput,
5764+
projectPath?: string
57515765
): Promise<{ success: boolean; executionId: string; command: string }> {
57525766
return fetchApi<{ success: boolean; executionId: string; command: string }>(
5753-
`/api/cli-sessions/${encodeURIComponent(sessionKey)}/execute`,
5767+
withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/execute`, projectPath),
57545768
{ method: 'POST', body: JSON.stringify(input) }
57555769
);
57565770
}
57575771

57585772
export async function resizeCliSession(
57595773
sessionKey: string,
5760-
input: { cols: number; rows: number }
5774+
input: { cols: number; rows: number },
5775+
projectPath?: string
57615776
): Promise<{ success: boolean }> {
5762-
return fetchApi<{ success: boolean }>(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/resize`, {
5777+
return fetchApi<{ success: boolean }>(withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/resize`, projectPath), {
57635778
method: 'POST',
57645779
body: JSON.stringify(input),
57655780
});
57665781
}
57675782

5768-
export async function closeCliSession(sessionKey: string): Promise<{ success: boolean }> {
5769-
return fetchApi<{ success: boolean }>(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/close`, {
5783+
export async function closeCliSession(sessionKey: string, projectPath?: string): Promise<{ success: boolean }> {
5784+
return fetchApi<{ success: boolean }>(withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/close`, projectPath), {
57705785
method: 'POST',
57715786
body: JSON.stringify({}),
57725787
});
57735788
}
5789+
5790+
export async function createCliSessionShareToken(
5791+
sessionKey: string,
5792+
input: { mode?: 'read' | 'write'; ttlMs?: number },
5793+
projectPath?: string
5794+
): Promise<{ success: boolean; shareToken: string; expiresAt: string; mode: 'read' | 'write' }> {
5795+
return fetchApi<{ success: boolean; shareToken: string; expiresAt: string; mode: 'read' | 'write' }>(
5796+
withPath(`/api/cli-sessions/${encodeURIComponent(sessionKey)}/share`, projectPath),
5797+
{ method: 'POST', body: JSON.stringify(input) }
5798+
);
5799+
}

ccw/frontend/src/locales/en/issues.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@
119119
"none": "No sessions",
120120
"refresh": "Refresh",
121121
"new": "New Session",
122-
"close": "Close"
122+
"close": "Close",
123+
"share": "Share (Read-only)"
123124
},
124125
"exec": {
125126
"tool": "Tool",

ccw/frontend/src/locales/zh/issues.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@
119119
"none": "暂无会话",
120120
"refresh": "刷新",
121121
"new": "新建会话",
122-
"close": "关闭"
122+
"close": "关闭",
123+
"share": "分享(只读)"
123124
},
124125
"exec": {
125126
"tool": "工具",

0 commit comments

Comments
 (0)