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
5 changes: 5 additions & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@
"@trpc/react-query": "^11.7.1",
"@trpc/server": "^11.7.1",
"@types/express": "^5.0.5",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-image": "^0.8.0",
"@xterm/addon-search": "^0.15.0",
"@xterm/addon-serialize": "^0.13.0",
"@xterm/addon-unicode11": "^0.8.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0",
Expand All @@ -51,6 +55,7 @@
"fast-glob": "^3.3.3",
"framer-motion": "^12.23.24",
"http-proxy": "^1.18.1",
"line-column-path": "^3.0.0",
"lodash": "^4.17.21",
"lowdb": "^7.0.1",
"nanoid": "^5.1.6",
Expand Down
103 changes: 103 additions & 0 deletions apps/desktop/src/lib/trpc/routers/external/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { spawn } from "node:child_process";
import path from "node:path";
import { shell } from "electron";
import { z } from "zod";
import { publicProcedure, router } from "../..";

/**
* Spawns a process and waits for it to complete
* @throws Error if the process exits with non-zero code or fails to spawn
*/
const spawnAsync = (command: string, args: string[]): Promise<void> => {
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
stdio: "ignore",
detached: false,
});

child.on("error", (error) => {
reject(error);
});

child.on("exit", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Process exited with code ${code}`));
}
});
});
};

/**
* External operations router
* Handles opening URLs and files in external applications
*/
export const createExternalRouter = () => {
return router({
openUrl: publicProcedure.input(z.string()).mutation(async ({ input }) => {
await shell.openExternal(input);
}),

openFileInEditor: publicProcedure
.input(
z.object({
path: z.string(),
line: z.number().optional(),
column: z.number().optional(),
cwd: z.string().optional(),
}),
)
.mutation(async ({ input }) => {
console.log("[external] openFileInEditor called with:", input);
let filePath = input.path;

// Expand home directory - needed because editors expect absolute paths
if (filePath.startsWith("~")) {
const home = process.env.HOME || process.env.USERPROFILE;
if (home) {
filePath = filePath.replace(/^~/, home);
}
}

// Convert to absolute path - required for editor commands to work reliably
if (!path.isAbsolute(filePath)) {
filePath = input.cwd
? path.resolve(input.cwd, filePath)
: path.resolve(filePath);
}

console.log("[external] Resolved file path:", filePath);

const editors = ["cursor", "code"];

// Build the file location string (file:line:column format expected by --goto flag)
let location = filePath;
if (input.line) {
location += `:${input.line}`;
if (input.column) {
location += `:${input.column}`;
}
}

console.log("[external] Opening location:", location);

for (const editor of editors) {
try {
console.log(`[external] Trying editor: ${editor}`);
await spawnAsync(editor, ["--goto", location]);
console.log(`[external] Successfully opened with ${editor}`);
return;
} catch (error) {
console.log(`[external] ${editor} failed:`, error);
}
}

console.log("[external] Falling back to system default");

await shell.openPath(filePath);
}),
});
};

export type ExternalRouter = ReturnType<typeof createExternalRouter>;
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { BrowserWindow } from "electron";
import { router } from "..";
import { createExternalRouter } from "./external";
import { createNotificationsRouter } from "./notifications";
import { createProjectsRouter } from "./projects";
import { createTerminalRouter } from "./terminal";
Expand All @@ -17,6 +18,7 @@ export const createAppRouter = (window: BrowserWindow) => {
workspaces: createWorkspacesRouter(),
terminal: createTerminalRouter(),
notifications: createNotificationsRouter(),
external: createExternalRouter(),
});
};

Expand Down
18 changes: 18 additions & 0 deletions apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,24 @@ export const createTerminalRouter = () => {
return terminalManager.getSession(tabId);
}),

/**
* Get the current working directory for a workspace
* This is used for resolving relative file paths in terminal output
*/
getWorkspaceCwd: publicProcedure
.input(z.string())
.query(async ({ input: workspaceId }) => {
const workspace = db.data.workspaces.find((w) => w.id === workspaceId);
if (!workspace) {
return undefined;
}

const worktree = db.data.worktrees.find(
(wt) => wt.id === workspace.worktreeId,
);
return worktree?.path;
}),

stream: publicProcedure
.input(z.string())
.subscription(({ input: tabId }) => {
Expand Down
15 changes: 7 additions & 8 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import path from "node:path";
import { app } from "electron";
import { makeAppSetup } from "lib/electron-app/factories/app/setup";
import { setupAgentHooks } from "./lib/agent-setup";
import { initDb } from "./lib/db";
import { registerStorageHandlers } from "./lib/storage-ipcs";
import { terminalManager } from "./lib/terminal-manager";
import { setupAgentHooks } from "./lib/agent-setup";
import { MainWindow } from "./windows/main";

// Protocol scheme for deep linking
Expand All @@ -27,7 +27,6 @@ app.on("open-url", (event, url) => {
event.preventDefault();
});

// Register storage IPC handlers
registerStorageHandlers();

// Allow multiple instances - removed single instance lock
Expand All @@ -36,12 +35,12 @@ registerStorageHandlers();

await initDb();

try {
setupAgentHooks();
} catch (error) {
console.error("[main] Failed to set up agent hooks:", error);
// App can continue without agent hooks, but log the failure
}
try {
setupAgentHooks();
} catch (error) {
console.error("[main] Failed to set up agent hooks:", error);
// App can continue without agent hooks, but log the failure
}

await makeAppSetup(() => MainWindow());

Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/main/lib/terminal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export class TerminalManager extends EventEmitter {
const { tabId, workspaceId, tabTitle, workspaceName, cwd, cols, rows } =
params;


const existing = this.sessions.get(tabId);
if (existing?.isAlive) {
existing.lastActive = Date.now();
Expand Down
12 changes: 9 additions & 3 deletions apps/desktop/src/main/windows/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ export async function MainWindow() {
});

// Start notifications HTTP server
const server = notificationsApp.listen(NOTIFICATIONS_PORT, "127.0.0.1", () => {
console.log(`[notifications] Listening on http://127.0.0.1:${NOTIFICATIONS_PORT}`);
});
const server = notificationsApp.listen(
NOTIFICATIONS_PORT,
"127.0.0.1",
() => {
console.log(
`[notifications] Listening on http://127.0.0.1:${NOTIFICATIONS_PORT}`,
);
},
);

// Handle agent completion notifications
notificationsEmitter.on("agent-complete", (event: AgentCompleteEvent) => {
Expand Down
15 changes: 15 additions & 0 deletions apps/desktop/src/renderer/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,21 @@
width: 100% !important;
}

/* Style links in terminal (like iTerm) */
/* xterm uses underline-5 (dashed) for links on hover */
.xterm .xterm-rows a {
color: inherit;
cursor: pointer !important;
}

.xterm .xterm-rows a.xterm-underline-5 {
text-decoration-color: #3b8eea !important;
}

.xterm .xterm-rows a:hover.xterm-underline-5 {
text-decoration-color: #5ca8ff !important;
}

/* Hide scrollbar for workspace carousel and tabs */
.hide-scrollbar {
scrollbar-width: none; /* Firefox */
Expand Down
Loading
Loading