Merged
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
266 changes: 266 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
import { assert, describe, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import type * as Electron from "electron";
import { beforeEach, vi } from "vite-plus/test";

interface FakeNotificationEntry {
readonly options: { readonly title?: string; readonly body?: string };
readonly listeners: Map<string, (...args: readonly unknown[]) => void>;
shown: number;
closed: number;
}

const { notificationState, FakeNotification } = vi.hoisted(() => {
const notificationState = {
supported: true,
constructorThrows: null as Error | null,
instances: [] as FakeNotificationEntry[],
};

class FakeNotification {
static isSupported(): boolean {
return notificationState.supported;
}

readonly entry: FakeNotificationEntry;

constructor(options: { title?: string; body?: string }) {
if (notificationState.constructorThrows !== null) {
throw notificationState.constructorThrows;
}
this.entry = { options, listeners: new Map(), shown: 0, closed: 0 };
notificationState.instances.push(this.entry);
}

on(event: string, listener: (...args: readonly unknown[]) => void): this {
this.entry.listeners.set(event, listener);
return this;
}

show(): void {
this.entry.shown += 1;
}

// Electron dismisses the notification and then emits `close`; the fake
// mirrors that so the service's bookkeeping is exercised.
close(): void {
this.entry.closed += 1;
this.entry.listeners.get("close")?.();
}
}

return { notificationState, FakeNotification };
});

vi.mock("electron", () => ({ Notification: FakeNotification }));

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronNotification from "./ElectronNotification.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

const request = {
id: "thread-1",
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
environmentId: "local",
threadId: "thread-1",
} as const;

const clickNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const click = entry.listeners.get("click");
if (click === undefined) {
throw new Error("Expected the notification to register a click listener.");
}
click();
};

const clickFirstNotification = () => clickNotification(0);

const closeNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const close = entry.listeners.get("close");
if (close === undefined) {
throw new Error("Expected the notification to register a close listener.");
}
close();
};

const silentWindowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: () => Effect.void,
});

describe("ElectronNotification", () => {
beforeEach(() => {
notificationState.supported = true;
notificationState.constructorThrows = null;
notificationState.instances = [];
});

it.effect("shows a native notification carrying the request title and body", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 1);
assert.deepEqual(notificationState.instances[0]?.options, {
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
});
assert.equal(notificationState.instances[0]?.shown, 1);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("reveals the target window and broadcasts the activation on click", () =>
Effect.gen(function* () {
const window = { id: 7 } as Electron.BrowserWindow;
const revealedRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.some(window)),
reveal: (target) => Ref.set(revealedRef, Option.some(target)),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

const broadcast = yield* Deferred.await(sent);
assert.deepEqual(broadcast, [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
assert.strictEqual(Option.getOrNull(yield* Ref.get(revealedRef)), window);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("broadcasts the activation even when there is no window to reveal", () =>
Effect.gen(function* () {
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

assert.deepEqual(yield* Deferred.await(sent), [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("no-ops when the platform does not support native notifications", () =>
Effect.gen(function* () {
notificationState.supported = false;

const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("closes the previous notification when the same id is shown again", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);
yield* notifications.show(request);
yield* notifications.show({ ...request, id: "thread-2", threadId: "thread-2" });

assert.equal(notificationState.instances.length, 3);
// Only the first is superseded: the third carries a different id.
assert.equal(notificationState.instances[0]?.closed, 1);
assert.equal(notificationState.instances[1]?.closed, 0);
assert.equal(notificationState.instances[2]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a dismissed notification so it is never closed twice", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

// The OS dismissal emits `close` without routing through `close()`.
closeNotification(0);
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a clicked notification so it is never closed afterwards", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("swallows Electron notification failures instead of failing the caller", () =>
Effect.gen(function* () {
notificationState.constructorThrows = new Error("notification center unavailable");

const notifications = yield* ElectronNotification.ElectronNotification;
const exit = yield* Effect.exit(notifications.show(request));

assert.equal(exit._tag, "Success");
assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);
});
113 changes: 113 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import type { DesktopNotificationActivation, DesktopNotificationRequest } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as Electron from "electron";

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

export class ElectronNotificationShowError extends Schema.TaggedErrorClass<ElectronNotificationShowError>()(
"ElectronNotificationShowError",
{
notificationId: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to show the Electron notification ${JSON.stringify(this.notificationId)}.`;
}
}

export class ElectronNotification extends Context.Service<
ElectronNotification,
{
/**
* Raises a native OS notification. Resolves once the notification has been
* handed to the platform, and no-ops when the platform has no notification
* support. Clicking the notification reveals the current window and
* broadcasts a `DesktopNotificationActivation` to every renderer.
*/
readonly show: (request: DesktopNotificationRequest) => Effect.Effect<void>;
}
>()("@t3tools/desktop/electron/ElectronNotification") {}

export const make = Effect.gen(function* () {
const electronWindow = yield* ElectronWindow.ElectronWindow;
// The click listener is a bare Electron callback, so the activation effect
// has to be run against the captured context (keeps logging/tracing intact).
const context = yield* Effect.context<never>();
const runFork = Effect.runForkWith(context);

// Live notifications keyed by `DesktopNotificationRequest.id`, which the
// contract defines as a coalescing key: showing again for the same id
// replaces the previous entry instead of stacking a second one in
// Notification Center (the web path gets this from the notification `tag`).
// Holding the JS reference also keeps the notification from being collected
// while it is on screen, which is what makes Electron's `click` reliable.
const liveNotifications = new Map<string, Electron.Notification>();

const activate = Effect.fn("desktop.electron.notification.activate")(function* (
request: DesktopNotificationRequest,
) {
const window = yield* electronWindow.focusedMainOrFirst;
if (Option.isSome(window)) {
yield* electronWindow.reveal(window.value);
}

const activation: DesktopNotificationActivation = {
environmentId: request.environmentId,
threadId: request.threadId,
};
// Broadcast rather than target the revealed window: any renderer may own
// the thread, and the activation is idempotent for the ones that don't.
yield* electronWindow.sendAll(NOTIFICATION_ACTIVATED_CHANNEL, activation);
});

const show = Effect.fn("desktop.electron.notification.show")(function* (
request: DesktopNotificationRequest,
) {
const supported = yield* Effect.try({
try: () => Electron.Notification.isSupported(),
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
if (!supported) {
return;
}

yield* Effect.try({
try: () => {
const notification = new Electron.Notification({
title: request.title,
body: request.body,
});
const forget = () => {
if (liveNotifications.get(request.id) === notification) {
liveNotifications.delete(request.id);
}
};
notification.on("click", () => {
forget();
runFork(activate(request));
});
notification.on("close", forget);

liveNotifications.get(request.id)?.close();
liveNotifications.set(request.id, notification);
notification.show();
},
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
});

return ElectronNotification.of({
// Notifications are advisory: failing to raise one must never fail the
// renderer's invoke, so failures are logged and swallowed here.
show: (request) => show(request).pipe(Effect.catch((error) => Effect.logWarning(error))),
});
});

export const layer = Layer.effect(ElectronNotification, make);
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import {
issueSshWebSocketTicket,
resolveSshPasswordPrompt,
} from "./methods/sshEnvironment.ts";
import { showNotification } from "./methods/notifications.ts";
import {
checkForUpdate,
downloadUpdate,
Expand DownExpand Up@@ -83,6 +84,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setTheme);
yield* ipc.handle(showContextMenu);
yield* ipc.handle(openExternal);
yield* ipc.handle(showNotification);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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
266 changes: 266 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
import { assert, describe, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import type * as Electron from "electron";
import { beforeEach, vi } from "vite-plus/test";

interface FakeNotificationEntry {
readonly options: { readonly title?: string; readonly body?: string };
readonly listeners: Map<string, (...args: readonly unknown[]) => void>;
shown: number;
closed: number;
}

const { notificationState, FakeNotification } = vi.hoisted(() => {
const notificationState = {
supported: true,
constructorThrows: null as Error | null,
instances: [] as FakeNotificationEntry[],
};

class FakeNotification {
static isSupported(): boolean {
return notificationState.supported;
}

readonly entry: FakeNotificationEntry;

constructor(options: { title?: string; body?: string }) {
if (notificationState.constructorThrows !== null) {
throw notificationState.constructorThrows;
}
this.entry = { options, listeners: new Map(), shown: 0, closed: 0 };
notificationState.instances.push(this.entry);
}

on(event: string, listener: (...args: readonly unknown[]) => void): this {
this.entry.listeners.set(event, listener);
return this;
}

show(): void {
this.entry.shown += 1;
}

// Electron dismisses the notification and then emits `close`; the fake
// mirrors that so the service's bookkeeping is exercised.
close(): void {
this.entry.closed += 1;
this.entry.listeners.get("close")?.();
}
}

return { notificationState, FakeNotification };
});

vi.mock("electron", () => ({ Notification: FakeNotification }));

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronNotification from "./ElectronNotification.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

const request = {
id: "thread-1",
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
environmentId: "local",
threadId: "thread-1",
} as const;

const clickNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const click = entry.listeners.get("click");
if (click === undefined) {
throw new Error("Expected the notification to register a click listener.");
}
click();
};

const clickFirstNotification = () => clickNotification(0);

const closeNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const close = entry.listeners.get("close");
if (close === undefined) {
throw new Error("Expected the notification to register a close listener.");
}
close();
};

const silentWindowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: () => Effect.void,
});

describe("ElectronNotification", () => {
beforeEach(() => {
notificationState.supported = true;
notificationState.constructorThrows = null;
notificationState.instances = [];
});

it.effect("shows a native notification carrying the request title and body", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 1);
assert.deepEqual(notificationState.instances[0]?.options, {
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
});
assert.equal(notificationState.instances[0]?.shown, 1);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("reveals the target window and broadcasts the activation on click", () =>
Effect.gen(function* () {
const window = { id: 7 } as Electron.BrowserWindow;
const revealedRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.some(window)),
reveal: (target) => Ref.set(revealedRef, Option.some(target)),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

const broadcast = yield* Deferred.await(sent);
assert.deepEqual(broadcast, [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
assert.strictEqual(Option.getOrNull(yield* Ref.get(revealedRef)), window);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("broadcasts the activation even when there is no window to reveal", () =>
Effect.gen(function* () {
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

assert.deepEqual(yield* Deferred.await(sent), [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("no-ops when the platform does not support native notifications", () =>
Effect.gen(function* () {
notificationState.supported = false;

const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("closes the previous notification when the same id is shown again", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);
yield* notifications.show(request);
yield* notifications.show({ ...request, id: "thread-2", threadId: "thread-2" });

assert.equal(notificationState.instances.length, 3);
// Only the first is superseded: the third carries a different id.
assert.equal(notificationState.instances[0]?.closed, 1);
assert.equal(notificationState.instances[1]?.closed, 0);
assert.equal(notificationState.instances[2]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a dismissed notification so it is never closed twice", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

// The OS dismissal emits `close` without routing through `close()`.
closeNotification(0);
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a clicked notification so it is never closed afterwards", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("swallows Electron notification failures instead of failing the caller", () =>
Effect.gen(function* () {
notificationState.constructorThrows = new Error("notification center unavailable");

const notifications = yield* ElectronNotification.ElectronNotification;
const exit = yield* Effect.exit(notifications.show(request));

assert.equal(exit._tag, "Success");
assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);
});
113 changes: 113 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import type { DesktopNotificationActivation, DesktopNotificationRequest } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as Electron from "electron";

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

export class ElectronNotificationShowError extends Schema.TaggedErrorClass<ElectronNotificationShowError>()(
"ElectronNotificationShowError",
{
notificationId: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to show the Electron notification ${JSON.stringify(this.notificationId)}.`;
}
}

export class ElectronNotification extends Context.Service<
ElectronNotification,
{
/**
* Raises a native OS notification. Resolves once the notification has been
* handed to the platform, and no-ops when the platform has no notification
* support. Clicking the notification reveals the current window and
* broadcasts a `DesktopNotificationActivation` to every renderer.
*/
readonly show: (request: DesktopNotificationRequest) => Effect.Effect<void>;
}
>()("@t3tools/desktop/electron/ElectronNotification") {}

export const make = Effect.gen(function* () {
const electronWindow = yield* ElectronWindow.ElectronWindow;
// The click listener is a bare Electron callback, so the activation effect
// has to be run against the captured context (keeps logging/tracing intact).
const context = yield* Effect.context<never>();
const runFork = Effect.runForkWith(context);

// Live notifications keyed by `DesktopNotificationRequest.id`, which the
// contract defines as a coalescing key: showing again for the same id
// replaces the previous entry instead of stacking a second one in
// Notification Center (the web path gets this from the notification `tag`).
// Holding the JS reference also keeps the notification from being collected
// while it is on screen, which is what makes Electron's `click` reliable.
const liveNotifications = new Map<string, Electron.Notification>();

const activate = Effect.fn("desktop.electron.notification.activate")(function* (
request: DesktopNotificationRequest,
) {
const window = yield* electronWindow.focusedMainOrFirst;
if (Option.isSome(window)) {
yield* electronWindow.reveal(window.value);
}

const activation: DesktopNotificationActivation = {
environmentId: request.environmentId,
threadId: request.threadId,
};
// Broadcast rather than target the revealed window: any renderer may own
// the thread, and the activation is idempotent for the ones that don't.
yield* electronWindow.sendAll(NOTIFICATION_ACTIVATED_CHANNEL, activation);
});

const show = Effect.fn("desktop.electron.notification.show")(function* (
request: DesktopNotificationRequest,
) {
const supported = yield* Effect.try({
try: () => Electron.Notification.isSupported(),
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
if (!supported) {
return;
}

yield* Effect.try({
try: () => {
const notification = new Electron.Notification({
title: request.title,
body: request.body,
});
const forget = () => {
if (liveNotifications.get(request.id) === notification) {
liveNotifications.delete(request.id);
}
};
notification.on("click", () => {
forget();
runFork(activate(request));
});
notification.on("close", forget);

liveNotifications.get(request.id)?.close();
liveNotifications.set(request.id, notification);
notification.show();
},
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
});

return ElectronNotification.of({
// Notifications are advisory: failing to raise one must never fail the
// renderer's invoke, so failures are logged and swallowed here.
show: (request) => show(request).pipe(Effect.catch((error) => Effect.logWarning(error))),
});
});

export const layer = Layer.effect(ElectronNotification, make);
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import {
issueSshWebSocketTicket,
resolveSshPasswordPrompt,
} from "./methods/sshEnvironment.ts";
import { showNotification } from "./methods/notifications.ts";
import {
checkForUpdate,
downloadUpdate,
Expand DownExpand Up@@ -83,6 +84,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setTheme);
yield* ipc.handle(showContextMenu);
yield* ipc.handle(openExternal);
yield* ipc.handle(showNotification);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
266 changes: 266 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
import { assert, describe, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import type * as Electron from "electron";
import { beforeEach, vi } from "vite-plus/test";

interface FakeNotificationEntry {
readonly options: { readonly title?: string; readonly body?: string };
readonly listeners: Map<string, (...args: readonly unknown[]) => void>;
shown: number;
closed: number;
}

const { notificationState, FakeNotification } = vi.hoisted(() => {
const notificationState = {
supported: true,
constructorThrows: null as Error | null,
instances: [] as FakeNotificationEntry[],
};

class FakeNotification {
static isSupported(): boolean {
return notificationState.supported;
}

readonly entry: FakeNotificationEntry;

constructor(options: { title?: string; body?: string }) {
if (notificationState.constructorThrows !== null) {
throw notificationState.constructorThrows;
}
this.entry = { options, listeners: new Map(), shown: 0, closed: 0 };
notificationState.instances.push(this.entry);
}

on(event: string, listener: (...args: readonly unknown[]) => void): this {
this.entry.listeners.set(event, listener);
return this;
}

show(): void {
this.entry.shown += 1;
}

// Electron dismisses the notification and then emits `close`; the fake
// mirrors that so the service's bookkeeping is exercised.
close(): void {
this.entry.closed += 1;
this.entry.listeners.get("close")?.();
}
}

return { notificationState, FakeNotification };
});

vi.mock("electron", () => ({ Notification: FakeNotification }));

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronNotification from "./ElectronNotification.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

const request = {
id: "thread-1",
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
environmentId: "local",
threadId: "thread-1",
} as const;

const clickNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const click = entry.listeners.get("click");
if (click === undefined) {
throw new Error("Expected the notification to register a click listener.");
}
click();
};

const clickFirstNotification = () => clickNotification(0);

const closeNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const close = entry.listeners.get("close");
if (close === undefined) {
throw new Error("Expected the notification to register a close listener.");
}
close();
};

const silentWindowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: () => Effect.void,
});

describe("ElectronNotification", () => {
beforeEach(() => {
notificationState.supported = true;
notificationState.constructorThrows = null;
notificationState.instances = [];
});

it.effect("shows a native notification carrying the request title and body", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 1);
assert.deepEqual(notificationState.instances[0]?.options, {
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
});
assert.equal(notificationState.instances[0]?.shown, 1);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("reveals the target window and broadcasts the activation on click", () =>
Effect.gen(function* () {
const window = { id: 7 } as Electron.BrowserWindow;
const revealedRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.some(window)),
reveal: (target) => Ref.set(revealedRef, Option.some(target)),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

const broadcast = yield* Deferred.await(sent);
assert.deepEqual(broadcast, [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
assert.strictEqual(Option.getOrNull(yield* Ref.get(revealedRef)), window);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("broadcasts the activation even when there is no window to reveal", () =>
Effect.gen(function* () {
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

assert.deepEqual(yield* Deferred.await(sent), [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("no-ops when the platform does not support native notifications", () =>
Effect.gen(function* () {
notificationState.supported = false;

const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("closes the previous notification when the same id is shown again", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);
yield* notifications.show(request);
yield* notifications.show({ ...request, id: "thread-2", threadId: "thread-2" });

assert.equal(notificationState.instances.length, 3);
// Only the first is superseded: the third carries a different id.
assert.equal(notificationState.instances[0]?.closed, 1);
assert.equal(notificationState.instances[1]?.closed, 0);
assert.equal(notificationState.instances[2]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a dismissed notification so it is never closed twice", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

// The OS dismissal emits `close` without routing through `close()`.
closeNotification(0);
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a clicked notification so it is never closed afterwards", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("swallows Electron notification failures instead of failing the caller", () =>
Effect.gen(function* () {
notificationState.constructorThrows = new Error("notification center unavailable");

const notifications = yield* ElectronNotification.ElectronNotification;
const exit = yield* Effect.exit(notifications.show(request));

assert.equal(exit._tag, "Success");
assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);
});
113 changes: 113 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import type { DesktopNotificationActivation, DesktopNotificationRequest } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as Electron from "electron";

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

export class ElectronNotificationShowError extends Schema.TaggedErrorClass<ElectronNotificationShowError>()(
"ElectronNotificationShowError",
{
notificationId: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to show the Electron notification ${JSON.stringify(this.notificationId)}.`;
}
}

export class ElectronNotification extends Context.Service<
ElectronNotification,
{
/**
* Raises a native OS notification. Resolves once the notification has been
* handed to the platform, and no-ops when the platform has no notification
* support. Clicking the notification reveals the current window and
* broadcasts a `DesktopNotificationActivation` to every renderer.
*/
readonly show: (request: DesktopNotificationRequest) => Effect.Effect<void>;
}
>()("@t3tools/desktop/electron/ElectronNotification") {}

export const make = Effect.gen(function* () {
const electronWindow = yield* ElectronWindow.ElectronWindow;
// The click listener is a bare Electron callback, so the activation effect
// has to be run against the captured context (keeps logging/tracing intact).
const context = yield* Effect.context<never>();
const runFork = Effect.runForkWith(context);

// Live notifications keyed by `DesktopNotificationRequest.id`, which the
// contract defines as a coalescing key: showing again for the same id
// replaces the previous entry instead of stacking a second one in
// Notification Center (the web path gets this from the notification `tag`).
// Holding the JS reference also keeps the notification from being collected
// while it is on screen, which is what makes Electron's `click` reliable.
const liveNotifications = new Map<string, Electron.Notification>();

const activate = Effect.fn("desktop.electron.notification.activate")(function* (
request: DesktopNotificationRequest,
) {
const window = yield* electronWindow.focusedMainOrFirst;
if (Option.isSome(window)) {
yield* electronWindow.reveal(window.value);
}

const activation: DesktopNotificationActivation = {
environmentId: request.environmentId,
threadId: request.threadId,
};
// Broadcast rather than target the revealed window: any renderer may own
// the thread, and the activation is idempotent for the ones that don't.
yield* electronWindow.sendAll(NOTIFICATION_ACTIVATED_CHANNEL, activation);
});

const show = Effect.fn("desktop.electron.notification.show")(function* (
request: DesktopNotificationRequest,
) {
const supported = yield* Effect.try({
try: () => Electron.Notification.isSupported(),
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
if (!supported) {
return;
}

yield* Effect.try({
try: () => {
const notification = new Electron.Notification({
title: request.title,
body: request.body,
});
const forget = () => {
if (liveNotifications.get(request.id) === notification) {
liveNotifications.delete(request.id);
}
};
notification.on("click", () => {
forget();
runFork(activate(request));
});
notification.on("close", forget);

liveNotifications.get(request.id)?.close();
liveNotifications.set(request.id, notification);
notification.show();
},
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
});

return ElectronNotification.of({
// Notifications are advisory: failing to raise one must never fail the
// renderer's invoke, so failures are logged and swallowed here.
show: (request) => show(request).pipe(Effect.catch((error) => Effect.logWarning(error))),
});
});

export const layer = Layer.effect(ElectronNotification, make);
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import {
issueSshWebSocketTicket,
resolveSshPasswordPrompt,
} from "./methods/sshEnvironment.ts";
import { showNotification } from "./methods/notifications.ts";
import {
checkForUpdate,
downloadUpdate,
Expand DownExpand Up@@ -83,6 +84,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setTheme);
yield* ipc.handle(showContextMenu);
yield* ipc.handle(openExternal);
yield* ipc.handle(showNotification);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
266 changes: 266 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
import { assert, describe, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import type * as Electron from "electron";
import { beforeEach, vi } from "vite-plus/test";

interface FakeNotificationEntry {
readonly options: { readonly title?: string; readonly body?: string };
readonly listeners: Map<string, (...args: readonly unknown[]) => void>;
shown: number;
closed: number;
}

const { notificationState, FakeNotification } = vi.hoisted(() => {
const notificationState = {
supported: true,
constructorThrows: null as Error | null,
instances: [] as FakeNotificationEntry[],
};

class FakeNotification {
static isSupported(): boolean {
return notificationState.supported;
}

readonly entry: FakeNotificationEntry;

constructor(options: { title?: string; body?: string }) {
if (notificationState.constructorThrows !== null) {
throw notificationState.constructorThrows;
}
this.entry = { options, listeners: new Map(), shown: 0, closed: 0 };
notificationState.instances.push(this.entry);
}

on(event: string, listener: (...args: readonly unknown[]) => void): this {
this.entry.listeners.set(event, listener);
return this;
}

show(): void {
this.entry.shown += 1;
}

// Electron dismisses the notification and then emits `close`; the fake
// mirrors that so the service's bookkeeping is exercised.
close(): void {
this.entry.closed += 1;
this.entry.listeners.get("close")?.();
}
}

return { notificationState, FakeNotification };
});

vi.mock("electron", () => ({ Notification: FakeNotification }));

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronNotification from "./ElectronNotification.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

const request = {
id: "thread-1",
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
environmentId: "local",
threadId: "thread-1",
} as const;

const clickNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const click = entry.listeners.get("click");
if (click === undefined) {
throw new Error("Expected the notification to register a click listener.");
}
click();
};

const clickFirstNotification = () => clickNotification(0);

const closeNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const close = entry.listeners.get("close");
if (close === undefined) {
throw new Error("Expected the notification to register a close listener.");
}
close();
};

const silentWindowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: () => Effect.void,
});

describe("ElectronNotification", () => {
beforeEach(() => {
notificationState.supported = true;
notificationState.constructorThrows = null;
notificationState.instances = [];
});

it.effect("shows a native notification carrying the request title and body", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 1);
assert.deepEqual(notificationState.instances[0]?.options, {
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
});
assert.equal(notificationState.instances[0]?.shown, 1);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("reveals the target window and broadcasts the activation on click", () =>
Effect.gen(function* () {
const window = { id: 7 } as Electron.BrowserWindow;
const revealedRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.some(window)),
reveal: (target) => Ref.set(revealedRef, Option.some(target)),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

const broadcast = yield* Deferred.await(sent);
assert.deepEqual(broadcast, [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
assert.strictEqual(Option.getOrNull(yield* Ref.get(revealedRef)), window);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("broadcasts the activation even when there is no window to reveal", () =>
Effect.gen(function* () {
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

assert.deepEqual(yield* Deferred.await(sent), [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("no-ops when the platform does not support native notifications", () =>
Effect.gen(function* () {
notificationState.supported = false;

const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("closes the previous notification when the same id is shown again", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);
yield* notifications.show(request);
yield* notifications.show({ ...request, id: "thread-2", threadId: "thread-2" });

assert.equal(notificationState.instances.length, 3);
// Only the first is superseded: the third carries a different id.
assert.equal(notificationState.instances[0]?.closed, 1);
assert.equal(notificationState.instances[1]?.closed, 0);
assert.equal(notificationState.instances[2]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a dismissed notification so it is never closed twice", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

// The OS dismissal emits `close` without routing through `close()`.
closeNotification(0);
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a clicked notification so it is never closed afterwards", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("swallows Electron notification failures instead of failing the caller", () =>
Effect.gen(function* () {
notificationState.constructorThrows = new Error("notification center unavailable");

const notifications = yield* ElectronNotification.ElectronNotification;
const exit = yield* Effect.exit(notifications.show(request));

assert.equal(exit._tag, "Success");
assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);
});
113 changes: 113 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import type { DesktopNotificationActivation, DesktopNotificationRequest } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as Electron from "electron";

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

export class ElectronNotificationShowError extends Schema.TaggedErrorClass<ElectronNotificationShowError>()(
"ElectronNotificationShowError",
{
notificationId: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to show the Electron notification ${JSON.stringify(this.notificationId)}.`;
}
}

export class ElectronNotification extends Context.Service<
ElectronNotification,
{
/**
* Raises a native OS notification. Resolves once the notification has been
* handed to the platform, and no-ops when the platform has no notification
* support. Clicking the notification reveals the current window and
* broadcasts a `DesktopNotificationActivation` to every renderer.
*/
readonly show: (request: DesktopNotificationRequest) => Effect.Effect<void>;
}
>()("@t3tools/desktop/electron/ElectronNotification") {}

export const make = Effect.gen(function* () {
const electronWindow = yield* ElectronWindow.ElectronWindow;
// The click listener is a bare Electron callback, so the activation effect
// has to be run against the captured context (keeps logging/tracing intact).
const context = yield* Effect.context<never>();
const runFork = Effect.runForkWith(context);

// Live notifications keyed by `DesktopNotificationRequest.id`, which the
// contract defines as a coalescing key: showing again for the same id
// replaces the previous entry instead of stacking a second one in
// Notification Center (the web path gets this from the notification `tag`).
// Holding the JS reference also keeps the notification from being collected
// while it is on screen, which is what makes Electron's `click` reliable.
const liveNotifications = new Map<string, Electron.Notification>();

const activate = Effect.fn("desktop.electron.notification.activate")(function* (
request: DesktopNotificationRequest,
) {
const window = yield* electronWindow.focusedMainOrFirst;
if (Option.isSome(window)) {
yield* electronWindow.reveal(window.value);
}

const activation: DesktopNotificationActivation = {
environmentId: request.environmentId,
threadId: request.threadId,
};
// Broadcast rather than target the revealed window: any renderer may own
// the thread, and the activation is idempotent for the ones that don't.
yield* electronWindow.sendAll(NOTIFICATION_ACTIVATED_CHANNEL, activation);
});

const show = Effect.fn("desktop.electron.notification.show")(function* (
request: DesktopNotificationRequest,
) {
const supported = yield* Effect.try({
try: () => Electron.Notification.isSupported(),
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
if (!supported) {
return;
}

yield* Effect.try({
try: () => {
const notification = new Electron.Notification({
title: request.title,
body: request.body,
});
const forget = () => {
if (liveNotifications.get(request.id) === notification) {
liveNotifications.delete(request.id);
}
};
notification.on("click", () => {
forget();
runFork(activate(request));
});
notification.on("close", forget);

liveNotifications.get(request.id)?.close();
liveNotifications.set(request.id, notification);
notification.show();
},
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
});

return ElectronNotification.of({
// Notifications are advisory: failing to raise one must never fail the
// renderer's invoke, so failures are logged and swallowed here.
show: (request) => show(request).pipe(Effect.catch((error) => Effect.logWarning(error))),
});
});

export const layer = Layer.effect(ElectronNotification, make);
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import {
issueSshWebSocketTicket,
resolveSshPasswordPrompt,
} from "./methods/sshEnvironment.ts";
import { showNotification } from "./methods/notifications.ts";
import {
checkForUpdate,
downloadUpdate,
Expand DownExpand Up@@ -83,6 +84,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setTheme);
yield* ipc.handle(showContextMenu);
yield* ipc.handle(openExternal);
yield* ipc.handle(showNotification);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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
266 changes: 266 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
import { assert, describe, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import type * as Electron from "electron";
import { beforeEach, vi } from "vite-plus/test";

interface FakeNotificationEntry {
readonly options: { readonly title?: string; readonly body?: string };
readonly listeners: Map<string, (...args: readonly unknown[]) => void>;
shown: number;
closed: number;
}

const { notificationState, FakeNotification } = vi.hoisted(() => {
const notificationState = {
supported: true,
constructorThrows: null as Error | null,
instances: [] as FakeNotificationEntry[],
};

class FakeNotification {
static isSupported(): boolean {
return notificationState.supported;
}

readonly entry: FakeNotificationEntry;

constructor(options: { title?: string; body?: string }) {
if (notificationState.constructorThrows !== null) {
throw notificationState.constructorThrows;
}
this.entry = { options, listeners: new Map(), shown: 0, closed: 0 };
notificationState.instances.push(this.entry);
}

on(event: string, listener: (...args: readonly unknown[]) => void): this {
this.entry.listeners.set(event, listener);
return this;
}

show(): void {
this.entry.shown += 1;
}

// Electron dismisses the notification and then emits `close`; the fake
// mirrors that so the service's bookkeeping is exercised.
close(): void {
this.entry.closed += 1;
this.entry.listeners.get("close")?.();
}
}

return { notificationState, FakeNotification };
});

vi.mock("electron", () => ({ Notification: FakeNotification }));

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronNotification from "./ElectronNotification.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

const request = {
id: "thread-1",
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
environmentId: "local",
threadId: "thread-1",
} as const;

const clickNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const click = entry.listeners.get("click");
if (click === undefined) {
throw new Error("Expected the notification to register a click listener.");
}
click();
};

const clickFirstNotification = () => clickNotification(0);

const closeNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const close = entry.listeners.get("close");
if (close === undefined) {
throw new Error("Expected the notification to register a close listener.");
}
close();
};

const silentWindowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: () => Effect.void,
});

describe("ElectronNotification", () => {
beforeEach(() => {
notificationState.supported = true;
notificationState.constructorThrows = null;
notificationState.instances = [];
});

it.effect("shows a native notification carrying the request title and body", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 1);
assert.deepEqual(notificationState.instances[0]?.options, {
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
});
assert.equal(notificationState.instances[0]?.shown, 1);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("reveals the target window and broadcasts the activation on click", () =>
Effect.gen(function* () {
const window = { id: 7 } as Electron.BrowserWindow;
const revealedRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.some(window)),
reveal: (target) => Ref.set(revealedRef, Option.some(target)),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

const broadcast = yield* Deferred.await(sent);
assert.deepEqual(broadcast, [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
assert.strictEqual(Option.getOrNull(yield* Ref.get(revealedRef)), window);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("broadcasts the activation even when there is no window to reveal", () =>
Effect.gen(function* () {
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

assert.deepEqual(yield* Deferred.await(sent), [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("no-ops when the platform does not support native notifications", () =>
Effect.gen(function* () {
notificationState.supported = false;

const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("closes the previous notification when the same id is shown again", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);
yield* notifications.show(request);
yield* notifications.show({ ...request, id: "thread-2", threadId: "thread-2" });

assert.equal(notificationState.instances.length, 3);
// Only the first is superseded: the third carries a different id.
assert.equal(notificationState.instances[0]?.closed, 1);
assert.equal(notificationState.instances[1]?.closed, 0);
assert.equal(notificationState.instances[2]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a dismissed notification so it is never closed twice", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

// The OS dismissal emits `close` without routing through `close()`.
closeNotification(0);
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a clicked notification so it is never closed afterwards", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("swallows Electron notification failures instead of failing the caller", () =>
Effect.gen(function* () {
notificationState.constructorThrows = new Error("notification center unavailable");

const notifications = yield* ElectronNotification.ElectronNotification;
const exit = yield* Effect.exit(notifications.show(request));

assert.equal(exit._tag, "Success");
assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);
});
113 changes: 113 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import type { DesktopNotificationActivation, DesktopNotificationRequest } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as Electron from "electron";

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

export class ElectronNotificationShowError extends Schema.TaggedErrorClass<ElectronNotificationShowError>()(
"ElectronNotificationShowError",
{
notificationId: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to show the Electron notification ${JSON.stringify(this.notificationId)}.`;
}
}

export class ElectronNotification extends Context.Service<
ElectronNotification,
{
/**
* Raises a native OS notification. Resolves once the notification has been
* handed to the platform, and no-ops when the platform has no notification
* support. Clicking the notification reveals the current window and
* broadcasts a `DesktopNotificationActivation` to every renderer.
*/
readonly show: (request: DesktopNotificationRequest) => Effect.Effect<void>;
}
>()("@t3tools/desktop/electron/ElectronNotification") {}

export const make = Effect.gen(function* () {
const electronWindow = yield* ElectronWindow.ElectronWindow;
// The click listener is a bare Electron callback, so the activation effect
// has to be run against the captured context (keeps logging/tracing intact).
const context = yield* Effect.context<never>();
const runFork = Effect.runForkWith(context);

// Live notifications keyed by `DesktopNotificationRequest.id`, which the
// contract defines as a coalescing key: showing again for the same id
// replaces the previous entry instead of stacking a second one in
// Notification Center (the web path gets this from the notification `tag`).
// Holding the JS reference also keeps the notification from being collected
// while it is on screen, which is what makes Electron's `click` reliable.
const liveNotifications = new Map<string, Electron.Notification>();

const activate = Effect.fn("desktop.electron.notification.activate")(function* (
request: DesktopNotificationRequest,
) {
const window = yield* electronWindow.focusedMainOrFirst;
if (Option.isSome(window)) {
yield* electronWindow.reveal(window.value);
}

const activation: DesktopNotificationActivation = {
environmentId: request.environmentId,
threadId: request.threadId,
};
// Broadcast rather than target the revealed window: any renderer may own
// the thread, and the activation is idempotent for the ones that don't.
yield* electronWindow.sendAll(NOTIFICATION_ACTIVATED_CHANNEL, activation);
});

const show = Effect.fn("desktop.electron.notification.show")(function* (
request: DesktopNotificationRequest,
) {
const supported = yield* Effect.try({
try: () => Electron.Notification.isSupported(),
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
if (!supported) {
return;
}

yield* Effect.try({
try: () => {
const notification = new Electron.Notification({
title: request.title,
body: request.body,
});
const forget = () => {
if (liveNotifications.get(request.id) === notification) {
liveNotifications.delete(request.id);
}
};
notification.on("click", () => {
forget();
runFork(activate(request));
});
notification.on("close", forget);

liveNotifications.get(request.id)?.close();
liveNotifications.set(request.id, notification);
notification.show();
},
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
});

return ElectronNotification.of({
// Notifications are advisory: failing to raise one must never fail the
// renderer's invoke, so failures are logged and swallowed here.
show: (request) => show(request).pipe(Effect.catch((error) => Effect.logWarning(error))),
});
});

export const layer = Layer.effect(ElectronNotification, make);
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import {
issueSshWebSocketTicket,
resolveSshPasswordPrompt,
} from "./methods/sshEnvironment.ts";
import { showNotification } from "./methods/notifications.ts";
import {
checkForUpdate,
downloadUpdate,
Expand DownExpand Up@@ -83,6 +84,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setTheme);
yield* ipc.handle(showContextMenu);
yield* ipc.handle(openExternal);
yield* ipc.handle(showNotification);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
266 changes: 266 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
import { assert, describe, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import type * as Electron from "electron";
import { beforeEach, vi } from "vite-plus/test";

interface FakeNotificationEntry {
readonly options: { readonly title?: string; readonly body?: string };
readonly listeners: Map<string, (...args: readonly unknown[]) => void>;
shown: number;
closed: number;
}

const { notificationState, FakeNotification } = vi.hoisted(() => {
const notificationState = {
supported: true,
constructorThrows: null as Error | null,
instances: [] as FakeNotificationEntry[],
};

class FakeNotification {
static isSupported(): boolean {
return notificationState.supported;
}

readonly entry: FakeNotificationEntry;

constructor(options: { title?: string; body?: string }) {
if (notificationState.constructorThrows !== null) {
throw notificationState.constructorThrows;
}
this.entry = { options, listeners: new Map(), shown: 0, closed: 0 };
notificationState.instances.push(this.entry);
}

on(event: string, listener: (...args: readonly unknown[]) => void): this {
this.entry.listeners.set(event, listener);
return this;
}

show(): void {
this.entry.shown += 1;
}

// Electron dismisses the notification and then emits `close`; the fake
// mirrors that so the service's bookkeeping is exercised.
close(): void {
this.entry.closed += 1;
this.entry.listeners.get("close")?.();
}
}

return { notificationState, FakeNotification };
});

vi.mock("electron", () => ({ Notification: FakeNotification }));

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronNotification from "./ElectronNotification.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

const request = {
id: "thread-1",
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
environmentId: "local",
threadId: "thread-1",
} as const;

const clickNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const click = entry.listeners.get("click");
if (click === undefined) {
throw new Error("Expected the notification to register a click listener.");
}
click();
};

const clickFirstNotification = () => clickNotification(0);

const closeNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const close = entry.listeners.get("close");
if (close === undefined) {
throw new Error("Expected the notification to register a close listener.");
}
close();
};

const silentWindowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: () => Effect.void,
});

describe("ElectronNotification", () => {
beforeEach(() => {
notificationState.supported = true;
notificationState.constructorThrows = null;
notificationState.instances = [];
});

it.effect("shows a native notification carrying the request title and body", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 1);
assert.deepEqual(notificationState.instances[0]?.options, {
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
});
assert.equal(notificationState.instances[0]?.shown, 1);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("reveals the target window and broadcasts the activation on click", () =>
Effect.gen(function* () {
const window = { id: 7 } as Electron.BrowserWindow;
const revealedRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.some(window)),
reveal: (target) => Ref.set(revealedRef, Option.some(target)),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

const broadcast = yield* Deferred.await(sent);
assert.deepEqual(broadcast, [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
assert.strictEqual(Option.getOrNull(yield* Ref.get(revealedRef)), window);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("broadcasts the activation even when there is no window to reveal", () =>
Effect.gen(function* () {
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

assert.deepEqual(yield* Deferred.await(sent), [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("no-ops when the platform does not support native notifications", () =>
Effect.gen(function* () {
notificationState.supported = false;

const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("closes the previous notification when the same id is shown again", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);
yield* notifications.show(request);
yield* notifications.show({ ...request, id: "thread-2", threadId: "thread-2" });

assert.equal(notificationState.instances.length, 3);
// Only the first is superseded: the third carries a different id.
assert.equal(notificationState.instances[0]?.closed, 1);
assert.equal(notificationState.instances[1]?.closed, 0);
assert.equal(notificationState.instances[2]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a dismissed notification so it is never closed twice", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

// The OS dismissal emits `close` without routing through `close()`.
closeNotification(0);
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a clicked notification so it is never closed afterwards", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("swallows Electron notification failures instead of failing the caller", () =>
Effect.gen(function* () {
notificationState.constructorThrows = new Error("notification center unavailable");

const notifications = yield* ElectronNotification.ElectronNotification;
const exit = yield* Effect.exit(notifications.show(request));

assert.equal(exit._tag, "Success");
assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);
});
113 changes: 113 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import type { DesktopNotificationActivation, DesktopNotificationRequest } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as Electron from "electron";

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

export class ElectronNotificationShowError extends Schema.TaggedErrorClass<ElectronNotificationShowError>()(
"ElectronNotificationShowError",
{
notificationId: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to show the Electron notification ${JSON.stringify(this.notificationId)}.`;
}
}

export class ElectronNotification extends Context.Service<
ElectronNotification,
{
/**
* Raises a native OS notification. Resolves once the notification has been
* handed to the platform, and no-ops when the platform has no notification
* support. Clicking the notification reveals the current window and
* broadcasts a `DesktopNotificationActivation` to every renderer.
*/
readonly show: (request: DesktopNotificationRequest) => Effect.Effect<void>;
}
>()("@t3tools/desktop/electron/ElectronNotification") {}

export const make = Effect.gen(function* () {
const electronWindow = yield* ElectronWindow.ElectronWindow;
// The click listener is a bare Electron callback, so the activation effect
// has to be run against the captured context (keeps logging/tracing intact).
const context = yield* Effect.context<never>();
const runFork = Effect.runForkWith(context);

// Live notifications keyed by `DesktopNotificationRequest.id`, which the
// contract defines as a coalescing key: showing again for the same id
// replaces the previous entry instead of stacking a second one in
// Notification Center (the web path gets this from the notification `tag`).
// Holding the JS reference also keeps the notification from being collected
// while it is on screen, which is what makes Electron's `click` reliable.
const liveNotifications = new Map<string, Electron.Notification>();

const activate = Effect.fn("desktop.electron.notification.activate")(function* (
request: DesktopNotificationRequest,
) {
const window = yield* electronWindow.focusedMainOrFirst;
if (Option.isSome(window)) {
yield* electronWindow.reveal(window.value);
}

const activation: DesktopNotificationActivation = {
environmentId: request.environmentId,
threadId: request.threadId,
};
// Broadcast rather than target the revealed window: any renderer may own
// the thread, and the activation is idempotent for the ones that don't.
yield* electronWindow.sendAll(NOTIFICATION_ACTIVATED_CHANNEL, activation);
});

const show = Effect.fn("desktop.electron.notification.show")(function* (
request: DesktopNotificationRequest,
) {
const supported = yield* Effect.try({
try: () => Electron.Notification.isSupported(),
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
if (!supported) {
return;
}

yield* Effect.try({
try: () => {
const notification = new Electron.Notification({
title: request.title,
body: request.body,
});
const forget = () => {
if (liveNotifications.get(request.id) === notification) {
liveNotifications.delete(request.id);
}
};
notification.on("click", () => {
forget();
runFork(activate(request));
});
notification.on("close", forget);

liveNotifications.get(request.id)?.close();
liveNotifications.set(request.id, notification);
notification.show();
},
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
});

return ElectronNotification.of({
// Notifications are advisory: failing to raise one must never fail the
// renderer's invoke, so failures are logged and swallowed here.
show: (request) => show(request).pipe(Effect.catch((error) => Effect.logWarning(error))),
});
});

export const layer = Layer.effect(ElectronNotification, make);
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import {
issueSshWebSocketTicket,
resolveSshPasswordPrompt,
} from "./methods/sshEnvironment.ts";
import { showNotification } from "./methods/notifications.ts";
import {
checkForUpdate,
downloadUpdate,
Expand DownExpand Up@@ -83,6 +84,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setTheme);
yield* ipc.handle(showContextMenu);
yield* ipc.handle(openExternal);
yield* ipc.handle(showNotification);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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
266 changes: 266 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
import { assert, describe, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import type * as Electron from "electron";
import { beforeEach, vi } from "vite-plus/test";

interface FakeNotificationEntry {
readonly options: { readonly title?: string; readonly body?: string };
readonly listeners: Map<string, (...args: readonly unknown[]) => void>;
shown: number;
closed: number;
}

const { notificationState, FakeNotification } = vi.hoisted(() => {
const notificationState = {
supported: true,
constructorThrows: null as Error | null,
instances: [] as FakeNotificationEntry[],
};

class FakeNotification {
static isSupported(): boolean {
return notificationState.supported;
}

readonly entry: FakeNotificationEntry;

constructor(options: { title?: string; body?: string }) {
if (notificationState.constructorThrows !== null) {
throw notificationState.constructorThrows;
}
this.entry = { options, listeners: new Map(), shown: 0, closed: 0 };
notificationState.instances.push(this.entry);
}

on(event: string, listener: (...args: readonly unknown[]) => void): this {
this.entry.listeners.set(event, listener);
return this;
}

show(): void {
this.entry.shown += 1;
}

// Electron dismisses the notification and then emits `close`; the fake
// mirrors that so the service's bookkeeping is exercised.
close(): void {
this.entry.closed += 1;
this.entry.listeners.get("close")?.();
}
}

return { notificationState, FakeNotification };
});

vi.mock("electron", () => ({ Notification: FakeNotification }));

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronNotification from "./ElectronNotification.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

const request = {
id: "thread-1",
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
environmentId: "local",
threadId: "thread-1",
} as const;

const clickNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const click = entry.listeners.get("click");
if (click === undefined) {
throw new Error("Expected the notification to register a click listener.");
}
click();
};

const clickFirstNotification = () => clickNotification(0);

const closeNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const close = entry.listeners.get("close");
if (close === undefined) {
throw new Error("Expected the notification to register a close listener.");
}
close();
};

const silentWindowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: () => Effect.void,
});

describe("ElectronNotification", () => {
beforeEach(() => {
notificationState.supported = true;
notificationState.constructorThrows = null;
notificationState.instances = [];
});

it.effect("shows a native notification carrying the request title and body", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 1);
assert.deepEqual(notificationState.instances[0]?.options, {
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
});
assert.equal(notificationState.instances[0]?.shown, 1);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("reveals the target window and broadcasts the activation on click", () =>
Effect.gen(function* () {
const window = { id: 7 } as Electron.BrowserWindow;
const revealedRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.some(window)),
reveal: (target) => Ref.set(revealedRef, Option.some(target)),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

const broadcast = yield* Deferred.await(sent);
assert.deepEqual(broadcast, [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
assert.strictEqual(Option.getOrNull(yield* Ref.get(revealedRef)), window);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("broadcasts the activation even when there is no window to reveal", () =>
Effect.gen(function* () {
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

assert.deepEqual(yield* Deferred.await(sent), [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("no-ops when the platform does not support native notifications", () =>
Effect.gen(function* () {
notificationState.supported = false;

const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("closes the previous notification when the same id is shown again", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);
yield* notifications.show(request);
yield* notifications.show({ ...request, id: "thread-2", threadId: "thread-2" });

assert.equal(notificationState.instances.length, 3);
// Only the first is superseded: the third carries a different id.
assert.equal(notificationState.instances[0]?.closed, 1);
assert.equal(notificationState.instances[1]?.closed, 0);
assert.equal(notificationState.instances[2]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a dismissed notification so it is never closed twice", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

// The OS dismissal emits `close` without routing through `close()`.
closeNotification(0);
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a clicked notification so it is never closed afterwards", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("swallows Electron notification failures instead of failing the caller", () =>
Effect.gen(function* () {
notificationState.constructorThrows = new Error("notification center unavailable");

const notifications = yield* ElectronNotification.ElectronNotification;
const exit = yield* Effect.exit(notifications.show(request));

assert.equal(exit._tag, "Success");
assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);
});
113 changes: 113 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import type { DesktopNotificationActivation, DesktopNotificationRequest } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as Electron from "electron";

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

export class ElectronNotificationShowError extends Schema.TaggedErrorClass<ElectronNotificationShowError>()(
"ElectronNotificationShowError",
{
notificationId: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to show the Electron notification ${JSON.stringify(this.notificationId)}.`;
}
}

export class ElectronNotification extends Context.Service<
ElectronNotification,
{
/**
* Raises a native OS notification. Resolves once the notification has been
* handed to the platform, and no-ops when the platform has no notification
* support. Clicking the notification reveals the current window and
* broadcasts a `DesktopNotificationActivation` to every renderer.
*/
readonly show: (request: DesktopNotificationRequest) => Effect.Effect<void>;
}
>()("@t3tools/desktop/electron/ElectronNotification") {}

export const make = Effect.gen(function* () {
const electronWindow = yield* ElectronWindow.ElectronWindow;
// The click listener is a bare Electron callback, so the activation effect
// has to be run against the captured context (keeps logging/tracing intact).
const context = yield* Effect.context<never>();
const runFork = Effect.runForkWith(context);

// Live notifications keyed by `DesktopNotificationRequest.id`, which the
// contract defines as a coalescing key: showing again for the same id
// replaces the previous entry instead of stacking a second one in
// Notification Center (the web path gets this from the notification `tag`).
// Holding the JS reference also keeps the notification from being collected
// while it is on screen, which is what makes Electron's `click` reliable.
const liveNotifications = new Map<string, Electron.Notification>();

const activate = Effect.fn("desktop.electron.notification.activate")(function* (
request: DesktopNotificationRequest,
) {
const window = yield* electronWindow.focusedMainOrFirst;
if (Option.isSome(window)) {
yield* electronWindow.reveal(window.value);
}

const activation: DesktopNotificationActivation = {
environmentId: request.environmentId,
threadId: request.threadId,
};
// Broadcast rather than target the revealed window: any renderer may own
// the thread, and the activation is idempotent for the ones that don't.
yield* electronWindow.sendAll(NOTIFICATION_ACTIVATED_CHANNEL, activation);
});

const show = Effect.fn("desktop.electron.notification.show")(function* (
request: DesktopNotificationRequest,
) {
const supported = yield* Effect.try({
try: () => Electron.Notification.isSupported(),
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
if (!supported) {
return;
}

yield* Effect.try({
try: () => {
const notification = new Electron.Notification({
title: request.title,
body: request.body,
});
const forget = () => {
if (liveNotifications.get(request.id) === notification) {
liveNotifications.delete(request.id);
}
};
notification.on("click", () => {
forget();
runFork(activate(request));
});
notification.on("close", forget);

liveNotifications.get(request.id)?.close();
liveNotifications.set(request.id, notification);
notification.show();
},
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
});

return ElectronNotification.of({
// Notifications are advisory: failing to raise one must never fail the
// renderer's invoke, so failures are logged and swallowed here.
show: (request) => show(request).pipe(Effect.catch((error) => Effect.logWarning(error))),
});
});

export const layer = Layer.effect(ElectronNotification, make);
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import {
issueSshWebSocketTicket,
resolveSshPasswordPrompt,
} from "./methods/sshEnvironment.ts";
import { showNotification } from "./methods/notifications.ts";
import {
checkForUpdate,
downloadUpdate,
Expand DownExpand Up@@ -83,6 +84,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setTheme);
yield* ipc.handle(showContextMenu);
yield* ipc.handle(openExternal);
yield* ipc.handle(showNotification);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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
266 changes: 266 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
import { assert, describe, it } from "@effect/vitest";
import * as Deferred from "effect/Deferred";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Ref from "effect/Ref";
import type * as Electron from "electron";
import { beforeEach, vi } from "vite-plus/test";

interface FakeNotificationEntry {
readonly options: { readonly title?: string; readonly body?: string };
readonly listeners: Map<string, (...args: readonly unknown[]) => void>;
shown: number;
closed: number;
}

const { notificationState, FakeNotification } = vi.hoisted(() => {
const notificationState = {
supported: true,
constructorThrows: null as Error | null,
instances: [] as FakeNotificationEntry[],
};

class FakeNotification {
static isSupported(): boolean {
return notificationState.supported;
}

readonly entry: FakeNotificationEntry;

constructor(options: { title?: string; body?: string }) {
if (notificationState.constructorThrows !== null) {
throw notificationState.constructorThrows;
}
this.entry = { options, listeners: new Map(), shown: 0, closed: 0 };
notificationState.instances.push(this.entry);
}

on(event: string, listener: (...args: readonly unknown[]) => void): this {
this.entry.listeners.set(event, listener);
return this;
}

show(): void {
this.entry.shown += 1;
}

// Electron dismisses the notification and then emits `close`; the fake
// mirrors that so the service's bookkeeping is exercised.
close(): void {
this.entry.closed += 1;
this.entry.listeners.get("close")?.();
}
}

return { notificationState, FakeNotification };
});

vi.mock("electron", () => ({ Notification: FakeNotification }));

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronNotification from "./ElectronNotification.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

const request = {
id: "thread-1",
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
environmentId: "local",
threadId: "thread-1",
} as const;

const clickNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const click = entry.listeners.get("click");
if (click === undefined) {
throw new Error("Expected the notification to register a click listener.");
}
click();
};

const clickFirstNotification = () => clickNotification(0);

const closeNotification = (index: number) => {
const entry = notificationState.instances[index];
assert.isDefined(entry);
const close = entry.listeners.get("close");
if (close === undefined) {
throw new Error("Expected the notification to register a close listener.");
}
close();
};

const silentWindowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: () => Effect.void,
});

describe("ElectronNotification", () => {
beforeEach(() => {
notificationState.supported = true;
notificationState.constructorThrows = null;
notificationState.instances = [];
});

it.effect("shows a native notification carrying the request title and body", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 1);
assert.deepEqual(notificationState.instances[0]?.options, {
title: "T3 Code needs your input",
body: "Claude is waiting on your answer.",
});
assert.equal(notificationState.instances[0]?.shown, 1);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("reveals the target window and broadcasts the activation on click", () =>
Effect.gen(function* () {
const window = { id: 7 } as Electron.BrowserWindow;
const revealedRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.some(window)),
reveal: (target) => Ref.set(revealedRef, Option.some(target)),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

const broadcast = yield* Deferred.await(sent);
assert.deepEqual(broadcast, [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
assert.strictEqual(Option.getOrNull(yield* Ref.get(revealedRef)), window);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("broadcasts the activation even when there is no window to reveal", () =>
Effect.gen(function* () {
const sent = yield* Deferred.make<readonly unknown[]>();
const windowLayer = Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
sendAll: (channel, ...args) =>
Deferred.succeed(sent, [channel, ...args]).pipe(Effect.asVoid),
});

yield* Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();

assert.deepEqual(yield* Deferred.await(sent), [
NOTIFICATION_ACTIVATED_CHANNEL,
{ environmentId: "local", threadId: "thread-1" },
]);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(windowLayer))));
}),
);

it.effect("no-ops when the platform does not support native notifications", () =>
Effect.gen(function* () {
notificationState.supported = false;

const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);

it.effect("closes the previous notification when the same id is shown again", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);
yield* notifications.show(request);
yield* notifications.show({ ...request, id: "thread-2", threadId: "thread-2" });

assert.equal(notificationState.instances.length, 3);
// Only the first is superseded: the third carries a different id.
assert.equal(notificationState.instances[0]?.closed, 1);
assert.equal(notificationState.instances[1]?.closed, 0);
assert.equal(notificationState.instances[2]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a dismissed notification so it is never closed twice", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

// The OS dismissal emits `close` without routing through `close()`.
closeNotification(0);
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("forgets a clicked notification so it is never closed afterwards", () =>
Effect.gen(function* () {
const notifications = yield* ElectronNotification.ElectronNotification;
yield* notifications.show(request);

clickFirstNotification();
yield* notifications.show(request);

assert.equal(notificationState.instances.length, 2);
assert.equal(notificationState.instances[0]?.closed, 0);
}).pipe(Effect.provide(ElectronNotification.layer.pipe(Layer.provide(silentWindowLayer)))),
);

it.effect("swallows Electron notification failures instead of failing the caller", () =>
Effect.gen(function* () {
notificationState.constructorThrows = new Error("notification center unavailable");

const notifications = yield* ElectronNotification.ElectronNotification;
const exit = yield* Effect.exit(notifications.show(request));

assert.equal(exit._tag, "Success");
assert.equal(notificationState.instances.length, 0);
}).pipe(
Effect.provide(
ElectronNotification.layer.pipe(
Layer.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
focusedMainOrFirst: Effect.succeed(Option.none()),
}),
),
),
),
),
);
});
113 changes: 113 additions & 0 deletions apps/desktop/src/electron/ElectronNotification.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
import type { DesktopNotificationActivation, DesktopNotificationRequest } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";

import * as Electron from "electron";

import { NOTIFICATION_ACTIVATED_CHANNEL } from "../ipc/channels.ts";
import * as ElectronWindow from "./ElectronWindow.ts";

export class ElectronNotificationShowError extends Schema.TaggedErrorClass<ElectronNotificationShowError>()(
"ElectronNotificationShowError",
{
notificationId: Schema.String,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to show the Electron notification ${JSON.stringify(this.notificationId)}.`;
}
}

export class ElectronNotification extends Context.Service<
ElectronNotification,
{
/**
* Raises a native OS notification. Resolves once the notification has been
* handed to the platform, and no-ops when the platform has no notification
* support. Clicking the notification reveals the current window and
* broadcasts a `DesktopNotificationActivation` to every renderer.
*/
readonly show: (request: DesktopNotificationRequest) => Effect.Effect<void>;
}
>()("@t3tools/desktop/electron/ElectronNotification") {}

export const make = Effect.gen(function* () {
const electronWindow = yield* ElectronWindow.ElectronWindow;
// The click listener is a bare Electron callback, so the activation effect
// has to be run against the captured context (keeps logging/tracing intact).
const context = yield* Effect.context<never>();
const runFork = Effect.runForkWith(context);

// Live notifications keyed by `DesktopNotificationRequest.id`, which the
// contract defines as a coalescing key: showing again for the same id
// replaces the previous entry instead of stacking a second one in
// Notification Center (the web path gets this from the notification `tag`).
// Holding the JS reference also keeps the notification from being collected
// while it is on screen, which is what makes Electron's `click` reliable.
const liveNotifications = new Map<string, Electron.Notification>();

const activate = Effect.fn("desktop.electron.notification.activate")(function* (
request: DesktopNotificationRequest,
) {
const window = yield* electronWindow.focusedMainOrFirst;
if (Option.isSome(window)) {
yield* electronWindow.reveal(window.value);
}

const activation: DesktopNotificationActivation = {
environmentId: request.environmentId,
threadId: request.threadId,
};
// Broadcast rather than target the revealed window: any renderer may own
// the thread, and the activation is idempotent for the ones that don't.
yield* electronWindow.sendAll(NOTIFICATION_ACTIVATED_CHANNEL, activation);
});

const show = Effect.fn("desktop.electron.notification.show")(function* (
request: DesktopNotificationRequest,
) {
const supported = yield* Effect.try({
try: () => Electron.Notification.isSupported(),
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
if (!supported) {
return;
}

yield* Effect.try({
try: () => {
const notification = new Electron.Notification({
title: request.title,
body: request.body,
});
const forget = () => {
if (liveNotifications.get(request.id) === notification) {
liveNotifications.delete(request.id);
}
};
notification.on("click", () => {
forget();
runFork(activate(request));
});
notification.on("close", forget);

liveNotifications.get(request.id)?.close();
liveNotifications.set(request.id, notification);
notification.show();
},
catch: (cause) => new ElectronNotificationShowError({ notificationId: request.id, cause }),
});
});

return ElectronNotification.of({
// Notifications are advisory: failing to raise one must never fail the
// renderer's invoke, so failures are logged and swallowed here.
show: (request) => show(request).pipe(Effect.catch((error) => Effect.logWarning(error))),
});
});

export const layer = Layer.effect(ElectronNotification, make);
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,6 +23,7 @@ import {
issueSshWebSocketTicket,
resolveSshPasswordPrompt,
} from "./methods/sshEnvironment.ts";
import { showNotification } from "./methods/notifications.ts";
import {
checkForUpdate,
downloadUpdate,
Expand DownExpand Up@@ -83,6 +84,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setTheme);
yield* ipc.handle(showContextMenu);
yield* ipc.handle(openExternal);
yield* ipc.handle(showNotification);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
Expand Down
Loading