diff --git a/bun.lock b/bun.lock index 8faaf7a3c..115100e10 100644 --- a/bun.lock +++ b/bun.lock @@ -46,7 +46,7 @@ "@solidjs/router": "catalog:", "@thisbeyond/solid-dnd": "0.7.5", "diff": "catalog:", - "effect": "4.0.0-beta.31", + "effect": "catalog:", "fuzzysort": "catalog:", "ghostty-web": "github:anomalyco/ghostty-web#main", "luxon": "catalog:", @@ -227,7 +227,7 @@ "@solid-primitives/storage": "catalog:", "@solidjs/meta": "catalog:", "@solidjs/router": "0.15.4", - "effect": "4.0.0-beta.31", + "effect": "catalog:", "electron-log": "^5", "electron-store": "^10", "electron-updater": "^6", @@ -324,7 +324,7 @@ "@ai-sdk/xai": "2.0.51", "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", - "@effect/platform-node": "4.0.0-beta.31", + "@effect/platform-node": "catalog:", "@gitlab/gitlab-ai-provider": "3.6.0", "@gitlab/opencode-gitlab-auth": "1.3.3", "@hono/standard-validator": "0.1.5", @@ -594,6 +594,7 @@ }, "catalog": { "@cloudflare/workers-types": "4.20251008.0", + "@effect/platform-node": "4.0.0-beta.35", "@hono/zod-validator": "0.4.2", "@kobalte/core": "0.13.11", "@octokit/rest": "22.0.0", @@ -617,7 +618,7 @@ "dompurify": "3.3.1", "drizzle-kit": "1.0.0-beta.16-ea816b6", "drizzle-orm": "1.0.0-beta.16-ea816b6", - "effect": "4.0.0-beta.31", + "effect": "4.0.0-beta.35", "fuzzysort": "3.1.0", "hono": "4.10.7", "hono-openapi": "1.1.2", @@ -975,9 +976,9 @@ "@effect/language-service": ["@effect/language-service@0.79.0", "", { "bin": { "effect-language-service": "cli.js" } }, "sha512-DEmIOsg1GjjP6s9HXH1oJrW+gDmzkhVv9WOZl6to5eNyyCrjz1S2PDqQ7aYrW/HuifhfwI5Bik1pK4pj7Z+lrg=="], - "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.31", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.31", "mime": "^4.1.0", "undici": "^7.20.0" }, "peerDependencies": { "effect": "^4.0.0-beta.31", "ioredis": "^5.7.0" } }, "sha512-KmVZwGsQRBMZZYPJwpL2vj6sxjBzfXhyA8RgsH5/cmckDTsZpVTyqODQ/FFzmCnMWuYjZoJGPghTDrVVDn/6ZA=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.35", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.35", "mime": "^4.1.0", "undici": "^7.24.0" }, "peerDependencies": { "effect": "^4.0.0-beta.35", "ioredis": "^5.7.0" } }, "sha512-HPc2xZASl9F9y/xJ01bQgFD6Jf9XP4Fcv/BlVTvG0Yr/uN63lwKZYr/VXor5K5krHfBDeCBD8y7/SICPYZoq3A=="], - "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.33", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.33" } }, "sha512-jaJnvYz1IiPZyN//fCJsvwnmujJS5KD8noCVVLhb4ZGCWKhQpt0x2iuax6HFzMlPEQSfl04GLU+PVKh0nkzPyA=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.35", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.35" } }, "sha512-9bPqNV988itKJ7MQoJuzmR014DB9EZRDOnhJt/+iJlb8qLoR9HnCzNJb9gfBdYhFmVYc8DMsQxG81rdJzpv9tg=="], "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], @@ -2751,7 +2752,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "effect": ["effect@4.0.0-beta.31", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-w3QwJnlaLtWWiUSzhCXUTIisnULPsxLzpO6uqaBFjXybKx6FvCqsLJT6v4dV7G9eA9jeTtG6Gv7kF+jGe3HxzA=="], + "effect": ["effect@4.0.0-beta.35", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.5.3", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.8", "multipasta": "^0.2.7", "toml": "^3.0.0", "uuid": "^13.0.0", "yaml": "^2.8.2" } }, "sha512-64j8dgJmoEMeq6Y3WLYcZIRqPZ5E/lqnULCf6QW5te3hQ/sa13UodWLGwBEviEqBoq72U8lArhVX+T7ntzhJGQ=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], @@ -5019,6 +5020,8 @@ "@dot/log/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "@effect/platform-node/undici": ["undici@7.24.4", "", {}, "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w=="], + "@effect/platform-node-shared/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], diff --git a/package.json b/package.json index 00e251f50..b8329dd94 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "packages/slack" ], "catalog": { + "@effect/platform-node": "4.0.0-beta.35", "@types/bun": "1.3.9", "@octokit/rest": "22.0.0", "@hono/zod-validator": "0.4.2", @@ -44,7 +45,7 @@ "dompurify": "3.3.1", "drizzle-kit": "1.0.0-beta.16-ea816b6", "drizzle-orm": "1.0.0-beta.16-ea816b6", - "effect": "4.0.0-beta.31", + "effect": "4.0.0-beta.35", "ai": "5.0.124", "hono": "4.10.7", "hono-openapi": "1.1.2", diff --git a/packages/app/e2e/prompt/prompt-multiline.spec.ts b/packages/app/e2e/prompt/prompt-multiline.spec.ts index 216aa3fda..3584773bb 100644 --- a/packages/app/e2e/prompt/prompt-multiline.spec.ts +++ b/packages/app/e2e/prompt/prompt-multiline.spec.ts @@ -7,12 +7,18 @@ test("shift+enter inserts a newline without submitting", async ({ page, gotoSess await expect(page).toHaveURL(/\/session\/?$/) const prompt = page.locator(promptSelector) - await prompt.click() - await page.keyboard.type("line one") - await page.keyboard.press("Shift+Enter") - await page.keyboard.type("line two") + await prompt.focus() + await expect(prompt).toBeFocused() + + await prompt.pressSequentially("line one") + await expect(prompt).toBeFocused() + + await prompt.press("Shift+Enter") + await expect(page).toHaveURL(/\/session\/?$/) + await expect(prompt).toBeFocused() + + await prompt.pressSequentially("line two") await expect(page).toHaveURL(/\/session\/?$/) - await expect(prompt).toContainText("line one") - await expect(prompt).toContainText("line two") + await expect.poll(() => prompt.evaluate((el) => el.innerText)).toBe("line one\nline two") }) diff --git a/packages/app/package.json b/packages/app/package.json index 878cfb8e3..545d31309 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -56,7 +56,7 @@ "@solidjs/router": "catalog:", "@thisbeyond/solid-dnd": "0.7.5", "diff": "catalog:", - "effect": "4.0.0-beta.31", + "effect": "catalog:", "fuzzysort": "catalog:", "ghostty-web": "github:anomalyco/ghostty-web#main", "luxon": "catalog:", diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index c84c7272d..4c3a00dfb 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -566,6 +566,7 @@ export default function Layout(props: ParentProps) { const [autoselecting] = createResource(async () => { await ready.promise await layout.ready.promise + if (!untrack(() => state.autoselect)) return const list = layout.projects.list() const last = server.projects.last() diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index 2af3196e1..bcb80d9e6 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -30,7 +30,7 @@ "@solid-primitives/storage": "catalog:", "@solidjs/meta": "catalog:", "@solidjs/router": "0.15.4", - "effect": "4.0.0-beta.31", + "effect": "catalog:", "electron-log": "^5", "electron-store": "^10", "electron-updater": "^6", diff --git a/packages/opencode/package.json b/packages/opencode/package.json index a6caca8ad..049573e3e 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -82,7 +82,6 @@ "@ai-sdk/xai": "2.0.51", "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", - "@effect/platform-node": "4.0.0-beta.31", "@gitlab/gitlab-ai-provider": "3.6.0", "@gitlab/opencode-gitlab-auth": "1.3.3", "@hono/standard-validator": "0.1.5", @@ -98,6 +97,7 @@ "@openrouter/ai-sdk-provider": "1.5.4", "@opentui/core": "0.1.87", "@opentui/solid": "0.1.87", + "@effect/platform-node": "catalog:", "@parcel/watcher": "2.5.1", "@pierre/diffs": "catalog:", "@solid-primitives/event-bus": "1.1.2", diff --git a/packages/opencode/src/file/watcher.ts b/packages/opencode/src/file/watcher.ts index ad683303c..16ab3c6d3 100644 --- a/packages/opencode/src/file/watcher.ts +++ b/packages/opencode/src/file/watcher.ts @@ -51,6 +51,13 @@ export namespace FileWatcher { if (process.platform === "linux") return "inotify" } + function protecteds(dir: string) { + return Protected.paths().filter((item) => { + const rel = path.relative(dir, item) + return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel) + }) + } + export const hasNativeBinding = () => !!watcher() export class Service extends ServiceMap.Service()("@opencode/FileWatcher") {} @@ -105,7 +112,7 @@ export namespace FileWatcher { const cfgIgnores = cfg.watcher?.ignore ?? [] if (yield* Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) { - yield* subscribe(instance.directory, [...FileIgnore.PATTERNS, ...cfgIgnores, ...Protected.paths()]) + yield* subscribe(instance.directory, [...FileIgnore.PATTERNS, ...cfgIgnores, ...protecteds(instance.directory)]) } if (instance.project.vcs === "git") { diff --git a/packages/opencode/src/util/process.ts b/packages/opencode/src/util/process.ts index 8cf1f5b9f..7e6be0e20 100644 --- a/packages/opencode/src/util/process.ts +++ b/packages/opencode/src/util/process.ts @@ -98,6 +98,7 @@ export namespace Process { reject(error) }) }) + void exited.catch(() => undefined) if (opts.abort) { opts.abort.addEventListener("abort", abort, { once: true }) diff --git a/packages/opencode/test/file/watcher.test.ts b/packages/opencode/test/file/watcher.test.ts index 8a3d30d31..2cd27643e 100644 --- a/packages/opencode/test/file/watcher.test.ts +++ b/packages/opencode/test/file/watcher.test.ts @@ -2,7 +2,7 @@ import { $ } from "bun" import { afterEach, describe, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" -import { Deferred, Effect, Fiber, Option } from "effect" +import { Deferred, Effect, Option } from "effect" import { tmpdir } from "../fixture/fixture" import { watcherConfigLayer, withServices } from "../fixture/instance" import { FileWatcher } from "../../src/file/watcher" @@ -25,6 +25,7 @@ function withWatcher(directory: string, body: Effect.Effect) { directory, FileWatcher.layer, async (rt) => { + await rt.runPromise(FileWatcher.Service.use(() => Effect.void)) await Effect.runPromise(ready(directory)) await Effect.runPromise(body) }, @@ -54,24 +55,29 @@ function listen(directory: string, check: (evt: WatcherEvent) => boolean, hit: ( } function wait(directory: string, check: (evt: WatcherEvent) => boolean) { - return Effect.callback((resume) => { - const cleanup = listen(directory, check, (evt) => { - cleanup() - resume(Effect.succeed(evt)) + return Effect.gen(function* () { + const deferred = yield* Deferred.make() + const cleanup = yield* Effect.sync(() => { + let off = () => {} + off = listen(directory, check, (evt) => { + off() + Deferred.doneUnsafe(deferred, Effect.succeed(evt)) + }) + return off }) - return Effect.sync(cleanup) - }).pipe(Effect.timeout("5 seconds")) + return { cleanup, deferred } + }) } function nextUpdate(directory: string, check: (evt: WatcherEvent) => boolean, trigger: Effect.Effect) { return Effect.acquireUseRelease( - wait(directory, check).pipe(Effect.forkChild({ startImmediately: true })), - (fiber) => + wait(directory, check), + ({ deferred }) => Effect.gen(function* () { yield* trigger - return yield* Fiber.join(fiber) + return yield* Deferred.await(deferred).pipe(Effect.timeout("5 seconds")) }), - Fiber.interrupt, + ({ cleanup }) => Effect.sync(cleanup), ) } @@ -82,23 +88,15 @@ function noUpdate( trigger: Effect.Effect, ms = 500, ) { - return Effect.gen(function* () { - const deferred = yield* Deferred.make() - - yield* Effect.acquireUseRelease( - Effect.sync(() => - listen(directory, check, (evt) => { - Effect.runSync(Deferred.succeed(deferred, evt)) - }), - ), - () => - Effect.gen(function* () { - yield* trigger - expect(yield* Deferred.await(deferred).pipe(Effect.timeoutOption(`${ms} millis`))).toEqual(Option.none()) - }), - (cleanup) => Effect.sync(cleanup), - ) - }) + return Effect.acquireUseRelease( + wait(directory, check), + ({ deferred }) => + Effect.gen(function* () { + yield* trigger + expect(yield* Deferred.await(deferred).pipe(Effect.timeoutOption(`${ms} millis`))).toEqual(Option.none()) + }), + ({ cleanup }) => Effect.sync(cleanup), + ) } function ready(directory: string) { diff --git a/packages/opencode/test/util/process.test.ts b/packages/opencode/test/util/process.test.ts index b9bc50f9b..1d08cba6b 100644 --- a/packages/opencode/test/util/process.test.ts +++ b/packages/opencode/test/util/process.test.ts @@ -109,4 +109,20 @@ describe("util.process", () => { expect(await proc.exited).toBe(0) }) + + test("rejects missing commands without leaking unhandled errors", async () => { + await using tmp = await tmpdir() + const cmd = path.join(tmp.path, "missing" + (process.platform === "win32" ? ".cmd" : "")) + const err = await Process.spawn([cmd], { + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }).exited.catch((err) => err) + + expect(err).toBeInstanceOf(Error) + if (!(err instanceof Error)) throw err + expect(err).toMatchObject({ + code: "ENOENT", + }) + }) })