mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-25 15:24:58 +00:00
Add `run --interactive` (`-i`) which renders a split-screen TUI inside the run command: scrollback output on top, input footer on the bottom with prompt, permission, and question views. This bridges the gap between headless `run` and the full TUI, giving users an interactive experience without leaving the CLI pipeline. Tool rendering, session data reduction, and stream transport are extracted into run/ submodules to keep the orchestration manageable. *.shared.ts files contain shared logic for both run and the TUI, but are intentionally kept in /run for now until later refactor
95 lines
2.2 KiB
TypeScript
95 lines
2.2 KiB
TypeScript
// Session message extraction and prompt history.
|
|
//
|
|
// Fetches session messages from the SDK and extracts user turn text for
|
|
// the prompt history ring. Also finds the most recently used variant for
|
|
// the current model so the footer can pre-select it.
|
|
import type { RunInput } from "./types"
|
|
|
|
const LIMIT = 200
|
|
|
|
export type SessionMessages = NonNullable<Awaited<ReturnType<RunInput["sdk"]["session"]["messages"]>>["data"]>
|
|
|
|
type Turn = {
|
|
text: string
|
|
provider: string | undefined
|
|
model: string | undefined
|
|
variant: string | undefined
|
|
}
|
|
|
|
export type RunSession = {
|
|
first: boolean
|
|
turns: Turn[]
|
|
}
|
|
|
|
function text(msg: SessionMessages[number]): string {
|
|
return msg.parts
|
|
.filter((part) => part.type === "text")
|
|
.map((part) => part.text.trim())
|
|
.filter((part) => part.length > 0)
|
|
.join("\n")
|
|
}
|
|
|
|
function turn(msg: SessionMessages[number]): Turn | undefined {
|
|
if (msg.info.role !== "user") {
|
|
return
|
|
}
|
|
|
|
return {
|
|
text: text(msg),
|
|
provider: msg.info.model.providerID,
|
|
model: msg.info.model.modelID,
|
|
variant: msg.info.variant,
|
|
}
|
|
}
|
|
|
|
export function createSession(messages: SessionMessages): RunSession {
|
|
return {
|
|
first: messages.length === 0,
|
|
turns: messages.flatMap((msg) => {
|
|
const item = turn(msg)
|
|
return item ? [item] : []
|
|
}),
|
|
}
|
|
}
|
|
|
|
export async function resolveSession(sdk: RunInput["sdk"], sessionID: string, limit = LIMIT): Promise<RunSession> {
|
|
const response = await sdk.session.messages({
|
|
sessionID,
|
|
limit,
|
|
})
|
|
return createSession(response.data ?? [])
|
|
}
|
|
|
|
export function sessionHistory(session: RunSession, limit = LIMIT): string[] {
|
|
const out: string[] = []
|
|
|
|
for (const turn of session.turns) {
|
|
if (!turn.text) {
|
|
continue
|
|
}
|
|
|
|
if (out[out.length - 1] === turn.text) {
|
|
continue
|
|
}
|
|
|
|
out.push(turn.text)
|
|
}
|
|
|
|
return out.slice(-limit)
|
|
}
|
|
|
|
export function sessionVariant(session: RunSession, model: RunInput["model"]): string | undefined {
|
|
if (!model) {
|
|
return
|
|
}
|
|
|
|
for (let idx = session.turns.length - 1; idx >= 0; idx -= 1) {
|
|
const turn = session.turns[idx]
|
|
if (turn.provider !== model.providerID || turn.model !== model.modelID) {
|
|
continue
|
|
}
|
|
|
|
return turn.variant
|
|
}
|
|
}
|