feat(cli): add real-time streaming support to run command with tool execution visibility
- Added message.part.updated event handling for incremental text streaming - Added tool.execute event to display tool calls with input previews - Added tool.result event to show truncated tool result outputs - Enhanced EventState with lastPartText and currentTool tracking - Defined MessagePartUpdatedProps, ToolExecuteProps, ToolResultProps types - Updated event tests to cover new state fields This enables the CLI run command to display real-time agent output similar to the native opencode run command, improving user experience with immediate feedback on tool execution. 🤖 Generated with assistance of OhMyOpenCode (https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
@@ -16,13 +16,15 @@ async function* toAsyncIterable<T>(items: T[]): AsyncIterable<T> {
|
||||
}
|
||||
|
||||
describe("createEventState", () => {
|
||||
it("creates initial state with mainSessionIdle false and empty lastOutput", () => {
|
||||
it("creates initial state with correct defaults", () => {
|
||||
// #given / #when
|
||||
const state = createEventState()
|
||||
|
||||
// #then
|
||||
expect(state.mainSessionIdle).toBe(false)
|
||||
expect(state.lastOutput).toBe("")
|
||||
expect(state.lastPartText).toBe("")
|
||||
expect(state.currentTool).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,6 +75,8 @@ describe("event handling", () => {
|
||||
const state: EventState = {
|
||||
mainSessionIdle: true,
|
||||
lastOutput: "",
|
||||
lastPartText: "",
|
||||
currentTool: null,
|
||||
}
|
||||
|
||||
const payload: EventPayload = {
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import pc from "picocolors"
|
||||
import type {
|
||||
RunContext,
|
||||
EventPayload,
|
||||
SessionIdleProps,
|
||||
SessionStatusProps,
|
||||
MessageUpdatedProps,
|
||||
MessagePartUpdatedProps,
|
||||
ToolExecuteProps,
|
||||
ToolResultProps,
|
||||
} from "./types"
|
||||
|
||||
export interface EventState {
|
||||
mainSessionIdle: boolean
|
||||
lastOutput: string
|
||||
lastPartText: string
|
||||
currentTool: string | null
|
||||
}
|
||||
|
||||
export function createEventState(): EventState {
|
||||
return {
|
||||
mainSessionIdle: false,
|
||||
lastOutput: "",
|
||||
lastPartText: "",
|
||||
currentTool: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +40,10 @@ export async function processEvents(
|
||||
|
||||
handleSessionIdle(ctx, payload, state)
|
||||
handleSessionStatus(ctx, payload, state)
|
||||
handleMessagePartUpdated(ctx, payload, state)
|
||||
handleMessageUpdated(ctx, payload, state)
|
||||
handleToolExecute(ctx, payload, state)
|
||||
handleToolResult(ctx, payload, state)
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +74,29 @@ function handleSessionStatus(
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessagePartUpdated(
|
||||
ctx: RunContext,
|
||||
payload: EventPayload,
|
||||
state: EventState
|
||||
): void {
|
||||
if (payload.type !== "message.part.updated") return
|
||||
|
||||
const props = payload.properties as MessagePartUpdatedProps | undefined
|
||||
if (props?.info?.sessionID !== ctx.sessionID) return
|
||||
if (props?.info?.role !== "assistant") return
|
||||
|
||||
const part = props.part
|
||||
if (!part) return
|
||||
|
||||
if (part.type === "text" && part.text) {
|
||||
const newText = part.text.slice(state.lastPartText.length)
|
||||
if (newText) {
|
||||
process.stdout.write(newText)
|
||||
}
|
||||
state.lastPartText = part.text
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessageUpdated(
|
||||
ctx: RunContext,
|
||||
payload: EventPayload,
|
||||
@@ -77,9 +111,66 @@ function handleMessageUpdated(
|
||||
const content = props.content
|
||||
if (!content || content === state.lastOutput) return
|
||||
|
||||
const newContent = content.slice(state.lastOutput.length)
|
||||
if (newContent) {
|
||||
process.stdout.write(newContent)
|
||||
if (state.lastPartText.length === 0) {
|
||||
const newContent = content.slice(state.lastOutput.length)
|
||||
if (newContent) {
|
||||
process.stdout.write(newContent)
|
||||
}
|
||||
}
|
||||
state.lastOutput = content
|
||||
}
|
||||
|
||||
function handleToolExecute(
|
||||
ctx: RunContext,
|
||||
payload: EventPayload,
|
||||
state: EventState
|
||||
): void {
|
||||
if (payload.type !== "tool.execute") return
|
||||
|
||||
const props = payload.properties as ToolExecuteProps | undefined
|
||||
if (props?.sessionID !== ctx.sessionID) return
|
||||
|
||||
const toolName = props?.name || "unknown"
|
||||
state.currentTool = toolName
|
||||
|
||||
let inputPreview = ""
|
||||
if (props?.input) {
|
||||
const input = props.input
|
||||
if (input.command) {
|
||||
inputPreview = ` ${pc.dim(String(input.command).slice(0, 60))}`
|
||||
} else if (input.pattern) {
|
||||
inputPreview = ` ${pc.dim(String(input.pattern).slice(0, 40))}`
|
||||
} else if (input.filePath) {
|
||||
inputPreview = ` ${pc.dim(String(input.filePath))}`
|
||||
} else if (input.query) {
|
||||
inputPreview = ` ${pc.dim(String(input.query).slice(0, 40))}`
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(`\n${pc.cyan("⚡")} ${pc.bold(toolName)}${inputPreview}\n`)
|
||||
}
|
||||
|
||||
function handleToolResult(
|
||||
ctx: RunContext,
|
||||
payload: EventPayload,
|
||||
state: EventState
|
||||
): void {
|
||||
if (payload.type !== "tool.result") return
|
||||
|
||||
const props = payload.properties as ToolResultProps | undefined
|
||||
if (props?.sessionID !== ctx.sessionID) return
|
||||
|
||||
const output = props?.output || ""
|
||||
const maxLen = 200
|
||||
const preview = output.length > maxLen
|
||||
? output.slice(0, maxLen) + "..."
|
||||
: output
|
||||
|
||||
if (preview.trim()) {
|
||||
const lines = preview.split("\n").slice(0, 3)
|
||||
process.stdout.write(pc.dim(` └─ ${lines.join("\n ")}\n`))
|
||||
}
|
||||
|
||||
state.currentTool = null
|
||||
state.lastPartText = ""
|
||||
}
|
||||
|
||||
@@ -47,3 +47,25 @@ export interface MessageUpdatedProps {
|
||||
info?: { sessionID?: string; role?: string }
|
||||
content?: string
|
||||
}
|
||||
|
||||
export interface MessagePartUpdatedProps {
|
||||
info?: { sessionID?: string; role?: string }
|
||||
part?: {
|
||||
type?: string
|
||||
text?: string
|
||||
name?: string
|
||||
input?: unknown
|
||||
}
|
||||
}
|
||||
|
||||
export interface ToolExecuteProps {
|
||||
sessionID?: string
|
||||
name?: string
|
||||
input?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface ToolResultProps {
|
||||
sessionID?: string
|
||||
name?: string
|
||||
output?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user