summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-16 21:51:02 -0400
committerGitHub <[email protected]>2026-04-17 01:51:02 +0000
commit4f8986aa48cbab66ca6e72272c3c7d27ffc8e0eb (patch)
tree291215bf3ebe0df9f04545b3835e79c9af55b89c
parent9c87a144e879dd9b76c90cb1415e63005aac2843 (diff)
downloadopencode-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.ts11
-rw-r--r--packages/opencode/src/question/index.ts388
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 "."