Skip to content
Draft
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
23 changes: 23 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ For the procedure to follow when porting a new feature, see [REFACTOR.md](./REFA

---

## 2026-05-28 — file-watcher (workspace-server owns orchestration, hook is pure useSubscription)

- 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.
- **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.
- 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.
- 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`.
- 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()`.
- 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).
- 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.
- 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.
- 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`.

---

## 2026-05-28 — api-client (transport only)

- 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).
- Cleaned: `__APP_VERSION__` Vite global removed from fetcher — now an `appVersion` field on `ApiFetcherConfig`. Renderer wrapper passes the global at construction.
- 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".
- New import path: `@posthog/api-client` (was `@renderer/api/{fetcher,generated}`). Also updated `scripts/update-openapi-client.ts` to write into the new package.

---

## 2026-05-27 — diff-stats

- Moved: `apps/code/src/main/services/git/getDiffStats` → `packages/workspace-server/src/services/git/service.ts` + `packages/ui/src/features/diff-stats/`
Expand Down
105 changes: 75 additions & 30 deletions REFACTOR.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions apps/code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"@pierre/diffs": "^1.1.21",
"@posthog/agent": "workspace:*",
"@posthog/api-client": "workspace:*",
"@posthog/core": "workspace:*",
"@posthog/electron-trpc": "workspace:*",
"@posthog/enricher": "workspace:*",
"@posthog/git": "workspace:*",
Expand Down
2 changes: 0 additions & 2 deletions apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import { DeepLinkService } from "../services/deep-link/service";
import { EnrichmentService } from "../services/enrichment/service";
import { EnvironmentService } from "../services/environment/service";
import { ExternalAppsService } from "../services/external-apps/service";
import { FileWatcherService } from "../services/file-watcher/service";
import { FocusService } from "../services/focus/service";
import { FocusSyncService } from "../services/focus/sync-service";
import { FoldersService } from "../services/folders/service";
Expand Down Expand Up @@ -125,7 +124,6 @@ container.bind(MAIN_TOKENS.ProvisioningService).to(ProvisioningService);
container.bind(MAIN_TOKENS.ExternalAppsService).to(ExternalAppsService);
container.bind(MAIN_TOKENS.LlmGatewayService).to(LlmGatewayService);
container.bind(MAIN_TOKENS.McpAppsService).to(McpAppsService);
container.bind(MAIN_TOKENS.FileWatcherService).to(FileWatcherService);
container.bind(MAIN_TOKENS.FocusService).to(FocusService);
container.bind(MAIN_TOKENS.FocusSyncService).to(FocusSyncService);
container.bind(MAIN_TOKENS.FoldersService).to(FoldersService);
Expand Down
16 changes: 12 additions & 4 deletions apps/code/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import "reflect-metadata";
import os from "node:os";
import { createWorkspaceClient } from "@posthog/workspace-client/client";
import { app, BrowserWindow, dialog } from "electron";
import log from "electron-log/main";
import { FileWatcherBridge } from "./services/file-watcher/bridge";
import "./utils/logger";
import "./services/index.js";
import { ANALYTICS_EVENTS } from "@shared/types/analytics";
Expand Down Expand Up @@ -231,12 +233,18 @@ app.whenReady().then(async () => {
ensureClaudeConfigDir();
registerMcpSandboxProtocol();
createWindow();

const wsServer = container.get<WorkspaceServerService>(
MAIN_TOKENS.WorkspaceServerService,
);
const connection = await wsServer.start();
const workspaceClient = createWorkspaceClient(connection);
container
.bind(MAIN_TOKENS.FileWatcherService)
.toConstantValue(new FileWatcherBridge(workspaceClient));

await initializeServices();
initializeDeepLinks();
container
.get<WorkspaceServerService>(MAIN_TOKENS.WorkspaceServerService)
.start()
.catch((err) => log.error("workspace-server failed to start", err));
});

app.on("window-all-closed", () => {
Expand Down
4 changes: 2 additions & 2 deletions apps/code/src/main/services/archive/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import type { WorktreeRepository } from "../../db/repositories/worktree-reposito
import { MAIN_TOKENS } from "../../di/tokens";
import { logger } from "../../utils/logger";
import type { AgentService } from "../agent/service";
import type { FileWatcherService } from "../file-watcher/service";
import type { FileWatcherBridge } from "../file-watcher/bridge";
import type { ProcessTrackingService } from "../process-tracking/service";
import { getWorktreeLocation } from "../settingsStore";
import type { ArchivedTask, ArchiveTaskInput } from "./schemas";
Expand All @@ -44,7 +44,7 @@ export class ArchiveService {
@inject(MAIN_TOKENS.ProcessTrackingService)
private readonly processTracking: ProcessTrackingService,
@inject(MAIN_TOKENS.FileWatcherService)
private readonly fileWatcher: FileWatcherService,
private readonly fileWatcher: FileWatcherBridge,
@inject(MAIN_TOKENS.RepositoryRepository)
private readonly repositoryRepo: RepositoryRepository,
@inject(MAIN_TOKENS.WorkspaceRepository)
Expand Down
36 changes: 36 additions & 0 deletions apps/code/src/main/services/file-watcher/bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { WorkspaceClient } from "@posthog/workspace-client/client";
import type { FileWatcherEvent } from "@posthog/workspace-client/types";
import { TypedEventEmitter } from "../../utils/typed-event-emitter";

type FileWatcherEventsByKind = {
[K in FileWatcherEvent["kind"]]: Extract<FileWatcherEvent, { kind: K }>;
};

export class FileWatcherBridge extends TypedEventEmitter<FileWatcherEventsByKind> {
private subs = new Map<string, { unsubscribe: () => void }>();

constructor(private workspace: WorkspaceClient) {
super();
}

startWatching(repoPath: string): void {
if (this.subs.has(repoPath)) return;
const sub = this.workspace.fileWatcher.watch.subscribe(
{ repoPath },
{
onData: (event) => {
this.emit(event.kind, event as never);
},
onError: () => {},
},
);
this.subs.set(repoPath, sub);
}

stopWatching(repoPath: string): void {
const sub = this.subs.get(repoPath);
if (!sub) return;
sub.unsubscribe();
this.subs.delete(repoPath);
}
}
60 changes: 0 additions & 60 deletions apps/code/src/main/services/file-watcher/schemas.ts

This file was deleted.

90 changes: 0 additions & 90 deletions apps/code/src/main/services/file-watcher/service.test.ts

This file was deleted.

Loading
Loading