summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorLuke Parker <[email protected]>2026-03-19 09:54:01 +1000
committerGitHub <[email protected]>2026-03-19 09:54:01 +1000
commit5d2f8d77f964d7be6b9c9e0602f0eb6bb68993b9 (patch)
tree4677a8eb6e6d093579489bcd71c9dd812aca0da1 /packages
parent81be544981d04cc48b2aa5193c1b2b7096ec8bc4 (diff)
downloadopencode-5d2f8d77f964d7be6b9c9e0602f0eb6bb68993b9.tar.gz
opencode-5d2f8d77f964d7be6b9c9e0602f0eb6bb68993b9.zip
fix: restore recent test regressions and upgrade effect beta (#18158)
Diffstat (limited to 'packages')
-rw-r--r--packages/app/e2e/prompt/prompt-multiline.spec.ts18
-rw-r--r--packages/app/package.json2
-rw-r--r--packages/app/src/pages/layout.tsx1
-rw-r--r--packages/desktop-electron/package.json2
-rw-r--r--packages/opencode/package.json2
-rw-r--r--packages/opencode/src/file/watcher.ts9
-rw-r--r--packages/opencode/src/util/process.ts1
-rw-r--r--packages/opencode/test/file/watcher.test.ts54
-rw-r--r--packages/opencode/test/util/process.test.ts16
9 files changed, 67 insertions, 38 deletions
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<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<E>(directory: string, body: Effect.Effect<void, E>) {
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<WatcherEvent>((resume) => {
- const cleanup = listen(directory, check, (evt) => {
- cleanup()
- resume(Effect.succeed(evt))
+ return Effect.gen(function* () {
+ const deferred = yield* Deferred.make<WatcherEvent>()
+ 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<E>(directory: string, check: (evt: WatcherEvent) => boolean, trigger: Effect.Effect<void, E>) {
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<E>(
trigger: Effect.Effect<void, E>,
ms = 500,
) {
- return Effect.gen(function* () {
- const deferred = yield* Deferred.make<WatcherEvent>()
-
- 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",
+ })
+ })
})