Skip to content

Commit 8816ba5

Browse files
committed
Fix 5 bugs: project deletion in browser mode, hardcoded model string, dead code, server state overwrite, stale query caches
- Fix project deletion in browser mode by removing isElectron guard around api.projects.remove() call (Sidebar.tsx) - Replace hardcoded 'gpt-5-codex' with DEFAULT_MODEL / 'gpt-5.3-codex' in wsServer.ts and core reducer.ts - Remove unused bindTerminalEvents method and its imports from coreRuntime.ts - Preserve client-local terminal UI state (terminalOpen, terminalHeight, activeTerminalId, terminalGroups, activeTerminalGroupId) during SET_SERVER_STATE merges in store.ts - Invalidate git and checkpoint diff query caches when turn completions are detected in server state updates (__root.tsx)
1 parent ac44561 commit 8816ba5

File tree

6 files changed

+195
-108
lines changed

6 files changed

+195
-108
lines changed

apps/server/src/coreRuntime.ts

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import path from "node:path";
22
import crypto from "node:crypto";
3-
import type { ProviderEvent, ProviderSession, TerminalEvent } from "@t3tools/contracts";
4-
import { EffectFanout, OrchestrationEngine, QueueProjector, type AppViewState } from "@t3tools/core";
3+
import type { ProviderEvent, ProviderSession } from "@t3tools/contracts";
4+
import {
5+
EffectFanout,
6+
OrchestrationEngine,
7+
QueueProjector,
8+
type AppViewState,
9+
} from "@t3tools/core";
510
import { createSqliteStores } from "@t3tools/infra-sqlite";
611

712
import type { ProviderManager } from "./providerManager";
8-
import type { TerminalManager } from "./terminalManager";
913

1014
function nowIso(): string {
1115
return new Date().toISOString();
@@ -21,7 +25,11 @@ export class CoreRuntime {
2125
const dbPath = path.join(dbDir, "event-store.sqlite");
2226
this.stores = createSqliteStores(dbPath);
2327
this.projector = new QueueProjector(this.stores.projectionStore, this.fanout);
24-
this.engine = new OrchestrationEngine(this.stores.eventStore, this.stores.projectionStore, this.projector);
28+
this.engine = new OrchestrationEngine(
29+
this.stores.eventStore,
30+
this.stores.projectionStore,
31+
this.projector,
32+
);
2533
}
2634

2735
async start(cwd: string, projectName: string): Promise<void> {
@@ -57,22 +65,6 @@ export class CoreRuntime {
5765
});
5866
}
5967

60-
bindTerminalEvents(terminalManager: TerminalManager): void {
61-
terminalManager.on("event", (event: TerminalEvent) => {
62-
if (event.type !== "activity" && event.type !== "error" && event.type !== "exited") return;
63-
void this.dispatch({
64-
id: crypto.randomUUID(),
65-
type: "thread.setTerminalActivity",
66-
issuedAt: event.createdAt,
67-
payload: {
68-
threadId: event.threadId,
69-
terminalId: event.terminalId,
70-
running: event.type === "activity" ? event.hasRunningSubprocess : false,
71-
},
72-
});
73-
});
74-
}
75-
7668
async bindProviderSession(threadId: string, session: ProviderSession): Promise<void> {
7769
await this.dispatch({
7870
id: crypto.randomUUID(),

apps/server/src/wsServer.ts

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import crypto from "node:crypto";
77
import type { Duplex } from "node:stream";
88

99
import {
10+
DEFAULT_MODEL,
1011
EDITORS,
1112
WS_CHANNELS,
1213
WS_METHODS,
@@ -304,7 +305,8 @@ export function createServer(options: ServerOptions) {
304305
switch (request.method) {
305306
case WS_METHODS.providersStartSession: {
306307
const session = await providerManager.startSession(request.params as never);
307-
const uiThreadId = typeof paramsObj.uiThreadId === "string" ? paramsObj.uiThreadId : undefined;
308+
const uiThreadId =
309+
typeof paramsObj.uiThreadId === "string" ? paramsObj.uiThreadId : undefined;
308310
if (uiThreadId) {
309311
await coreRuntime.bindProviderSession(uiThreadId, session);
310312
}
@@ -313,8 +315,11 @@ export function createServer(options: ServerOptions) {
313315

314316
case WS_METHODS.providersSendTurn: {
315317
const sessionId = typeof paramsObj.sessionId === "string" ? paramsObj.sessionId : undefined;
316-
const uiThreadId = typeof paramsObj.uiThreadId === "string" ? paramsObj.uiThreadId : undefined;
317-
const targetThread = uiThreadId ? state.threads.find((thread) => thread.id === uiThreadId) : findThreadBySessionId(sessionId);
318+
const uiThreadId =
319+
typeof paramsObj.uiThreadId === "string" ? paramsObj.uiThreadId : undefined;
320+
const targetThread = uiThreadId
321+
? state.threads.find((thread) => thread.id === uiThreadId)
322+
: findThreadBySessionId(sessionId);
318323
const inputText = typeof paramsObj.input === "string" ? paramsObj.input : undefined;
319324
if (targetThread && inputText && inputText.trim().length > 0) {
320325
await coreRuntime.dispatch({
@@ -363,52 +368,49 @@ export function createServer(options: ServerOptions) {
363368
case WS_METHODS.projectsList:
364369
return projectRegistry.list();
365370

366-
case WS_METHODS.projectsAdd:
367-
{
368-
const result = projectRegistry.add(request.params as never);
371+
case WS_METHODS.projectsAdd: {
372+
const result = projectRegistry.add(request.params as never);
373+
await coreRuntime.dispatch({
374+
id: crypto.randomUUID(),
375+
type: "project.add",
376+
issuedAt: requestNow,
377+
payload: {
378+
id: result.project.id,
379+
name: result.project.name,
380+
cwd: result.project.cwd,
381+
model: DEFAULT_MODEL,
382+
scripts: result.project.scripts,
383+
},
384+
});
385+
return result;
386+
}
387+
388+
case WS_METHODS.projectsRemove: {
389+
const projectId = typeof paramsObj.id === "string" ? paramsObj.id : undefined;
390+
if (projectId) {
369391
await coreRuntime.dispatch({
370392
id: crypto.randomUUID(),
371-
type: "project.add",
393+
type: "project.remove",
372394
issuedAt: requestNow,
373-
payload: {
374-
id: result.project.id,
375-
name: result.project.name,
376-
cwd: result.project.cwd,
377-
model: "gpt-5-codex",
378-
scripts: result.project.scripts,
379-
},
395+
payload: { id: projectId },
380396
});
381-
return result;
382397
}
383-
384-
case WS_METHODS.projectsRemove:
385-
{
386-
const projectId = typeof paramsObj.id === "string" ? paramsObj.id : undefined;
387-
if (projectId) {
388-
await coreRuntime.dispatch({
389-
id: crypto.randomUUID(),
390-
type: "project.remove",
391-
issuedAt: requestNow,
392-
payload: { id: projectId },
393-
});
394-
}
395398
projectRegistry.remove(request.params as never);
396399
return undefined;
397-
}
400+
}
398401

399402
case WS_METHODS.projectsSearchEntries:
400403
return searchWorkspaceEntries(request.params as never);
401-
case WS_METHODS.projectsUpdateScripts:
402-
{
403-
const result = projectRegistry.updateScripts(request.params as never);
404-
await coreRuntime.dispatch({
405-
id: crypto.randomUUID(),
406-
type: "project.updateScripts",
407-
issuedAt: requestNow,
408-
payload: { id: result.project.id, scripts: result.project.scripts },
409-
});
410-
return result;
411-
}
404+
case WS_METHODS.projectsUpdateScripts: {
405+
const result = projectRegistry.updateScripts(request.params as never);
406+
await coreRuntime.dispatch({
407+
id: crypto.randomUUID(),
408+
type: "project.updateScripts",
409+
issuedAt: requestNow,
410+
payload: { id: result.project.id, scripts: result.project.scripts },
411+
});
412+
return result;
413+
}
412414

413415
case WS_METHODS.shellOpenInEditor: {
414416
const params = request.params as {

apps/web/src/components/Sidebar.tsx

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -379,22 +379,18 @@ export default function Sidebar() {
379379
);
380380
if (!confirmed) return;
381381

382-
if (isElectron) {
383-
try {
384-
await api.projects.remove({ id: projectId });
385-
} catch (error) {
386-
const message =
387-
error instanceof Error ? error.message : "Unknown error deleting project.";
388-
console.error("Failed to remove project", { projectId, error });
389-
toastManager.add({
390-
type: "error",
391-
title: `Failed to delete "${project.name}"`,
392-
description: message,
393-
});
394-
return;
395-
}
382+
try {
383+
await api.projects.remove({ id: projectId });
384+
} catch (error) {
385+
const message = error instanceof Error ? error.message : "Unknown error deleting project.";
386+
console.error("Failed to remove project", { projectId, error });
387+
toastManager.add({
388+
type: "error",
389+
title: `Failed to delete "${project.name}"`,
390+
description: message,
391+
});
392+
return;
396393
}
397-
398394
},
399395
[api, dispatch, state.projects, state.threads],
400396
);

apps/web/src/routes/__root.tsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import {
44
type ErrorComponentProps,
55
useParams,
66
} from "@tanstack/react-router";
7-
import { useEffect } from "react";
8-
import { QueryClient } from "@tanstack/react-query";
7+
import { useEffect, useRef } from "react";
8+
import { QueryClient, useQueryClient } from "@tanstack/react-query";
99

1010
import { APP_DISPLAY_NAME } from "../branding";
1111
import { Button } from "../components/ui/button";
1212
import { AnchoredToastProvider, ToastProvider } from "../components/ui/toast";
1313
import { isElectron } from "../env";
1414
import { useNativeApi } from "../hooks/useNativeApi";
15+
import { invalidateGitQueries } from "../lib/gitReactQuery";
16+
import { providerQueryKeys } from "../lib/providerReactQuery";
1517
import { type AppState, useStore } from "../store";
1618
import { onServerStateUpdate, onServerWelcome } from "../wsNativeApi";
1719

@@ -123,10 +125,12 @@ function errorDetails(error: unknown): string {
123125
function EventRouter() {
124126
const api = useNativeApi();
125127
const { dispatch } = useStore();
128+
const queryClient = useQueryClient();
126129
const activeThreadId = useParams({
127130
strict: false,
128131
select: (params) => params.threadId,
129132
});
133+
const prevTurnCompletionsRef = useRef<Map<string, string | undefined>>(new Map());
130134

131135
useEffect(() => {
132136
if (!api) return;
@@ -142,9 +146,28 @@ function EventRouter() {
142146

143147
useEffect(() => {
144148
return onServerStateUpdate((snapshot) => {
145-
dispatch({ type: "SET_SERVER_STATE", state: snapshot as AppState });
149+
const serverState = snapshot as AppState;
150+
dispatch({ type: "SET_SERVER_STATE", state: serverState });
151+
152+
let hasNewTurnCompletion = false;
153+
for (const thread of serverState.threads) {
154+
const prev = prevTurnCompletionsRef.current.get(thread.id);
155+
if (thread.latestTurnCompletedAt && thread.latestTurnCompletedAt !== prev) {
156+
hasNewTurnCompletion = true;
157+
}
158+
}
159+
const nextMap = new Map<string, string | undefined>();
160+
for (const thread of serverState.threads) {
161+
nextMap.set(thread.id, thread.latestTurnCompletedAt);
162+
}
163+
prevTurnCompletionsRef.current = nextMap;
164+
165+
if (hasNewTurnCompletion) {
166+
void invalidateGitQueries(queryClient);
167+
void queryClient.invalidateQueries({ queryKey: providerQueryKeys.all });
168+
}
146169
});
147-
}, [dispatch]);
170+
}, [dispatch, queryClient]);
148171

149172
useEffect(() => {
150173
if (!activeThreadId) return;

apps/web/src/store.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ function normalizeRunningTerminalIds(
127127
.filter((id) => id.length > 0 && validTerminalIdSet.has(id));
128128
}
129129

130-
131130
function normalizeTerminalGroupIds(terminalIds: string[]): string[] {
132131
return [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))];
133132
}
@@ -297,11 +296,26 @@ function closeThreadTerminal(thread: Thread, terminalId: string): Thread {
297296

298297
export function reducer(state: AppState, action: Action): AppState {
299298
switch (action.type) {
300-
case "SET_SERVER_STATE":
299+
case "SET_SERVER_STATE": {
300+
const previousThreadById = new Map(
301+
state.threads.map((thread) => [thread.id, thread] as const),
302+
);
301303
return {
302304
...action.state,
303-
threads: action.state.threads.map((thread) => normalizeThreadTerminals(thread)),
305+
threads: action.state.threads.map((thread) => {
306+
const prev = previousThreadById.get(thread.id);
307+
if (!prev) return normalizeThreadTerminals(thread);
308+
return normalizeThreadTerminals({
309+
...thread,
310+
terminalOpen: prev.terminalOpen,
311+
terminalHeight: prev.terminalHeight,
312+
activeTerminalId: prev.activeTerminalId,
313+
terminalGroups: prev.terminalGroups,
314+
activeTerminalGroupId: prev.activeTerminalGroupId,
315+
});
316+
}),
304317
};
318+
}
305319

306320
case "ADD_PROJECT":
307321
if (state.projects.some((project) => project.cwd === action.project.cwd)) {
@@ -447,10 +461,7 @@ export function reducer(state: AppState, action: Action): AppState {
447461
threads: updateThread(state.threads, action.threadId, (thread) => {
448462
const normalizedThread = normalizeThreadTerminals(thread);
449463
const isNewTerminal = !normalizedThread.terminalIds.includes(action.terminalId);
450-
if (
451-
isNewTerminal &&
452-
normalizedThread.terminalIds.length >= MAX_THREAD_TERMINAL_COUNT
453-
) {
464+
if (isNewTerminal && normalizedThread.terminalIds.length >= MAX_THREAD_TERMINAL_COUNT) {
454465
return normalizedThread;
455466
}
456467
const terminalIds = normalizedThread.terminalIds.includes(action.terminalId)
@@ -520,10 +531,7 @@ export function reducer(state: AppState, action: Action): AppState {
520531
threads: updateThread(state.threads, action.threadId, (thread) => {
521532
const normalizedThread = normalizeThreadTerminals(thread);
522533
const isNewTerminal = !normalizedThread.terminalIds.includes(action.terminalId);
523-
if (
524-
isNewTerminal &&
525-
normalizedThread.terminalIds.length >= MAX_THREAD_TERMINAL_COUNT
526-
) {
534+
if (isNewTerminal && normalizedThread.terminalIds.length >= MAX_THREAD_TERMINAL_COUNT) {
527535
return normalizedThread;
528536
}
529537
const terminalIds = normalizedThread.terminalIds.includes(action.terminalId)

0 commit comments

Comments
 (0)