Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/opencode/src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const Info = Schema.Struct({
// Some command templates are lazy promises from MCP prompt resolution.
template: Schema.Unknown,
subtask: Schema.optional(Schema.Boolean),
noReply: Schema.optional(Schema.Boolean),
hints: Schema.Array(Schema.String),
}).annotate({ identifier: "Command" })

Expand Down Expand Up @@ -105,6 +106,7 @@ export const layer = Layer.effect(
return command.template
},
subtask: command.subtask,
noReply: command.noReply,
hints: hints(command.template),
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/config/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const Info = Schema.Struct({
agent: Schema.optional(Schema.String),
model: Schema.optional(ConfigModelID),
subtask: Schema.optional(Schema.Boolean),
noReply: Schema.optional(Schema.Boolean),
})

export type Info = Schema.Schema.Type<typeof Info>
Expand Down
31 changes: 30 additions & 1 deletion packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1224,7 +1224,35 @@ export const layer = Layer.effect(
yield* sessions.setPermission({ sessionID: session.id, permission: permissions })
}

if (input.noReply === true) return message
if (input.noReply === true) {
const ctx = yield* InstanceState.context
const assistantMsg: MessageV2.Assistant = {
id: MessageID.ascending(),
parentID: message.info.id,
role: "assistant",
mode: message.info.agent,
agent: message.info.agent,
variant: message.info.model.variant,
path: { cwd: ctx.directory, root: ctx.worktree },
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
modelID: message.info.model.modelID,
providerID: message.info.model.providerID,
time: { created: Date.now(), completed: Date.now() },
sessionID: input.sessionID,
}
yield* sessions.updateMessage(assistantMsg)

for (const part of input.parts) {
yield* sessions.updatePart({
...part,
id: PartID.ascending(),
messageID: assistantMsg.id,
sessionID: input.sessionID,
} as MessageV2.Part)
}
return message
}
return yield* loop({ sessionID: input.sessionID })
})

Expand Down Expand Up @@ -1601,6 +1629,7 @@ export const layer = Layer.effect(
agent: userAgent,
parts,
variant: input.variant,
noReply: cmd.noReply ?? false,
})
yield* bus.publish(Command.Event.Executed, {
name: input.command,
Expand Down
45 changes: 45 additions & 0 deletions packages/opencode/test/session/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2314,3 +2314,48 @@ noLLMServer.instance(
}),
30_000,
)

// noReply command semantics

noLLMServer.instance(
"noReply command skips LLM and creates assistant message with parts",
() =>
Effect.gen(function* () {
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const session = yield* sessions.create({})

const result = yield* prompt.command({
sessionID: session.id,
command: "noreply-test",
arguments: "hello",
})

// command() returns the user message
expect(result.info.role).toBe("user")

// check that an assistant message was created with the template text
const messages = yield* sessions.messages({ sessionID: session.id })
const assistants = messages.filter((msg) => msg.info.role === "assistant")
expect(assistants.length).toBe(1)
const assistant = assistants[0]
expect(assistant.info.role).toBe("assistant")
if (assistant.info.role === "assistant") {
expect(assistant.info.time.completed).toBeDefined()
}
const textParts = assistant.parts.filter((p) => p.type === "text")
expect(textParts.length).toBeGreaterThan(0)
expect(textParts.some((p) => p.type === "text" && p.text.includes("hello"))).toBe(true)
}),
{
config: {
...cfg,
command: {
"noreply-test": {
template: "NoReply test: $ARGUMENTS",
noReply: true,
},
},
},
},
)
Loading