Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions apps/desktop/src/main/lib/workspace-ipcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,15 @@ export function registerWorkspaceIPCs() {
// Update preview tab
ipcMain.handle(
"tab-update-preview",
async (_event, input: UpdatePreviewTabInput) => {
return await workspaceManager.updatePreviewTab(input);
async (event, input: UpdatePreviewTabInput) => {
const result = await workspaceManager.updatePreviewTab(input);

// Emit event to notify renderer that workspace was updated
if (result.success && event.sender) {
event.sender.send("workspace-data-updated", input.workspaceId);
}

return result;
},
);

Expand All @@ -155,6 +162,22 @@ export function registerWorkspaceIPCs() {
},
);

// Clear all preview tab URLs in workspace
ipcMain.handle(
"workspace-clear-preview-urls",
async (_event, workspaceId: string) => {
return await workspaceManager.clearPreviewUrls(workspaceId);
},
);

// Clear all workspace state (worktrees, tabs, terminals)
ipcMain.handle(
"workspace-clear-state",
async (_event, workspaceId: string) => {
return await workspaceManager.clearWorkspaceState(workspaceId);
},
);

// Scan and import existing worktrees
ipcMain.handle(
"workspace-scan-worktrees",
Expand Down
107 changes: 107 additions & 0 deletions apps/desktop/src/main/lib/workspace-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import type {
Workspace,
Worktree,
} from "shared/types";
import configManager from "./config-manager";
import { proxyManager } from "./proxy-manager";
import terminalManager from "./terminal";
import * as tabOps from "./workspace/tab-operations";
import * as workspaceOps from "./workspace/workspace-operations";
import * as worktreeOps from "./workspace/worktree-operations";
Expand Down Expand Up @@ -320,6 +322,111 @@ class WorkspaceManager {
return tabOps.deleteTab(workspace, input);
}

/**
* Clear all preview tab URLs in workspace
*/
async clearPreviewUrls(
workspaceId: string,
): Promise<{ success: boolean; error?: string }> {
const workspace = await this.get(workspaceId);
if (!workspace) {
return { success: false, error: "Workspace not found" };
}

try {
// Recursively clear preview tab URLs
const clearTabUrls = (tabs: any[]): void => {
for (const tab of tabs) {
if (tab.type === "preview" || tab.type === "browser") {
tab.url = "";
}
if (tab.type === "group" && tab.tabs) {
clearTabUrls(tab.tabs);
}
}
};

for (const worktree of workspace.worktrees) {
clearTabUrls(worktree.tabs || []);
}

workspace.updatedAt = new Date().toISOString();

const config = configManager.read();
const index = config.workspaces.findIndex((ws) => ws.id === workspace.id);
if (index !== -1) {
config.workspaces[index] = workspace;
configManager.write(config);
}

return { success: true };
} catch (error) {
console.error("Failed to clear preview URLs:", error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}

/**
* Clear all workspace state - removes all worktrees, tabs, and terminals
*/
async clearWorkspaceState(
workspaceId: string,
): Promise<{ success: boolean; error?: string }> {
const workspace = await this.get(workspaceId);
if (!workspace) {
return { success: false, error: "Workspace not found" };
}

try {
// Close all terminals for all worktrees
for (const worktree of workspace.worktrees) {
const collectTabIds = (tabs: any[]): string[] => {
const ids: string[] = [];
for (const tab of tabs) {
ids.push(tab.id);
if (tab.type === "group" && tab.tabs) {
ids.push(...collectTabIds(tab.tabs));
}
}
return ids;
};

const tabIds = collectTabIds(worktree.tabs || []);
for (const tabId of tabIds) {
try {
await terminalManager.kill(tabId);
} catch (error) {
console.error(`Failed to kill terminal ${tabId}:`, error);
}
}
}

// Clear all worktrees
workspace.worktrees = [];
workspace.activeWorktreeId = null;
workspace.activeTabId = null;
workspace.updatedAt = new Date().toISOString();

const config = configManager.read();
const index = config.workspaces.findIndex((ws) => ws.id === workspace.id);
if (index !== -1) {
config.workspaces[index] = workspace;
configManager.write(config);
}

return { success: true };
} catch (error) {
console.error("Failed to clear workspace state:", error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}

/**
* Reorder tabs
*/
Expand Down
30 changes: 29 additions & 1 deletion apps/desktop/src/renderer/screens/main/MainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,31 @@ export function MainScreen() {
};
}, []);

// Listen for workspace data updates (e.g., when preview URLs change)
useEffect(() => {
const handler = async (workspaceId: string) => {
// Only reload if it's the current workspace
if (currentWorkspace?.id === workspaceId) {
console.log(
"[MainScreen] Workspace data updated, reloading:",
workspaceId,
);
const workspace = await window.ipcRenderer.invoke(
"workspace-get",
workspaceId,
);
if (workspace) {
setCurrentWorkspace(workspace);
}
}
};

window.ipcRenderer.on("workspace-data-updated", handler);
return () => {
window.ipcRenderer.off("workspace-data-updated", handler);
};
}, [currentWorkspace?.id]);

// Helper: recursively find a tab by ID
const findTabById = (tabs: Tab[], tabId: string): Tab | null => {
for (const tab of tabs) {
Expand Down Expand Up @@ -545,7 +570,10 @@ export function MainScreen() {
});

if (!moveResult.success) {
console.error("[MainScreen] Failed to move tab:", moveResult.error);
console.error(
"[MainScreen] Failed to move tab:",
moveResult.error,
);
return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,66 @@ export function Sidebar({
}
};

const handleClearPreviewUrls = async () => {
if (!currentWorkspace) return;

const confirmed = window.confirm(
"Clear all preview tab URLs in this workspace?\n\nThis will reset all preview tabs to empty state.",
);

if (!confirmed) return;

try {
const result = await window.ipcRenderer.invoke(
"workspace-clear-preview-urls",
currentWorkspace.id,
);

if (result.success) {
console.log("[Sidebar] Cleared preview URLs");
// Reload to reflect changes
window.location.reload();
} else {
alert(
`Failed to clear preview URLs: ${result.error || "Unknown error"}`,
);
}
} catch (error) {
console.error("[Sidebar] Error clearing preview URLs:", error);
alert(`Error: ${error instanceof Error ? error.message : String(error)}`);
}
};

const handleClearWorkspaceState = async () => {
if (!currentWorkspace) return;

const confirmed = window.confirm(
"Clear ALL workspace state?\n\nThis will:\n- Delete all worktrees from config\n- Close all terminals\n- Reset to fresh state\n\nGit worktrees on disk will NOT be deleted.",
);

if (!confirmed) return;

try {
const result = await window.ipcRenderer.invoke(
"workspace-clear-state",
currentWorkspace.id,
);

if (result.success) {
console.log("[Sidebar] Cleared workspace state");
// Reload to reflect changes
window.location.reload();
} else {
alert(
`Failed to clear workspace state: ${result.error || "Unknown error"}`,
);
}
} catch (error) {
console.error("[Sidebar] Error clearing workspace state:", error);
alert(`Error: ${error instanceof Error ? error.message : String(error)}`);
}
};

return (
<div className="flex flex-col h-full w-full select-none text-neutral-300">
<SidebarHeader
Expand Down Expand Up @@ -332,6 +392,24 @@ export function Sidebar({
)}
</WorkspaceCarousel>

{/* Debug buttons */}
{currentWorkspace && (
<div className="px-3 pb-2 space-y-2">
<button
onClick={handleClearPreviewUrls}
className="w-full text-xs text-neutral-500 hover:text-neutral-300 py-2 border border-neutral-800 rounded hover:bg-neutral-900 transition-colors"
>
Clear Preview URLs
</button>
<button
onClick={handleClearWorkspaceState}
className="w-full text-xs text-red-500 hover:text-red-300 py-2 border border-red-900 rounded hover:bg-red-950 transition-colors"
>
Clear All Workspace State
</button>
</div>
)}

<WorkspaceSwitcher
workspaces={workspaces}
currentWorkspaceId={currentWorkspace?.id || null}
Expand Down
Loading
Loading