Skip to content

Commit 24f3361

Browse files
authored
refactor: move shell service to trpc (#279)
1 parent 75b9ea0 commit 24f3361

File tree

16 files changed

+417
-198
lines changed

16 files changed

+417
-198
lines changed

ARCHITECTURE.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ export class MyService extends TypedEventEmitter<MyServiceEvents> {
349349

350350
### 3. Create Subscriptions in Router
351351

352-
Use a helper to reduce boilerplate:
352+
Use a helper to reduce boilerplate. For global events (broadcast to all subscribers):
353353

354354
```typescript
355355
// src/main/trpc/routers/my-router.ts
@@ -372,17 +372,61 @@ export const myRouter = router({
372372
});
373373
```
374374

375+
For per-instance events (e.g., shell sessions), filter by an identifier:
376+
377+
```typescript
378+
// Events include an identifier to filter on
379+
export interface ShellEvents {
380+
[ShellEvent.Data]: { sessionId: string; data: string };
381+
[ShellEvent.Exit]: { sessionId: string; exitCode: number };
382+
}
383+
384+
// Router filters events to the specific session
385+
function subscribeToSession<K extends keyof ShellEvents>(event: K) {
386+
return publicProcedure
387+
.input(sessionIdInput)
388+
.subscription(async function* (opts) {
389+
const service = getService();
390+
const targetSessionId = opts.input.sessionId;
391+
392+
for await (const [payload] of on(service, event, { signal: opts.signal })) {
393+
const data = payload as ShellEvents[K];
394+
if (data.sessionId === targetSessionId) {
395+
yield data;
396+
}
397+
}
398+
});
399+
}
400+
401+
export const shellRouter = router({
402+
onData: subscribeToSession(ShellEvent.Data),
403+
onExit: subscribeToSession(ShellEvent.Exit),
404+
});
405+
```
406+
375407
### 4. Subscribe in Renderer
376408

377409
```typescript
378-
// React component
410+
// React component - global events
379411
trpcReact.my.onItemCreated.useSubscription(undefined, {
380412
enabled: true,
381413
onData: (item) => {
382414
// item is typed as { id: string; name: string }
383415
console.log("Created:", item);
384416
},
385417
});
418+
419+
// React component - per-session events
420+
trpcReact.shell.onData.useSubscription(
421+
{ sessionId },
422+
{
423+
enabled: !!sessionId,
424+
onData: (event) => {
425+
// event is typed as { sessionId: string; data: string }
426+
terminal.write(event.data);
427+
},
428+
},
429+
);
386430
```
387431

388432
## Code Style

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ExternalAppsService } from "../services/external-apps/service.js";
66
import { FileWatcherService } from "../services/file-watcher/service.js";
77
import { FsService } from "../services/fs/service.js";
88
import { GitService } from "../services/git/service.js";
9+
import { ShellService } from "../services/shell/service.js";
910
import { MAIN_TOKENS } from "./tokens.js";
1011

1112
export const container = new Container({
@@ -18,3 +19,4 @@ container.bind(MAIN_TOKENS.ExternalAppsService).to(ExternalAppsService);
1819
container.bind(MAIN_TOKENS.FileWatcherService).to(FileWatcherService);
1920
container.bind(MAIN_TOKENS.FsService).to(FsService);
2021
container.bind(MAIN_TOKENS.GitService).to(GitService);
22+
container.bind(MAIN_TOKENS.ShellService).to(ShellService);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export const MAIN_TOKENS = Object.freeze({
1212
FileWatcherService: Symbol.for("Main.FileWatcherService"),
1313
FsService: Symbol.for("Main.FsService"),
1414
GitService: Symbol.for("Main.GitService"),
15+
ShellService: Symbol.for("Main.ShellService"),
1516
});

apps/array/src/main/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ import {
4343
shutdownPostHog,
4444
trackAppEvent,
4545
} from "./services/posthog-analytics.js";
46-
import { registerShellIpc } from "./services/shell.js";
4746
import { registerAutoUpdater } from "./services/updates.js";
4847
import { registerWorkspaceIpc } from "./services/workspace/index.js";
4948

@@ -297,5 +296,4 @@ registerOAuthHandlers();
297296
registerGitIpc();
298297
registerAgentIpc(taskControllers, () => mainWindow);
299298
registerFoldersIpc(() => mainWindow);
300-
registerShellIpc();
301299
registerWorkspaceIpc(() => mainWindow);

apps/array/src/main/preload.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -197,28 +197,6 @@ contextBridge.exposeInMainWorld("electronAPI", {
197197
): (() => void) => createIpcListener("updates:status", listener),
198198
onCheckForUpdatesMenu: (listener: () => void): (() => void) =>
199199
createVoidIpcListener("check-for-updates-menu", listener),
200-
shellCreate: (
201-
sessionId: string,
202-
cwd?: string,
203-
taskId?: string,
204-
): Promise<void> =>
205-
ipcRenderer.invoke("shell:create", sessionId, cwd, taskId),
206-
shellWrite: (sessionId: string, data: string): Promise<void> =>
207-
ipcRenderer.invoke("shell:write", sessionId, data),
208-
shellResize: (sessionId: string, cols: number, rows: number): Promise<void> =>
209-
ipcRenderer.invoke("shell:resize", sessionId, cols, rows),
210-
shellCheck: (sessionId: string): Promise<boolean> =>
211-
ipcRenderer.invoke("shell:check", sessionId),
212-
shellDestroy: (sessionId: string): Promise<void> =>
213-
ipcRenderer.invoke("shell:destroy", sessionId),
214-
shellGetProcess: (sessionId: string): Promise<string | null> =>
215-
ipcRenderer.invoke("shell:get-process", sessionId),
216-
onShellData: (
217-
sessionId: string,
218-
listener: (data: string) => void,
219-
): (() => void) => createIpcListener(`shell:data:${sessionId}`, listener),
220-
onShellExit: (sessionId: string, listener: () => void): (() => void) =>
221-
createVoidIpcListener(`shell:exit:${sessionId}`, listener),
222200
folders: {
223201
getFolders: (): Promise<RegisteredFolder[]> =>
224202
ipcRenderer.invoke("get-folders"),
@@ -278,9 +256,4 @@ contextBridge.exposeInMainWorld("electronAPI", {
278256
}>,
279257
): (() => void) => createIpcListener("workspace:warning", listener),
280258
},
281-
shellExecute: (
282-
cwd: string,
283-
command: string,
284-
): Promise<{ stdout: string; stderr: string; exitCode: number }> =>
285-
ipcRenderer.invoke("shell:execute", cwd, command),
286259
});

apps/array/src/main/services/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,5 @@ import "./oauth.js";
99
import "./posthog-analytics.js";
1010
import "./session-manager.js";
1111
import "./settingsStore.js";
12-
import "./shell.js";
1312
import "./transcription-prompts.js";
1413
import "./updates.js";

apps/array/src/main/services/shell.ts

Lines changed: 0 additions & 79 deletions
This file was deleted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { z } from "zod";
2+
3+
export const sessionIdInput = z.object({
4+
sessionId: z.string(),
5+
});
6+
7+
export const createInput = sessionIdInput.extend({
8+
cwd: z.string().optional(),
9+
taskId: z.string().optional(),
10+
});
11+
12+
export const writeInput = sessionIdInput.extend({
13+
data: z.string(),
14+
});
15+
16+
export const resizeInput = sessionIdInput.extend({
17+
cols: z.number(),
18+
rows: z.number(),
19+
});
20+
21+
export const executeInput = z.object({
22+
cwd: z.string(),
23+
command: z.string(),
24+
});
25+
26+
export const executeOutput = z.object({
27+
stdout: z.string(),
28+
stderr: z.string(),
29+
exitCode: z.number(),
30+
});
31+
32+
export type SessionIdInput = z.infer<typeof sessionIdInput>;
33+
export type CreateInput = z.infer<typeof createInput>;
34+
export type WriteInput = z.infer<typeof writeInput>;
35+
export type ResizeInput = z.infer<typeof resizeInput>;
36+
export type ExecuteInput = z.infer<typeof executeInput>;
37+
export type ExecuteOutput = z.infer<typeof executeOutput>;
38+
39+
export const ShellEvent = {
40+
Data: "data",
41+
Exit: "exit",
42+
} as const;
43+
44+
export interface ShellEvents {
45+
[ShellEvent.Data]: { sessionId: string; data: string };
46+
[ShellEvent.Exit]: { sessionId: string; exitCode: number };
47+
}

0 commit comments

Comments
 (0)