From 56a7856dafff976ff36a700cb0930df5814b9340 Mon Sep 17 00:00:00 2001 From: QuinnWan <144975606+somewan820@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:04:51 +0800 Subject: [PATCH 01/22] feat(desktop): expose the Work Board store through main-process IPC Phase 1 slice 1: the Desktop main process owns WorkBoardStore and registers workBoard:list/create/update/archive/unarchive/remove handlers plus a workBoard:changed signal. Renderer code stays read-only through IPC; Runtime Host and model tools are not involved. Generated-by: Codex --- .../__tests__/work-board-ipc-main.test.ts | 193 ++++++++++++++++++ apps/desktop/src/main/runtime-host-boot.ts | 2 + apps/desktop/src/main/work-board-ipc-main.ts | 162 +++++++++++++++ 3 files changed, 357 insertions(+) create mode 100644 apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts create mode 100644 apps/desktop/src/main/work-board-ipc-main.ts diff --git a/apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts b/apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts new file mode 100644 index 0000000000..11c4588253 --- /dev/null +++ b/apps/desktop/src/main/__tests__/work-board-ipc-main.test.ts @@ -0,0 +1,193 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, test } from 'node:test'; +import type { IpcMain } from 'electron'; +import { + registerWorkBoardIpc, + type WorkBoardChangedEvent, + type WorkBoardIpcResult, +} from '../work-board-ipc-main.js'; + +interface FakeIpcMain { + handle(channel: string, handler: (...args: unknown[]) => unknown): void; + invoke(channel: string, ...args: unknown[]): Promise; +} + +function createFakeIpcMain(): FakeIpcMain & { readonly channels: string[] } { + const handlers = new Map unknown>(); + const channels: string[] = []; + return { + channels, + handle(channel, handler) { + channels.push(channel); + handlers.set(channel, handler); + }, + invoke(channel: string, ...args: unknown[]): Promise { + const handler = handlers.get(channel); + if (!handler) throw new Error(`No handler registered for ${channel}`); + return Promise.resolve(handler(undefined, ...args)) as Promise; + }, + }; +} + +function createFakeWindowController(): { + readonly events: Array<{ channel: string; args: unknown[] }>; + send(channel: string, ...args: unknown[]): void; +} { + const events: Array<{ channel: string; args: unknown[] }> = []; + return { + events, + send(channel, ...args) { + events.push({ channel, args }); + }, + }; +} + +function itemInput(): { + scope: { kind: 'inbox' }; + title: string; + creator: { kind: 'user' }; + provenance: { kind: 'manual' }; +} { + return { + scope: { kind: 'inbox' }, + title: 'Review auth', + creator: { kind: 'user' }, + provenance: { kind: 'manual' }, + }; +} + +async function withTempRoot(run: (root: string) => Promise): Promise { + const root = await mkdtemp(join(tmpdir(), 'maka-work-board-ipc-')); + try { + await run(root); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +describe('Work Board IPC', () => { + test('creates and lists items and emits change signals', async () => { + await withTempRoot(async (root) => { + const ipc = createFakeIpcMain(); + const window = createFakeWindowController(); + registerWorkBoardIpc({ + ipcMain: ipc as unknown as Pick, + workspaceRoot: root, + mainWindowController: window, + }); + + const created = await ipc.invoke>( + 'workBoard:create', + itemInput(), + ); + assert.equal(created.ok, true); + assert.ok(created.ok && created.value.id); + + const page = await ipc.invoke }>>( + 'workBoard:list', + {}, + ); + assert.equal(page.ok, true); + assert.ok(page.ok); + assert.equal(page.value.items.length, 1); + assert.equal(page.value.items[0]?.id, created.ok ? created.value.id : undefined); + + const changed = window.events.filter( + (event) => event.channel === 'workBoard:changed', + ); + assert.equal(changed.length, 1); + const event = changed[0]?.args[0] as WorkBoardChangedEvent; + assert.equal(event.type, 'work_board_changed'); + assert.ok(typeof event.ts === 'number'); + assert.deepEqual(ipc.channels, [ + 'workBoard:list', + 'workBoard:create', + 'workBoard:update', + 'workBoard:archive', + 'workBoard:unarchive', + 'workBoard:remove', + ]); + }); + }); + + test('applies lifecycle mutations and fails closed on invalid input', async () => { + await withTempRoot(async (root) => { + const ipc = createFakeIpcMain(); + const window = createFakeWindowController(); + registerWorkBoardIpc({ + ipcMain: ipc as unknown as Pick, + workspaceRoot: root, + mainWindowController: window, + }); + + const created = await ipc.invoke>( + 'workBoard:create', + itemInput(), + ); + assert.ok(created.ok); + const id = created.ok ? created.value.id : ''; + + const renamed = await ipc.invoke< + WorkBoardIpcResult<{ title: string; revision: number; state: string }> + >('workBoard:update', id, { title: 'Review auth v2' }); + assert.ok(renamed.ok); + assert.equal(renamed.ok && renamed.value.revision, 2); + + const removedBeforeArchive = await ipc.invoke>( + 'workBoard:remove', + id, + ); + assert.equal(removedBeforeArchive.ok, false); + if (!removedBeforeArchive.ok) { + assert.equal(removedBeforeArchive.code, 'must_archive_first'); + } + + const archived = await ipc.invoke< + WorkBoardIpcResult<{ archived: boolean; revision: number }> + >('workBoard:archive', id); + assert.ok(archived.ok); + assert.equal(archived.ok && archived.value.archived, true); + + const reopened = await ipc.invoke< + WorkBoardIpcResult<{ archived: boolean; revision: number }> + >('workBoard:unarchive', id); + assert.ok(reopened.ok); + assert.equal(reopened.ok && reopened.value.archived, false); + + const invalidPatch = await ipc.invoke>( + 'workBoard:update', + id, + { titel: 'x' }, + ); + assert.equal(invalidPatch.ok, false); + if (!invalidPatch.ok) assert.equal(invalidPatch.code, 'invalid_input'); + + const invalidCreate = await ipc.invoke>( + 'workBoard:create', + { ...itemInput(), notes: null }, + ); + assert.equal(invalidCreate.ok, false); + if (!invalidCreate.ok) assert.equal(invalidCreate.code, 'invalid_input'); + + await ipc.invoke('workBoard:archive', id); + const removed = await ipc.invoke>('workBoard:remove', id); + assert.ok(removed.ok); + + const page = await ipc.invoke>( + 'workBoard:list', + {}, + ); + assert.ok(page.ok); + assert.equal(page.ok && page.value.items.length, 0); + + // create, update, archive, unarchive, archive, remove = 6 mutations + const changed = window.events.filter( + (event) => event.channel === 'workBoard:changed', + ); + assert.equal(changed.length, 6); + }); + }); +}); diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index 9d67cf7ac5..78cbab6f8a 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -121,6 +121,7 @@ import { import { registerNotificationsIpc } from "./notifications-ipc-main.js"; import { registerMarkdownSaveIpc } from "./markdown-save-ipc-main.js"; import { registerPetPackIpc } from "./pet-pack-import.js"; +import { registerWorkBoardIpc } from "./work-board-ipc-main.js"; import { createPermissionOverlayMain, registerPermissionOverlayIpc, @@ -665,6 +666,7 @@ mcpManager.onChange(() => { registerPersistentClientIpc(); registerPetPackIpc({ ipcMain, workspaceRoot, mainWindowController, settingsStore }); +registerWorkBoardIpc({ ipcMain, workspaceRoot, mainWindowController }); const browserIpc = registerBrowserIpc({ mainWindowController, isHostActive: (scope) => runtimeHostManager?.ownsScope(scope) === true, diff --git a/apps/desktop/src/main/work-board-ipc-main.ts b/apps/desktop/src/main/work-board-ipc-main.ts new file mode 100644 index 0000000000..561ceba46d --- /dev/null +++ b/apps/desktop/src/main/work-board-ipc-main.ts @@ -0,0 +1,162 @@ +import type { IpcMain } from 'electron'; +import type { WorkBoardItem, WorkBoardPage } from '@maka/core/work-board'; +import { + createWorkBoardStore, + WorkBoardStoreError, + type WorkBoardStore, + type WorkBoardStoreErrorCode, + type WorkBoardMutationOptions, +} from '@maka/storage/work-board-store'; +import type { createMainWindowController } from './main-window.js'; + +type MainWindowController = Pick, 'send'>; + +export type WorkBoardIpcResult = + | { readonly ok: true; readonly value: T } + | { + readonly ok: false; + readonly code: WorkBoardStoreErrorCode | 'unknown'; + readonly message: string; + }; + +export interface WorkBoardChangedEvent { + readonly type: 'work_board_changed'; + readonly ts: number; +} + +/** + * Desktop main process owns the Work Board store (the v1 mutation boundary). + * Renderer code reads a projection through IPC and reloads on the change + * signal; the Runtime Host and model tools are intentionally not involved. + */ +export function registerWorkBoardIpc(input: { + readonly ipcMain: Pick; + readonly workspaceRoot: string; + readonly mainWindowController: MainWindowController; + readonly store?: WorkBoardStore; + readonly now?: () => number; +}): void { + const store = input.store ?? createWorkBoardStore(input.workspaceRoot); + const now = input.now ?? Date.now; + const emitChanged = (): void => { + input.mainWindowController.send('workBoard:changed', { + type: 'work_board_changed', + ts: now(), + } satisfies WorkBoardChangedEvent); + }; + + input.ipcMain.handle( + 'workBoard:list', + async (_event, query: unknown): Promise> => { + try { + return { ok: true, value: await store.list(query) }; + } catch (error) { + return { ok: false, ...workBoardFailure(error) }; + } + }, + ); + + input.ipcMain.handle( + 'workBoard:create', + async (_event, item: unknown): Promise> => { + try { + const created = await store.create(item); + emitChanged(); + return { ok: true, value: created }; + } catch (error) { + return { ok: false, ...workBoardFailure(error) }; + } + }, + ); + + input.ipcMain.handle( + 'workBoard:update', + async ( + _event, + id: unknown, + patch: unknown, + options?: unknown, + ): Promise> => { + try { + const updated = await store.update( + requireWorkBoardId(id), + patch, + options as WorkBoardMutationOptions | undefined, + ); + emitChanged(); + return { ok: true, value: updated }; + } catch (error) { + return { ok: false, ...workBoardFailure(error) }; + } + }, + ); + + input.ipcMain.handle( + 'workBoard:archive', + async (_event, id: unknown, options?: unknown): Promise> => { + try { + const archived = await store.archive( + requireWorkBoardId(id), + options as WorkBoardMutationOptions | undefined, + ); + emitChanged(); + return { ok: true, value: archived }; + } catch (error) { + return { ok: false, ...workBoardFailure(error) }; + } + }, + ); + + input.ipcMain.handle( + 'workBoard:unarchive', + async (_event, id: unknown, options?: unknown): Promise> => { + try { + const unarchived = await store.unarchive( + requireWorkBoardId(id), + options as WorkBoardMutationOptions | undefined, + ); + emitChanged(); + return { ok: true, value: unarchived }; + } catch (error) { + return { ok: false, ...workBoardFailure(error) }; + } + }, + ); + + input.ipcMain.handle( + 'workBoard:remove', + async ( + _event, + id: unknown, + options?: unknown, + ): Promise> => { + try { + await store.remove(requireWorkBoardId(id), options as WorkBoardMutationOptions | undefined); + emitChanged(); + return { ok: true, value: null }; + } catch (error) { + return { ok: false, ...workBoardFailure(error) }; + } + }, + ); +} + +function requireWorkBoardId(id: unknown): string { + if (typeof id !== 'string') { + throw new WorkBoardStoreError('invalid_input', 'Work Board item id must be a string'); + } + return id; +} + +function workBoardFailure(error: unknown): { + readonly code: WorkBoardStoreErrorCode | 'unknown'; + readonly message: string; +} { + if (error instanceof WorkBoardStoreError) { + return { code: error.code, message: error.message }; + } + return { + code: 'unknown', + message: error instanceof Error ? error.message : 'Work Board operation failed', + }; +} From 0e05f60b933a9b3256483075374757c7556a3a40 Mon Sep 17 00:00:00 2001 From: QuinnWan <144975606+somewan820@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:11:06 +0800 Subject: [PATCH 02/22] feat(desktop): expose Work Board IPC through the preload bridge Phase 1 slice 2: shared IPC result/change types, window.maka.workBoard namespace in the preload bridge, and a renderer useWorkBoard hook that reloads on the workBoard:changed signal. Generated-by: Codex --- apps/desktop/src/main/work-board-ipc-main.ts | 16 +-- apps/desktop/src/preload/bridge-contract.d.ts | 23 ++++ apps/desktop/src/preload/preload.ts | 27 +++++ apps/desktop/src/renderer/use-work-board.ts | 113 ++++++++++++++++++ apps/desktop/src/shared/work-board-ipc.ts | 14 +++ 5 files changed, 180 insertions(+), 13 deletions(-) create mode 100644 apps/desktop/src/renderer/use-work-board.ts create mode 100644 apps/desktop/src/shared/work-board-ipc.ts diff --git a/apps/desktop/src/main/work-board-ipc-main.ts b/apps/desktop/src/main/work-board-ipc-main.ts index 561ceba46d..ae99945fcf 100644 --- a/apps/desktop/src/main/work-board-ipc-main.ts +++ b/apps/desktop/src/main/work-board-ipc-main.ts @@ -8,21 +8,11 @@ import { type WorkBoardMutationOptions, } from '@maka/storage/work-board-store'; import type { createMainWindowController } from './main-window.js'; +import type { WorkBoardChangedEvent, WorkBoardIpcResult } from '../shared/work-board-ipc.js'; -type MainWindowController = Pick, 'send'>; - -export type WorkBoardIpcResult = - | { readonly ok: true; readonly value: T } - | { - readonly ok: false; - readonly code: WorkBoardStoreErrorCode | 'unknown'; - readonly message: string; - }; +export type { WorkBoardChangedEvent, WorkBoardIpcResult } from '../shared/work-board-ipc.js'; -export interface WorkBoardChangedEvent { - readonly type: 'work_board_changed'; - readonly ts: number; -} +type MainWindowController = Pick, 'send'>; /** * Desktop main process owns the Work Board store (the v1 mutation boundary). diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index f05bdac90d..f7f1d862c3 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -101,6 +101,8 @@ import type { DesktopTranscriptHandle, } from './transcript-contract.js'; import type { PetPackManifestV1 } from '@maka/core/pet'; +import type { WorkBoardItem, WorkBoardListQuery, WorkBoardPage } from '@maka/core/work-board'; +import type { WorkBoardMutationOptions } from '@maka/storage/work-board-store'; import type { OperationInput, OperationOutput, @@ -149,6 +151,7 @@ export type AppIconImportResult = }; export type { DesktopSessionSummary } from '../shared/desktop-session-projection.js'; +export type { WorkBoardChangedEvent, WorkBoardIpcResult } from '../shared/work-board-ipc.js'; import type { DesktopConnectionSnapshot } from '../shared/desktop-connection-snapshot.js'; import type { DesktopExternalSessionCatalogItem } from './external-session-catalog.js'; import type { DesktopDiagnosticInput } from './diagnostics-contract.js'; @@ -657,6 +660,26 @@ export interface MakaBridge { subscribeChanges(handler: (event: PetPackChangedEvent) => void): () => void; }; + workBoard: { + list(query?: WorkBoardListQuery): Promise>; + create(item: unknown): Promise>; + update( + id: string, + patch: unknown, + options?: WorkBoardMutationOptions, + ): Promise>; + archive( + id: string, + options?: WorkBoardMutationOptions, + ): Promise>; + unarchive( + id: string, + options?: WorkBoardMutationOptions, + ): Promise>; + remove(id: string, options?: WorkBoardMutationOptions): Promise>; + subscribeChanges(handler: (event: WorkBoardChangedEvent) => void): () => void; + }; + tasks: { list(sessionId: string): Promise; subscribeChanges(handler: (event: TaskLedgerChangedEvent) => void): () => void; diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index 67b2c15bb7..cd01d8d958 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -36,6 +36,7 @@ import type { AppUpdateStatus, WindowCommand, PetPackChangedEvent, + WorkBoardChangedEvent, DesktopRuntimeHostProfileAddInput, DesktopRuntimeHostProfileChangedEvent, DesktopRuntimeHostProfileSnapshot, @@ -1341,6 +1342,32 @@ const makaBridge = { return () => ipcRenderer.off('pets:changed', listener); }, }, + workBoard: { + list(query) { + return ipcRenderer.invoke('workBoard:list', query); + }, + create(item) { + return ipcRenderer.invoke('workBoard:create', item); + }, + update(id, patch, options) { + return ipcRenderer.invoke('workBoard:update', id, patch, options); + }, + archive(id, options) { + return ipcRenderer.invoke('workBoard:archive', id, options); + }, + unarchive(id, options) { + return ipcRenderer.invoke('workBoard:unarchive', id, options); + }, + remove(id, options) { + return ipcRenderer.invoke('workBoard:remove', id, options); + }, + subscribeChanges(handler: (event: WorkBoardChangedEvent) => void): () => void { + const listener = (_event: Electron.IpcRendererEvent, payload: WorkBoardChangedEvent) => + handler(payload); + ipcRenderer.on('workBoard:changed', listener); + return () => ipcRenderer.off('workBoard:changed', listener); + }, + }, tasks: { list(sessionId: string): Promise { return invokeProjectedSessionRuntimeHost('tasks:list', sessionId); diff --git a/apps/desktop/src/renderer/use-work-board.ts b/apps/desktop/src/renderer/use-work-board.ts new file mode 100644 index 0000000000..0c43f073a6 --- /dev/null +++ b/apps/desktop/src/renderer/use-work-board.ts @@ -0,0 +1,113 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import type { + CreateWorkBoardItemInput, + UpdateWorkBoardItemInput, + WorkBoardItem, + WorkBoardListQuery, +} from '@maka/core/work-board'; +import type { WorkBoardMutationOptions } from '@maka/storage/work-board-store'; +import type { WorkBoardChangedEvent, WorkBoardIpcResult } from '../shared/work-board-ipc.js'; + +interface WorkBoardSnapshot { + items: WorkBoardItem[]; + loading: boolean; + error?: string; +} + +export interface UseWorkBoardResult extends WorkBoardSnapshot { + retry(): void; + create(input: CreateWorkBoardItemInput): Promise; + update( + id: string, + patch: UpdateWorkBoardItemInput, + options?: WorkBoardMutationOptions, + ): Promise; + archive(id: string, options?: WorkBoardMutationOptions): Promise; + unarchive(id: string, options?: WorkBoardMutationOptions): Promise; + remove(id: string, options?: WorkBoardMutationOptions): Promise; +} + +const EMPTY_SNAPSHOT: WorkBoardSnapshot = { + items: [], + loading: false, +}; + +function requireResult(result: WorkBoardIpcResult): T { + if (result.ok) return result.value; + throw new Error(result.message); +} + +/** + * Read-only renderer projection of the Work Board store. All mutations are + * routed through the Desktop main process; the change signal triggers a reload + * so the panel never caches a second execution authority. + */ +export function useWorkBoard(query?: WorkBoardListQuery): UseWorkBoardResult { + const revisionRef = useRef(0); + const [snapshot, setSnapshot] = useState(EMPTY_SNAPSHOT); + + const load = useCallback( + (preserveItems: boolean) => { + const revision = ++revisionRef.current; + setSnapshot((current) => ({ + items: preserveItems ? current.items : [], + loading: true, + })); + void window.maka.workBoard.list(query).then( + (result) => { + if (revision !== revisionRef.current) return; + setSnapshot( + result.ok + ? { items: result.value.items, loading: false } + : { items: [], loading: false, error: result.message }, + ); + }, + (error: unknown) => { + if (revision !== revisionRef.current) return; + setSnapshot((current) => ({ + items: current.items, + loading: false, + error: error instanceof Error ? error.message : 'Work Board load failed', + })); + }, + ); + }, + [query], + ); + + useEffect(() => { + revisionRef.current += 1; + const unsubscribe = window.maka.workBoard.subscribeChanges( + (_event: WorkBoardChangedEvent) => load(true), + ); + load(false); + return () => { + revisionRef.current += 1; + unsubscribe(); + }; + }, [load]); + + const retry = useCallback(() => load(true), [load]); + + const mutate = useCallback( + async (operation: () => Promise>): Promise => { + const value = requireResult(await operation()); + await load(true); + return value; + }, + [load], + ); + + return { + ...snapshot, + retry, + create: (input) => mutate(() => window.maka.workBoard.create(input)), + update: (id, patch, options) => + mutate(() => window.maka.workBoard.update(id, patch, options)), + archive: (id, options) => mutate(() => window.maka.workBoard.archive(id, options)), + unarchive: (id, options) => mutate(() => window.maka.workBoard.unarchive(id, options)), + remove: async (id, options) => { + await mutate(() => window.maka.workBoard.remove(id, options)); + }, + }; +} diff --git a/apps/desktop/src/shared/work-board-ipc.ts b/apps/desktop/src/shared/work-board-ipc.ts new file mode 100644 index 0000000000..3b0589febf --- /dev/null +++ b/apps/desktop/src/shared/work-board-ipc.ts @@ -0,0 +1,14 @@ +import type { WorkBoardStoreErrorCode } from '@maka/storage/work-board-store'; + +export type WorkBoardIpcResult = + | { readonly ok: true; readonly value: T } + | { + readonly ok: false; + readonly code: WorkBoardStoreErrorCode | 'unknown'; + readonly message: string; + }; + +export interface WorkBoardChangedEvent { + readonly type: 'work_board_changed'; + readonly ts: number; +} From 5a4e5b586b150bf4fe1f9f07a439bd0942c80bd9 Mon Sep 17 00:00:00 2001 From: QuinnWan <144975606+somewan820@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:15:46 +0800 Subject: [PATCH 03/22] feat(desktop): add the Work Board panel as a workbar tab Phase 1 slice 3: compact capture/list MVP in the session workbar with Inbox / current-project filtering, manual create, rename, move, complete, reopen, archive, restore, and delete. The panel is a read-only renderer projection over the main-process WorkBoardStore IPC. Generated-by: Codex --- .../features/workbar/model/workbar-tabs.ts | 2 + .../features/workbar/ui/workbar-host.tsx | 2 + .../features/workbar/ui/workbar-surface.tsx | 109 ++++-- .../src/renderer/locales/conversation-copy.ts | 6 + apps/desktop/src/renderer/styles.css | 1 + .../src/renderer/styles/work-board.css | 81 +++++ .../desktop/src/renderer/work-board-panel.tsx | 341 ++++++++++++++++++ 7 files changed, 505 insertions(+), 37 deletions(-) create mode 100644 apps/desktop/src/renderer/styles/work-board.css create mode 100644 apps/desktop/src/renderer/work-board-panel.tsx diff --git a/apps/desktop/src/renderer/features/workbar/model/workbar-tabs.ts b/apps/desktop/src/renderer/features/workbar/model/workbar-tabs.ts index b6b49b5b0e..03c9c02817 100644 --- a/apps/desktop/src/renderer/features/workbar/model/workbar-tabs.ts +++ b/apps/desktop/src/renderer/features/workbar/model/workbar-tabs.ts @@ -24,6 +24,7 @@ export type SessionWorkbarTabKind = | 'review' | 'terminal' | 'tasks' + | 'work-board' | 'browser' | 'files' | 'inspector' @@ -133,6 +134,7 @@ const STATIC_TAB_IDS: Record, string review: 'workbar:review', terminal: 'workbar:terminal', tasks: 'workbar:tasks', + 'work-board': 'workbar:work-board', browser: 'workbar:browser', files: 'workbar:files', inspector: 'workbar:inspector', diff --git a/apps/desktop/src/renderer/features/workbar/ui/workbar-host.tsx b/apps/desktop/src/renderer/features/workbar/ui/workbar-host.tsx index 566fe0d94a..6e1805e56e 100644 --- a/apps/desktop/src/renderer/features/workbar/ui/workbar-host.tsx +++ b/apps/desktop/src/renderer/features/workbar/ui/workbar-host.tsx @@ -69,6 +69,7 @@ function SessionWorkbarFallback() { export interface WorkbarHostModel { activeId?: string; + projectId?: string | null; rightCollapsed: boolean; bottomOpen: boolean; hidden: boolean; @@ -164,6 +165,7 @@ export function WorkbarHost({ model: props }: { model: WorkbarHostModel }) {