diff options
| author | Kit Langton <[email protected]> | 2026-04-16 21:51:02 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-04-17 01:51:02 +0000 |
| commit | 4f8986aa48cbab66ca6e72272c3c7d27ffc8e0eb (patch) | |
| tree | 291215bf3ebe0df9f04545b3835e79c9af55b89c | |
| parent | 9c87a144e879dd9b76c90cb1415e63005aac2843 (diff) | |
| download | opencode-4f8986aa48cbab66ca6e72272c3c7d27ffc8e0eb.tar.gz opencode-4f8986aa48cbab66ca6e72272c3c7d27ffc8e0eb.zip | |
refactor: unwrap Question namespace + fix script to emit "." for index.ts (#22992)
| -rw-r--r-- | packages/opencode/script/unwrap-and-self-reexport.ts | 11 | ||||
| -rw-r--r-- | packages/opencode/src/question/index.ts | 388 |
2 files changed, 202 insertions, 197 deletions
diff --git a/packages/opencode/script/unwrap-and-self-reexport.ts b/packages/opencode/script/unwrap-and-self-reexport.ts index 5ae703182..09256f3a5 100644 --- a/packages/opencode/script/unwrap-and-self-reexport.ts +++ b/packages/opencode/script/unwrap-and-self-reexport.ts @@ -207,10 +207,15 @@ const rewrittenBody = dedented.map(rewriteLine) // Assemble the new file. Collapse multiple trailing blank lines so the // self-reexport sits cleanly at the end. +// +// When the file is itself `index.ts`, prefer `"."` over `"./index"` — both are +// valid but `"."` matches the existing convention in the codebase (e.g. +// pty/index.ts, file/index.ts, etc.) and avoids referencing "index" literally. const basename = path.basename(absPath, ".ts") +const reexportSource = basename === "index" ? "." : `./${basename}` const assembled = [...before, ...rewrittenBody, ...after].join("\n") const trimmed = assembled.replace(/\s+$/g, "") -const output = `${trimmed}\n\nexport * as ${nsName} from "./${basename}"\n` +const output = `${trimmed}\n\nexport * as ${nsName} from "${reexportSource}"\n` if (dryRun) { console.log(`--- dry run: ${path.relative(process.cwd(), absPath)} ---`) @@ -218,7 +223,7 @@ if (dryRun) { console.log(`body lines: ${body.length}`) console.log(`declared names: ${Array.from(declaredNames).join(", ") || "(none)"}`) console.log(`self-refs rewr: ${rewriteCount}`) - console.log(`self-reexport: export * as ${nsName} from "./${basename}"`) + console.log(`self-reexport: export * as ${nsName} from "${reexportSource}"`) console.log(`output preview (last 10 lines):`) const outputLines = output.split("\n") for (const l of outputLines.slice(Math.max(0, outputLines.length - 10))) { @@ -231,7 +236,7 @@ fs.writeFileSync(absPath, output) console.log(`unwrapped ${path.relative(process.cwd(), absPath)} → ${nsName}`) console.log(` body lines: ${body.length}`) console.log(` self-refs rewr: ${rewriteCount}`) -console.log(` self-reexport: export * as ${nsName} from "./${basename}"`) +console.log(` self-reexport: export * as ${nsName} from "${reexportSource}"`) console.log("") console.log("Next: verify with") console.log(" bunx --bun tsgo --noEmit") diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts index 627d04564..3b377c982 100644 --- a/packages/opencode/src/question/index.ts +++ b/packages/opencode/src/question/index.ts @@ -8,222 +8,222 @@ import { Log } from "@/util" import { withStatics } from "@/util/schema" import { QuestionID } from "./schema" -export namespace Question { - const log = Log.create({ service: "question" }) - - // Schemas - - export class Option extends Schema.Class<Option>("QuestionOption")({ - label: Schema.String.annotate({ - description: "Display text (1-5 words, concise)", - }), - description: Schema.String.annotate({ - description: "Explanation of choice", - }), - }) { - static readonly zod = zod(this) - } +const log = Log.create({ service: "question" }) + +// Schemas + +export class Option extends Schema.Class<Option>("QuestionOption")({ + label: Schema.String.annotate({ + description: "Display text (1-5 words, concise)", + }), + description: Schema.String.annotate({ + description: "Explanation of choice", + }), +}) { + static readonly zod = zod(this) +} - const base = { - question: Schema.String.annotate({ - description: "Complete question", - }), - header: Schema.String.annotate({ - description: "Very short label (max 30 chars)", - }), - options: Schema.Array(Option).annotate({ - description: "Available choices", - }), - multiple: Schema.optional(Schema.Boolean).annotate({ - description: "Allow selecting multiple choices", - }), - } +const base = { + question: Schema.String.annotate({ + description: "Complete question", + }), + header: Schema.String.annotate({ + description: "Very short label (max 30 chars)", + }), + options: Schema.Array(Option).annotate({ + description: "Available choices", + }), + multiple: Schema.optional(Schema.Boolean).annotate({ + description: "Allow selecting multiple choices", + }), +} - export class Info extends Schema.Class<Info>("QuestionInfo")({ - ...base, - custom: Schema.optional(Schema.Boolean).annotate({ - description: "Allow typing a custom answer (default: true)", - }), - }) { - static readonly zod = zod(this) - } +export class Info extends Schema.Class<Info>("QuestionInfo")({ + ...base, + custom: Schema.optional(Schema.Boolean).annotate({ + description: "Allow typing a custom answer (default: true)", + }), +}) { + static readonly zod = zod(this) +} - export class Prompt extends Schema.Class<Prompt>("QuestionPrompt")(base) { - static readonly zod = zod(this) - } +export class Prompt extends Schema.Class<Prompt>("QuestionPrompt")(base) { + static readonly zod = zod(this) +} - export class Tool extends Schema.Class<Tool>("QuestionTool")({ - messageID: MessageID, - callID: Schema.String, - }) { - static readonly zod = zod(this) - } +export class Tool extends Schema.Class<Tool>("QuestionTool")({ + messageID: MessageID, + callID: Schema.String, +}) { + static readonly zod = zod(this) +} - export class Request extends Schema.Class<Request>("QuestionRequest")({ - id: QuestionID, - sessionID: SessionID, - questions: Schema.Array(Info).annotate({ - description: "Questions to ask", - }), - tool: Schema.optional(Tool), - }) { - static readonly zod = zod(this) - } +export class Request extends Schema.Class<Request>("QuestionRequest")({ + id: QuestionID, + sessionID: SessionID, + questions: Schema.Array(Info).annotate({ + description: "Questions to ask", + }), + tool: Schema.optional(Tool), +}) { + static readonly zod = zod(this) +} - export const Answer = Schema.Array(Schema.String) - .annotate({ identifier: "QuestionAnswer" }) - .pipe(withStatics((s) => ({ zod: zod(s) }))) - export type Answer = Schema.Schema.Type<typeof Answer> - - export class Reply extends Schema.Class<Reply>("QuestionReply")({ - answers: Schema.Array(Answer).annotate({ - description: "User answers in order of questions (each answer is an array of selected labels)", - }), - }) { - static readonly zod = zod(this) - } +export const Answer = Schema.Array(Schema.String) + .annotate({ identifier: "QuestionAnswer" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type Answer = Schema.Schema.Type<typeof Answer> + +export class Reply extends Schema.Class<Reply>("QuestionReply")({ + answers: Schema.Array(Answer).annotate({ + description: "User answers in order of questions (each answer is an array of selected labels)", + }), +}) { + static readonly zod = zod(this) +} - class Replied extends Schema.Class<Replied>("QuestionReplied")({ - sessionID: SessionID, - requestID: QuestionID, - answers: Schema.Array(Answer), - }) {} - - class Rejected extends Schema.Class<Rejected>("QuestionRejected")({ - sessionID: SessionID, - requestID: QuestionID, - }) {} - - export const Event = { - Asked: BusEvent.define("question.asked", Request.zod), - Replied: BusEvent.define("question.replied", zod(Replied)), - Rejected: BusEvent.define("question.rejected", zod(Rejected)), - } +class Replied extends Schema.Class<Replied>("QuestionReplied")({ + sessionID: SessionID, + requestID: QuestionID, + answers: Schema.Array(Answer), +}) {} + +class Rejected extends Schema.Class<Rejected>("QuestionRejected")({ + sessionID: SessionID, + requestID: QuestionID, +}) {} + +export const Event = { + Asked: BusEvent.define("question.asked", Request.zod), + Replied: BusEvent.define("question.replied", zod(Replied)), + Rejected: BusEvent.define("question.rejected", zod(Rejected)), +} - export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) { - override get message() { - return "The user dismissed this question" - } +export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("QuestionRejectedError", {}) { + override get message() { + return "The user dismissed this question" } +} - interface PendingEntry { - info: Request - deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError> - } +interface PendingEntry { + info: Request + deferred: Deferred.Deferred<ReadonlyArray<Answer>, RejectedError> +} - interface State { - pending: Map<QuestionID, PendingEntry> - } +interface State { + pending: Map<QuestionID, PendingEntry> +} + +// Service + +export interface Interface { + readonly ask: (input: { + sessionID: SessionID + questions: ReadonlyArray<Info> + tool?: Tool + }) => Effect.Effect<ReadonlyArray<Answer>, RejectedError> + readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void> + readonly reject: (requestID: QuestionID) => Effect.Effect<void> + readonly list: () => Effect.Effect<ReadonlyArray<Request>> +} + +export class Service extends Context.Service<Service, Interface>()("@opencode/Question") {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const bus = yield* Bus.Service + const state = yield* InstanceState.make<State>( + Effect.fn("Question.state")(function* () { + const state = { + pending: new Map<QuestionID, PendingEntry>(), + } - // Service + yield* Effect.addFinalizer(() => + Effect.gen(function* () { + for (const item of state.pending.values()) { + yield* Deferred.fail(item.deferred, new RejectedError()) + } + state.pending.clear() + }), + ) - export interface Interface { - readonly ask: (input: { + return state + }), + ) + + const ask = Effect.fn("Question.ask")(function* (input: { sessionID: SessionID questions: ReadonlyArray<Info> tool?: Tool - }) => Effect.Effect<ReadonlyArray<Answer>, RejectedError> - readonly reply: (input: { requestID: QuestionID; answers: ReadonlyArray<Answer> }) => Effect.Effect<void> - readonly reject: (requestID: QuestionID) => Effect.Effect<void> - readonly list: () => Effect.Effect<ReadonlyArray<Request>> - } + }) { + const pending = (yield* InstanceState.get(state)).pending + const id = QuestionID.ascending() + log.info("asking", { id, questions: input.questions.length }) + + const deferred = yield* Deferred.make<ReadonlyArray<Answer>, RejectedError>() + const info = Schema.decodeUnknownSync(Request)({ + id, + sessionID: input.sessionID, + questions: input.questions, + tool: input.tool, + }) + pending.set(id, { info, deferred }) + yield* bus.publish(Event.Asked, info) - export class Service extends Context.Service<Service, Interface>()("@opencode/Question") {} - - export const layer = Layer.effect( - Service, - Effect.gen(function* () { - const bus = yield* Bus.Service - const state = yield* InstanceState.make<State>( - Effect.fn("Question.state")(function* () { - const state = { - pending: new Map<QuestionID, PendingEntry>(), - } - - yield* Effect.addFinalizer(() => - Effect.gen(function* () { - for (const item of state.pending.values()) { - yield* Deferred.fail(item.deferred, new RejectedError()) - } - state.pending.clear() - }), - ) - - return state + return yield* Effect.ensuring( + Deferred.await(deferred), + Effect.sync(() => { + pending.delete(id) }), ) - - const ask = Effect.fn("Question.ask")(function* (input: { - sessionID: SessionID - questions: ReadonlyArray<Info> - tool?: Tool - }) { - const pending = (yield* InstanceState.get(state)).pending - const id = QuestionID.ascending() - log.info("asking", { id, questions: input.questions.length }) - - const deferred = yield* Deferred.make<ReadonlyArray<Answer>, RejectedError>() - const info = Schema.decodeUnknownSync(Request)({ - id, - sessionID: input.sessionID, - questions: input.questions, - tool: input.tool, - }) - pending.set(id, { info, deferred }) - yield* bus.publish(Event.Asked, info) - - return yield* Effect.ensuring( - Deferred.await(deferred), - Effect.sync(() => { - pending.delete(id) - }), - ) + }) + + const reply = Effect.fn("Question.reply")(function* (input: { + requestID: QuestionID + answers: ReadonlyArray<Answer> + }) { + const pending = (yield* InstanceState.get(state)).pending + const existing = pending.get(input.requestID) + if (!existing) { + log.warn("reply for unknown request", { requestID: input.requestID }) + return + } + pending.delete(input.requestID) + log.info("replied", { requestID: input.requestID, answers: input.answers }) + yield* bus.publish(Event.Replied, { + sessionID: existing.info.sessionID, + requestID: existing.info.id, + answers: input.answers, }) - - const reply = Effect.fn("Question.reply")(function* (input: { - requestID: QuestionID - answers: ReadonlyArray<Answer> - }) { - const pending = (yield* InstanceState.get(state)).pending - const existing = pending.get(input.requestID) - if (!existing) { - log.warn("reply for unknown request", { requestID: input.requestID }) - return - } - pending.delete(input.requestID) - log.info("replied", { requestID: input.requestID, answers: input.answers }) - yield* bus.publish(Event.Replied, { - sessionID: existing.info.sessionID, - requestID: existing.info.id, - answers: input.answers, - }) - yield* Deferred.succeed(existing.deferred, input.answers) + yield* Deferred.succeed(existing.deferred, input.answers) + }) + + const reject = Effect.fn("Question.reject")(function* (requestID: QuestionID) { + const pending = (yield* InstanceState.get(state)).pending + const existing = pending.get(requestID) + if (!existing) { + log.warn("reject for unknown request", { requestID }) + return + } + pending.delete(requestID) + log.info("rejected", { requestID }) + yield* bus.publish(Event.Rejected, { + sessionID: existing.info.sessionID, + requestID: existing.info.id, }) + yield* Deferred.fail(existing.deferred, new RejectedError()) + }) - const reject = Effect.fn("Question.reject")(function* (requestID: QuestionID) { - const pending = (yield* InstanceState.get(state)).pending - const existing = pending.get(requestID) - if (!existing) { - log.warn("reject for unknown request", { requestID }) - return - } - pending.delete(requestID) - log.info("rejected", { requestID }) - yield* bus.publish(Event.Rejected, { - sessionID: existing.info.sessionID, - requestID: existing.info.id, - }) - yield* Deferred.fail(existing.deferred, new RejectedError()) - }) + const list = Effect.fn("Question.list")(function* () { + const pending = (yield* InstanceState.get(state)).pending + return Array.from(pending.values(), (x) => x.info) + }) - const list = Effect.fn("Question.list")(function* () { - const pending = (yield* InstanceState.get(state)).pending - return Array.from(pending.values(), (x) => x.info) - }) + return Service.of({ ask, reply, reject, list }) + }), +) - return Service.of({ ask, reply, reject, list }) - }), - ) +export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) - export const defaultLayer = layer.pipe(Layer.provide(Bus.layer)) -} +export * as Question from "." |
