Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit cc1980c

Browse files
committed
FEA-1550: Fix pack install UI contract
- Align pack install IPC results and streamed output payloads across main, preload, and renderer types - Handle failed catalog mutation starts without opening a stale install modal - Pass selected recent project cwd through project-scoped install and uninstall actions - Keep local-only agent session user and org identity explicitly null until server-owned identity is available Testing: Desktop typecheck, lint, and renderer build passed Risks: Low; scoped to pack catalog install UI flow and typed IPC contracts
1 parent d11071c commit cc1980c

7 files changed

Lines changed: 260 additions & 21 deletions

File tree

apps/desktop/src/main/agent-dashboard-design-system-runtime.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,12 +400,13 @@ function registerDesignSystemDbIpcHandlers(
400400
});
401401
});
402402

403-
ipcMain.handle("desktop:db:catalog-uninstall", async (_event, packId: unknown, harness: unknown) => {
403+
ipcMain.handle("desktop:db:catalog-uninstall", async (_event, packId: unknown, harness: unknown, cwd?: unknown) => {
404404
if (typeof packId !== "string" || typeof harness !== "string") return { started: false };
405405
return streamRun(dbForStores, {
406406
pack_id: packId,
407407
harness,
408408
action: "uninstall",
409+
cwd: typeof cwd === "string" ? cwd : undefined,
409410
getWindow: options.getWindow,
410411
onComplete: () => void runPackScanner(dbForStores).catch(() => {}),
411412
});

apps/desktop/src/main/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,8 @@ export class DesktopApplication {
14471447
await createAgentDashboardDesignSystemRuntime({
14481448
userDataPath: app.getPath("userData"),
14491449
getWindow: () => this.desktopWindow.getWindow(),
1450+
// User/org IDs are server-owned. Local-only sessions keep these columns null.
1451+
getUserIdentity: () => null,
14501452
onTerminalFailure: (reason) => {
14511453
const notification = new Notification({
14521454
title: "ClosedLoop Agent Monitor",

apps/desktop/src/main/preload-design-system.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
AgentRow,
55
AnalyticsData,
66
CatalogEntry,
7+
CatalogMutationResult,
78
DashboardCoreFeatures,
89
DashboardPackSummary,
910
DashboardPlanSummary,
@@ -15,6 +16,7 @@ import type {
1516
EventCountByType,
1617
EventRow,
1718
EventWithSession,
19+
InstallOutputChunk,
1820
InstallRunRecord,
1921
InstalledPack,
2022
InstalledPackDetail,
@@ -67,8 +69,8 @@ const designSystemDashboardApi = {
6769
getCatalogReadme: (packId: string) => ipcRenderer.invoke("desktop:db:get-catalog-readme", packId) as Promise<string | null>,
6870
getCatalogContents: (packId: string) => ipcRenderer.invoke("desktop:db:get-catalog-contents", packId) as Promise<unknown[] | null>,
6971
getCatalogHistory: (packId: string) => ipcRenderer.invoke("desktop:db:get-catalog-history", packId) as Promise<Array<{ fetchedAt: string; stars: number; forks: number }>>,
70-
catalogInstall: (packId: string, harness: string, cwd?: string) => ipcRenderer.invoke("desktop:db:catalog-install", packId, harness, cwd) as Promise<{ runId: number }>,
71-
catalogUninstall: (packId: string, harness: string) => ipcRenderer.invoke("desktop:db:catalog-uninstall", packId, harness) as Promise<{ runId: number }>,
72+
catalogInstall: (packId: string, harness: string, cwd?: string) => ipcRenderer.invoke("desktop:db:catalog-install", packId, harness, cwd) as Promise<CatalogMutationResult>,
73+
catalogUninstall: (packId: string, harness: string, cwd?: string) => ipcRenderer.invoke("desktop:db:catalog-uninstall", packId, harness, cwd) as Promise<CatalogMutationResult>,
7274
catalogRefresh: () => ipcRenderer.invoke("desktop:db:catalog-refresh") as Promise<void>,
7375
getInstallRuns: (packId?: string) => ipcRenderer.invoke("desktop:db:get-install-runs", packId) as Promise<InstallRunRecord[]>,
7476

@@ -107,8 +109,8 @@ const designSystemDashboardApi = {
107109
return () => ipcRenderer.removeListener("desktop:db:changed", handler);
108110
},
109111
/** Subscribe to streamed pack install/uninstall output (FEA-1314). */
110-
onInstallOutput: (callback: (payload: { runId: number; type: string; data: string }) => void) => {
111-
const handler = (_event: unknown, payload: { runId: number; type: string; data: string }) =>
112+
onInstallOutput: (callback: (payload: InstallOutputChunk) => void) => {
113+
const handler = (_event: unknown, payload: InstallOutputChunk) =>
112114
callback(payload);
113115
ipcRenderer.on("desktop:pack:install-output", handler);
114116
return () => ipcRenderer.removeListener("desktop:pack:install-output", handler);

apps/desktop/src/renderer/components/features/InstallModal.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,18 @@ export function InstallModal({
5353
const unsubscribe = window.desktopApi.onInstallOutput?.((payload) => {
5454
if (payload.runId !== runId) return;
5555

56-
if (payload.type === "exit") {
57-
const code = parseInt(payload.data, 10);
58-
setExitCode(Number.isNaN(code) ? null : code);
56+
if (payload.type === "complete") {
57+
const code = extractExitCode(payload.data);
5958
setDone(true);
59+
setExitCode(code);
6060
return;
6161
}
6262

63-
setLines((prev) => [...prev, { type: payload.type, data: payload.data }]);
63+
const rendered = formatOutputPayload(payload.type, payload.data);
64+
if (!rendered) {
65+
return;
66+
}
67+
setLines((prev) => [...prev, { type: payload.type, data: rendered }]);
6468
});
6569

6670
return () => {
@@ -166,3 +170,41 @@ export function InstallModal({
166170
</Dialog>
167171
);
168172
}
173+
174+
function extractExitCode(data: unknown): number | null {
175+
if (typeof data === "number" && Number.isFinite(data)) {
176+
return data;
177+
}
178+
if (typeof data === "string") {
179+
const parsed = Number.parseInt(data, 10);
180+
return Number.isNaN(parsed) ? null : parsed;
181+
}
182+
if (data && typeof data === "object" && "exit_code" in data) {
183+
const value = (data as { exit_code?: unknown }).exit_code;
184+
return typeof value === "number" && Number.isFinite(value) ? value : null;
185+
}
186+
return null;
187+
}
188+
189+
function formatOutputPayload(type: string, data: unknown): string | null {
190+
if (type === "start") {
191+
return null;
192+
}
193+
if (typeof data === "string") {
194+
return data;
195+
}
196+
if (data && typeof data === "object" && "message" in data) {
197+
const message = (data as { message?: unknown }).message;
198+
if (typeof message === "string") {
199+
return message;
200+
}
201+
}
202+
if (type === "post_install" || type === "copy_command" || type === "error") {
203+
try {
204+
return JSON.stringify(data, null, 2);
205+
} catch {
206+
return String(data);
207+
}
208+
}
209+
return null;
210+
}

0 commit comments

Comments
 (0)