diff --git a/apps/desktop/src/ipc/DesktopIpc.ts b/apps/desktop/src/ipc/DesktopIpc.ts
index dd645a3379f6614fdb70bafd3b4e495abd286a57..37d68ec05ddec3fd3566b9e3f5ff2337522e090b 100644
--- a/apps/desktop/src/ipc/DesktopIpc.ts
+++ b/apps/desktop/src/ipc/DesktopIpc.ts
@@ -6,6 +6,7 @@ import * as Scope from "effect/Scope";
export interface DesktopIpcInvokeEvent {
readonly sender: { readonly id: number };
+ readonly senderFrame?: { readonly url: string; readonly parent: unknown } | null;
}
export interface DesktopIpcSyncEvent {
diff --git a/apps/desktop/src/ipc/DesktopIpcHandlers.ts b/apps/desktop/src/ipc/DesktopIpcHandlers.ts
index caed223e90e8666176c1f76e59f835a567a63531..585bbb3864b0fbe8ddbca903baefdc6740976ccf 100644
--- a/apps/desktop/src/ipc/DesktopIpcHandlers.ts
+++ b/apps/desktop/src/ipc/DesktopIpcHandlers.ts
@@ -1,3 +1,4 @@
+import { openVideo } from "./methods/media.ts";
import * as Effect from "effect/Effect";
import { receiveProviderAuthCallback, cancelProviderAuthCallback } from "./methods/providerAuth.ts";
@@ -145,6 +146,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(setTheme);
yield* ipc.handle(showContextMenu);
yield* ipc.handle(openExternal);
+ yield* ipc.handle(openVideo);
yield* ipc.handle(receiveProviderAuthCallback);
yield* ipc.handle(cancelProviderAuthCallback);
yield* ipc.handle(openSystemSettings);
diff --git a/apps/desktop/src/ipc/channels.ts b/apps/desktop/src/ipc/channels.ts
index 8e2ebec04f95883050fa53341abdad0281d1582a..9a8e05d20ee25dda9a209ddcb9f574354a6117b4 100644
--- a/apps/desktop/src/ipc/channels.ts
+++ b/apps/desktop/src/ipc/channels.ts
@@ -124,3 +124,5 @@ export const CANCEL_PROVIDER_AUTH_CALLBACK_CHANNEL = "desktop:cancel-provider-au
export const TAKE_LEGACY_LOCAL_STORAGE_CHANNEL = "desktop:take-legacy-local-storage";
export const COMPLETE_LEGACY_LOCAL_STORAGE_CHANNEL = "desktop:complete-legacy-local-storage";
export const DESKTOP_TERMINAL_CHANNEL = "desktop:terminal";
+
+export const OPEN_VIDEO_CHANNEL = "desktop:open-video";
diff --git a/apps/desktop/src/ipc/methods/media.test.ts b/apps/desktop/src/ipc/methods/media.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c316641468abba0141fb4a5b9793c88508482a64
--- /dev/null
+++ b/apps/desktop/src/ipc/methods/media.test.ts
@@ -0,0 +1,56 @@
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import { assert, it } from "@effect/vitest";
+import * as Effect from "effect/Effect";
+import * as Layer from "effect/Layer";
+import type * as Electron from "electron";
+import { vi } from "vite-plus/test";
+
+vi.mock("electron", () => ({}));
+
+import * as DesktopConfig from "../../app/DesktopConfig.ts";
+import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts";
+import * as ElectronWindow from "../../electron/ElectronWindow.ts";
+import * as DesktopVideo from "../../media/DesktopVideo.ts";
+import { getDesktopUrl } from "../../electron/ElectronProtocol.ts";
+import { openVideo } from "./media.ts";
+
+it.effect("allows only the top-level app frame to open a video", () => {
+ const calls: unknown[] = [];
+ // Electron owns this object; only the native identity checks are exercised here.
+ const window = { isDestroyed: () => false, webContents: { id: 42 } } as Electron.BrowserWindow;
+ const layer = Layer.mergeAll(
+ DesktopEnvironment.layer({
+ dirname: "/repo/apps/desktop/dist-electron",
+ homeDirectory: "/tmp/t3-media-test",
+ platform: "darwin",
+ processArch: "arm64",
+ appVersion: "0.0.45",
+ appPath: "/tmp/T3 Code.app",
+ isPackaged: true,
+ resourcesPath: "/tmp/resources",
+ runningUnderArm64Translation: false,
+ }).pipe(Layer.provide(Layer.mergeAll(NodeServices.layer, DesktopConfig.layerTest({})))),
+ Layer.mock(ElectronWindow.ElectronWindow, { main: Effect.succeedSome(window) }),
+ Layer.succeed(DesktopVideo.DesktopVideo, {
+ open: (input) =>
+ Effect.sync(() => {
+ calls.push(input);
+ }),
+ }),
+ );
+ const input = { url: "https://remote.test/clip.mp4", name: "clip.mp4" };
+ const trustedFrame = { parent: null, url: getDesktopUrl(false) };
+ return Effect.gen(function* () {
+ for (const event of [
+ undefined,
+ { sender: { id: 100 }, senderFrame: trustedFrame },
+ { sender: { id: 42 }, senderFrame: { parent: {}, url: trustedFrame.url } },
+ { sender: { id: 42 }, senderFrame: { parent: null, url: "https://preview.test" } },
+ { sender: { id: 42 } },
+ ])
+ assert.equal((yield* Effect.result(openVideo.handler(input, event)))._tag, "Failure");
+ assert.equal(calls.length, 0);
+ yield* openVideo.handler(input, { sender: { id: 42 }, senderFrame: trustedFrame });
+ assert.deepEqual(calls, [input]);
+ }).pipe(Effect.provide(layer));
+});
diff --git a/apps/desktop/src/ipc/methods/media.ts b/apps/desktop/src/ipc/methods/media.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c98502645f4ac6d995668312a30d7619daa07a15
--- /dev/null
+++ b/apps/desktop/src/ipc/methods/media.ts
@@ -0,0 +1,37 @@
+import * as Effect from "effect/Effect";
+import * as Option from "effect/Option";
+import * as Schema from "effect/Schema";
+import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts";
+import * as ElectronWindow from "../../electron/ElectronWindow.ts";
+import { getDesktopUrl } from "../../electron/ElectronProtocol.ts";
+import * as DesktopVideo from "../../media/DesktopVideo.ts";
+import * as DesktopIpc from "../DesktopIpc.ts";
+import * as IpcChannels from "../channels.ts";
+
+export const openVideo = DesktopIpc.makeIpcMethod({
+ channel: IpcChannels.OPEN_VIDEO_CHANNEL,
+ payload: Schema.Struct({
+ url: Schema.String.check(Schema.isMaxLength(16_384)),
+ name: Schema.String.check(Schema.isMaxLength(255)),
+ }),
+ result: Schema.Void,
+ handler: Effect.fn("desktop.ipc.media.openVideo")(function* (input, event) {
+ const environment = yield* DesktopEnvironment.DesktopEnvironment;
+ const main = yield* (yield* ElectronWindow.ElectronWindow).main;
+ const expected = new URL(getDesktopUrl(environment.isDevelopment));
+ const frame = event?.senderFrame;
+ const source = frame && URL.canParse(frame.url) ? new URL(frame.url) : null;
+ if (
+ event === undefined ||
+ Option.isNone(main) ||
+ main.value.isDestroyed() ||
+ main.value.webContents.id !== event.sender.id ||
+ frame?.parent !== null ||
+ source?.protocol !== expected.protocol ||
+ source.host !== expected.host
+ )
+ return yield* new DesktopVideo.DesktopVideoError({ stage: "source" });
+
+ return yield* (yield* DesktopVideo.DesktopVideo).open(input);
+ }),
+});
diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts
index 5720858f6993a31beae743e5c4dd9acfa1dfd99f..9c0b083f00fc28a6d911ab780a140e377f46920d 100644
--- a/apps/desktop/src/main.ts
+++ b/apps/desktop/src/main.ts
@@ -23,6 +23,7 @@ import type { RemoteT3RunnerOptions } from "@t3tools/ssh/tunnel";
import serverPackageJson from "../../server/package.json" with { type: "json" };
import * as DesktopIpc from "./ipc/DesktopIpc.ts";
+import * as DesktopVideo from "./media/DesktopVideo.ts";
import * as ElectronApp from "./electron/ElectronApp.ts";
import * as ElectronDialog from "./electron/ElectronDialog.ts";
import * as ElectronMenu from "./electron/ElectronMenu.ts";
@@ -216,6 +217,7 @@ const layerDesktopLocalEnvironmentAuth = DesktopLocalEnvironmentAuth.layer.pipe(
const layerDesktopApplication = Layer.mergeAll(
DesktopLifecycle.layer,
+ DesktopVideo.layer,
layerDesktopAppActivation,
DesktopWebLinks.layer,
DesktopApplicationMenu.layer,
diff --git a/apps/desktop/src/media/DesktopVideo.test.ts b/apps/desktop/src/media/DesktopVideo.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..896f79edcb90179be7d75077f13a4a0339578eb8
--- /dev/null
+++ b/apps/desktop/src/media/DesktopVideo.test.ts
@@ -0,0 +1,167 @@
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import { assert, it } from "@effect/vitest";
+import * as HostProcess from "@t3tools/shared/HostProcess";
+import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
+import * as Layer from "effect/Layer";
+import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
+import { beforeEach, vi } from "vite-plus/test";
+
+const { fetchVideo, open } = vi.hoisted(() => ({ fetchVideo: vi.fn(), open: vi.fn() }));
+vi.mock("electron", () => ({ net: { fetch: fetchVideo } }));
+
+import * as DesktopVideo from "./DesktopVideo.ts";
+
+const spawner = Layer.mock(ChildProcessSpawner.ChildProcessSpawner, {
+ exitCode: (command) =>
+ Effect.sync(() => {
+ open(command);
+ return ChildProcessSpawner.ExitCode(0);
+ }),
+});
+
+beforeEach(() => {
+ fetchVideo.mockReset().mockImplementation(
+ async () =>
+ new Response("video bytes", {
+ headers: { "content-type": "video/mp4" },
+ }),
+ );
+ open.mockReset();
+});
+
+const layer = DesktopVideo.layer.pipe(Layer.provide(spawner));
+
+it.effect(
+ "streams to a private client temp file, opens only QuickTime, and cleans up on app scope exit",
+ () => {
+ let openedPath = "";
+ return Effect.gen(function* () {
+ const fs = yield* FileSystem.FileSystem;
+ yield* Effect.gen(function* () {
+ const video = yield* DesktopVideo.DesktopVideo;
+ yield* video.open({
+ url: "https://remote.test/api/assets/signed/clip.mp4",
+ name: "../../clip.mp4",
+ });
+ const command = open.mock.lastCall![0];
+ assert.equal(command.command, "/usr/bin/open");
+ assert.deepEqual(command.args.slice(0, 2), [
+ "-a",
+ "/System/Applications/QuickTime Player.app",
+ ]);
+ openedPath = command.args[2];
+ assert.match(openedPath, /\/t3-video-[^/]+\/clip\.mp4$/);
+ assert.equal(yield* fs.readFileString(openedPath), "video bytes");
+ assert.equal((yield* fs.stat(openedPath)).mode & 0o777, 0o600);
+ }).pipe(Effect.provide(layer));
+ assert.isFalse(yield* fs.exists(openedPath));
+ }).pipe(
+ Effect.provideService(HostProcess.Platform, "darwin"),
+ Effect.provide(NodeServices.layer),
+ );
+ },
+);
+
+it.effect("rejects non-macOS before downloading", () =>
+ Effect.gen(function* () {
+ const result = yield* Effect.result(
+ (yield* DesktopVideo.DesktopVideo).open({
+ url: "https://remote.test/clip.mp4",
+ name: "clip.mp4",
+ }),
+ );
+ assert.equal(result._tag, "Failure");
+ assert.equal(fetchVideo.mock.calls.length, 0);
+ assert.equal(open.mock.calls.length, 0);
+ }).pipe(
+ Effect.provide(layer),
+ Effect.provideService(HostProcess.Platform, "linux"),
+ Effect.provide(NodeServices.layer),
+ ),
+);
+
+it.effect("rejects file URLs and nonvideo filenames without reading local files", () =>
+ Effect.gen(function* () {
+ const video = yield* DesktopVideo.DesktopVideo;
+ for (const input of [
+ { url: "file:///Users/private/clip.mp4", name: "clip.mp4" },
+ { url: "https://remote.test/run", name: "run.command" },
+ { url: "https://user:password@remote.test/clip.mp4", name: "clip.mp4" },
+ ])
+ assert.equal((yield* Effect.result(video.open(input)))._tag, "Failure");
+ assert.equal(fetchVideo.mock.calls.length, 0);
+ assert.equal(open.mock.calls.length, 0);
+ }).pipe(
+ Effect.provide(layer),
+ Effect.provideService(HostProcess.Platform, "darwin"),
+ Effect.provide(NodeServices.layer),
+ ),
+);
+
+it.effect("does not open an authorization error or HTML error page as a video", () =>
+ Effect.gen(function* () {
+ const video = yield* DesktopVideo.DesktopVideo;
+ for (const response of [
+ new Response("unauthorized", { status: 401 }),
+ new Response("sign in", { headers: { "content-type": "text/html" } }),
+ ]) {
+ fetchVideo.mockResolvedValueOnce(response);
+ assert.equal(
+ (yield* Effect.result(
+ video.open({ url: "https://remote.test/clip.mp4", name: "clip.mp4" }),
+ ))._tag,
+ "Failure",
+ );
+ }
+ assert.equal(open.mock.calls.length, 0);
+ }).pipe(
+ Effect.provide(layer),
+ Effect.provideService(HostProcess.Platform, "darwin"),
+ Effect.provide(NodeServices.layer),
+ ),
+);
+
+it.effect("does not launch a partial download when its stream fails", () =>
+ Effect.gen(function* () {
+ fetchVideo.mockResolvedValueOnce(
+ new Response(
+ new ReadableStream({
+ start(controller) {
+ controller.enqueue(new TextEncoder().encode("partial"));
+ controller.error(new Error("disconnected"));
+ },
+ }),
+ { headers: { "content-type": "video/mp4" } },
+ ),
+ );
+ const video = yield* DesktopVideo.DesktopVideo;
+ assert.equal(
+ (yield* Effect.result(video.open({ url: "https://remote.test/clip.mp4", name: "clip.mp4" })))
+ ._tag,
+ "Failure",
+ );
+ assert.equal(open.mock.calls.length, 0);
+ }).pipe(
+ Effect.provide(layer),
+ Effect.provideService(HostProcess.Platform, "darwin"),
+ Effect.provide(NodeServices.layer),
+ ),
+);
+
+it.effect("uses the video response type for an extensionless remote download", () =>
+ Effect.gen(function* () {
+ fetchVideo.mockResolvedValueOnce(
+ new Response("movie", { headers: { "content-type": "video/quicktime" } }),
+ );
+ yield* (yield* DesktopVideo.DesktopVideo).open({
+ url: "https://remote.test/download?id=clip",
+ name: "recording",
+ });
+ assert.match(open.mock.lastCall![0].args[2], /\/recording\.mov$/);
+ }).pipe(
+ Effect.provide(layer),
+ Effect.provideService(HostProcess.Platform, "darwin"),
+ Effect.provide(NodeServices.layer),
+ ),
+);
diff --git a/apps/desktop/src/media/DesktopVideo.ts b/apps/desktop/src/media/DesktopVideo.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7c0f090a6177eed0f11104055ea1c0dd6a5e2985
--- /dev/null
+++ b/apps/desktop/src/media/DesktopVideo.ts
@@ -0,0 +1,122 @@
+import { VIDEO_FILE_EXTENSIONS, videoMimeType } from "@t3tools/shared/video";
+import * as HostProcess from "@t3tools/shared/HostProcess";
+import * as Context from "effect/Context";
+import * as Effect from "effect/Effect";
+import * as FileSystem from "effect/FileSystem";
+import * as Layer from "effect/Layer";
+import * as Path from "effect/Path";
+import * as Schema from "effect/Schema";
+import * as Scope from "effect/Scope";
+import * as Stream from "effect/Stream";
+import * as ChildProcess from "effect/process/ChildProcess";
+import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
+import * as Electron from "electron";
+
+export class DesktopVideoError extends Schema.TaggedError\n\n
',
+ ],
+ ])("groups %s without changing playback sources or accessible names", async (_case, text) => {
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
+ const pause = vi.spyOn(HTMLMediaElement.prototype, "pause").mockImplementation(() => {});
+ const container = document.createElement("div");
+ const root = createRoot(container);
+ try {
+ await act(() => root.render(
{label}
+{skill?.description ?? "No description is available for this skill."}
+ {skill?.path ? ( + + ) : null} +{skillLabel}
-- {skill?.description ?? - skillDescription ?? - "No description is available for this skill."} -
- {skill?.path ? ( - - ) : null} -
- {/* A caret needs a line to sit on even before any code is typed. */}
-
- as="code"
- className="block min-h-[1lh] font-mono whitespace-pre-wrap text-inherit [overflow-wrap:anywhere]"
- />
-
-
{props.isLoading
- ? props.triggerKind === "skill"
- ? "Searching workspace skills..."
- : props.triggerKind === "pull-request"
- ? "Finding pull request..."
- : "Searching workspace files..."
+ ? props.triggerKind === "emoji"
+ ? "Loading emoji…"
+ : props.triggerKind === "skill"
+ ? "Searching workspace skills..."
+ : props.triggerKind === "pull-request"
+ ? "Finding pull request..."
+ : "Searching workspace files..."
: (props.emptyStateText ??
(props.triggerKind === "skill"
? "No skills found. Try / to browse provider commands."
@@ -190,6 +200,11 @@ const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: {
props.onSelect(props.item);
}}
>
+ {props.item.type === "emoji" ? (
+
+ ) : null}
{props.item.type === "path" ? (