Explorar o código

chore: generate

opencode-agent[bot] hai 1 mes
pai
achega
bc949af623

+ 10 - 11
packages/opencode/src/effect/instance-context.ts

@@ -1,15 +1,14 @@
-import { ServiceMap } from "effect";
-import type { Project } from "@/project/project";
+import { ServiceMap } from "effect"
+import type { Project } from "@/project/project"
 
 export declare namespace InstanceContext {
-	export interface Shape {
-		readonly directory: string;
-		readonly worktree: string;
-		readonly project: Project.Info;
-	}
+  export interface Shape {
+    readonly directory: string
+    readonly worktree: string
+    readonly project: Project.Info
+  }
 }
 
-export class InstanceContext extends ServiceMap.Service<
-	InstanceContext,
-	InstanceContext.Shape
->()("opencode/InstanceContext") {}
+export class InstanceContext extends ServiceMap.Service<InstanceContext, InstanceContext.Shape>()(
+  "opencode/InstanceContext",
+) {}

+ 58 - 67
packages/opencode/src/effect/instances.ts

@@ -1,31 +1,31 @@
-import { Effect, Layer, LayerMap, ServiceMap } from "effect";
-import { FileService } from "@/file";
-import { FileTimeService } from "@/file/time";
-import { FileWatcherService } from "@/file/watcher";
-import { FormatService } from "@/format";
-import { PermissionService } from "@/permission/service";
-import { Instance } from "@/project/instance";
-import { VcsService } from "@/project/vcs";
-import { ProviderAuthService } from "@/provider/auth-service";
-import { QuestionService } from "@/question/service";
-import { SkillService } from "@/skill/skill";
-import { SnapshotService } from "@/snapshot";
-import { InstanceContext } from "./instance-context";
-import { registerDisposer } from "./instance-registry";
+import { Effect, Layer, LayerMap, ServiceMap } from "effect"
+import { FileService } from "@/file"
+import { FileTimeService } from "@/file/time"
+import { FileWatcherService } from "@/file/watcher"
+import { FormatService } from "@/format"
+import { PermissionService } from "@/permission/service"
+import { Instance } from "@/project/instance"
+import { VcsService } from "@/project/vcs"
+import { ProviderAuthService } from "@/provider/auth-service"
+import { QuestionService } from "@/question/service"
+import { SkillService } from "@/skill/skill"
+import { SnapshotService } from "@/snapshot"
+import { InstanceContext } from "./instance-context"
+import { registerDisposer } from "./instance-registry"
 
-export { InstanceContext } from "./instance-context";
+export { InstanceContext } from "./instance-context"
 
 export type InstanceServices =
-	| QuestionService
-	| PermissionService
-	| ProviderAuthService
-	| FileWatcherService
-	| VcsService
-	| FileTimeService
-	| FormatService
-	| FileService
-	| SkillService
-	| SnapshotService;
+  | QuestionService
+  | PermissionService
+  | ProviderAuthService
+  | FileWatcherService
+  | VcsService
+  | FileTimeService
+  | FormatService
+  | FileService
+  | SkillService
+  | SnapshotService
 
 // NOTE: LayerMap only passes the key (directory string) to lookup, but we need
 // the full instance context (directory, worktree, project). We read from the
@@ -34,50 +34,41 @@ export type InstanceServices =
 // This should go away once the old Instance type is removed and lookup can load
 // the full context directly.
 function lookup(_key: string) {
-	const ctx = Layer.sync(InstanceContext, () =>
-		InstanceContext.of(Instance.current),
-	);
-	return Layer.mergeAll(
-		Layer.fresh(QuestionService.layer),
-		Layer.fresh(PermissionService.layer),
-		Layer.fresh(ProviderAuthService.layer),
-		Layer.fresh(FileWatcherService.layer).pipe(Layer.orDie),
-		Layer.fresh(VcsService.layer),
-		Layer.fresh(FileTimeService.layer).pipe(Layer.orDie),
-		Layer.fresh(FormatService.layer),
-		Layer.fresh(FileService.layer),
-		Layer.fresh(SkillService.layer),
-		Layer.fresh(SnapshotService.layer),
-	).pipe(Layer.provide(ctx));
+  const ctx = Layer.sync(InstanceContext, () => InstanceContext.of(Instance.current))
+  return Layer.mergeAll(
+    Layer.fresh(QuestionService.layer),
+    Layer.fresh(PermissionService.layer),
+    Layer.fresh(ProviderAuthService.layer),
+    Layer.fresh(FileWatcherService.layer).pipe(Layer.orDie),
+    Layer.fresh(VcsService.layer),
+    Layer.fresh(FileTimeService.layer).pipe(Layer.orDie),
+    Layer.fresh(FormatService.layer),
+    Layer.fresh(FileService.layer),
+    Layer.fresh(SkillService.layer),
+    Layer.fresh(SnapshotService.layer),
+  ).pipe(Layer.provide(ctx))
 }
 
-export class Instances extends ServiceMap.Service<
-	Instances,
-	LayerMap.LayerMap<string, InstanceServices>
->()("opencode/Instances") {
-	static readonly layer = Layer.effect(
-		Instances,
-		Effect.gen(function* () {
-			const layerMap = yield* LayerMap.make(lookup, {
-				idleTimeToLive: Infinity,
-			});
-			const unregister = registerDisposer((directory) =>
-				Effect.runPromise(layerMap.invalidate(directory)),
-			);
-			yield* Effect.addFinalizer(() => Effect.sync(unregister));
-			return Instances.of(layerMap);
-		}),
-	);
+export class Instances extends ServiceMap.Service<Instances, LayerMap.LayerMap<string, InstanceServices>>()(
+  "opencode/Instances",
+) {
+  static readonly layer = Layer.effect(
+    Instances,
+    Effect.gen(function* () {
+      const layerMap = yield* LayerMap.make(lookup, {
+        idleTimeToLive: Infinity,
+      })
+      const unregister = registerDisposer((directory) => Effect.runPromise(layerMap.invalidate(directory)))
+      yield* Effect.addFinalizer(() => Effect.sync(unregister))
+      return Instances.of(layerMap)
+    }),
+  )
 
-	static get(
-		directory: string,
-	): Layer.Layer<InstanceServices, never, Instances> {
-		return Layer.unwrap(
-			Instances.use((map) => Effect.succeed(map.get(directory))),
-		);
-	}
+  static get(directory: string): Layer.Layer<InstanceServices, never, Instances> {
+    return Layer.unwrap(Instances.use((map) => Effect.succeed(map.get(directory))))
+  }
 
-	static invalidate(directory: string): Effect.Effect<void, never, Instances> {
-		return Instances.use((map) => map.invalidate(directory));
-	}
+  static invalidate(directory: string): Effect.Effect<void, never, Instances> {
+    return Instances.use((map) => map.invalidate(directory))
+  }
 }

+ 145 - 164
packages/opencode/src/project/instance.ts

@@ -1,185 +1,166 @@
-import { GlobalBus } from "@/bus/global";
-import { disposeInstance } from "@/effect/instance-registry";
-import { Filesystem } from "@/util/filesystem";
-import { iife } from "@/util/iife";
-import { Log } from "@/util/log";
-import { Context } from "../util/context";
-import { Project } from "./project";
-import { State } from "./state";
+import { GlobalBus } from "@/bus/global"
+import { disposeInstance } from "@/effect/instance-registry"
+import { Filesystem } from "@/util/filesystem"
+import { iife } from "@/util/iife"
+import { Log } from "@/util/log"
+import { Context } from "../util/context"
+import { Project } from "./project"
+import { State } from "./state"
 
 interface Context {
-	directory: string;
-	worktree: string;
-	project: Project.Info;
+  directory: string
+  worktree: string
+  project: Project.Info
 }
-const context = Context.create<Context>("instance");
-const cache = new Map<string, Promise<Context>>();
+const context = Context.create<Context>("instance")
+const cache = new Map<string, Promise<Context>>()
 
 const disposal = {
-	all: undefined as Promise<void> | undefined,
-};
+  all: undefined as Promise<void> | undefined,
+}
 
 function emit(directory: string) {
-	GlobalBus.emit("event", {
-		directory,
-		payload: {
-			type: "server.instance.disposed",
-			properties: {
-				directory,
-			},
-		},
-	});
+  GlobalBus.emit("event", {
+    directory,
+    payload: {
+      type: "server.instance.disposed",
+      properties: {
+        directory,
+      },
+    },
+  })
 }
 
-function boot(input: {
-	directory: string;
-	init?: () => Promise<any>;
-	project?: Project.Info;
-	worktree?: string;
-}) {
-	return iife(async () => {
-		const ctx =
-			input.project && input.worktree
-				? {
-						directory: input.directory,
-						worktree: input.worktree,
-						project: input.project,
-					}
-				: await Project.fromDirectory(input.directory).then(
-						({ project, sandbox }) => ({
-							directory: input.directory,
-							worktree: sandbox,
-							project,
-						}),
-					);
-		await context.provide(ctx, async () => {
-			await input.init?.();
-		});
-		return ctx;
-	});
+function boot(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
+  return iife(async () => {
+    const ctx =
+      input.project && input.worktree
+        ? {
+            directory: input.directory,
+            worktree: input.worktree,
+            project: input.project,
+          }
+        : await Project.fromDirectory(input.directory).then(({ project, sandbox }) => ({
+            directory: input.directory,
+            worktree: sandbox,
+            project,
+          }))
+    await context.provide(ctx, async () => {
+      await input.init?.()
+    })
+    return ctx
+  })
 }
 
 function track(directory: string, next: Promise<Context>) {
-	const task = next.catch((error) => {
-		if (cache.get(directory) === task) cache.delete(directory);
-		throw error;
-	});
-	cache.set(directory, task);
-	return task;
+  const task = next.catch((error) => {
+    if (cache.get(directory) === task) cache.delete(directory)
+    throw error
+  })
+  cache.set(directory, task)
+  return task
 }
 
 export const Instance = {
-	async provide<R>(input: {
-		directory: string;
-		init?: () => Promise<any>;
-		fn: () => R;
-	}): Promise<R> {
-		const directory = Filesystem.resolve(input.directory);
-		let existing = cache.get(directory);
-		if (!existing) {
-			Log.Default.info("creating instance", { directory });
-			existing = track(
-				directory,
-				boot({
-					directory,
-					init: input.init,
-				}),
-			);
-		}
-		const ctx = await existing;
-		return context.provide(ctx, async () => {
-			return input.fn();
-		});
-	},
-	get current() {
-		return context.use();
-	},
-	get directory() {
-		return context.use().directory;
-	},
-	get worktree() {
-		return context.use().worktree;
-	},
-	get project() {
-		return context.use().project;
-	},
-	/**
-	 * Check if a path is within the project boundary.
-	 * Returns true if path is inside Instance.directory OR Instance.worktree.
-	 * Paths within the worktree but outside the working directory should not trigger external_directory permission.
-	 */
-	containsPath(filepath: string) {
-		if (Filesystem.contains(Instance.directory, filepath)) return true;
-		// Non-git projects set worktree to "/" which would match ANY absolute path.
-		// Skip worktree check in this case to preserve external_directory permissions.
-		if (Instance.worktree === "/") return false;
-		return Filesystem.contains(Instance.worktree, filepath);
-	},
-	/**
-	 * Captures the current instance ALS context and returns a wrapper that
-	 * restores it when called. Use this for callbacks that fire outside the
-	 * instance async context (native addons, event emitters, timers, etc.).
-	 */
-	bind<F extends (...args: any[]) => any>(fn: F): F {
-		const ctx = context.use();
-		return ((...args: any[]) => context.provide(ctx, () => fn(...args))) as F;
-	},
-	state<S>(
-		init: () => S,
-		dispose?: (state: Awaited<S>) => Promise<void>,
-	): () => S {
-		return State.create(() => Instance.directory, init, dispose);
-	},
-	async reload(input: {
-		directory: string;
-		init?: () => Promise<any>;
-		project?: Project.Info;
-		worktree?: string;
-	}) {
-		const directory = Filesystem.resolve(input.directory);
-		Log.Default.info("reloading instance", { directory });
-		await Promise.all([State.dispose(directory), disposeInstance(directory)]);
-		cache.delete(directory);
-		const next = track(directory, boot({ ...input, directory }));
-		emit(directory);
-		return await next;
-	},
-	async dispose() {
-		const directory = Instance.directory;
-		Log.Default.info("disposing instance", { directory });
-		await Promise.all([State.dispose(directory), disposeInstance(directory)]);
-		cache.delete(directory);
-		emit(directory);
-	},
-	async disposeAll() {
-		if (disposal.all) return disposal.all;
+  async provide<R>(input: { directory: string; init?: () => Promise<any>; fn: () => R }): Promise<R> {
+    const directory = Filesystem.resolve(input.directory)
+    let existing = cache.get(directory)
+    if (!existing) {
+      Log.Default.info("creating instance", { directory })
+      existing = track(
+        directory,
+        boot({
+          directory,
+          init: input.init,
+        }),
+      )
+    }
+    const ctx = await existing
+    return context.provide(ctx, async () => {
+      return input.fn()
+    })
+  },
+  get current() {
+    return context.use()
+  },
+  get directory() {
+    return context.use().directory
+  },
+  get worktree() {
+    return context.use().worktree
+  },
+  get project() {
+    return context.use().project
+  },
+  /**
+   * Check if a path is within the project boundary.
+   * Returns true if path is inside Instance.directory OR Instance.worktree.
+   * Paths within the worktree but outside the working directory should not trigger external_directory permission.
+   */
+  containsPath(filepath: string) {
+    if (Filesystem.contains(Instance.directory, filepath)) return true
+    // Non-git projects set worktree to "/" which would match ANY absolute path.
+    // Skip worktree check in this case to preserve external_directory permissions.
+    if (Instance.worktree === "/") return false
+    return Filesystem.contains(Instance.worktree, filepath)
+  },
+  /**
+   * Captures the current instance ALS context and returns a wrapper that
+   * restores it when called. Use this for callbacks that fire outside the
+   * instance async context (native addons, event emitters, timers, etc.).
+   */
+  bind<F extends (...args: any[]) => any>(fn: F): F {
+    const ctx = context.use()
+    return ((...args: any[]) => context.provide(ctx, () => fn(...args))) as F
+  },
+  state<S>(init: () => S, dispose?: (state: Awaited<S>) => Promise<void>): () => S {
+    return State.create(() => Instance.directory, init, dispose)
+  },
+  async reload(input: { directory: string; init?: () => Promise<any>; project?: Project.Info; worktree?: string }) {
+    const directory = Filesystem.resolve(input.directory)
+    Log.Default.info("reloading instance", { directory })
+    await Promise.all([State.dispose(directory), disposeInstance(directory)])
+    cache.delete(directory)
+    const next = track(directory, boot({ ...input, directory }))
+    emit(directory)
+    return await next
+  },
+  async dispose() {
+    const directory = Instance.directory
+    Log.Default.info("disposing instance", { directory })
+    await Promise.all([State.dispose(directory), disposeInstance(directory)])
+    cache.delete(directory)
+    emit(directory)
+  },
+  async disposeAll() {
+    if (disposal.all) return disposal.all
 
-		disposal.all = iife(async () => {
-			Log.Default.info("disposing all instances");
-			const entries = [...cache.entries()];
-			for (const [key, value] of entries) {
-				if (cache.get(key) !== value) continue;
+    disposal.all = iife(async () => {
+      Log.Default.info("disposing all instances")
+      const entries = [...cache.entries()]
+      for (const [key, value] of entries) {
+        if (cache.get(key) !== value) continue
 
-				const ctx = await value.catch((error) => {
-					Log.Default.warn("instance dispose failed", { key, error });
-					return undefined;
-				});
+        const ctx = await value.catch((error) => {
+          Log.Default.warn("instance dispose failed", { key, error })
+          return undefined
+        })
 
-				if (!ctx) {
-					if (cache.get(key) === value) cache.delete(key);
-					continue;
-				}
+        if (!ctx) {
+          if (cache.get(key) === value) cache.delete(key)
+          continue
+        }
 
-				if (cache.get(key) !== value) continue;
+        if (cache.get(key) !== value) continue
 
-				await context.provide(ctx, async () => {
-					await Instance.dispose();
-				});
-			}
-		}).finally(() => {
-			disposal.all = undefined;
-		});
+        await context.provide(ctx, async () => {
+          await Instance.dispose()
+        })
+      }
+    }).finally(() => {
+      disposal.all = undefined
+    })
 
-		return disposal.all;
-	},
-};
+    return disposal.all
+  },
+}

+ 368 - 503
packages/opencode/src/snapshot/index.ts

@@ -1,516 +1,381 @@
-import {
-	NodeChildProcessSpawner,
-	NodeFileSystem,
-	NodePath,
-} from "@effect/platform-node";
-import {
-	Cause,
-	Duration,
-	Effect,
-	FileSystem,
-	Layer,
-	Schedule,
-	ServiceMap,
-	Stream,
-} from "effect";
-import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
-import path from "path";
-import z from "zod";
-import { InstanceContext } from "@/effect/instance-context";
-import { runPromiseInstance } from "@/effect/runtime";
-import { Config } from "../config/config";
-import { Global } from "../global";
-import { Log } from "../util/log";
-
-const log = Log.create({ service: "snapshot" });
-const PRUNE = "7.days";
+import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node"
+import { Cause, Duration, Effect, FileSystem, Layer, Schedule, ServiceMap, Stream } from "effect"
+import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
+import path from "path"
+import z from "zod"
+import { InstanceContext } from "@/effect/instance-context"
+import { runPromiseInstance } from "@/effect/runtime"
+import { Config } from "../config/config"
+import { Global } from "../global"
+import { Log } from "../util/log"
+
+const log = Log.create({ service: "snapshot" })
+const PRUNE = "7.days"
 
 // Common git config flags shared across snapshot operations
-const GIT_CORE = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"];
-const GIT_CFG = ["-c", "core.autocrlf=false", ...GIT_CORE];
-const GIT_CFG_QUOTE = [...GIT_CFG, "-c", "core.quotepath=false"];
+const GIT_CORE = ["-c", "core.longpaths=true", "-c", "core.symlinks=true"]
+const GIT_CFG = ["-c", "core.autocrlf=false", ...GIT_CORE]
+const GIT_CFG_QUOTE = [...GIT_CFG, "-c", "core.quotepath=false"]
 
 interface GitResult {
-	readonly code: ChildProcessSpawner.ExitCode;
-	readonly text: string;
-	readonly stderr: string;
+  readonly code: ChildProcessSpawner.ExitCode
+  readonly text: string
+  readonly stderr: string
 }
 
 export namespace Snapshot {
-	export const Patch = z.object({
-		hash: z.string(),
-		files: z.string().array(),
-	});
-	export type Patch = z.infer<typeof Patch>;
-
-	export const FileDiff = z
-		.object({
-			file: z.string(),
-			before: z.string(),
-			after: z.string(),
-			additions: z.number(),
-			deletions: z.number(),
-			status: z.enum(["added", "deleted", "modified"]).optional(),
-		})
-		.meta({
-			ref: "FileDiff",
-		});
-	export type FileDiff = z.infer<typeof FileDiff>;
-
-	// Promise facade — existing callers use these
-	export function init() {
-		void runPromiseInstance(SnapshotService.use((s) => s.init()));
-	}
-
-	export async function cleanup() {
-		return runPromiseInstance(SnapshotService.use((s) => s.cleanup()));
-	}
-
-	export async function track() {
-		return runPromiseInstance(SnapshotService.use((s) => s.track()));
-	}
-
-	export async function patch(hash: string) {
-		return runPromiseInstance(SnapshotService.use((s) => s.patch(hash)));
-	}
-
-	export async function restore(snapshot: string) {
-		return runPromiseInstance(SnapshotService.use((s) => s.restore(snapshot)));
-	}
-
-	export async function revert(patches: Patch[]) {
-		return runPromiseInstance(SnapshotService.use((s) => s.revert(patches)));
-	}
-
-	export async function diff(hash: string) {
-		return runPromiseInstance(SnapshotService.use((s) => s.diff(hash)));
-	}
-
-	export async function diffFull(from: string, to: string) {
-		return runPromiseInstance(SnapshotService.use((s) => s.diffFull(from, to)));
-	}
+  export const Patch = z.object({
+    hash: z.string(),
+    files: z.string().array(),
+  })
+  export type Patch = z.infer<typeof Patch>
+
+  export const FileDiff = z
+    .object({
+      file: z.string(),
+      before: z.string(),
+      after: z.string(),
+      additions: z.number(),
+      deletions: z.number(),
+      status: z.enum(["added", "deleted", "modified"]).optional(),
+    })
+    .meta({
+      ref: "FileDiff",
+    })
+  export type FileDiff = z.infer<typeof FileDiff>
+
+  // Promise facade — existing callers use these
+  export function init() {
+    void runPromiseInstance(SnapshotService.use((s) => s.init()))
+  }
+
+  export async function cleanup() {
+    return runPromiseInstance(SnapshotService.use((s) => s.cleanup()))
+  }
+
+  export async function track() {
+    return runPromiseInstance(SnapshotService.use((s) => s.track()))
+  }
+
+  export async function patch(hash: string) {
+    return runPromiseInstance(SnapshotService.use((s) => s.patch(hash)))
+  }
+
+  export async function restore(snapshot: string) {
+    return runPromiseInstance(SnapshotService.use((s) => s.restore(snapshot)))
+  }
+
+  export async function revert(patches: Patch[]) {
+    return runPromiseInstance(SnapshotService.use((s) => s.revert(patches)))
+  }
+
+  export async function diff(hash: string) {
+    return runPromiseInstance(SnapshotService.use((s) => s.diff(hash)))
+  }
+
+  export async function diffFull(from: string, to: string) {
+    return runPromiseInstance(SnapshotService.use((s) => s.diffFull(from, to)))
+  }
 }
 
 export namespace SnapshotService {
-	export interface Service {
-		readonly init: () => Effect.Effect<void>;
-		readonly cleanup: () => Effect.Effect<void>;
-		readonly track: () => Effect.Effect<string | undefined>;
-		readonly patch: (hash: string) => Effect.Effect<Snapshot.Patch>;
-		readonly restore: (snapshot: string) => Effect.Effect<void>;
-		readonly revert: (patches: Snapshot.Patch[]) => Effect.Effect<void>;
-		readonly diff: (hash: string) => Effect.Effect<string>;
-		readonly diffFull: (
-			from: string,
-			to: string,
-		) => Effect.Effect<Snapshot.FileDiff[]>;
-	}
+  export interface Service {
+    readonly init: () => Effect.Effect<void>
+    readonly cleanup: () => Effect.Effect<void>
+    readonly track: () => Effect.Effect<string | undefined>
+    readonly patch: (hash: string) => Effect.Effect<Snapshot.Patch>
+    readonly restore: (snapshot: string) => Effect.Effect<void>
+    readonly revert: (patches: Snapshot.Patch[]) => Effect.Effect<void>
+    readonly diff: (hash: string) => Effect.Effect<string>
+    readonly diffFull: (from: string, to: string) => Effect.Effect<Snapshot.FileDiff[]>
+  }
 }
 
-export class SnapshotService extends ServiceMap.Service<
-	SnapshotService,
-	SnapshotService.Service
->()("@opencode/Snapshot") {
-	static readonly layer = Layer.effect(
-		SnapshotService,
-		Effect.gen(function* () {
-			const ctx = yield* InstanceContext;
-			const fileSystem = yield* FileSystem.FileSystem;
-			const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
-			const { directory, worktree, project } = ctx;
-			const isGit = project.vcs === "git";
-			const snapshotGit = path.join(Global.Path.data, "snapshot", project.id);
-
-			const gitArgs = (cmd: string[]) => [
-				"--git-dir",
-				snapshotGit,
-				"--work-tree",
-				worktree,
-				...cmd,
-			];
-
-			// Run git with nothrow semantics — always returns a result, never fails
-			const git = (
-				args: string[],
-				opts?: { cwd?: string; env?: Record<string, string> },
-			): Effect.Effect<GitResult> =>
-				Effect.gen(function* () {
-					const command = ChildProcess.make("git", args, {
-						cwd: opts?.cwd,
-						env: opts?.env,
-						extendEnv: true,
-					});
-					const handle = yield* spawner.spawn(command);
-					const [text, stderr] = yield* Effect.all(
-						[
-							Stream.mkString(Stream.decodeText(handle.stdout)),
-							Stream.mkString(Stream.decodeText(handle.stderr)),
-						],
-						{ concurrency: 2 },
-					);
-					const code = yield* handle.exitCode;
-					return { code, text, stderr };
-				}).pipe(
-					Effect.scoped,
-					Effect.catch((err) =>
-						Effect.succeed({
-							code: ChildProcessSpawner.ExitCode(1),
-							text: "",
-							stderr: String(err),
-						}),
-					),
-				);
-
-			// FileSystem helpers — orDie converts PlatformError to defects
-			const exists = (p: string) => fileSystem.exists(p).pipe(Effect.orDie);
-			const mkdir = (p: string) =>
-				fileSystem.makeDirectory(p, { recursive: true }).pipe(Effect.orDie);
-			const writeFile = (p: string, content: string) =>
-				fileSystem.writeFileString(p, content).pipe(Effect.orDie);
-			const readFile = (p: string) =>
-				fileSystem
-					.readFileString(p)
-					.pipe(Effect.catch(() => Effect.succeed("")));
-			const removeFile = (p: string) =>
-				fileSystem.remove(p).pipe(Effect.catch(() => Effect.void));
-
-			// --- internal Effect helpers ---
-
-			const isEnabled = Effect.gen(function* () {
-				if (!isGit) return false;
-				const cfg = yield* Effect.promise(() => Config.get());
-				return cfg.snapshot !== false;
-			});
-
-			const excludesPath = Effect.gen(function* () {
-				const result = yield* git(
-					["rev-parse", "--path-format=absolute", "--git-path", "info/exclude"],
-					{
-						cwd: worktree,
-					},
-				);
-				const file = result.text.trim();
-				if (!file) return undefined;
-				if (!(yield* exists(file))) return undefined;
-				return file;
-			});
-
-			const syncExclude = Effect.gen(function* () {
-				const file = yield* excludesPath;
-				const target = path.join(snapshotGit, "info", "exclude");
-				yield* mkdir(path.join(snapshotGit, "info"));
-				if (!file) {
-					yield* writeFile(target, "");
-					return;
-				}
-				const text = yield* readFile(file);
-				yield* writeFile(target, text);
-			});
-
-			const add = Effect.gen(function* () {
-				yield* syncExclude;
-				yield* git([...GIT_CFG, ...gitArgs(["add", "."])], { cwd: directory });
-			});
-
-			// --- service methods ---
-
-			const cleanup = Effect.fn("SnapshotService.cleanup")(function* () {
-				if (!(yield* isEnabled)) return;
-				if (!(yield* exists(snapshotGit))) return;
-				const result = yield* git(gitArgs(["gc", `--prune=${PRUNE}`]), {
-					cwd: directory,
-				});
-				if (result.code !== 0) {
-					log.warn("cleanup failed", {
-						exitCode: result.code,
-						stderr: result.stderr,
-					});
-					return;
-				}
-				log.info("cleanup", { prune: PRUNE });
-			});
-
-			const track = Effect.fn("SnapshotService.track")(function* () {
-				if (!(yield* isEnabled)) return undefined;
-				const existed = yield* exists(snapshotGit);
-				yield* mkdir(snapshotGit);
-				if (!existed) {
-					yield* git(["init"], {
-						env: { GIT_DIR: snapshotGit, GIT_WORK_TREE: worktree },
-					});
-					yield* git([
-						"--git-dir",
-						snapshotGit,
-						"config",
-						"core.autocrlf",
-						"false",
-					]);
-					yield* git([
-						"--git-dir",
-						snapshotGit,
-						"config",
-						"core.longpaths",
-						"true",
-					]);
-					yield* git([
-						"--git-dir",
-						snapshotGit,
-						"config",
-						"core.symlinks",
-						"true",
-					]);
-					yield* git([
-						"--git-dir",
-						snapshotGit,
-						"config",
-						"core.fsmonitor",
-						"false",
-					]);
-					log.info("initialized");
-				}
-				yield* add;
-				const result = yield* git(gitArgs(["write-tree"]), { cwd: directory });
-				const hash = result.text.trim();
-				log.info("tracking", { hash, cwd: directory, git: snapshotGit });
-				return hash;
-			});
-
-			const patch = Effect.fn("SnapshotService.patch")(function* (
-				hash: string,
-			) {
-				yield* add;
-				const result = yield* git(
-					[
-						...GIT_CFG_QUOTE,
-						...gitArgs([
-							"diff",
-							"--no-ext-diff",
-							"--name-only",
-							hash,
-							"--",
-							".",
-						]),
-					],
-					{ cwd: directory },
-				);
-
-				if (result.code !== 0) {
-					log.warn("failed to get diff", { hash, exitCode: result.code });
-					return { hash, files: [] } as Snapshot.Patch;
-				}
-
-				return {
-					hash,
-					files: result.text
-						.trim()
-						.split("\n")
-						.map((x: string) => x.trim())
-						.filter(Boolean)
-						.map((x: string) => path.join(worktree, x).replaceAll("\\", "/")),
-				} as Snapshot.Patch;
-			});
-
-			const restore = Effect.fn("SnapshotService.restore")(function* (
-				snapshot: string,
-			) {
-				log.info("restore", { commit: snapshot });
-				const result = yield* git(
-					[...GIT_CORE, ...gitArgs(["read-tree", snapshot])],
-					{ cwd: worktree },
-				);
-				if (result.code === 0) {
-					const checkout = yield* git(
-						[...GIT_CORE, ...gitArgs(["checkout-index", "-a", "-f"])],
-						{ cwd: worktree },
-					);
-					if (checkout.code === 0) return;
-					log.error("failed to restore snapshot", {
-						snapshot,
-						exitCode: checkout.code,
-						stderr: checkout.stderr,
-					});
-					return;
-				}
-				log.error("failed to restore snapshot", {
-					snapshot,
-					exitCode: result.code,
-					stderr: result.stderr,
-				});
-			});
-
-			const revert = Effect.fn("SnapshotService.revert")(function* (
-				patches: Snapshot.Patch[],
-			) {
-				const seen = new Set<string>();
-				for (const item of patches) {
-					for (const file of item.files) {
-						if (seen.has(file)) continue;
-						log.info("reverting", { file, hash: item.hash });
-						const result = yield* git(
-							[...GIT_CORE, ...gitArgs(["checkout", item.hash, "--", file])],
-							{
-								cwd: worktree,
-							},
-						);
-						if (result.code !== 0) {
-							const relativePath = path.relative(worktree, file);
-							const checkTree = yield* git(
-								[
-									...GIT_CORE,
-									...gitArgs(["ls-tree", item.hash, "--", relativePath]),
-								],
-								{
-									cwd: worktree,
-								},
-							);
-							if (checkTree.code === 0 && checkTree.text.trim()) {
-								log.info(
-									"file existed in snapshot but checkout failed, keeping",
-									{ file },
-								);
-							} else {
-								log.info("file did not exist in snapshot, deleting", { file });
-								yield* removeFile(file);
-							}
-						}
-						seen.add(file);
-					}
-				}
-			});
-
-			const diff = Effect.fn("SnapshotService.diff")(function* (hash: string) {
-				yield* add;
-				const result = yield* git(
-					[
-						...GIT_CFG_QUOTE,
-						...gitArgs(["diff", "--no-ext-diff", hash, "--", "."]),
-					],
-					{
-						cwd: worktree,
-					},
-				);
-
-				if (result.code !== 0) {
-					log.warn("failed to get diff", {
-						hash,
-						exitCode: result.code,
-						stderr: result.stderr,
-					});
-					return "";
-				}
-
-				return result.text.trim();
-			});
-
-			const diffFull = Effect.fn("SnapshotService.diffFull")(function* (
-				from: string,
-				to: string,
-			) {
-				const result: Snapshot.FileDiff[] = [];
-				const status = new Map<string, "added" | "deleted" | "modified">();
-
-				const statuses = yield* git(
-					[
-						...GIT_CFG_QUOTE,
-						...gitArgs([
-							"diff",
-							"--no-ext-diff",
-							"--name-status",
-							"--no-renames",
-							from,
-							to,
-							"--",
-							".",
-						]),
-					],
-					{ cwd: directory },
-				);
-
-				for (const line of statuses.text.trim().split("\n")) {
-					if (!line) continue;
-					const [code, file] = line.split("\t");
-					if (!code || !file) continue;
-					const kind = code.startsWith("A")
-						? "added"
-						: code.startsWith("D")
-							? "deleted"
-							: "modified";
-					status.set(file, kind);
-				}
-
-				const numstat = yield* git(
-					[
-						...GIT_CFG_QUOTE,
-						...gitArgs([
-							"diff",
-							"--no-ext-diff",
-							"--no-renames",
-							"--numstat",
-							from,
-							to,
-							"--",
-							".",
-						]),
-					],
-					{ cwd: directory },
-				);
-
-				for (const line of numstat.text.trim().split("\n")) {
-					if (!line) continue;
-					const [additions, deletions, file] = line.split("\t");
-					const isBinaryFile = additions === "-" && deletions === "-";
-					const [before, after] = isBinaryFile
-						? ["", ""]
-						: yield* Effect.all(
-								[
-									git([
-										...GIT_CFG,
-										...gitArgs(["show", `${from}:${file}`]),
-									]).pipe(Effect.map((r) => r.text)),
-									git([...GIT_CFG, ...gitArgs(["show", `${to}:${file}`])]).pipe(
-										Effect.map((r) => r.text),
-									),
-								],
-								{ concurrency: 2 },
-							);
-					const added = isBinaryFile ? 0 : parseInt(additions!);
-					const deleted = isBinaryFile ? 0 : parseInt(deletions!);
-					result.push({
-						file: file!,
-						before,
-						after,
-						additions: Number.isFinite(added) ? added : 0,
-						deletions: Number.isFinite(deleted) ? deleted : 0,
-						status: status.get(file!) ?? "modified",
-					});
-				}
-				return result;
-			});
-
-			// Start hourly cleanup fiber — scoped to instance lifetime
-			yield* cleanup().pipe(
-				Effect.catchCause((cause) => {
-					log.error("cleanup loop failed", { cause: Cause.pretty(cause) });
-					return Effect.void;
-				}),
-				Effect.repeat(Schedule.spaced(Duration.hours(1))),
-				Effect.forkScoped,
-			);
-
-			return SnapshotService.of({
-				init: Effect.fn("SnapshotService.init")(function* () {}),
-				cleanup,
-				track,
-				patch,
-				restore,
-				revert,
-				diff,
-				diffFull,
-			});
-		}),
-	).pipe(
-		Layer.provide(NodeChildProcessSpawner.layer),
-		Layer.provide(NodeFileSystem.layer),
-		Layer.provide(NodePath.layer),
-	);
+export class SnapshotService extends ServiceMap.Service<SnapshotService, SnapshotService.Service>()(
+  "@opencode/Snapshot",
+) {
+  static readonly layer = Layer.effect(
+    SnapshotService,
+    Effect.gen(function* () {
+      const ctx = yield* InstanceContext
+      const fileSystem = yield* FileSystem.FileSystem
+      const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
+      const { directory, worktree, project } = ctx
+      const isGit = project.vcs === "git"
+      const snapshotGit = path.join(Global.Path.data, "snapshot", project.id)
+
+      const gitArgs = (cmd: string[]) => ["--git-dir", snapshotGit, "--work-tree", worktree, ...cmd]
+
+      // Run git with nothrow semantics — always returns a result, never fails
+      const git = (args: string[], opts?: { cwd?: string; env?: Record<string, string> }): Effect.Effect<GitResult> =>
+        Effect.gen(function* () {
+          const command = ChildProcess.make("git", args, {
+            cwd: opts?.cwd,
+            env: opts?.env,
+            extendEnv: true,
+          })
+          const handle = yield* spawner.spawn(command)
+          const [text, stderr] = yield* Effect.all(
+            [Stream.mkString(Stream.decodeText(handle.stdout)), Stream.mkString(Stream.decodeText(handle.stderr))],
+            { concurrency: 2 },
+          )
+          const code = yield* handle.exitCode
+          return { code, text, stderr }
+        }).pipe(
+          Effect.scoped,
+          Effect.catch((err) =>
+            Effect.succeed({
+              code: ChildProcessSpawner.ExitCode(1),
+              text: "",
+              stderr: String(err),
+            }),
+          ),
+        )
+
+      // FileSystem helpers — orDie converts PlatformError to defects
+      const exists = (p: string) => fileSystem.exists(p).pipe(Effect.orDie)
+      const mkdir = (p: string) => fileSystem.makeDirectory(p, { recursive: true }).pipe(Effect.orDie)
+      const writeFile = (p: string, content: string) => fileSystem.writeFileString(p, content).pipe(Effect.orDie)
+      const readFile = (p: string) => fileSystem.readFileString(p).pipe(Effect.catch(() => Effect.succeed("")))
+      const removeFile = (p: string) => fileSystem.remove(p).pipe(Effect.catch(() => Effect.void))
+
+      // --- internal Effect helpers ---
+
+      const isEnabled = Effect.gen(function* () {
+        if (!isGit) return false
+        const cfg = yield* Effect.promise(() => Config.get())
+        return cfg.snapshot !== false
+      })
+
+      const excludesPath = Effect.gen(function* () {
+        const result = yield* git(["rev-parse", "--path-format=absolute", "--git-path", "info/exclude"], {
+          cwd: worktree,
+        })
+        const file = result.text.trim()
+        if (!file) return undefined
+        if (!(yield* exists(file))) return undefined
+        return file
+      })
+
+      const syncExclude = Effect.gen(function* () {
+        const file = yield* excludesPath
+        const target = path.join(snapshotGit, "info", "exclude")
+        yield* mkdir(path.join(snapshotGit, "info"))
+        if (!file) {
+          yield* writeFile(target, "")
+          return
+        }
+        const text = yield* readFile(file)
+        yield* writeFile(target, text)
+      })
+
+      const add = Effect.gen(function* () {
+        yield* syncExclude
+        yield* git([...GIT_CFG, ...gitArgs(["add", "."])], { cwd: directory })
+      })
+
+      // --- service methods ---
+
+      const cleanup = Effect.fn("SnapshotService.cleanup")(function* () {
+        if (!(yield* isEnabled)) return
+        if (!(yield* exists(snapshotGit))) return
+        const result = yield* git(gitArgs(["gc", `--prune=${PRUNE}`]), {
+          cwd: directory,
+        })
+        if (result.code !== 0) {
+          log.warn("cleanup failed", {
+            exitCode: result.code,
+            stderr: result.stderr,
+          })
+          return
+        }
+        log.info("cleanup", { prune: PRUNE })
+      })
+
+      const track = Effect.fn("SnapshotService.track")(function* () {
+        if (!(yield* isEnabled)) return undefined
+        const existed = yield* exists(snapshotGit)
+        yield* mkdir(snapshotGit)
+        if (!existed) {
+          yield* git(["init"], {
+            env: { GIT_DIR: snapshotGit, GIT_WORK_TREE: worktree },
+          })
+          yield* git(["--git-dir", snapshotGit, "config", "core.autocrlf", "false"])
+          yield* git(["--git-dir", snapshotGit, "config", "core.longpaths", "true"])
+          yield* git(["--git-dir", snapshotGit, "config", "core.symlinks", "true"])
+          yield* git(["--git-dir", snapshotGit, "config", "core.fsmonitor", "false"])
+          log.info("initialized")
+        }
+        yield* add
+        const result = yield* git(gitArgs(["write-tree"]), { cwd: directory })
+        const hash = result.text.trim()
+        log.info("tracking", { hash, cwd: directory, git: snapshotGit })
+        return hash
+      })
+
+      const patch = Effect.fn("SnapshotService.patch")(function* (hash: string) {
+        yield* add
+        const result = yield* git(
+          [...GIT_CFG_QUOTE, ...gitArgs(["diff", "--no-ext-diff", "--name-only", hash, "--", "."])],
+          { cwd: directory },
+        )
+
+        if (result.code !== 0) {
+          log.warn("failed to get diff", { hash, exitCode: result.code })
+          return { hash, files: [] } as Snapshot.Patch
+        }
+
+        return {
+          hash,
+          files: result.text
+            .trim()
+            .split("\n")
+            .map((x: string) => x.trim())
+            .filter(Boolean)
+            .map((x: string) => path.join(worktree, x).replaceAll("\\", "/")),
+        } as Snapshot.Patch
+      })
+
+      const restore = Effect.fn("SnapshotService.restore")(function* (snapshot: string) {
+        log.info("restore", { commit: snapshot })
+        const result = yield* git([...GIT_CORE, ...gitArgs(["read-tree", snapshot])], { cwd: worktree })
+        if (result.code === 0) {
+          const checkout = yield* git([...GIT_CORE, ...gitArgs(["checkout-index", "-a", "-f"])], { cwd: worktree })
+          if (checkout.code === 0) return
+          log.error("failed to restore snapshot", {
+            snapshot,
+            exitCode: checkout.code,
+            stderr: checkout.stderr,
+          })
+          return
+        }
+        log.error("failed to restore snapshot", {
+          snapshot,
+          exitCode: result.code,
+          stderr: result.stderr,
+        })
+      })
+
+      const revert = Effect.fn("SnapshotService.revert")(function* (patches: Snapshot.Patch[]) {
+        const seen = new Set<string>()
+        for (const item of patches) {
+          for (const file of item.files) {
+            if (seen.has(file)) continue
+            log.info("reverting", { file, hash: item.hash })
+            const result = yield* git([...GIT_CORE, ...gitArgs(["checkout", item.hash, "--", file])], {
+              cwd: worktree,
+            })
+            if (result.code !== 0) {
+              const relativePath = path.relative(worktree, file)
+              const checkTree = yield* git([...GIT_CORE, ...gitArgs(["ls-tree", item.hash, "--", relativePath])], {
+                cwd: worktree,
+              })
+              if (checkTree.code === 0 && checkTree.text.trim()) {
+                log.info("file existed in snapshot but checkout failed, keeping", { file })
+              } else {
+                log.info("file did not exist in snapshot, deleting", { file })
+                yield* removeFile(file)
+              }
+            }
+            seen.add(file)
+          }
+        }
+      })
+
+      const diff = Effect.fn("SnapshotService.diff")(function* (hash: string) {
+        yield* add
+        const result = yield* git([...GIT_CFG_QUOTE, ...gitArgs(["diff", "--no-ext-diff", hash, "--", "."])], {
+          cwd: worktree,
+        })
+
+        if (result.code !== 0) {
+          log.warn("failed to get diff", {
+            hash,
+            exitCode: result.code,
+            stderr: result.stderr,
+          })
+          return ""
+        }
+
+        return result.text.trim()
+      })
+
+      const diffFull = Effect.fn("SnapshotService.diffFull")(function* (from: string, to: string) {
+        const result: Snapshot.FileDiff[] = []
+        const status = new Map<string, "added" | "deleted" | "modified">()
+
+        const statuses = yield* git(
+          [
+            ...GIT_CFG_QUOTE,
+            ...gitArgs(["diff", "--no-ext-diff", "--name-status", "--no-renames", from, to, "--", "."]),
+          ],
+          { cwd: directory },
+        )
+
+        for (const line of statuses.text.trim().split("\n")) {
+          if (!line) continue
+          const [code, file] = line.split("\t")
+          if (!code || !file) continue
+          const kind = code.startsWith("A") ? "added" : code.startsWith("D") ? "deleted" : "modified"
+          status.set(file, kind)
+        }
+
+        const numstat = yield* git(
+          [...GIT_CFG_QUOTE, ...gitArgs(["diff", "--no-ext-diff", "--no-renames", "--numstat", from, to, "--", "."])],
+          { cwd: directory },
+        )
+
+        for (const line of numstat.text.trim().split("\n")) {
+          if (!line) continue
+          const [additions, deletions, file] = line.split("\t")
+          const isBinaryFile = additions === "-" && deletions === "-"
+          const [before, after] = isBinaryFile
+            ? ["", ""]
+            : yield* Effect.all(
+                [
+                  git([...GIT_CFG, ...gitArgs(["show", `${from}:${file}`])]).pipe(Effect.map((r) => r.text)),
+                  git([...GIT_CFG, ...gitArgs(["show", `${to}:${file}`])]).pipe(Effect.map((r) => r.text)),
+                ],
+                { concurrency: 2 },
+              )
+          const added = isBinaryFile ? 0 : parseInt(additions!)
+          const deleted = isBinaryFile ? 0 : parseInt(deletions!)
+          result.push({
+            file: file!,
+            before,
+            after,
+            additions: Number.isFinite(added) ? added : 0,
+            deletions: Number.isFinite(deleted) ? deleted : 0,
+            status: status.get(file!) ?? "modified",
+          })
+        }
+        return result
+      })
+
+      // Start hourly cleanup fiber — scoped to instance lifetime
+      yield* cleanup().pipe(
+        Effect.catchCause((cause) => {
+          log.error("cleanup loop failed", { cause: Cause.pretty(cause) })
+          return Effect.void
+        }),
+        Effect.repeat(Schedule.spaced(Duration.hours(1))),
+        Effect.forkScoped,
+      )
+
+      return SnapshotService.of({
+        init: Effect.fn("SnapshotService.init")(function* () {}),
+        cleanup,
+        track,
+        patch,
+        restore,
+        revert,
+        diff,
+        diffFull,
+      })
+    }),
+  ).pipe(
+    Layer.provide(NodeChildProcessSpawner.layer),
+    Layer.provide(NodeFileSystem.layer),
+    Layer.provide(NodePath.layer),
+  )
 }

+ 36 - 38
packages/opencode/test/fixture/instance.ts

@@ -1,14 +1,14 @@
-import { ConfigProvider, Layer, ManagedRuntime } from "effect";
-import { InstanceContext } from "../../src/effect/instance-context";
-import { Instance } from "../../src/project/instance";
+import { ConfigProvider, Layer, ManagedRuntime } from "effect"
+import { InstanceContext } from "../../src/effect/instance-context"
+import { Instance } from "../../src/project/instance"
 
 /** ConfigProvider that enables the experimental file watcher. */
 export const watcherConfigLayer = ConfigProvider.layer(
-	ConfigProvider.fromUnknown({
-		OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
-		OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false",
-	}),
-);
+  ConfigProvider.fromUnknown({
+    OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
+    OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false",
+  }),
+)
 
 /**
  * Boot an Instance with the given service layers and run `body` with
@@ -19,35 +19,33 @@ export const watcherConfigLayer = ConfigProvider.layer(
  * Pass extra layers via `options.provide` (e.g. ConfigProvider.layer).
  */
 export function withServices<S>(
-	directory: string,
-	layer: Layer.Layer<S, any, InstanceContext>,
-	body: (rt: ManagedRuntime.ManagedRuntime<S, never>) => Promise<void>,
-	options?: { provide?: Layer.Layer<never>[] },
+  directory: string,
+  layer: Layer.Layer<S, any, InstanceContext>,
+  body: (rt: ManagedRuntime.ManagedRuntime<S, never>) => Promise<void>,
+  options?: { provide?: Layer.Layer<never>[] },
 ) {
-	return Instance.provide({
-		directory,
-		fn: async () => {
-			const ctx = Layer.sync(InstanceContext, () =>
-				InstanceContext.of({
-					directory: Instance.directory,
-					worktree: Instance.worktree,
-					project: Instance.project,
-				}),
-			);
-			let resolved: Layer.Layer<S> = Layer.fresh(layer).pipe(
-				Layer.provide(ctx),
-			) as any;
-			if (options?.provide) {
-				for (const l of options.provide) {
-					resolved = resolved.pipe(Layer.provide(l)) as any;
-				}
-			}
-			const rt = ManagedRuntime.make(resolved);
-			try {
-				await body(rt);
-			} finally {
-				await rt.dispose();
-			}
-		},
-	});
+  return Instance.provide({
+    directory,
+    fn: async () => {
+      const ctx = Layer.sync(InstanceContext, () =>
+        InstanceContext.of({
+          directory: Instance.directory,
+          worktree: Instance.worktree,
+          project: Instance.project,
+        }),
+      )
+      let resolved: Layer.Layer<S> = Layer.fresh(layer).pipe(Layer.provide(ctx)) as any
+      if (options?.provide) {
+        for (const l of options.provide) {
+          resolved = resolved.pipe(Layer.provide(l)) as any
+        }
+      }
+      const rt = ManagedRuntime.make(resolved)
+      try {
+        await body(rt)
+      } finally {
+        await rt.dispose()
+      }
+    },
+  })
 }

+ 56 - 56
packages/sdk/js/src/v2/gen/types.gen.ts

@@ -47,6 +47,13 @@ export type EventProjectUpdated = {
   properties: Project
 }
 
+export type EventFileEdited = {
+  type: "file.edited"
+  properties: {
+    file: string
+  }
+}
+
 export type EventServerInstanceDisposed = {
   type: "server.instance.disposed"
   properties: {
@@ -54,6 +61,50 @@ export type EventServerInstanceDisposed = {
   }
 }
 
+export type EventFileWatcherUpdated = {
+  type: "file.watcher.updated"
+  properties: {
+    file: string
+    event: "add" | "change" | "unlink"
+  }
+}
+
+export type PermissionRequest = {
+  id: string
+  sessionID: string
+  permission: string
+  patterns: Array<string>
+  metadata: {
+    [key: string]: unknown
+  }
+  always: Array<string>
+  tool?: {
+    messageID: string
+    callID: string
+  }
+}
+
+export type EventPermissionAsked = {
+  type: "permission.asked"
+  properties: PermissionRequest
+}
+
+export type EventPermissionReplied = {
+  type: "permission.replied"
+  properties: {
+    sessionID: string
+    requestID: string
+    reply: "once" | "always" | "reject"
+  }
+}
+
+export type EventVcsBranchUpdated = {
+  type: "vcs.branch.updated"
+  properties: {
+    branch?: string
+  }
+}
+
 export type QuestionOption = {
   /**
    * Display text (1-5 words, concise)
@@ -125,57 +176,6 @@ export type EventQuestionRejected = {
   }
 }
 
-export type PermissionRequest = {
-  id: string
-  sessionID: string
-  permission: string
-  patterns: Array<string>
-  metadata: {
-    [key: string]: unknown
-  }
-  always: Array<string>
-  tool?: {
-    messageID: string
-    callID: string
-  }
-}
-
-export type EventPermissionAsked = {
-  type: "permission.asked"
-  properties: PermissionRequest
-}
-
-export type EventPermissionReplied = {
-  type: "permission.replied"
-  properties: {
-    sessionID: string
-    requestID: string
-    reply: "once" | "always" | "reject"
-  }
-}
-
-export type EventFileWatcherUpdated = {
-  type: "file.watcher.updated"
-  properties: {
-    file: string
-    event: "add" | "change" | "unlink"
-  }
-}
-
-export type EventVcsBranchUpdated = {
-  type: "vcs.branch.updated"
-  properties: {
-    branch?: string
-  }
-}
-
-export type EventFileEdited = {
-  type: "file.edited"
-  properties: {
-    file: string
-  }
-}
-
 export type EventServerConnected = {
   type: "server.connected"
   properties: {
@@ -961,15 +961,15 @@ export type Event =
   | EventInstallationUpdated
   | EventInstallationUpdateAvailable
   | EventProjectUpdated
+  | EventFileEdited
   | EventServerInstanceDisposed
-  | EventQuestionAsked
-  | EventQuestionReplied
-  | EventQuestionRejected
+  | EventFileWatcherUpdated
   | EventPermissionAsked
   | EventPermissionReplied
-  | EventFileWatcherUpdated
   | EventVcsBranchUpdated
-  | EventFileEdited
+  | EventQuestionAsked
+  | EventQuestionReplied
+  | EventQuestionRejected
   | EventServerConnected
   | EventGlobalDisposed
   | EventLspClientDiagnostics

+ 163 - 163
packages/sdk/openapi.json

@@ -7043,171 +7043,75 @@
         },
         "required": ["type", "properties"]
       },
-      "Event.server.instance.disposed": {
+      "Event.file.edited": {
         "type": "object",
         "properties": {
           "type": {
             "type": "string",
-            "const": "server.instance.disposed"
+            "const": "file.edited"
           },
           "properties": {
             "type": "object",
             "properties": {
-              "directory": {
-                "type": "string"
-              }
-            },
-            "required": ["directory"]
-          }
-        },
-        "required": ["type", "properties"]
-      },
-      "QuestionOption": {
-        "type": "object",
-        "properties": {
-          "label": {
-            "description": "Display text (1-5 words, concise)",
-            "type": "string"
-          },
-          "description": {
-            "description": "Explanation of choice",
-            "type": "string"
-          }
-        },
-        "required": ["label", "description"]
-      },
-      "QuestionInfo": {
-        "type": "object",
-        "properties": {
-          "question": {
-            "description": "Complete question",
-            "type": "string"
-          },
-          "header": {
-            "description": "Very short label (max 30 chars)",
-            "type": "string"
-          },
-          "options": {
-            "description": "Available choices",
-            "type": "array",
-            "items": {
-              "$ref": "#/components/schemas/QuestionOption"
-            }
-          },
-          "multiple": {
-            "description": "Allow selecting multiple choices",
-            "type": "boolean"
-          },
-          "custom": {
-            "description": "Allow typing a custom answer (default: true)",
-            "type": "boolean"
-          }
-        },
-        "required": ["question", "header", "options"]
-      },
-      "QuestionRequest": {
-        "type": "object",
-        "properties": {
-          "id": {
-            "type": "string",
-            "pattern": "^que.*"
-          },
-          "sessionID": {
-            "type": "string",
-            "pattern": "^ses.*"
-          },
-          "questions": {
-            "description": "Questions to ask",
-            "type": "array",
-            "items": {
-              "$ref": "#/components/schemas/QuestionInfo"
-            }
-          },
-          "tool": {
-            "type": "object",
-            "properties": {
-              "messageID": {
-                "type": "string",
-                "pattern": "^msg.*"
-              },
-              "callID": {
+              "file": {
                 "type": "string"
               }
             },
-            "required": ["messageID", "callID"]
-          }
-        },
-        "required": ["id", "sessionID", "questions"]
-      },
-      "Event.question.asked": {
-        "type": "object",
-        "properties": {
-          "type": {
-            "type": "string",
-            "const": "question.asked"
-          },
-          "properties": {
-            "$ref": "#/components/schemas/QuestionRequest"
+            "required": ["file"]
           }
         },
         "required": ["type", "properties"]
       },
-      "QuestionAnswer": {
-        "type": "array",
-        "items": {
-          "type": "string"
-        }
-      },
-      "Event.question.replied": {
+      "Event.server.instance.disposed": {
         "type": "object",
         "properties": {
           "type": {
             "type": "string",
-            "const": "question.replied"
+            "const": "server.instance.disposed"
           },
           "properties": {
             "type": "object",
             "properties": {
-              "sessionID": {
-                "type": "string",
-                "pattern": "^ses.*"
-              },
-              "requestID": {
-                "type": "string",
-                "pattern": "^que.*"
-              },
-              "answers": {
-                "type": "array",
-                "items": {
-                  "$ref": "#/components/schemas/QuestionAnswer"
-                }
+              "directory": {
+                "type": "string"
               }
             },
-            "required": ["sessionID", "requestID", "answers"]
+            "required": ["directory"]
           }
         },
         "required": ["type", "properties"]
       },
-      "Event.question.rejected": {
+      "Event.file.watcher.updated": {
         "type": "object",
         "properties": {
           "type": {
             "type": "string",
-            "const": "question.rejected"
+            "const": "file.watcher.updated"
           },
           "properties": {
             "type": "object",
             "properties": {
-              "sessionID": {
-                "type": "string",
-                "pattern": "^ses.*"
+              "file": {
+                "type": "string"
               },
-              "requestID": {
-                "type": "string",
-                "pattern": "^que.*"
+              "event": {
+                "anyOf": [
+                  {
+                    "type": "string",
+                    "const": "add"
+                  },
+                  {
+                    "type": "string",
+                    "const": "change"
+                  },
+                  {
+                    "type": "string",
+                    "const": "unlink"
+                  }
+                ]
               }
             },
-            "required": ["sessionID", "requestID"]
+            "required": ["file", "event"]
           }
         },
         "required": ["type", "properties"]
@@ -7302,74 +7206,170 @@
         },
         "required": ["type", "properties"]
       },
-      "Event.file.watcher.updated": {
+      "Event.vcs.branch.updated": {
         "type": "object",
         "properties": {
           "type": {
             "type": "string",
-            "const": "file.watcher.updated"
+            "const": "vcs.branch.updated"
           },
           "properties": {
             "type": "object",
             "properties": {
-              "file": {
+              "branch": {
                 "type": "string"
+              }
+            }
+          }
+        },
+        "required": ["type", "properties"]
+      },
+      "QuestionOption": {
+        "type": "object",
+        "properties": {
+          "label": {
+            "description": "Display text (1-5 words, concise)",
+            "type": "string"
+          },
+          "description": {
+            "description": "Explanation of choice",
+            "type": "string"
+          }
+        },
+        "required": ["label", "description"]
+      },
+      "QuestionInfo": {
+        "type": "object",
+        "properties": {
+          "question": {
+            "description": "Complete question",
+            "type": "string"
+          },
+          "header": {
+            "description": "Very short label (max 30 chars)",
+            "type": "string"
+          },
+          "options": {
+            "description": "Available choices",
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/QuestionOption"
+            }
+          },
+          "multiple": {
+            "description": "Allow selecting multiple choices",
+            "type": "boolean"
+          },
+          "custom": {
+            "description": "Allow typing a custom answer (default: true)",
+            "type": "boolean"
+          }
+        },
+        "required": ["question", "header", "options"]
+      },
+      "QuestionRequest": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "string",
+            "pattern": "^que.*"
+          },
+          "sessionID": {
+            "type": "string",
+            "pattern": "^ses.*"
+          },
+          "questions": {
+            "description": "Questions to ask",
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/QuestionInfo"
+            }
+          },
+          "tool": {
+            "type": "object",
+            "properties": {
+              "messageID": {
+                "type": "string",
+                "pattern": "^msg.*"
               },
-              "event": {
-                "anyOf": [
-                  {
-                    "type": "string",
-                    "const": "add"
-                  },
-                  {
-                    "type": "string",
-                    "const": "change"
-                  },
-                  {
-                    "type": "string",
-                    "const": "unlink"
-                  }
-                ]
+              "callID": {
+                "type": "string"
               }
             },
-            "required": ["file", "event"]
+            "required": ["messageID", "callID"]
+          }
+        },
+        "required": ["id", "sessionID", "questions"]
+      },
+      "Event.question.asked": {
+        "type": "object",
+        "properties": {
+          "type": {
+            "type": "string",
+            "const": "question.asked"
+          },
+          "properties": {
+            "$ref": "#/components/schemas/QuestionRequest"
           }
         },
         "required": ["type", "properties"]
       },
-      "Event.vcs.branch.updated": {
+      "QuestionAnswer": {
+        "type": "array",
+        "items": {
+          "type": "string"
+        }
+      },
+      "Event.question.replied": {
         "type": "object",
         "properties": {
           "type": {
             "type": "string",
-            "const": "vcs.branch.updated"
+            "const": "question.replied"
           },
           "properties": {
             "type": "object",
             "properties": {
-              "branch": {
-                "type": "string"
+              "sessionID": {
+                "type": "string",
+                "pattern": "^ses.*"
+              },
+              "requestID": {
+                "type": "string",
+                "pattern": "^que.*"
+              },
+              "answers": {
+                "type": "array",
+                "items": {
+                  "$ref": "#/components/schemas/QuestionAnswer"
+                }
               }
-            }
+            },
+            "required": ["sessionID", "requestID", "answers"]
           }
         },
         "required": ["type", "properties"]
       },
-      "Event.file.edited": {
+      "Event.question.rejected": {
         "type": "object",
         "properties": {
           "type": {
             "type": "string",
-            "const": "file.edited"
+            "const": "question.rejected"
           },
           "properties": {
             "type": "object",
             "properties": {
-              "file": {
-                "type": "string"
+              "sessionID": {
+                "type": "string",
+                "pattern": "^ses.*"
+              },
+              "requestID": {
+                "type": "string",
+                "pattern": "^que.*"
               }
             },
-            "required": ["file"]
+            "required": ["sessionID", "requestID"]
           }
         },
         "required": ["type", "properties"]
@@ -9609,16 +9609,13 @@
             "$ref": "#/components/schemas/Event.project.updated"
           },
           {
-            "$ref": "#/components/schemas/Event.server.instance.disposed"
-          },
-          {
-            "$ref": "#/components/schemas/Event.question.asked"
+            "$ref": "#/components/schemas/Event.file.edited"
           },
           {
-            "$ref": "#/components/schemas/Event.question.replied"
+            "$ref": "#/components/schemas/Event.server.instance.disposed"
           },
           {
-            "$ref": "#/components/schemas/Event.question.rejected"
+            "$ref": "#/components/schemas/Event.file.watcher.updated"
           },
           {
             "$ref": "#/components/schemas/Event.permission.asked"
@@ -9627,13 +9624,16 @@
             "$ref": "#/components/schemas/Event.permission.replied"
           },
           {
-            "$ref": "#/components/schemas/Event.file.watcher.updated"
+            "$ref": "#/components/schemas/Event.vcs.branch.updated"
           },
           {
-            "$ref": "#/components/schemas/Event.vcs.branch.updated"
+            "$ref": "#/components/schemas/Event.question.asked"
           },
           {
-            "$ref": "#/components/schemas/Event.file.edited"
+            "$ref": "#/components/schemas/Event.question.replied"
+          },
+          {
+            "$ref": "#/components/schemas/Event.question.rejected"
           },
           {
             "$ref": "#/components/schemas/Event.server.connected"