Skip to content
Closed
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
185 changes: 185 additions & 0 deletions apps/desktop/src/computerHistory/manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// @effect-diagnostics nodeBuiltinImport:off
// @effect-diagnostics globalDate:off
import { spawn, type ChildProcess } from "node:child_process";
import * as NodeFs from "node:fs";
import * as NodePath from "node:path";
import { shell } from "electron";

import type {
ComputerHistoryClearScope,
ComputerHistoryStatus,
ComputerHistoryTimeline,
ComputerHistorySettings,
} from "@t3tools/contracts";
import {
clearHistory,
defaultCodexHome,
deleteMemory,
ensureComputerHistoryLayout,
listTimeline,
readStatusFile,
resolveComputerHistoryRoot,
writeControlFile,
} from "@t3tools/shared/computerHistory";

import { resolveDesktopMcpBinaryPathSync } from "./resolveBinary.ts";

let daemon: ChildProcess | null = null;
let rootPath: string | null = null;

process.on("exit", () => {
if (daemon && !daemon.killed) {
try {
daemon.kill("SIGTERM");
} catch {
// ignore
}
}
});

export function computerHistoryRootForStateDir(stateDir: string): string {
return resolveComputerHistoryRoot(stateDir);
}

export async function syncControl(
stateDir: string,
settings: ComputerHistorySettings,
): Promise<void> {
const root = resolveComputerHistoryRoot(stateDir);
rootPath = root;
await ensureComputerHistoryLayout(root);
await writeControlFile(root, {
enabled: settings.enabled,
paused: settings.paused,
appFilterMode: settings.appFilterMode,
apps: [...settings.apps],
websiteFilterMode: settings.websiteFilterMode,
websites: [...settings.websites],
});
}

export async function ensureDaemon(
stateDir: string,
settings: ComputerHistorySettings,
): Promise<void> {
await syncControl(stateDir, settings);
const root = resolveComputerHistoryRoot(stateDir);
rootPath = root;

if (!settings.enabled) {
stopDaemon();
return;
}

if (daemon && !daemon.killed) {
return;
}

const binary = resolveDesktopMcpBinaryPathSync();
if (!binary) {
await writeUnavailableStatus(root, "t3-desktop-mcp binary not found");
return;
}

daemon = spawn(binary, ["computer-history", "--root", root], {
stdio: ["ignore", "ignore", "pipe"],
detached: false,
});
daemon.stderr?.on("data", (chunk: Buffer) => {
process.stderr.write(chunk);
});
daemon.on("exit", () => {
daemon = null;
});
}

export function stopDaemon(): void {
if (!daemon) return;
try {
daemon.kill("SIGTERM");
} catch {
// ignore
}
daemon = null;
}

async function writeUnavailableStatus(root: string, lastError: string): Promise<void> {
await ensureComputerHistoryLayout(root);
const payload = {
phase: "unavailable",
accessibilityGranted: false,
eventCount: 0,
platform: process.platform,
updatedAt: new Date().toISOString(),
lastError,
};
await NodeFs.promises.writeFile(
NodePath.join(root, "status.json"),
`${JSON.stringify(payload, null, 2)}\n`,
"utf8",
);
}

export async function getStatus(
stateDir: string,
settings: ComputerHistorySettings,
): Promise<ComputerHistoryStatus> {
const root = resolveComputerHistoryRoot(stateDir);
await ensureComputerHistoryLayout(root);
const file = await readStatusFile(root);
const memoriesPath = NodePath.join(root, "memories", "resources");
const codexMirrorPath = settings.mirrorToCodex
? NodePath.join(defaultCodexHome(), "memories", "extensions", "skysight", "resources")
: undefined;

return {
enabled: settings.enabled,
paused: settings.paused,
phase: !settings.enabled ? "stopped" : (file?.phase ?? (daemon ? "starting" : "stopped")),
accessibilityGranted: file?.accessibilityGranted ?? false,
rootPath: root,
memoriesPath,
...(codexMirrorPath ? { codexMirrorPath } : {}),
...(file?.activeSegmentId ? { activeSegmentId: file.activeSegmentId } : {}),
eventCount: file?.eventCount ?? 0,
...(file?.lastError ? { lastError: file.lastError } : {}),
platform: file?.platform ?? process.platform,
};
}

export async function getTimeline(stateDir: string): Promise<ComputerHistoryTimeline> {
const root = resolveComputerHistoryRoot(stateDir);
return listTimeline(root);
}

export async function clear(
stateDir: string,
scope: ComputerHistoryClearScope,
settings: ComputerHistorySettings,
): Promise<ComputerHistoryTimeline> {
const root = resolveComputerHistoryRoot(stateDir);
return clearHistory(root, scope, {
...(settings.mirrorToCodex ? { codexHome: defaultCodexHome() } : {}),
});
}

export async function removeMemory(
stateDir: string,
path: string,
settings: ComputerHistorySettings,
): Promise<ComputerHistoryTimeline> {
const root = resolveComputerHistoryRoot(stateDir);
return deleteMemory(root, path, {
...(settings.mirrorToCodex ? { codexHome: defaultCodexHome() } : {}),
});
}

export async function revealMemory(path: string): Promise<boolean> {
if (!NodeFs.existsSync(path)) return false;
shell.showItemInFolder(path);
return true;
}

export function currentRoot(): string | null {
return rootPath;
}
47 changes: 47 additions & 0 deletions apps/desktop/src/computerHistory/resolveBinary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeFs from "node:fs";
import * as NodePath from "node:path";
import { fileURLToPath } from "node:url";

const DESKTOP_MCP = process.platform === "win32" ? "t3-desktop-mcp.exe" : "t3-desktop-mcp";

/**
* Locate the bundled/dev desktop MCP binary for Computer History daemon spawn.
* Mirrors server resolution but stays sync for Electron main.
*/
export function resolveDesktopMcpBinaryPathSync(): string | undefined {
const override = process.env.T3CODE_DESKTOP_MCP_PATH;
if (override && NodeFs.existsSync(override)) return override;

const here = NodePath.dirname(fileURLToPath(import.meta.url));
const candidates =
process.platform === "darwin"
? [
NodePath.resolve(
here,
"../../../../native/t3-desktop-mcp/.build/apple/Products/Release",
DESKTOP_MCP,
),
NodePath.resolve(here, "../../../../native/t3-desktop-mcp/.build/release", DESKTOP_MCP),
NodePath.resolve(
here,
"../../../native/t3-desktop-mcp/.build/apple/Products/Release",
DESKTOP_MCP,
),
NodePath.resolve(process.resourcesPath ?? "", "t3-desktop-mcp", DESKTOP_MCP),
]
: [
NodePath.resolve(
here,
"../../../../native/t3-desktop-mcp-rs/target/release",
DESKTOP_MCP,
),
NodePath.resolve(here, "../../../native/t3-desktop-mcp-rs/target/release", DESKTOP_MCP),
NodePath.resolve(process.resourcesPath ?? "", "t3-desktop-mcp", DESKTOP_MCP),
];

for (const candidate of candidates) {
if (NodeFs.existsSync(candidate)) return candidate;
}
return undefined;
}
14 changes: 14 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ import {
getComputerUsePermissions,
openComputerUsePrivacySettings,
} from "./methods/computerUse.ts";
import {
clearComputerHistory,
deleteComputerHistoryMemory,
getComputerHistoryStatus,
getComputerHistoryTimeline,
patchComputerHistorySettings,
revealComputerHistoryMemory,
} from "./methods/computerHistory.ts";
import {
bootstrapSshBearerSession,
disconnectSshEnvironment,
Expand Down Expand Up @@ -89,6 +97,12 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(openExternal);
yield* ipc.handle(getComputerUsePermissions);
yield* ipc.handle(openComputerUsePrivacySettings);
yield* ipc.handle(getComputerHistoryStatus);
yield* ipc.handle(getComputerHistoryTimeline);
yield* ipc.handle(patchComputerHistorySettings);
yield* ipc.handle(clearComputerHistory);
yield* ipc.handle(revealComputerHistoryMemory);
yield* ipc.handle(deleteComputerHistoryMemory);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
yield* ipc.handle(downloadUpdate);
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ export const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
export const GET_COMPUTER_USE_PERMISSIONS_CHANNEL = "desktop:get-computer-use-permissions";
export const OPEN_COMPUTER_USE_PRIVACY_SETTINGS_CHANNEL =
"desktop:open-computer-use-privacy-settings";
export const GET_COMPUTER_HISTORY_STATUS_CHANNEL = "desktop:get-computer-history-status";
export const GET_COMPUTER_HISTORY_TIMELINE_CHANNEL = "desktop:get-computer-history-timeline";
export const PATCH_COMPUTER_HISTORY_SETTINGS_CHANNEL = "desktop:patch-computer-history-settings";
export const CLEAR_COMPUTER_HISTORY_CHANNEL = "desktop:clear-computer-history";
export const REVEAL_COMPUTER_HISTORY_MEMORY_CHANNEL = "desktop:reveal-computer-history-memory";
export const DELETE_COMPUTER_HISTORY_MEMORY_CHANNEL = "desktop:delete-computer-history-memory";
export const MENU_ACTION_CHANNEL = "desktop:menu-action";
export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state";
export const WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:window-fullscreen-state";
Expand Down
Loading