Skip to content

Commit 00e999e

Browse files
committed
error toasts
1 parent 322f15f commit 00e999e

29 files changed

+431
-69
lines changed

.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,7 @@ VITE_POSTHOG_API_HOST=xxx
1212
VITE_POSTHOG_UI_HOST=xxx
1313

1414
# Use new LLM gateway locally (experimental, needs to be started in mprocs)
15-
LLM_GATEWAY_URL=http://localhost:3308
15+
LLM_GATEWAY_URL=http://localhost:3308
16+
17+
# Whether all errors/warnings show as dismissable toasts in dev
18+
VITE_DEV_ERROR_TOASTS=true

apps/array/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
"radix-themes-tw": "0.2.3",
138138
"react": "^18.2.0",
139139
"react-dom": "^18.2.0",
140+
"react-error-boundary": "^6.0.0",
140141
"react-hook-form": "^7.64.0",
141142
"react-hotkeys-hook": "^4.4.4",
142143
"react-markdown": "^10.1.0",

apps/array/src/main/di/container.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ShellService } from "../services/shell/service.js";
1414
import { TaskLinkService } from "../services/task-link/service.js";
1515
import { UIService } from "../services/ui/service.js";
1616
import { UpdatesService } from "../services/updates/service.js";
17+
import { UserNotificationService } from "../services/user-notification/service.js";
1718
import { WorkspaceService } from "../services/workspace/service.js";
1819
import { MAIN_TOKENS } from "./tokens.js";
1920

@@ -35,4 +36,5 @@ container.bind(MAIN_TOKENS.ShellService).to(ShellService);
3536
container.bind(MAIN_TOKENS.UIService).to(UIService);
3637
container.bind(MAIN_TOKENS.UpdatesService).to(UpdatesService);
3738
container.bind(MAIN_TOKENS.TaskLinkService).to(TaskLinkService);
39+
container.bind(MAIN_TOKENS.UserNotificationService).to(UserNotificationService);
3840
container.bind(MAIN_TOKENS.WorkspaceService).to(WorkspaceService);

apps/array/src/main/di/tokens.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ export const MAIN_TOKENS = Object.freeze({
2121
UpdatesService: Symbol.for("Main.UpdatesService"),
2222
TaskLinkService: Symbol.for("Main.TaskLinkService"),
2323
WorkspaceService: Symbol.for("Main.WorkspaceService"),
24+
UserNotificationService: Symbol.for("Main.UserNotificationService"),
2425
});

apps/array/src/main/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { mkdirSync } from "node:fs";
77
import os from "node:os";
88
import path from "node:path";
99
import { fileURLToPath } from "node:url";
10+
import { initializeMainErrorHandling } from "./lib/error-handling.js";
11+
12+
initializeMainErrorHandling();
13+
1014
import {
1115
app,
1216
BrowserWindow,
@@ -17,7 +21,6 @@ import {
1721
shell,
1822
} from "electron";
1923
import { createIPCHandler } from "trpc-electron/main";
20-
import "./lib/logger";
2124
import { ANALYTICS_EVENTS } from "../types/analytics.js";
2225
import { container } from "./di/container.js";
2326
import { MAIN_TOKENS } from "./di/tokens.js";
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ipcMain } from "electron";
2+
import { logger } from "./logger.js";
3+
4+
export function initializeMainErrorHandling(): void {
5+
process.on("uncaughtException", (error) => {
6+
logger.error("Uncaught exception", error);
7+
});
8+
9+
process.on("unhandledRejection", (reason) => {
10+
const error = reason instanceof Error ? reason : new Error(String(reason));
11+
logger.error("Unhandled rejection", error);
12+
});
13+
14+
ipcMain.on(
15+
"preload-error",
16+
(_, error: { message: string; stack?: string }) => {
17+
logger.error("Preload error", error);
18+
},
19+
);
20+
}

apps/array/src/main/lib/logger.ts

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,15 @@
1+
import type { Logger, ScopedLogger } from "@shared/lib/create-logger.js";
2+
import { createLogger } from "@shared/lib/create-logger.js";
13
import { app } from "electron";
24
import log from "electron-log/main";
35

4-
// Initialize IPC transport to forward main process logs to renderer dev tools
56
log.initialize();
67

7-
// Set levels - use debug in dev (check NODE_ENV since app.isPackaged may not be ready)
88
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
99
const level = isDev ? "debug" : "info";
1010
log.transports.file.level = level;
1111
log.transports.console.level = level;
12-
// IPC transport needs level set separately
1312
log.transports.ipc.level = level;
1413

15-
export const logger = {
16-
info: (message: string, ...args: unknown[]) => log.info(message, ...args),
17-
warn: (message: string, ...args: unknown[]) => log.warn(message, ...args),
18-
error: (message: string, ...args: unknown[]) => log.error(message, ...args),
19-
debug: (message: string, ...args: unknown[]) => log.debug(message, ...args),
20-
21-
scope: (name: string) => {
22-
const scoped = log.scope(name);
23-
return {
24-
info: (message: string, ...args: unknown[]) =>
25-
scoped.info(message, ...args),
26-
warn: (message: string, ...args: unknown[]) =>
27-
scoped.warn(message, ...args),
28-
error: (message: string, ...args: unknown[]) =>
29-
scoped.error(message, ...args),
30-
debug: (message: string, ...args: unknown[]) =>
31-
scoped.debug(message, ...args),
32-
};
33-
},
34-
};
35-
36-
export type Logger = typeof logger;
37-
export type ScopedLogger = ReturnType<typeof logger.scope>;
14+
export const logger = createLogger(log);
15+
export type { Logger, ScopedLogger };

apps/array/src/main/preload.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
1+
import { ipcRenderer } from "electron";
12
import { exposeElectronTRPC } from "trpc-electron/main";
23
import "electron-log/preload";
34

5+
// No TRPC available, so just use IPC
6+
process.on("uncaughtException", (error) => {
7+
ipcRenderer.send("preload-error", {
8+
message: error.message,
9+
stack: error.stack,
10+
});
11+
});
12+
13+
process.on("unhandledRejection", (reason) => {
14+
const error = reason instanceof Error ? reason : new Error(String(reason));
15+
ipcRenderer.send("preload-error", {
16+
message: error.message,
17+
stack: error.stack,
18+
});
19+
});
20+
421
process.once("loaded", async () => {
522
exposeElectronTRPC();
623
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export const UserNotificationEvent = {
2+
Notify: "notify",
3+
} as const;
4+
5+
export type NotificationSeverity = "error" | "warning" | "info";
6+
7+
export interface UserNotificationPayload {
8+
severity: NotificationSeverity;
9+
title: string;
10+
description?: string;
11+
}
12+
13+
export interface UserNotificationEvents {
14+
[UserNotificationEvent.Notify]: UserNotificationPayload;
15+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { app } from "electron";
2+
import { injectable, postConstruct } from "inversify";
3+
import { logger } from "../../lib/logger.js";
4+
import { TypedEventEmitter } from "../../lib/typed-event-emitter.js";
5+
import {
6+
UserNotificationEvent,
7+
type UserNotificationEvents,
8+
} from "./schemas.js";
9+
10+
const isDev = process.env.NODE_ENV === "development" || !app.isPackaged;
11+
const devErrorToastsEnabled =
12+
isDev && process.env.VITE_DEV_ERROR_TOASTS !== "false";
13+
14+
@injectable()
15+
export class UserNotificationService extends TypedEventEmitter<UserNotificationEvents> {
16+
@postConstruct()
17+
init(): void {
18+
if (devErrorToastsEnabled) {
19+
logger.setDevToastEmitter((title, desc) => this.error(title, desc));
20+
}
21+
}
22+
23+
error(title: string, description?: string): void {
24+
this.emit(UserNotificationEvent.Notify, {
25+
severity: "error",
26+
title,
27+
description,
28+
});
29+
}
30+
31+
warning(title: string, description?: string): void {
32+
this.emit(UserNotificationEvent.Notify, {
33+
severity: "warning",
34+
title,
35+
description,
36+
});
37+
}
38+
39+
info(title: string, description?: string): void {
40+
this.emit(UserNotificationEvent.Notify, {
41+
severity: "info",
42+
title,
43+
description,
44+
});
45+
}
46+
}

0 commit comments

Comments
 (0)