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
60 changes: 59 additions & 1 deletion packages/opencode/src/cli/cmd/run/runtime.queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,46 @@ type Deferred<T = void> = {
reject: (error?: unknown) => void
}

/**
* Hook for plugins to intercept footer prompt submissions before they enter
* the queue. Handlers receive context about the active turn and can either
* consume the prompt (output.consumed = true) or transform the text.
*
* Registered plugins' "tui.prompt.submit" handlers are wired here during
* Plugin.init(). Handlers run in FIFO registration order.
*/
type FooterSubmitHandler = (
input: {
text: string
sessionID: string
active: boolean
mode?: "shell" | "text"
command?: { name: string; arguments: string }
},
output: { text: string; consumed: boolean },
) => Promise<void>

const footerSubmitHandlers: FooterSubmitHandler[] = []

export const footerSubmit = {
register(handler: FooterSubmitHandler) {
footerSubmitHandlers.push(handler)
return () => {
const idx = footerSubmitHandlers.indexOf(handler)
if (idx >= 0) footerSubmitHandlers.splice(idx, 1)
}
},
}

export type QueueInput = {
footer: FooterApi
initialInput?: string
trace?: Trace
onSend?: (prompt: RunPrompt) => void
onNewSession?: () => void | Promise<void>
run: (prompt: RunPrompt, signal: AbortSignal) => Promise<void>
/** The current session ID, passed to tui.prompt.submit handlers. */
sessionID?: string
}

type State = {
Expand All @@ -38,6 +71,7 @@ type State = {
active?: RunPrompt
ctrl?: AbortController
closed: boolean
sessionID: string
}

function defer<T = void>(): Deferred<T> {
Expand All @@ -63,6 +97,7 @@ export async function runPromptQueue(input: QueueInput): Promise<void> {
queue: [],
queued: [],
closed: input.footer.isClosed,
sessionID: "",
}
let draining: Promise<void> | undefined

Expand Down Expand Up @@ -265,7 +300,30 @@ export async function runPromptQueue(input: QueueInput): Promise<void> {
})()
}

const submit = (prompt: RunPrompt) => {
const submit = async (prompt: RunPrompt) => {
// Dispatch to plugin-registered footer submit handlers.
// Plugins may transform or consume the prompt before queuing.
const turnActive = !!(state.active && state.active.mode !== "shell" && !state.active.command)
for (const handler of footerSubmitHandlers) {
const output = { text: prompt.text, consumed: false }
try {
await handler(
{
text: prompt.text,
sessionID: state.sessionID,
active: turnActive,
mode: prompt.mode,
command: prompt.command ? { name: prompt.command.name, arguments: prompt.command.arguments } : undefined,
},
output,
)
} catch {
// swallow per-handler errors to keep the queue alive
}
if (output.consumed) return
prompt.text = output.text
}

if (!prompt.text.trim() || state.closed) {
return
}
Expand Down
17 changes: 17 additions & 0 deletions packages/opencode/src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,23 @@ const layer = Layer.effect(
)
}

// Wire tui.prompt.submit handlers into the footer submit chain.
// These handlers fire in the TUI process when the user submits a prompt.
// The import is dynamic because runtime.queue is a TUI module that isn't
// available in server-only (headless) mode.
yield* Effect.promise(async () => {
try {
const { footerSubmit } = await import("../cli/cmd/run/runtime.queue")
for (const hook of hooks) {
const handler = (hook as any)["tui.prompt.submit"]
if (handler) footerSubmit.register(handler)
}
} catch {
// runtime.queue not available (server-only mode) — tui.prompt.submit
// is CLI-only in the initial implementation.
}
})

const unsubscribe = yield* events.listen((event) => {
if (event.location?.directory !== ctx.directory) return Effect.void
return Effect.sync(() => {
Expand Down
23 changes: 22 additions & 1 deletion packages/plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,27 @@ export interface Hooks {
) => Promise<void>
/**
* Modify tool definitions (description and parameters) sent to LLM
*/
*/
"tool.definition"?: (input: { toolID: string }, output: { description: string; parameters: any }) => Promise<void>
/**
* Fires when the user submits a prompt in the TUI footer, before the prompt
* enters the queue. Handlers receive the prompt text and context (session ID,
* whether a generation turn is active, mode, command). Set `output.consumed`
* to skip queuing entirely (e.g. for mid-generation steering). Set
* `output.text` to transform the text before it reaches the queue.
*
* Handlers run in FIFO order. Each handler receives the output of the
* previous handler. CLI-only in the initial implementation (not yet
* dispatched in server mode via RPC bridge).
*/
"tui.prompt.submit"?: (
input: {
text: string
sessionID: string
active: boolean
mode?: "shell" | "text"
command?: { name: string; arguments: string }
},
output: { text: string; consumed: boolean },
) => Promise<void>
}
Loading