From 2fd6001eb99fe3bcedc9b3db128676c90f5248fd Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 21 Oct 2026 14:34:56 -0500 Subject: [PATCH 1/2] refactor(core): tighten form, pty, plugin, and snapshot types and checks --- packages/core/src/form.ts | 6 +----- packages/core/src/plugin.ts | 4 ++-- packages/core/src/pty.ts | 6 +++--- packages/core/src/snapshot.ts | 11 +++++------ packages/core/src/tool/plugin/question.ts | 2 +- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts index fc8e2edb9186..97b33b70c5fc 100644 --- a/packages/core/src/form.ts +++ b/packages/core/src/form.ts @@ -343,7 +343,7 @@ function validateField(field: InputField, value: Form.Value): string | undefined return } if (field.type === "multiselect") { - if (!isStringArray(value)) return `Expected string array for form field: ${field.key}` + if (!Array.isArray(value)) return `Expected string array for form field: ${field.key}` if (field.required && value.length === 0) return `Missing required form field: ${field.key}` if (field.minItems !== undefined && value.length < field.minItems) return `Too few selections for form field: ${field.key}` @@ -355,10 +355,6 @@ function validateField(field: InputField, value: Form.Value): string | undefined } } -function isStringArray(value: Form.Value): value is ReadonlyArray { - return Array.isArray(value) && value.every((item): item is string => typeof item === "string") -} - function isDate(value: string) { if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false const date = new Date(`${value}T00:00:00.000Z`) diff --git a/packages/core/src/plugin.ts b/packages/core/src/plugin.ts index 8ade6826585f..94ca4c0463d1 100644 --- a/packages/core/src/plugin.ts +++ b/packages/core/src/plugin.ts @@ -260,7 +260,7 @@ const layer = Layer.effect( // `plugin` is the requested definition; `activation` is its last activation, which may have // failed or be an older fallback while the requested revision keeps failing setup. type Slot = { - readonly plugin: Generation + readonly plugin: Generation & { readonly id: Plugin.ID } readonly activation?: Activation readonly error?: string } @@ -285,7 +285,7 @@ type PendingFailure = { function slotInfo(slot: Slot): Plugin.Info { const failure = slot.activation?.failure ?? (slot.error === undefined ? undefined : { error: slot.error }) return { - id: Plugin.ID.make(slot.plugin.id), + id: slot.plugin.id, source: slot.plugin.source ?? { type: "builtin" }, state: failure === undefined ? { status: "active" } : { status: "failed", ...failure }, features: { server: true, ...slot.plugin.features }, diff --git a/packages/core/src/pty.ts b/packages/core/src/pty.ts index c5d7015e0594..6d4ac6d628fc 100644 --- a/packages/core/src/pty.ts +++ b/packages/core/src/pty.ts @@ -42,11 +42,11 @@ export type Info = Types.DeepMutable export const CreateInput = Pty.CreateInput -export type CreateInput = Types.DeepMutable +export type CreateInput = typeof CreateInput.Type export const UpdateInput = Pty.UpdateInput -export type UpdateInput = Types.DeepMutable +export type UpdateInput = typeof UpdateInput.Type export { Event } from "@opencode/schema/pty" @@ -275,7 +275,7 @@ const layer = Layer.effect( const from = input.cursor === -1 ? end - : typeof input.cursor === "number" && Number.isSafeInteger(input.cursor) + : input.cursor !== undefined && Number.isSafeInteger(input.cursor) ? Math.max(0, input.cursor) : 0 const replay = (() => { diff --git a/packages/core/src/snapshot.ts b/packages/core/src/snapshot.ts index 3ac6df3e2414..e9d5785b5987 100644 --- a/packages/core/src/snapshot.ts +++ b/packages/core/src/snapshot.ts @@ -168,6 +168,7 @@ const layer = Layer.effect( const changed = yield* git.tree .files({ repository: compared.repository, from: compared.from, to: compared.to }) .pipe(Effect.mapError((cause) => failure("files", cause))) + if (changed.length === 0) return [] const skipped = yield* ignored("files", compared.source, changed) return changed.filter((file) => !skipped.has(file)) }) @@ -185,12 +186,10 @@ const layer = Layer.effect( paths: input.paths, }) .pipe(Effect.mapError((cause) => failure("diff", cause))) - const skipped = yield* ignored( - "diff", - compared.source, - diffs.map((file) => RelativePath.make(file.file)), - ) - return diffs.filter((file) => !skipped.has(RelativePath.make(file.file))) + if (diffs.length === 0) return [] + const paths = diffs.map((file) => RelativePath.make(file.file)) + const skipped = yield* ignored("diff", compared.source, paths) + return diffs.filter((_, index) => !skipped.has(paths[index]!)) }) const plan = Effect.fnUntraced(function* (worktree: AbsolutePath, input: RestoreInput) { diff --git a/packages/core/src/tool/plugin/question.ts b/packages/core/src/tool/plugin/question.ts index 60d115eae35f..67febf3a81b7 100644 --- a/packages/core/src/tool/plugin/question.ts +++ b/packages/core/src/tool/plugin/question.ts @@ -99,7 +99,7 @@ export const Plugin = { answers: input.questions.map((_, index): Question.Answer => { const value = state.answer[`q${index}`] if (value === undefined) return [] - if (typeof value === "object") return Array.from(value) + if (Array.isArray(value)) return Array.from(value) return [String(value)] }), } From dfcbe64840d41eff573eab1df88b9945e1fffac8 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 21 Oct 2026 15:09:12 -0500 Subject: [PATCH 2/2] fix(core): keep isStringArray in Form.validateAnswer --- packages/core/src/form.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts index 97b33b70c5fc..fc8e2edb9186 100644 --- a/packages/core/src/form.ts +++ b/packages/core/src/form.ts @@ -343,7 +343,7 @@ function validateField(field: InputField, value: Form.Value): string | undefined return } if (field.type === "multiselect") { - if (!Array.isArray(value)) return `Expected string array for form field: ${field.key}` + if (!isStringArray(value)) return `Expected string array for form field: ${field.key}` if (field.required && value.length === 0) return `Missing required form field: ${field.key}` if (field.minItems !== undefined && value.length < field.minItems) return `Too few selections for form field: ${field.key}` @@ -355,6 +355,10 @@ function validateField(field: InputField, value: Form.Value): string | undefined } } +function isStringArray(value: Form.Value): value is ReadonlyArray { + return Array.isArray(value) && value.every((item): item is string => typeof item === "string") +} + function isDate(value: string) { if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false const date = new Date(`${value}T00:00:00.000Z`)