Skip to content

Commit cf74c0b

Browse files
committed
feat(workspace): unify workspace tool surface
1 parent e6448b3 commit cf74c0b

11 files changed

Lines changed: 1548 additions & 2697 deletions

File tree

desktop/src/main/windows/workspace-tool-window.ts

Lines changed: 142 additions & 233 deletions
Large diffs are not rendered by default.

desktop/src/preload/bridges/desktop-window.ts

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,25 @@ import {
2929
DESKTOP_WINDOW_CLOSE_CURRENT_CHANNEL,
3030
DESKTOP_WORKSPACE_TOOL_CONTEXT_SET_CHANNEL,
3131
DESKTOP_WORKSPACE_TOOL_DIALOG_OPEN_CHANNEL,
32+
DESKTOP_WORKSPACE_TOOL_SURFACE_CHANGED_CHANNEL,
33+
DESKTOP_WORKSPACE_TOOL_SURFACE_CONTEXT_SET_CHANNEL,
34+
DESKTOP_WORKSPACE_TOOL_SURFACE_FOCUS_CHANNEL,
35+
DESKTOP_WORKSPACE_TOOL_SURFACE_GET_CHANNEL,
36+
DESKTOP_WORKSPACE_TOOL_SURFACE_HOST_SET_CHANNEL,
37+
DESKTOP_WORKSPACE_TOOL_SURFACE_SNAPSHOT_SET_CHANNEL,
3238
DESKTOP_WORKSPACE_TOOL_INSTANCE_CLOSE_CHANNEL,
3339
DESKTOP_WORKSPACE_TOOL_INSTANCE_REGISTER_CHANNEL,
3440
DESKTOP_WORKSPACE_TOOL_INSTANCE_UPDATED_CHANNEL,
3541
DESKTOP_WORKSPACE_TOOL_WINDOW_GET_CHANNEL,
3642
DESKTOP_WORKSPACE_TOOL_WINDOW_CLOSED_CHANNEL,
3743
DESKTOP_WORKSPACE_TOOL_WINDOW_OPEN_CHANNEL,
3844
} from "../../shared/desktop-window";
45+
import type {
46+
WorkspaceToolSurfaceContextSetInput,
47+
WorkspaceToolSurfaceHostSetInput,
48+
WorkspaceToolSurfaceSnapshotSetInput,
49+
WorkspaceToolSurfaceState,
50+
} from "../../shared/workspace-tool-surface";
3951

4052
export function exposeDesktopWindowBridge() {
4153
contextBridge.exposeInMainWorld("desktopWindow", {
@@ -100,10 +112,11 @@ export function exposeDesktopWindowBridge() {
100112
};
101113
},
102114
onWorkspaceToolWindowClosed(handler: (toolId: string) => void) {
103-
const listener = (_event: IpcRendererEvent, payload: unknown) => {
104-
if (isWorkspaceToolWindowClosedEvent(payload)) {
105-
handler(payload.toolId);
106-
}
115+
const listener = (
116+
_event: IpcRendererEvent,
117+
payload: WorkspaceToolInstanceCloseInput,
118+
) => {
119+
handler(payload.toolId);
107120
};
108121

109122
ipcRenderer.on(DESKTOP_WORKSPACE_TOOL_WINDOW_CLOSED_CHANNEL, listener);
@@ -117,10 +130,11 @@ export function exposeDesktopWindowBridge() {
117130
onWorkspaceToolDialogRequested(
118131
handler: (instance: WorkspaceToolInstance) => void,
119132
) {
120-
const listener = (_event: IpcRendererEvent, payload: unknown) => {
121-
if (isWorkspaceToolInstance(payload)) {
122-
handler(payload);
123-
}
133+
const listener = (
134+
_event: IpcRendererEvent,
135+
payload: WorkspaceToolInstance,
136+
) => {
137+
handler(payload);
124138
};
125139

126140
ipcRenderer.on(DESKTOP_WORKSPACE_TOOL_DIALOG_OPEN_CHANNEL, listener);
@@ -134,10 +148,11 @@ export function exposeDesktopWindowBridge() {
134148
onWorkspaceToolInstanceUpdated(
135149
handler: (instance: WorkspaceToolInstance) => void,
136150
) {
137-
const listener = (_event: IpcRendererEvent, payload: unknown) => {
138-
if (isWorkspaceToolInstance(payload)) {
139-
handler(payload);
140-
}
151+
const listener = (
152+
_event: IpcRendererEvent,
153+
payload: WorkspaceToolInstance,
154+
) => {
155+
handler(payload);
141156
};
142157

143158
ipcRenderer.on(DESKTOP_WORKSPACE_TOOL_INSTANCE_UPDATED_CHANNEL, listener);
@@ -148,6 +163,24 @@ export function exposeDesktopWindowBridge() {
148163
);
149164
};
150165
},
166+
onWorkspaceToolSurfaceChanged(
167+
handler: (state: WorkspaceToolSurfaceState) => void,
168+
) {
169+
const listener = (
170+
_event: IpcRendererEvent,
171+
payload: WorkspaceToolSurfaceState,
172+
) => {
173+
handler(payload);
174+
};
175+
176+
ipcRenderer.on(DESKTOP_WORKSPACE_TOOL_SURFACE_CHANGED_CHANNEL, listener);
177+
return () => {
178+
ipcRenderer.removeListener(
179+
DESKTOP_WORKSPACE_TOOL_SURFACE_CHANGED_CHANNEL,
180+
listener,
181+
);
182+
};
183+
},
151184
async installUpdate() {
152185
return ipcRenderer.invoke(DESKTOP_INSTALL_UPDATE_CHANNEL);
153186
},
@@ -160,6 +193,11 @@ export function exposeDesktopWindowBridge() {
160193
toolId,
161194
) as Promise<WorkspaceToolInstance | null>;
162195
},
196+
async getWorkspaceToolSurfaceState() {
197+
return ipcRenderer.invoke(
198+
DESKTOP_WORKSPACE_TOOL_SURFACE_GET_CHANNEL,
199+
) as Promise<WorkspaceToolSurfaceState>;
200+
},
163201
openWorkspaceToolWindow(input: WorkspaceToolWindowOpenInput) {
164202
ipcRenderer.send(DESKTOP_WORKSPACE_TOOL_WINDOW_OPEN_CHANNEL, input);
165203
},
@@ -172,6 +210,9 @@ export function exposeDesktopWindowBridge() {
172210
registerWorkspaceToolWindowInstance(input: WorkspaceToolWindowOpenInput) {
173211
ipcRenderer.send(DESKTOP_WORKSPACE_TOOL_INSTANCE_REGISTER_CHANNEL, input);
174212
},
213+
focusWorkspaceToolSurface() {
214+
ipcRenderer.send(DESKTOP_WORKSPACE_TOOL_SURFACE_FOCUS_CHANNEL);
215+
},
175216
setActiveChatId(chatId: string | null) {
176217
ipcRenderer.send(DESKTOP_ACTIVE_CHAT_SET_CHANNEL, chatId);
177218
},
@@ -181,24 +222,26 @@ export function exposeDesktopWindowBridge() {
181222
setWorkspaceToolContext(input: WorkspaceToolContextSetInput) {
182223
ipcRenderer.send(DESKTOP_WORKSPACE_TOOL_CONTEXT_SET_CHANNEL, input);
183224
},
225+
setWorkspaceToolSurfaceContext(input: WorkspaceToolSurfaceContextSetInput) {
226+
ipcRenderer.send(
227+
DESKTOP_WORKSPACE_TOOL_SURFACE_CONTEXT_SET_CHANNEL,
228+
input,
229+
);
230+
},
231+
setWorkspaceToolSurfaceHost(input: WorkspaceToolSurfaceHostSetInput) {
232+
ipcRenderer.send(DESKTOP_WORKSPACE_TOOL_SURFACE_HOST_SET_CHANNEL, input);
233+
},
234+
setWorkspaceToolSurfaceSnapshot(
235+
input: WorkspaceToolSurfaceSnapshotSetInput,
236+
) {
237+
ipcRenderer.send(
238+
DESKTOP_WORKSPACE_TOOL_SURFACE_SNAPSHOT_SET_CHANNEL,
239+
input,
240+
);
241+
},
184242
});
185243
}
186244

187-
function isWorkspaceToolWindowClosedEvent(
188-
value: unknown,
189-
): value is { toolId: string } {
190-
if (typeof value !== "object" || value === null) return false;
191-
return typeof (value as { toolId?: unknown }).toolId === "string";
192-
}
193-
194-
function isWorkspaceToolInstance(
195-
value: unknown,
196-
): value is WorkspaceToolInstance {
197-
if (typeof value !== "object" || value === null) return false;
198-
const instance = value as Partial<WorkspaceToolInstance>;
199-
return typeof instance.id === "string" && typeof instance.title === "string";
200-
}
201-
202245
function isDesktopWindowCommandEvent(
203246
value: unknown,
204247
): value is { command: DesktopWindowCommand } {

desktop/src/renderer/app/router.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ export function AppRouter() {
2121
</Route>
2222
<Route
2323
component={WorkspaceToolWindowRoutePage}
24-
path="/workspace-tool/:toolId"
24+
path="/workspace-tools"
2525
/>
26+
<Route path="/workspace-tool/:toolId">
27+
<Redirect replace to="/workspace-tools" />
28+
</Route>
2629
<Route component={ChatRoutePage} path="/chat/:chatId" />
2730
<Route
2831
component={ProjectChatRoutePage}
@@ -37,10 +40,8 @@ export function AppRouter() {
3740
);
3841
}
3942

40-
function WorkspaceToolWindowRoutePage({
41-
params,
42-
}: RouteComponentProps<{ toolId: string }>) {
43-
return <WorkspaceToolWindowPage toolId={params.toolId} />;
43+
function WorkspaceToolWindowRoutePage() {
44+
return <WorkspaceToolWindowPage />;
4445
}
4546

4647
function ChatRoutePage({ params }: RouteComponentProps<{ chatId: string }>) {

desktop/src/renderer/app/workspace/workspace-page.tsx

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import {
6969
WorkspaceToolContextBridge,
7070
WorkspaceToolDialogHost,
7171
} from "@/app/workspace/workspace-tool-host";
72+
import { useWorkspaceToolStore } from "@/app/workspace/workspace-tool-store";
7273
import { useWorkspaceUiStore } from "@/app/workspace/workspace-ui-store";
7374
import {
7475
SidebarInset,
@@ -214,13 +215,7 @@ function WorkspacePageContent({
214215
const rightSidebarWidth = useWorkspaceUiStore(
215216
(state) => state.rightSidebarWidth,
216217
);
217-
const rightSidebarActiveTab = useWorkspaceUiStore(
218-
(state) => state.rightSidebarActiveTab,
219-
);
220218
const workspaceMode = useWorkspaceUiStore((state) => state.workspaceMode);
221-
const setRightSidebarActiveTab = useWorkspaceUiStore(
222-
(state) => state.setRightSidebarActiveTab,
223-
);
224219
const setRightSidebarOpen = useWorkspaceUiStore(
225220
(state) => state.setRightSidebarOpen,
226221
);
@@ -234,6 +229,10 @@ function WorkspacePageContent({
234229
const toggleRightSidebar = useWorkspaceUiStore(
235230
(state) => state.toggleRightSidebar,
236231
);
232+
const workspaceToolHost = useWorkspaceToolStore((state) => state.host);
233+
const focusWorkspaceToolSurface = useWorkspaceToolStore(
234+
(state) => state.focusWorkspaceToolSurface,
235+
);
237236
const worktreeDirtyPromptEnabled = useSettingsStore(
238237
(state) => state.worktreeDirtyPromptEnabled,
239238
);
@@ -254,6 +253,7 @@ function WorkspacePageContent({
254253
);
255254
const showRightSidebar = workspaceMode === "work";
256255
const previousWorkspaceModeRef = useRef(workspaceMode);
256+
const previousWorkspaceToolHostRef = useRef(workspaceToolHost);
257257

258258
useEffect(() => {
259259
if (
@@ -318,10 +318,44 @@ function WorkspacePageContent({
318318
: selectedProjectPath
319319
? getProjectDisplayName(selectedProjectPath)
320320
: undefined;
321-
const hasProjectContext = Boolean(
322-
routeDraftProjectId ?? routeProjectId ?? selectedChat?.projectId,
323-
);
324-
const canShowRightSidebar = showRightSidebar && hasProjectContext;
321+
const workspaceToolRoot = selectedChatId ? selectedProjectPath : undefined;
322+
const canShowRightSidebar = showRightSidebar && Boolean(workspaceToolRoot);
323+
const dockedWorkspaceToolContext =
324+
canShowRightSidebar &&
325+
workspaceToolHost === "sidebar" &&
326+
selectedChatId &&
327+
workspaceToolRoot
328+
? {
329+
chatId: selectedChatId,
330+
root: workspaceToolRoot,
331+
}
332+
: null;
333+
useEffect(() => {
334+
if (
335+
previousWorkspaceToolHostRef.current !== "sidebar" &&
336+
workspaceToolHost === "sidebar" &&
337+
canShowRightSidebar &&
338+
selectedChatId &&
339+
workspaceToolRoot
340+
) {
341+
setRightSidebarOpen(true);
342+
}
343+
previousWorkspaceToolHostRef.current = workspaceToolHost;
344+
}, [
345+
canShowRightSidebar,
346+
selectedChatId,
347+
setRightSidebarOpen,
348+
workspaceToolHost,
349+
workspaceToolRoot,
350+
]);
351+
const toggleWorkspaceTools = useCallback(() => {
352+
if (workspaceToolHost !== "sidebar") {
353+
focusWorkspaceToolSurface();
354+
return;
355+
}
356+
357+
toggleRightSidebar();
358+
}, [focusWorkspaceToolSurface, toggleRightSidebar, workspaceToolHost]);
325359
const workspaceTitle = getWorkspaceTitle({
326360
selectedChat,
327361
selectedProjectName,
@@ -1037,7 +1071,10 @@ function WorkspacePageContent({
10371071
onClose={closeWorktreeDirtyPrompt}
10381072
state={worktreeDirtyPrompt}
10391073
/>
1040-
<WorkspaceToolContextBridge root={selectedProjectPath ?? undefined} />
1074+
<WorkspaceToolContextBridge
1075+
chatId={selectedChatId ?? null}
1076+
root={workspaceToolRoot ?? null}
1077+
/>
10411078
<WorkspaceToolDialogHost api={api} />
10421079

10431080
{settingsActive ? (
@@ -1055,10 +1092,13 @@ function WorkspacePageContent({
10551092
<SidebarInset className="h-svh max-h-svh overflow-hidden">
10561093
<WorkspaceHeader
10571094
attention={chatAttention}
1058-
rightSidebarOpen={canShowRightSidebar && rightSidebarOpen}
1095+
rightSidebarOpen={
1096+
canShowRightSidebar &&
1097+
(rightSidebarOpen || workspaceToolHost !== "sidebar")
1098+
}
10591099
title={workspaceTitle}
10601100
onToggleRightSidebar={
1061-
canShowRightSidebar ? toggleRightSidebar : undefined
1101+
canShowRightSidebar ? toggleWorkspaceTools : undefined
10621102
}
10631103
/>
10641104
<main className="flex min-h-0 flex-1 overflow-hidden">
@@ -1148,14 +1188,13 @@ function WorkspacePageContent({
11481188
/>
11491189
)}
11501190
</section>
1151-
{canShowRightSidebar ? (
1191+
{dockedWorkspaceToolContext ? (
11521192
<WorkspaceRightSidebar
1153-
activeTab={rightSidebarActiveTab}
11541193
api={api}
1194+
chatId={dockedWorkspaceToolContext.chatId}
11551195
open={rightSidebarOpen}
1156-
root={selectedProjectPath ?? undefined}
1196+
root={dockedWorkspaceToolContext.root}
11571197
width={rightSidebarWidth}
1158-
onTabChange={setRightSidebarActiveTab}
11591198
onWidthChange={setRightSidebarWidth}
11601199
/>
11611200
) : null}

0 commit comments

Comments
 (0)