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:
YeonGyu-Kim
2025-12-25 19:05:00 +09:00
parent c804da43cf
commit 695f9e03fc
7 changed files with 137 additions and 20 deletions

View File

@@ -717,8 +717,8 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
有効時デフォルト、Sisyphus はオプションの特殊エージェントを備えた強力なオーケストレーターを提供します: 有効時デフォルト、Sisyphus はオプションの特殊エージェントを備えた強力なオーケストレーターを提供します:
- **Sisyphus**: プライマリオーケストレーターエージェント (Claude Opus 4.5) - **Sisyphus**: プライマリオーケストレーターエージェント (Claude Opus 4.5)
- **Builder-Sisyphus**: OhMyOpenCode 強化版のビルドエージェント(デフォルトで無効) - **Builder-Sisyphus**: OpenCode のデフォルトビルドエージェント(SDK 制限により名前変更、デフォルトで無効)
- **Planner-Sisyphus**: OhMyOpenCode 強化版のプランエージェント(デフォルトで有効) - **Planner-Sisyphus**: OpenCode のデフォルトプランエージェント(SDK 制限により名前変更、デフォルトで有効)
**設定オプション:** **設定オプション:**
@@ -779,8 +779,8 @@ Oh My OpenCode は以下の場所からフックを読み込んで実行しま
| オプション | デフォルト | 説明 | | オプション | デフォルト | 説明 |
| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `disabled` | `false` | `true` の場合、すべての Sisyphus オーケストレーションを無効化し、元の build/plan をプライマリとして復元します。 | | `disabled` | `false` | `true` の場合、すべての Sisyphus オーケストレーションを無効化し、元の build/plan をプライマリとして復元します。 |
| `builder_enabled` | `false` | `true` の場合、Builder-Sisyphus エージェントOhMyOpenCode 強化版ビルドモード)を有効化します。デフォルトの OpenCode ビルド体験を維持するため、デフォルトでは無効です。 | | `builder_enabled` | `false` | `true` の場合、Builder-Sisyphus エージェントを有効化しますOpenCode build と同じ、SDK 制限により名前変更)。デフォルトでは無効です。 |
| `planner_enabled` | `true` | `true` の場合、Planner-Sisyphus エージェントOhMyOpenCode 強化版プランモード)を有効化します。デフォルトで有効です。 | | `planner_enabled` | `true` | `true` の場合、Planner-Sisyphus エージェントを有効化しますOpenCode plan と同じ、SDK 制限により名前変更)。デフォルトで有効です。 |
| `replace_build` | `true` | `true` の場合、デフォルトのビルドエージェントをサブエージェントモードに降格させます。`false` に設定すると、Builder-Sisyphus とデフォルトのビルドの両方を利用できます。 | | `replace_build` | `true` | `true` の場合、デフォルトのビルドエージェントをサブエージェントモードに降格させます。`false` に設定すると、Builder-Sisyphus とデフォルトのビルドの両方を利用できます。 |
| `replace_plan` | `true` | `true` の場合、デフォルトのプランエージェントをサブエージェントモードに降格させます。`false` に設定すると、Planner-Sisyphus とデフォルトのプランの両方を利用できます。 | | `replace_plan` | `true` | `true` の場合、デフォルトのプランエージェントをサブエージェントモードに降格させます。`false` に設定すると、Planner-Sisyphus とデフォルトのプランの両方を利用できます。 |

View File

@@ -711,8 +711,8 @@ Schema 자동 완성이 지원됩니다:
활성화 시 (기본값), Sisyphus는 옵션으로 선택 가능한 특화 에이전트들과 함께 강력한 오케스트레이터를 제공합니다: 활성화 시 (기본값), Sisyphus는 옵션으로 선택 가능한 특화 에이전트들과 함께 강력한 오케스트레이터를 제공합니다:
- **Sisyphus**: Primary 오케스트레이터 에이전트 (Claude Opus 4.5) - **Sisyphus**: Primary 오케스트레이터 에이전트 (Claude Opus 4.5)
- **Builder-Sisyphus**: OhMyOpenCode 강화 버전 빌드 에이전트 (기본적으로 비활성화) - **Builder-Sisyphus**: OpenCode 기본 빌드 에이전트 (SDK 제한으로 이름만 변경, 기본적으로 비활성화)
- **Planner-Sisyphus**: OhMyOpenCode 강화 버전 플랜 에이전트 (기본적으로 활성화) - **Planner-Sisyphus**: OpenCode 기본 플랜 에이전트 (SDK 제한으로 이름만 변경, 기본적으로 활성화)
**설정 옵션:** **설정 옵션:**
@@ -773,8 +773,8 @@ Schema 자동 완성이 지원됩니다:
| 옵션 | 기본값 | 설명 | | 옵션 | 기본값 | 설명 |
| ------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `disabled` | `false` | `true`면 모든 Sisyphus 오케스트레이션을 비활성화하고 원래 build/plan을 primary로 복원합니다. | | `disabled` | `false` | `true`면 모든 Sisyphus 오케스트레이션을 비활성화하고 원래 build/plan을 primary로 복원합니다. |
| `builder_enabled` | `false` | `true`면 Builder-Sisyphus 에이전트 (OhMyOpenCode 강화 빌드 모드)를 활성화합니다. 기본 OpenCode 빌드 경험을 보존하기 위해 기본적으로 비활성화되어 있습니다. | | `builder_enabled` | `false` | `true`면 Builder-Sisyphus 에이전트를 활성화합니다 (OpenCode build와 동일, SDK 제한으로 이름만 변경). 기본적으로 비활성화되어 있습니다. |
| `planner_enabled` | `true` | `true`면 Planner-Sisyphus 에이전트 (OhMyOpenCode 강화 플랜 모드)를 활성화합니다. 기본적으로 활성화되어 있습니다. | | `planner_enabled` | `true` | `true`면 Planner-Sisyphus 에이전트를 활성화합니다 (OpenCode plan과 동일, SDK 제한으로 이름만 변경). 기본적으로 활성화되어 있습니다. |
| `replace_build` | `true` | `true`면 기본 빌드 에이전트를 subagent 모드로 강등시킵니다. `false`로 설정하면 Builder-Sisyphus와 기본 빌드를 모두 사용할 수 있습니다. | | `replace_build` | `true` | `true`면 기본 빌드 에이전트를 subagent 모드로 강등시킵니다. `false`로 설정하면 Builder-Sisyphus와 기본 빌드를 모두 사용할 수 있습니다. |
| `replace_plan` | `true` | `true`면 기본 플랜 에이전트를 subagent 모드로 강등시킵니다. `false`로 설정하면 Planner-Sisyphus와 기본 플랜을 모두 사용할 수 있습니다. | | `replace_plan` | `true` | `true`면 기본 플랜 에이전트를 subagent 모드로 강등시킵니다. `false`로 설정하면 Planner-Sisyphus와 기본 플랜을 모두 사용할 수 있습니다. |

View File

@@ -783,8 +783,8 @@ Available agents: `oracle`, `librarian`, `explore`, `frontend-ui-ux-engineer`, `
When enabled (default), Sisyphus provides a powerful orchestrator with optional specialized agents: When enabled (default), Sisyphus provides a powerful orchestrator with optional specialized agents:
- **Sisyphus**: Primary orchestrator agent (Claude Opus 4.5) - **Sisyphus**: Primary orchestrator agent (Claude Opus 4.5)
- **Builder-Sisyphus**: Optional build agent with OhMyOpenCode enhancements (disabled by default) - **Builder-Sisyphus**: OpenCode's default build agent, renamed due to SDK limitations (disabled by default)
- **Planner-Sisyphus**: Plan agent with OhMyOpenCode enhancements (enabled by default) - **Planner-Sisyphus**: OpenCode's default plan agent, renamed due to SDK limitations (enabled by default)
**Configuration Options:** **Configuration Options:**
@@ -845,8 +845,8 @@ You can also customize Sisyphus agents like other agents:
| Option | Default | Description | | Option | Default | Description |
| ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `disabled` | `false` | When `true`, disables all Sisyphus orchestration and restores original build/plan as primary. | | `disabled` | `false` | When `true`, disables all Sisyphus orchestration and restores original build/plan as primary. |
| `builder_enabled` | `false` | When `true`, enables Builder-Sisyphus agent (OhMyOpenCode enhanced build mode). Disabled by default to preserve default OpenCode build experience. | | `builder_enabled` | `false` | When `true`, enables Builder-Sisyphus agent (same as OpenCode build, renamed due to SDK limitations). Disabled by default. |
| `planner_enabled` | `true` | When `true`, enables Planner-Sisyphus agent (OhMyOpenCode enhanced plan mode). Enabled by default. | | `planner_enabled` | `true` | When `true`, enables Planner-Sisyphus agent (same as OpenCode plan, renamed due to SDK limitations). Enabled by default. |
| `replace_build` | `true` | When `true`, demotes default build agent to subagent mode. Set to `false` to keep both Builder-Sisyphus and default build available. | | `replace_build` | `true` | When `true`, demotes default build agent to subagent mode. Set to `false` to keep both Builder-Sisyphus and default build available. |
| `replace_plan` | `true` | When `true`, demotes default plan agent to subagent mode. Set to `false` to keep both Planner-Sisyphus and default plan available. | | `replace_plan` | `true` | When `true`, demotes default plan agent to subagent mode. Set to `false` to keep both Planner-Sisyphus and default plan available. |

View File

@@ -717,8 +717,8 @@ Agent 爽了,你自然也爽。但我还想直接让你爽。
默认开启。Sisyphus 提供一个强力的编排器,带可选的专门 Agent 默认开启。Sisyphus 提供一个强力的编排器,带可选的专门 Agent
- **Sisyphus**:主编排 AgentClaude Opus 4.5 - **Sisyphus**:主编排 AgentClaude Opus 4.5
- **Builder-Sisyphus**OhMyOpenCode 增强版构建 Agent默认禁用 - **Builder-Sisyphus**OpenCode 默认构建 Agent因 SDK 限制仅改名,默认禁用)
- **Planner-Sisyphus**OhMyOpenCode 增强版计划 Agent默认启用 - **Planner-Sisyphus**OpenCode 默认计划 Agent因 SDK 限制仅改名,默认启用)
**配置选项:** **配置选项:**
@@ -779,8 +779,8 @@ Sisyphus Agent 也能自定义:
| 选项 | 默认值 | 说明 | | 选项 | 默认值 | 说明 |
| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `disabled` | `false` | 设为 `true` 就禁用所有 Sisyphus 编排,恢复原来的 build/plan。 | | `disabled` | `false` | 设为 `true` 就禁用所有 Sisyphus 编排,恢复原来的 build/plan。 |
| `builder_enabled` | `false` | 设为 `true` 就启用 Builder-Sisyphus AgentOhMyOpenCode 增强构建模式)。为了保留默认 OpenCode 构建体验,默认禁用。 | | `builder_enabled` | `false` | 设为 `true` 就启用 Builder-Sisyphus AgentOpenCode build 相同,因 SDK 限制仅改名)。默认禁用。 |
| `planner_enabled` | `true` | 设为 `true` 就启用 Planner-Sisyphus AgentOhMyOpenCode 增强计划模式)。默认启用。 | | `planner_enabled` | `true` | 设为 `true` 就启用 Planner-Sisyphus AgentOpenCode plan 相同,因 SDK 限制仅改名)。默认启用。 |
| `replace_build` | `true` | 设为 `true` 就把默认构建 Agent 降级为子 Agent 模式。设为 `false` 可以同时保留 Builder-Sisyphus 和默认构建。 | | `replace_build` | `true` | 设为 `true` 就把默认构建 Agent 降级为子 Agent 模式。设为 `false` 可以同时保留 Builder-Sisyphus 和默认构建。 |
| `replace_plan` | `true` | 设为 `true` 就把默认计划 Agent 降级为子 Agent 模式。设为 `false` 可以同时保留 Planner-Sisyphus 和默认计划。 | | `replace_plan` | `true` | 设为 `true` 就把默认计划 Agent 降级为子 Agent 模式。设为 `false` 可以同时保留 Planner-Sisyphus 和默认计划。 |

View File

@@ -16,13 +16,15 @@ async function* toAsyncIterable<T>(items: T[]): AsyncIterable<T> {
} }
describe("createEventState", () => { describe("createEventState", () => {
it("creates initial state with mainSessionIdle false and empty lastOutput", () => { it("creates initial state with correct defaults", () => {
// #given / #when // #given / #when
const state = createEventState() const state = createEventState()
// #then // #then
expect(state.mainSessionIdle).toBe(false) expect(state.mainSessionIdle).toBe(false)
expect(state.lastOutput).toBe("") expect(state.lastOutput).toBe("")
expect(state.lastPartText).toBe("")
expect(state.currentTool).toBe(null)
}) })
}) })
@@ -73,6 +75,8 @@ describe("event handling", () => {
const state: EventState = { const state: EventState = {
mainSessionIdle: true, mainSessionIdle: true,
lastOutput: "", lastOutput: "",
lastPartText: "",
currentTool: null,
} }
const payload: EventPayload = { const payload: EventPayload = {

View File

@@ -1,20 +1,28 @@
import pc from "picocolors"
import type { import type {
RunContext, RunContext,
EventPayload, EventPayload,
SessionIdleProps, SessionIdleProps,
SessionStatusProps, SessionStatusProps,
MessageUpdatedProps, MessageUpdatedProps,
MessagePartUpdatedProps,
ToolExecuteProps,
ToolResultProps,
} from "./types" } from "./types"
export interface EventState { export interface EventState {
mainSessionIdle: boolean mainSessionIdle: boolean
lastOutput: string lastOutput: string
lastPartText: string
currentTool: string | null
} }
export function createEventState(): EventState { export function createEventState(): EventState {
return { return {
mainSessionIdle: false, mainSessionIdle: false,
lastOutput: "", lastOutput: "",
lastPartText: "",
currentTool: null,
} }
} }
@@ -32,7 +40,10 @@ export async function processEvents(
handleSessionIdle(ctx, payload, state) handleSessionIdle(ctx, payload, state)
handleSessionStatus(ctx, payload, state) handleSessionStatus(ctx, payload, state)
handleMessagePartUpdated(ctx, payload, state)
handleMessageUpdated(ctx, payload, state) handleMessageUpdated(ctx, payload, state)
handleToolExecute(ctx, payload, state)
handleToolResult(ctx, payload, state)
} catch {} } 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( function handleMessageUpdated(
ctx: RunContext, ctx: RunContext,
payload: EventPayload, payload: EventPayload,
@@ -77,9 +111,66 @@ function handleMessageUpdated(
const content = props.content const content = props.content
if (!content || content === state.lastOutput) return if (!content || content === state.lastOutput) return
const newContent = content.slice(state.lastOutput.length) if (state.lastPartText.length === 0) {
if (newContent) { const newContent = content.slice(state.lastOutput.length)
process.stdout.write(newContent) if (newContent) {
process.stdout.write(newContent)
}
} }
state.lastOutput = content 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 = ""
}

View File

@@ -47,3 +47,25 @@ export interface MessageUpdatedProps {
info?: { sessionID?: string; role?: string } info?: { sessionID?: string; role?: string }
content?: 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
}