Skip to content

Commit b8a8917

Browse files
authored
Fix desktop build and streamline thread UI (#334)
* Fix desktop build and runtime cleanup - Harden screenshot image loading cleanup - Simplify thread snapshot hydration and sidebar props - Fix widget notification event typing and test rename * Prune merged worktrees after thread cleanup (#335) * Clean up merged worktrees after thread deletion - Add merged worktree candidate detection and prune support - Auto-remove orphaned worktrees after merged thread cleanup - Surface cleanup state through the web client and WS contract * Add merged worktree cleanup dialog - Add a command-palette entry to review merged worktrees - Let users delete on-disk worktrees or prune stale Git records - Wire the dialog into the chat route and auto-delete flow * Improve merged worktree cleanup feedback - Use the active thread to choose the cleanup project - Rename prune action for stale records - Show an error toast when merged thread auto-delete fails * Format worktree cleanup and prune call - Reflow worktree cleanup dialog strings and layout - Reformat Git worktree prune call for consistency * Simplify merged worktree cleanup handling - Ignore prune failures silently during worktree cleanup - Add explicit git operation labels for merged-worktree cleanup calls
1 parent ac5d32b commit b8a8917

5 files changed

Lines changed: 48 additions & 40 deletions

File tree

apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ const ProjectionLatestTurnDbRowSchema = Schema.Struct({
8181
});
8282
const ProjectionStateDbRowSchema = ProjectionState;
8383

84+
function parseGithubRef(serialized: string | null): OrchestrationThread["githubRef"] | undefined {
85+
if (!serialized) return undefined;
86+
try {
87+
return JSON.parse(serialized) as OrchestrationThread["githubRef"];
88+
} catch {
89+
return undefined;
90+
}
91+
}
92+
8493
const REQUIRED_SNAPSHOT_PROJECTORS = [
8594
ORCHESTRATION_PROJECTOR_NAMES.projects,
8695
ORCHESTRATION_PROJECTOR_NAMES.threads,
@@ -543,39 +552,30 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
543552
deletedAt: row.deletedAt,
544553
}));
545554

546-
const threads: Array<OrchestrationThread> = threadRows.map((row) => {
547-
let githubRef: OrchestrationThread["githubRef"];
548-
try {
549-
if (row.githubRef) {
550-
githubRef = JSON.parse(row.githubRef);
551-
}
552-
} catch {
553-
// Ignore invalid JSON — treat as no ref
554-
}
555-
const thread = Object.assign(
556-
{
557-
id: row.threadId,
558-
projectId: row.projectId,
559-
title: row.title,
560-
model: row.model,
561-
runtimeMode: row.runtimeMode,
562-
interactionMode: row.interactionMode,
563-
branch: row.branch,
564-
worktreePath: row.worktreePath,
565-
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
566-
createdAt: row.createdAt,
567-
updatedAt: row.updatedAt,
568-
deletedAt: row.deletedAt,
569-
messages: messagesByThread.get(row.threadId) ?? [],
570-
proposedPlans: proposedPlansByThread.get(row.threadId) ?? [],
571-
activities: activitiesByThread.get(row.threadId) ?? [],
572-
checkpoints: checkpointsByThread.get(row.threadId) ?? [],
573-
session: sessionsByThread.get(row.threadId) ?? null,
574-
},
575-
githubRef ? { githubRef } : {},
576-
) as OrchestrationThread;
577-
return thread;
578-
});
555+
const threads: Array<OrchestrationThread> = [];
556+
for (const row of threadRows) {
557+
const githubRef = parseGithubRef(row.githubRef);
558+
const thread: OrchestrationThread = {
559+
id: row.threadId,
560+
projectId: row.projectId,
561+
title: row.title,
562+
model: row.model,
563+
runtimeMode: row.runtimeMode,
564+
interactionMode: row.interactionMode,
565+
branch: row.branch,
566+
worktreePath: row.worktreePath,
567+
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
568+
createdAt: row.createdAt,
569+
updatedAt: row.updatedAt,
570+
deletedAt: row.deletedAt,
571+
messages: messagesByThread.get(row.threadId) ?? [],
572+
proposedPlans: proposedPlansByThread.get(row.threadId) ?? [],
573+
activities: activitiesByThread.get(row.threadId) ?? [],
574+
checkpoints: checkpointsByThread.get(row.threadId) ?? [],
575+
session: sessionsByThread.get(row.threadId) ?? null,
576+
};
577+
threads.push(githubRef ? { ...thread, githubRef } : thread);
578+
}
579579

580580
const snapshot = {
581581
snapshotSequence: computeSnapshotSequence(stateRows),

apps/web/src/components/ScreenshotTool.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,20 @@ async function captureRegion(rect: {
8484
function loadImage(src: string): Promise<HTMLImageElement> {
8585
return new Promise((resolve, reject) => {
8686
const img = new Image();
87-
img.addEventListener("load", () => resolve(img), { once: true });
88-
img.addEventListener("error", reject, { once: true });
87+
const cleanup = () => {
88+
img.removeEventListener("load", onLoad);
89+
img.removeEventListener("error", onError);
90+
};
91+
const onLoad = () => {
92+
cleanup();
93+
resolve(img);
94+
};
95+
const onError = () => {
96+
cleanup();
97+
reject(new Error("Failed to load image"));
98+
};
99+
img.addEventListener("load", onLoad);
100+
img.addEventListener("error", onError);
89101
img.src = src;
90102
});
91103
}

apps/web/src/components/Sidebar.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ const SIDEBAR_THREAD_SORT_LABELS: Record<SidebarThreadSortOrder, string> = {
130130
updated_at: "Last user message",
131131
created_at: "Created at",
132132
};
133-
134133
interface PrStatusIndicator {
135134
label: "PR open" | "PR closed" | "PR merged";
136135
colorClass: string;
@@ -287,7 +286,6 @@ interface MemoizedThreadRowProps {
287286
thread: Thread;
288287
isActive: boolean;
289288
isSelected: boolean;
290-
routeThreadId: ThreadIdType | null;
291289
prByThreadId: Map<ThreadIdType, ThreadPr>;
292290
orderedProjectThreadIds: ThreadIdType[];
293291
selectedThreadIds: ReadonlySet<ThreadIdType>;
@@ -1339,7 +1337,6 @@ export default function Sidebar() {
13391337
thread={thread}
13401338
isActive={routeThreadId === thread.id}
13411339
isSelected={selectedThreadIds.has(thread.id)}
1342-
routeThreadId={routeThreadId}
13431340
prByThreadId={prByThreadId}
13441341
orderedProjectThreadIds={orderedProjectThreadIds}
13451342
selectedThreadIds={selectedThreadIds}

apps/web/src/components/widget/ChatWidgetShell.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import ThreadSidebar from "../Sidebar";
99
import { Sidebar, SidebarProvider, SidebarRail } from "../ui/sidebar";
1010
import { ChatWidgetBubble } from "./ChatWidgetBubble";
1111
import { ChatWidgetPanel } from "./ChatWidgetPanel";
12-
1312
const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width";
1413
const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16;
1514
const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16;
@@ -39,7 +38,7 @@ export function ChatWidgetShell() {
3938
useEffect(() => {
4039
const expand = useChatWidgetStore.getState().expand;
4140

42-
const onNotificationTap = (() => {
41+
const onNotificationTap = ((_event: CustomEvent<{ threadId?: string }>) => {
4342
expand();
4443
// Navigation to the thread is handled by the notification system.
4544
}) as EventListener;

0 commit comments

Comments
 (0)