Skip to content

Commit b745bd3

Browse files
jonathanlabcharlesvien
authored andcommitted
refactor: port over file watcher
1 parent 8651e1c commit b745bd3

36 files changed

Lines changed: 740 additions & 615 deletions

File tree

MIGRATION.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,29 @@ For the procedure to follow when porting a new feature, see [REFACTOR.md](./REFA
66

77
---
88

9+
## 2026-05-28 — file-watcher (workspace-server owns orchestration, hook is pure useSubscription)
10+
11+
- Moved: `apps/code/src/main/services/file-watcher/` deleted entirely. Orchestration (debounce, bulk threshold, git event filtering, git-dir resolution) lives in `packages/workspace-server/src/services/watcher/service.ts` as `WatcherService.watchRepo()`. New tRPC subscription procedure `fileWatcher.watch` emits the processed `FileWatcherEvent` discriminated union. Raw `watcher.watch` still available for unprocessed events.
12+
- **Nothing for file-watcher lives in `packages/core/`.** The "orchestration" we thought belonged in core (debounce, bulk threshold, git filtering) turned out to be *source-smoothing* — properties of the event source, not domain logic. Source-smoothing belongs with the source. Core is for business state machines, retries, cross-feature coordination — none of which file-watcher has.
13+
- New transport (still applies): `workspace-client` uses `splitLink` over `httpSubscriptionLink` (SSE) for subscriptions + `httpBatchLink` for queries/mutations. SSE auth via `?secret=` query param since EventSource can't send headers.
14+
- Renderer hook (`packages/ui/src/features/file-watcher/useFileWatcher.ts`) is a 5-line `useSubscription(trpc.fileWatcher.watch.subscriptionOptions(...))` wrapper. No `useEffect`, no `for-await`, no orchestration state — pure react-query idiom. Caller passes a single `onEvent` callback and switches on `event.kind`.
15+
- Main bridge: `apps/code/src/main/services/file-watcher/bridge.ts` is a small `FileWatcherBridge` class (~40 lines) that subscribes to `fileWatcher.watch` via workspace-client and re-emits via `TypedEventEmitter` for the four legacy in-process consumers (`fs`, `archive`, `suspension`, `workspace`). Bound at `MAIN_TOKENS.FileWatcherService` via `container.bind(...).toConstantValue(new FileWatcherBridge(workspaceClient))` in `index.ts` after `workspaceServer.start()`.
16+
- Bridge retirement: delete `FileWatcherBridge`, its router, and the renderer's `start`/`stop` mutation calls when **fs**, **archive**, **suspension**, **workspace** migrate. Those consumers will then use `useFileWatcher` directly (renderer) or subscribe via workspace-client (background work in workspace-server or main).
17+
- Cleaned: `WatcherRegistryService` dep dropped (its `isShutdown` check is unnecessary — subscriptions die naturally when workspace-server child or main process exits). Schemas split out of `trpc.ts` into per-service `schemas.ts`. Router is now strict one-liners.
18+
- Left as-is: two parallel watcher pipelines per repo (the bridge + the renderer each subscribe to workspace-server); workspace-server doesn't dedupe parcel watchers. `FsService` in main still owns its file-cache invalidation. `WatcherRegistryService` still used by focus + app-lifecycle.
19+
- New import paths: `import { useFileWatcher } from "@posthog/ui/features/file-watcher/useFileWatcher"`. For main consumers needing kind constants: `import { FileWatcherEventKind } from "@posthog/workspace-server/services/watcher/schemas"`. Bridge class: `apps/code/src/main/services/file-watcher/bridge.ts`.
20+
21+
---
22+
23+
## 2026-05-28 — api-client (transport only)
24+
25+
- Moved: `apps/code/src/renderer/api/{fetcher,generated,generated.augment,fetcher.test}.ts``packages/api-client/src/`. `generated.augment.d.ts``.ts` (side-effect import from `index.ts` so apps/code's tsc picks up the module augmentation through the package's exports).
26+
- Cleaned: `__APP_VERSION__` Vite global removed from fetcher — now an `appVersion` field on `ApiFetcherConfig`. Renderer wrapper passes the global at construction.
27+
- Left as-is: the 2929-line `posthogClient.ts` god-class. Tagged with a `PORT NOTE` — gets sliced into `packages/core/<feature>/service.ts` per feature, following REFACTOR.md "Coexistence and bridges".
28+
- New import path: `@posthog/api-client` (was `@renderer/api/{fetcher,generated}`). Also updated `scripts/update-openapi-client.ts` to write into the new package.
29+
30+
---
31+
932
## 2026-05-27 — diff-stats
1033

1134
- Moved: `apps/code/src/main/services/git/getDiffStats``packages/workspace-server/src/services/git/service.ts` + `packages/ui/src/features/diff-stats/`

REFACTOR.md

Lines changed: 75 additions & 30 deletions
Large diffs are not rendered by default.

apps/code/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
"@pierre/diffs": "^1.1.21",
133133
"@posthog/agent": "workspace:*",
134134
"@posthog/api-client": "workspace:*",
135+
"@posthog/core": "workspace:*",
135136
"@posthog/electron-trpc": "workspace:*",
136137
"@posthog/enricher": "workspace:*",
137138
"@posthog/git": "workspace:*",

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import { DeepLinkService } from "../services/deep-link/service";
3838
import { EnrichmentService } from "../services/enrichment/service";
3939
import { EnvironmentService } from "../services/environment/service";
4040
import { ExternalAppsService } from "../services/external-apps/service";
41-
import { FileWatcherService } from "../services/file-watcher/service";
4241
import { FocusService } from "../services/focus/service";
4342
import { FocusSyncService } from "../services/focus/sync-service";
4443
import { FoldersService } from "../services/folders/service";
@@ -125,7 +124,6 @@ container.bind(MAIN_TOKENS.ProvisioningService).to(ProvisioningService);
125124
container.bind(MAIN_TOKENS.ExternalAppsService).to(ExternalAppsService);
126125
container.bind(MAIN_TOKENS.LlmGatewayService).to(LlmGatewayService);
127126
container.bind(MAIN_TOKENS.McpAppsService).to(McpAppsService);
128-
container.bind(MAIN_TOKENS.FileWatcherService).to(FileWatcherService);
129127
container.bind(MAIN_TOKENS.FocusService).to(FocusService);
130128
container.bind(MAIN_TOKENS.FocusSyncService).to(FocusSyncService);
131129
container.bind(MAIN_TOKENS.FoldersService).to(FoldersService);

apps/code/src/main/index.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import "reflect-metadata";
22
import os from "node:os";
3+
import { createWorkspaceClient } from "@posthog/workspace-client/client";
34
import { app, BrowserWindow, dialog } from "electron";
45
import log from "electron-log/main";
6+
import { FileWatcherBridge } from "./services/file-watcher/bridge";
57
import "./utils/logger";
68
import "./services/index.js";
79
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
@@ -231,12 +233,18 @@ app.whenReady().then(async () => {
231233
ensureClaudeConfigDir();
232234
registerMcpSandboxProtocol();
233235
createWindow();
236+
237+
const wsServer = container.get<WorkspaceServerService>(
238+
MAIN_TOKENS.WorkspaceServerService,
239+
);
240+
const connection = await wsServer.start();
241+
const workspaceClient = createWorkspaceClient(connection);
242+
container
243+
.bind(MAIN_TOKENS.FileWatcherService)
244+
.toConstantValue(new FileWatcherBridge(workspaceClient));
245+
234246
await initializeServices();
235247
initializeDeepLinks();
236-
container
237-
.get<WorkspaceServerService>(MAIN_TOKENS.WorkspaceServerService)
238-
.start()
239-
.catch((err) => log.error("workspace-server failed to start", err));
240248
});
241249

242250
app.on("window-all-closed", () => {

apps/code/src/main/services/archive/service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import type { WorktreeRepository } from "../../db/repositories/worktree-reposito
2727
import { MAIN_TOKENS } from "../../di/tokens";
2828
import { logger } from "../../utils/logger";
2929
import type { AgentService } from "../agent/service";
30-
import type { FileWatcherService } from "../file-watcher/service";
30+
import type { FileWatcherBridge } from "../file-watcher/bridge";
3131
import type { ProcessTrackingService } from "../process-tracking/service";
3232
import { getWorktreeLocation } from "../settingsStore";
3333
import type { ArchivedTask, ArchiveTaskInput } from "./schemas";
@@ -44,7 +44,7 @@ export class ArchiveService {
4444
@inject(MAIN_TOKENS.ProcessTrackingService)
4545
private readonly processTracking: ProcessTrackingService,
4646
@inject(MAIN_TOKENS.FileWatcherService)
47-
private readonly fileWatcher: FileWatcherService,
47+
private readonly fileWatcher: FileWatcherBridge,
4848
@inject(MAIN_TOKENS.RepositoryRepository)
4949
private readonly repositoryRepo: RepositoryRepository,
5050
@inject(MAIN_TOKENS.WorkspaceRepository)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { WorkspaceClient } from "@posthog/workspace-client/client";
2+
import type { FileWatcherEvent } from "@posthog/workspace-client/types";
3+
import { TypedEventEmitter } from "../../utils/typed-event-emitter";
4+
5+
type FileWatcherEventsByKind = {
6+
[K in FileWatcherEvent["kind"]]: Extract<FileWatcherEvent, { kind: K }>;
7+
};
8+
9+
export class FileWatcherBridge extends TypedEventEmitter<FileWatcherEventsByKind> {
10+
private subs = new Map<string, { unsubscribe: () => void }>();
11+
12+
constructor(private workspace: WorkspaceClient) {
13+
super();
14+
}
15+
16+
startWatching(repoPath: string): void {
17+
if (this.subs.has(repoPath)) return;
18+
const sub = this.workspace.fileWatcher.watch.subscribe(
19+
{ repoPath },
20+
{
21+
onData: (event) => {
22+
this.emit(event.kind, event as never);
23+
},
24+
onError: () => {},
25+
},
26+
);
27+
this.subs.set(repoPath, sub);
28+
}
29+
30+
stopWatching(repoPath: string): void {
31+
const sub = this.subs.get(repoPath);
32+
if (!sub) return;
33+
sub.unsubscribe();
34+
this.subs.delete(repoPath);
35+
}
36+
}

apps/code/src/main/services/file-watcher/schemas.ts

Lines changed: 0 additions & 60 deletions
This file was deleted.

apps/code/src/main/services/file-watcher/service.test.ts

Lines changed: 0 additions & 90 deletions
This file was deleted.

0 commit comments

Comments
 (0)