Skip to content

Commit f751cb3

Browse files
authored
Merge pull request #70 from phantom5099/desktop-lag
Desktop Performance Optimization
2 parents 534e29c + c5c2626 commit f751cb3

19 files changed

Lines changed: 1299 additions & 502 deletions

packages/codingcode/test/client/http/sessions.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { createRequestHelpers } from '../../../src/client/http/request.js';
44

55
describe('createHttpSessionClient.setSessionPermissionMode', () => {
66
it('calls PUT /api/sessions/:id/permission-mode', async () => {
7-
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
8-
new Response(JSON.stringify({}), { status: 200 })
9-
);
7+
const fetchSpy = vi
8+
.spyOn(globalThis, 'fetch')
9+
.mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
1010

1111
const request = createRequestHelpers('http://localhost:8080');
1212
const client = createHttpSessionClient(request);

packages/desktop/electron/main.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { join } from 'path';
33
import { createMenu } from './menu';
44
import { registerFsHandlers } from './ipc/fs.handler';
55
import { registerGitHandlers } from './ipc/git.handler';
6-
import { startPolling } from './core/git.service';
6+
import { startPolling, stopPolling } from './core/git.service';
77
import { initBackend } from './core/backend';
88
import { startHttpServer } from './core/http-server';
99

@@ -15,6 +15,7 @@ process.on('unhandledRejection', (reason) => {
1515
});
1616

1717
let mainWindow: BrowserWindow | null = null;
18+
let currentWorkspaceCwd: string = process.cwd();
1819

1920
function createWindow(apiPort: number): BrowserWindow {
2021
const win = new BrowserWindow({
@@ -54,6 +55,13 @@ function createWindow(apiPort: number): BrowserWindow {
5455
return { action: 'deny' };
5556
});
5657

58+
win.on('closed', () => {
59+
stopPolling();
60+
if (mainWindow === win) {
61+
mainWindow = null;
62+
}
63+
});
64+
5765
return win;
5866
}
5967

@@ -73,11 +81,16 @@ app.whenReady().then(async () => {
7381
return result.canceled ? null : (result.filePaths[0] ?? null);
7482
});
7583

84+
// Renderer notifies main process of workspace cwd changes
85+
ipcMain.on('workspace:setCwd', (_e, cwd: string) => {
86+
currentWorkspaceCwd = cwd;
87+
});
88+
7689
registerFsHandlers();
7790
registerGitHandlers();
7891

79-
// Git polling uses the current project cwd from platform utility
80-
startPolling(mainWindow, () => process.cwd());
92+
// Git polling uses the workspace cwd from renderer
93+
startPolling(mainWindow, () => currentWorkspaceCwd);
8194

8295
app.on('activate', () => {
8396
if (BrowserWindow.getAllWindows().length === 0) {
@@ -87,6 +100,7 @@ app.whenReady().then(async () => {
87100
});
88101

89102
app.on('window-all-closed', () => {
103+
stopPolling();
90104
if (process.platform !== 'darwin') {
91105
app.quit();
92106
}

packages/desktop/electron/preload.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ const api = {
2828
// Folder dialog
2929
openFolderDialog: (): Promise<string | null> => ipcRenderer.invoke('project:openFolderDialog'),
3030

31+
// Workspace cwd sync (renderer -> main)
32+
setWorkspaceCwd: (cwd: string): void => ipcRenderer.send('workspace:setCwd', cwd),
33+
3134
// Git (explicit cwd)
3235
gitStatus: (cwd: string): Promise<unknown> => ipcRenderer.invoke('git:status', cwd),
3336
gitBranches: (cwd: string): Promise<string[]> => ipcRenderer.invoke('git:branches', cwd),

packages/desktop/src/App.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,24 @@ import ErrorBoundary from './shared/ErrorBoundary';
88
export default function App() {
99
const mode = useGlobalStore((s) => s.ui.mode);
1010
const setMode = useGlobalStore((s) => s.setMode);
11+
const rootPath = useGlobalStore((s) => s.workspace.rootPath);
12+
13+
// Sync workspace cwd to main process for git polling
14+
useEffect(() => {
15+
if (rootPath) {
16+
window.electronAPI?.setWorkspaceCwd?.(rootPath);
17+
}
18+
}, [rootPath]);
1119

1220
useEffect(() => {
1321
const off = window.electronAPI?.onFsChange?.(() => {});
14-
window.addEventListener('menu:switchMode', ((e: CustomEvent<'agent' | 'ide'>) => {
22+
const handler = ((e: CustomEvent<'agent' | 'ide'>) => {
1523
setMode(e.detail);
16-
}) as EventListener);
24+
}) as EventListener;
25+
window.addEventListener('menu:switchMode', handler);
1726
return () => {
1827
off?.();
28+
window.removeEventListener('menu:switchMode', handler);
1929
};
2030
}, [setMode]);
2131

packages/desktop/src/agent/AgentSidebar.tsx

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { useState } from 'react';
1+
import { useState, useMemo } from 'react';
22
import { useGlobalStore } from '../stores/global.store';
3-
import { API_BASE, api } from '../lib/api';
4-
import type { Thread } from '@shared/types';
3+
import { api } from '../lib/api';
54

65
function normalizeCwd(p: string): string {
76
return p.replace(/\\/g, '/').replace(/^([A-Z]):/, (_, l: string) => `${l.toLowerCase()}:`);
@@ -17,25 +16,24 @@ function relativeTime(ts: number): string {
1716
return `${Math.floor(days / 7)}周`;
1817
}
1918

20-
function getProjectThreads(threads: Record<string, Thread>, rootPath: string): Thread[] {
21-
const normalizedRoot = normalizeCwd(rootPath);
22-
return Object.values(threads)
23-
.filter((t) => {
24-
const tcwd = normalizeCwd(t.cwd);
25-
return tcwd.startsWith(normalizedRoot);
26-
})
27-
.sort((a, b) => b.updatedAt - a.updatedAt);
28-
}
29-
3019
export default function AgentSidebar() {
3120
const sidebarCollapsed = useGlobalStore((s) => s.ui.sidebarCollapsed);
32-
const threads = useGlobalStore((s) => s.agent.threads);
3321
const currentThreadId = useGlobalStore((s) => s.agent.currentThreadId);
22+
const rootPath = useGlobalStore((s) => s.workspace.rootPath);
3423
const workspace = useGlobalStore((s) => s.workspace);
3524
const setCurrentThread = useGlobalStore((s) => s.setCurrentThread);
3625
const toggleSidebar = useGlobalStore((s) => s.toggleSidebar);
3726

38-
const projectThreads = getProjectThreads(threads, workspace.rootPath);
27+
// Subscribe to raw threads, derive list with useMemo for stable reference
28+
const rawThreads = useGlobalStore((s) => s.agent.threads);
29+
const threadList = useMemo(() => {
30+
const normalizedRoot = normalizeCwd(rootPath);
31+
return Object.values(rawThreads)
32+
.filter((t) => normalizeCwd(t.cwd).startsWith(normalizedRoot))
33+
.map((t) => ({ id: t.id, title: t.title, cwd: t.cwd, updatedAt: t.updatedAt }))
34+
.sort((a, b) => b.updatedAt - a.updatedAt);
35+
}, [rawThreads, rootPath]);
36+
3937
const [hoveredThreadId, setHoveredThreadId] = useState<string | null>(null);
4038

4139
const handleDelete = async (threadId: string) => {
@@ -127,7 +125,7 @@ export default function AgentSidebar() {
127125
会话
128126
</span>
129127
</div>
130-
{projectThreads.slice(0, 15).map((t) => (
128+
{threadList.slice(0, 15).map((t) => (
131129
<button
132130
type="button"
133131
key={t.id}
@@ -175,15 +173,15 @@ export default function AgentSidebar() {
175173
)}
176174
</button>
177175
))}
178-
{projectThreads.length > 15 && (
176+
{threadList.length > 15 && (
179177
<button
180178
type="button"
181179
className="w-full text-left px-4 py-1.5 text-[12px] text-[#3a3a3a] hover:text-[#555] transition-colors"
182180
>
183-
+{projectThreads.length - 15} 条更多
181+
+{threadList.length - 15} 条更多
184182
</button>
185183
)}
186-
{projectThreads.length === 0 && (
184+
{threadList.length === 0 && (
187185
<div className="px-3 py-4 text-[13px] text-[#3a3a3a]">暂无对话</div>
188186
)}
189187
</div>

packages/desktop/src/agent/AgentWorkspace.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,11 +209,7 @@ function InputBox({
209209
const [text, setText] = useState('');
210210
const textareaRef = useRef<HTMLTextAreaElement>(null);
211211
const currentThreadId = useGlobalStore((s) => s.agent.currentThreadId);
212-
const isStreaming = useGlobalStore((s) => {
213-
const id = s.agent.currentThreadId;
214-
const turns = id ? s.agent.threads[id]?.turns : undefined;
215-
return turns ? turns.some((t) => t.status === 'running') : false;
216-
});
212+
const isStreaming = useGlobalStore((s) => s.agent.hasRunningTurn);
217213
const approvalPolicy = useGlobalStore((s) => s.agent.approvalPolicy);
218214
const workspace = useGlobalStore((s) => s.workspace);
219215
const setApprovalPolicy = useGlobalStore((s) => s.setApprovalPolicy);

packages/desktop/src/agent/ApprovalPanel.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useState } from 'react';
1+
import { useState, useMemo } from 'react';
22
import type { Item } from '@shared/types';
33
import { useGlobalStore } from '../stores/global.store';
4-
import { useAgent } from '../hooks/useAgent';
4+
import { useAgentApproval } from '../hooks/useAgent';
55
import ToolCallCard from '../shared/ToolCallCard';
66

77
interface ApprovalPanelProps {
@@ -10,15 +10,30 @@ interface ApprovalPanelProps {
1010

1111
export default function ApprovalPanel({ threadId }: ApprovalPanelProps) {
1212
const [collapsed, setCollapsed] = useState(false);
13-
const thread = useGlobalStore((s) => s.agent.threads[threadId]);
14-
const { approveTool, rejectTool } = useAgent();
13+
const { approveTool, rejectTool } = useAgentApproval();
1514

16-
const pendingItems =
17-
thread?.turns.flatMap((turn) =>
15+
// Stable string key: only changes when pending item IDs change, not on every content update
16+
const pendingKey = useGlobalStore((s) => {
17+
const thread = s.agent.threads[threadId];
18+
if (!thread) return '';
19+
return thread.turns
20+
.flatMap((t) => t.items)
21+
.filter((i) => i.type === 'tool_call' && i.status === 'pending')
22+
.map((i) => i.id)
23+
.join(',');
24+
});
25+
26+
// Only compute pending items when the key changes
27+
const pendingItems = useMemo(() => {
28+
if (!pendingKey) return [];
29+
const thread = useGlobalStore.getState().agent.threads[threadId];
30+
if (!thread) return [];
31+
return thread.turns.flatMap((turn) =>
1832
turn.items.filter(
1933
(i): i is Item & { type: 'tool_call' } => i.type === 'tool_call' && i.status === 'pending'
2034
)
21-
) ?? [];
35+
);
36+
}, [pendingKey, threadId]);
2237

2338
if (pendingItems.length === 0) return null;
2439

0 commit comments

Comments
 (0)