refactor(cli): split doctor/model-resolution and run/events into focused modules
Doctor checks: - model-resolution-cache.ts, model-resolution-config.ts - model-resolution-details.ts, model-resolution-effective-model.ts - model-resolution-types.ts, model-resolution-variant.ts Run events: - event-formatting.ts, event-handlers.ts - event-state.ts, event-stream-processor.ts
This commit is contained in:
@@ -15,6 +15,12 @@ export * from "./opencode"
|
||||
export * from "./plugin"
|
||||
export * from "./config"
|
||||
export * from "./model-resolution"
|
||||
export * from "./model-resolution-types"
|
||||
export * from "./model-resolution-cache"
|
||||
export * from "./model-resolution-config"
|
||||
export * from "./model-resolution-effective-model"
|
||||
export * from "./model-resolution-variant"
|
||||
export * from "./model-resolution-details"
|
||||
export * from "./auth"
|
||||
export * from "./dependencies"
|
||||
export * from "./gh"
|
||||
|
||||
36
src/cli/doctor/checks/model-resolution-cache.ts
Normal file
36
src/cli/doctor/checks/model-resolution-cache.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import type { AvailableModelsInfo } from "./model-resolution-types"
|
||||
|
||||
function getOpenCodeCacheDir(): string {
|
||||
const xdgCache = process.env.XDG_CACHE_HOME
|
||||
if (xdgCache) return join(xdgCache, "opencode")
|
||||
return join(homedir(), ".cache", "opencode")
|
||||
}
|
||||
|
||||
export function loadAvailableModelsFromCache(): AvailableModelsInfo {
|
||||
const cacheFile = join(getOpenCodeCacheDir(), "models.json")
|
||||
|
||||
if (!existsSync(cacheFile)) {
|
||||
return { providers: [], modelCount: 0, cacheExists: false }
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(cacheFile, "utf-8")
|
||||
const data = JSON.parse(content) as Record<string, { models?: Record<string, unknown> }>
|
||||
|
||||
const providers = Object.keys(data)
|
||||
let modelCount = 0
|
||||
for (const providerId of providers) {
|
||||
const models = data[providerId]?.models
|
||||
if (models && typeof models === "object") {
|
||||
modelCount += Object.keys(models).length
|
||||
}
|
||||
}
|
||||
|
||||
return { providers, modelCount, cacheExists: true }
|
||||
} catch {
|
||||
return { providers: [], modelCount: 0, cacheExists: false }
|
||||
}
|
||||
}
|
||||
34
src/cli/doctor/checks/model-resolution-config.ts
Normal file
34
src/cli/doctor/checks/model-resolution-config.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { readFileSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { detectConfigFile, parseJsonc } from "../../../shared"
|
||||
import type { OmoConfig } from "./model-resolution-types"
|
||||
|
||||
const PACKAGE_NAME = "oh-my-opencode"
|
||||
const USER_CONFIG_DIR = join(homedir(), ".config", "opencode")
|
||||
const USER_CONFIG_BASE = join(USER_CONFIG_DIR, PACKAGE_NAME)
|
||||
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
|
||||
|
||||
export function loadOmoConfig(): OmoConfig | null {
|
||||
const projectDetected = detectConfigFile(PROJECT_CONFIG_BASE)
|
||||
if (projectDetected.format !== "none") {
|
||||
try {
|
||||
const content = readFileSync(projectDetected.path, "utf-8")
|
||||
return parseJsonc<OmoConfig>(content)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const userDetected = detectConfigFile(USER_CONFIG_BASE)
|
||||
if (userDetected.format !== "none") {
|
||||
try {
|
||||
const content = readFileSync(userDetected.path, "utf-8")
|
||||
return parseJsonc<OmoConfig>(content)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
52
src/cli/doctor/checks/model-resolution-details.ts
Normal file
52
src/cli/doctor/checks/model-resolution-details.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { AvailableModelsInfo, ModelResolutionInfo, OmoConfig } from "./model-resolution-types"
|
||||
import { formatModelWithVariant, getCategoryEffectiveVariant, getEffectiveVariant } from "./model-resolution-variant"
|
||||
|
||||
export function buildModelResolutionDetails(options: {
|
||||
info: ModelResolutionInfo
|
||||
available: AvailableModelsInfo
|
||||
config: OmoConfig
|
||||
}): string[] {
|
||||
const details: string[] = []
|
||||
|
||||
details.push("═══ Available Models (from cache) ═══")
|
||||
details.push("")
|
||||
if (options.available.cacheExists) {
|
||||
details.push(` Providers in cache: ${options.available.providers.length}`)
|
||||
details.push(
|
||||
` Sample: ${options.available.providers.slice(0, 6).join(", ")}${options.available.providers.length > 6 ? "..." : ""}`
|
||||
)
|
||||
details.push(` Total models: ${options.available.modelCount}`)
|
||||
details.push(` Cache: ~/.cache/opencode/models.json`)
|
||||
details.push(` ℹ Runtime: only connected providers used`)
|
||||
details.push(` Refresh: opencode models --refresh`)
|
||||
} else {
|
||||
details.push(" ⚠ Cache not found. Run 'opencode' to populate.")
|
||||
}
|
||||
details.push("")
|
||||
|
||||
details.push("═══ Configured Models ═══")
|
||||
details.push("")
|
||||
details.push("Agents:")
|
||||
for (const agent of options.info.agents) {
|
||||
const marker = agent.userOverride ? "●" : "○"
|
||||
const display = formatModelWithVariant(
|
||||
agent.effectiveModel,
|
||||
getEffectiveVariant(agent.name, agent.requirement, options.config)
|
||||
)
|
||||
details.push(` ${marker} ${agent.name}: ${display}`)
|
||||
}
|
||||
details.push("")
|
||||
details.push("Categories:")
|
||||
for (const category of options.info.categories) {
|
||||
const marker = category.userOverride ? "●" : "○"
|
||||
const display = formatModelWithVariant(
|
||||
category.effectiveModel,
|
||||
getCategoryEffectiveVariant(category.name, category.requirement, options.config)
|
||||
)
|
||||
details.push(` ${marker} ${category.name}: ${display}`)
|
||||
}
|
||||
details.push("")
|
||||
details.push("● = user override, ○ = provider fallback")
|
||||
|
||||
return details
|
||||
}
|
||||
27
src/cli/doctor/checks/model-resolution-effective-model.ts
Normal file
27
src/cli/doctor/checks/model-resolution-effective-model.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { ModelRequirement } from "../../../shared/model-requirements"
|
||||
|
||||
function formatProviderChain(providers: string[]): string {
|
||||
return providers.join(" → ")
|
||||
}
|
||||
|
||||
export function getEffectiveModel(requirement: ModelRequirement, userOverride?: string): string {
|
||||
if (userOverride) {
|
||||
return userOverride
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
if (!firstEntry) {
|
||||
return "unknown"
|
||||
}
|
||||
return `${firstEntry.providers[0]}/${firstEntry.model}`
|
||||
}
|
||||
|
||||
export function buildEffectiveResolution(requirement: ModelRequirement, userOverride?: string): string {
|
||||
if (userOverride) {
|
||||
return `User override: ${userOverride}`
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
if (!firstEntry) {
|
||||
return "No fallback chain defined"
|
||||
}
|
||||
return `Provider fallback: ${formatProviderChain(firstEntry.providers)} → ${firstEntry.model}`
|
||||
}
|
||||
35
src/cli/doctor/checks/model-resolution-types.ts
Normal file
35
src/cli/doctor/checks/model-resolution-types.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { ModelRequirement } from "../../../shared/model-requirements"
|
||||
|
||||
export interface AgentResolutionInfo {
|
||||
name: string
|
||||
requirement: ModelRequirement
|
||||
userOverride?: string
|
||||
userVariant?: string
|
||||
effectiveModel: string
|
||||
effectiveResolution: string
|
||||
}
|
||||
|
||||
export interface CategoryResolutionInfo {
|
||||
name: string
|
||||
requirement: ModelRequirement
|
||||
userOverride?: string
|
||||
userVariant?: string
|
||||
effectiveModel: string
|
||||
effectiveResolution: string
|
||||
}
|
||||
|
||||
export interface ModelResolutionInfo {
|
||||
agents: AgentResolutionInfo[]
|
||||
categories: CategoryResolutionInfo[]
|
||||
}
|
||||
|
||||
export interface OmoConfig {
|
||||
agents?: Record<string, { model?: string; variant?: string; category?: string }>
|
||||
categories?: Record<string, { model?: string; variant?: string }>
|
||||
}
|
||||
|
||||
export interface AvailableModelsInfo {
|
||||
providers: string[]
|
||||
modelCount: number
|
||||
cacheExists: boolean
|
||||
}
|
||||
55
src/cli/doctor/checks/model-resolution-variant.ts
Normal file
55
src/cli/doctor/checks/model-resolution-variant.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { ModelRequirement } from "../../../shared/model-requirements"
|
||||
import type { OmoConfig } from "./model-resolution-types"
|
||||
|
||||
export function formatModelWithVariant(model: string, variant?: string): string {
|
||||
return variant ? `${model} (${variant})` : model
|
||||
}
|
||||
|
||||
function getAgentOverride(
|
||||
agentName: string,
|
||||
config: OmoConfig
|
||||
): { variant?: string; category?: string } | undefined {
|
||||
const agentOverrides = config.agents
|
||||
if (!agentOverrides) return undefined
|
||||
|
||||
return (
|
||||
agentOverrides[agentName] ??
|
||||
Object.entries(agentOverrides).find(([key]) => key.toLowerCase() === agentName.toLowerCase())?.[1]
|
||||
)
|
||||
}
|
||||
|
||||
export function getEffectiveVariant(
|
||||
agentName: string,
|
||||
requirement: ModelRequirement,
|
||||
config: OmoConfig
|
||||
): string | undefined {
|
||||
const agentOverride = getAgentOverride(agentName, config)
|
||||
|
||||
if (agentOverride?.variant) {
|
||||
return agentOverride.variant
|
||||
}
|
||||
|
||||
const categoryName = agentOverride?.category
|
||||
if (categoryName) {
|
||||
const categoryVariant = config.categories?.[categoryName]?.variant
|
||||
if (categoryVariant) {
|
||||
return categoryVariant
|
||||
}
|
||||
}
|
||||
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
return firstEntry?.variant ?? requirement.variant
|
||||
}
|
||||
|
||||
export function getCategoryEffectiveVariant(
|
||||
categoryName: string,
|
||||
requirement: ModelRequirement,
|
||||
config: OmoConfig
|
||||
): string | undefined {
|
||||
const categoryVariant = config.categories?.[categoryName]?.variant
|
||||
if (categoryVariant) {
|
||||
return categoryVariant
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
return firstEntry?.variant ?? requirement.variant
|
||||
}
|
||||
@@ -1,132 +1,14 @@
|
||||
import { readFileSync, existsSync } from "node:fs"
|
||||
import type { CheckResult, CheckDefinition } from "../types"
|
||||
import { CHECK_IDS, CHECK_NAMES } from "../constants"
|
||||
import { parseJsonc, detectConfigFile } from "../../../shared"
|
||||
import {
|
||||
AGENT_MODEL_REQUIREMENTS,
|
||||
CATEGORY_MODEL_REQUIREMENTS,
|
||||
type ModelRequirement,
|
||||
} from "../../../shared/model-requirements"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
function getOpenCodeCacheDir(): string {
|
||||
const xdgCache = process.env.XDG_CACHE_HOME
|
||||
if (xdgCache) return join(xdgCache, "opencode")
|
||||
return join(homedir(), ".cache", "opencode")
|
||||
}
|
||||
|
||||
function loadAvailableModels(): { providers: string[]; modelCount: number; cacheExists: boolean } {
|
||||
const cacheFile = join(getOpenCodeCacheDir(), "models.json")
|
||||
|
||||
if (!existsSync(cacheFile)) {
|
||||
return { providers: [], modelCount: 0, cacheExists: false }
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(cacheFile, "utf-8")
|
||||
const data = JSON.parse(content) as Record<string, { models?: Record<string, unknown> }>
|
||||
|
||||
const providers = Object.keys(data)
|
||||
let modelCount = 0
|
||||
for (const providerId of providers) {
|
||||
const models = data[providerId]?.models
|
||||
if (models && typeof models === "object") {
|
||||
modelCount += Object.keys(models).length
|
||||
}
|
||||
}
|
||||
|
||||
return { providers, modelCount, cacheExists: true }
|
||||
} catch {
|
||||
return { providers: [], modelCount: 0, cacheExists: false }
|
||||
}
|
||||
}
|
||||
|
||||
const PACKAGE_NAME = "oh-my-opencode"
|
||||
const USER_CONFIG_DIR = join(homedir(), ".config", "opencode")
|
||||
const USER_CONFIG_BASE = join(USER_CONFIG_DIR, PACKAGE_NAME)
|
||||
const PROJECT_CONFIG_BASE = join(process.cwd(), ".opencode", PACKAGE_NAME)
|
||||
|
||||
export interface AgentResolutionInfo {
|
||||
name: string
|
||||
requirement: ModelRequirement
|
||||
userOverride?: string
|
||||
userVariant?: string
|
||||
effectiveModel: string
|
||||
effectiveResolution: string
|
||||
}
|
||||
|
||||
export interface CategoryResolutionInfo {
|
||||
name: string
|
||||
requirement: ModelRequirement
|
||||
userOverride?: string
|
||||
userVariant?: string
|
||||
effectiveModel: string
|
||||
effectiveResolution: string
|
||||
}
|
||||
|
||||
export interface ModelResolutionInfo {
|
||||
agents: AgentResolutionInfo[]
|
||||
categories: CategoryResolutionInfo[]
|
||||
}
|
||||
|
||||
interface OmoConfig {
|
||||
agents?: Record<string, { model?: string; variant?: string; category?: string }>
|
||||
categories?: Record<string, { model?: string; variant?: string }>
|
||||
}
|
||||
|
||||
function loadConfig(): OmoConfig | null {
|
||||
const projectDetected = detectConfigFile(PROJECT_CONFIG_BASE)
|
||||
if (projectDetected.format !== "none") {
|
||||
try {
|
||||
const content = readFileSync(projectDetected.path, "utf-8")
|
||||
return parseJsonc<OmoConfig>(content)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const userDetected = detectConfigFile(USER_CONFIG_BASE)
|
||||
if (userDetected.format !== "none") {
|
||||
try {
|
||||
const content = readFileSync(userDetected.path, "utf-8")
|
||||
return parseJsonc<OmoConfig>(content)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function formatProviderChain(providers: string[]): string {
|
||||
return providers.join(" → ")
|
||||
}
|
||||
|
||||
function getEffectiveModel(requirement: ModelRequirement, userOverride?: string): string {
|
||||
if (userOverride) {
|
||||
return userOverride
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
if (!firstEntry) {
|
||||
return "unknown"
|
||||
}
|
||||
return `${firstEntry.providers[0]}/${firstEntry.model}`
|
||||
}
|
||||
|
||||
function buildEffectiveResolution(
|
||||
requirement: ModelRequirement,
|
||||
userOverride?: string,
|
||||
): string {
|
||||
if (userOverride) {
|
||||
return `User override: ${userOverride}`
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
if (!firstEntry) {
|
||||
return "No fallback chain defined"
|
||||
}
|
||||
return `Provider fallback: ${formatProviderChain(firstEntry.providers)} → ${firstEntry.model}`
|
||||
}
|
||||
import type { OmoConfig, ModelResolutionInfo, AgentResolutionInfo, CategoryResolutionInfo } from "./model-resolution-types"
|
||||
import { loadAvailableModelsFromCache } from "./model-resolution-cache"
|
||||
import { loadOmoConfig } from "./model-resolution-config"
|
||||
import { buildEffectiveResolution, getEffectiveModel } from "./model-resolution-effective-model"
|
||||
import { buildModelResolutionDetails } from "./model-resolution-details"
|
||||
|
||||
export function getModelResolutionInfo(): ModelResolutionInfo {
|
||||
const agents: AgentResolutionInfo[] = Object.entries(AGENT_MODEL_REQUIREMENTS).map(
|
||||
@@ -184,116 +66,10 @@ export function getModelResolutionInfoWithOverrides(config: OmoConfig): ModelRes
|
||||
return { agents, categories }
|
||||
}
|
||||
|
||||
function formatModelWithVariant(model: string, variant?: string): string {
|
||||
return variant ? `${model} (${variant})` : model
|
||||
}
|
||||
|
||||
function getAgentOverride(
|
||||
agentName: string,
|
||||
config: OmoConfig,
|
||||
): { variant?: string; category?: string } | undefined {
|
||||
const agentOverrides = config.agents
|
||||
if (!agentOverrides) return undefined
|
||||
|
||||
// Direct lookup first, then case-insensitive lookup (matches agent-variant.ts)
|
||||
return (
|
||||
agentOverrides[agentName] ??
|
||||
Object.entries(agentOverrides).find(
|
||||
([key]) => key.toLowerCase() === agentName.toLowerCase()
|
||||
)?.[1]
|
||||
)
|
||||
}
|
||||
|
||||
function getEffectiveVariant(
|
||||
name: string,
|
||||
requirement: ModelRequirement,
|
||||
config: OmoConfig,
|
||||
): string | undefined {
|
||||
const agentOverride = getAgentOverride(name, config)
|
||||
|
||||
// Priority 1: Agent's direct variant override
|
||||
if (agentOverride?.variant) {
|
||||
return agentOverride.variant
|
||||
}
|
||||
|
||||
// Priority 2: Agent's category -> category's variant (matches agent-variant.ts)
|
||||
const categoryName = agentOverride?.category
|
||||
if (categoryName) {
|
||||
const categoryVariant = config.categories?.[categoryName]?.variant
|
||||
if (categoryVariant) {
|
||||
return categoryVariant
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Fall back to requirement's fallback chain
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
return firstEntry?.variant ?? requirement.variant
|
||||
}
|
||||
|
||||
interface AvailableModelsInfo {
|
||||
providers: string[]
|
||||
modelCount: number
|
||||
cacheExists: boolean
|
||||
}
|
||||
|
||||
function getCategoryEffectiveVariant(
|
||||
categoryName: string,
|
||||
requirement: ModelRequirement,
|
||||
config: OmoConfig,
|
||||
): string | undefined {
|
||||
const categoryVariant = config.categories?.[categoryName]?.variant
|
||||
if (categoryVariant) {
|
||||
return categoryVariant
|
||||
}
|
||||
const firstEntry = requirement.fallbackChain[0]
|
||||
return firstEntry?.variant ?? requirement.variant
|
||||
}
|
||||
|
||||
function buildDetailsArray(info: ModelResolutionInfo, available: AvailableModelsInfo, config: OmoConfig): string[] {
|
||||
const details: string[] = []
|
||||
|
||||
details.push("═══ Available Models (from cache) ═══")
|
||||
details.push("")
|
||||
if (available.cacheExists) {
|
||||
details.push(` Providers in cache: ${available.providers.length}`)
|
||||
details.push(` Sample: ${available.providers.slice(0, 6).join(", ")}${available.providers.length > 6 ? "..." : ""}`)
|
||||
details.push(` Total models: ${available.modelCount}`)
|
||||
details.push(` Cache: ~/.cache/opencode/models.json`)
|
||||
details.push(` ℹ Runtime: only connected providers used`)
|
||||
details.push(` Refresh: opencode models --refresh`)
|
||||
} else {
|
||||
details.push(" ⚠ Cache not found. Run 'opencode' to populate.")
|
||||
}
|
||||
details.push("")
|
||||
|
||||
details.push("═══ Configured Models ═══")
|
||||
details.push("")
|
||||
details.push("Agents:")
|
||||
for (const agent of info.agents) {
|
||||
const marker = agent.userOverride ? "●" : "○"
|
||||
const display = formatModelWithVariant(agent.effectiveModel, getEffectiveVariant(agent.name, agent.requirement, config))
|
||||
details.push(` ${marker} ${agent.name}: ${display}`)
|
||||
}
|
||||
details.push("")
|
||||
details.push("Categories:")
|
||||
for (const category of info.categories) {
|
||||
const marker = category.userOverride ? "●" : "○"
|
||||
const display = formatModelWithVariant(
|
||||
category.effectiveModel,
|
||||
getCategoryEffectiveVariant(category.name, category.requirement, config)
|
||||
)
|
||||
details.push(` ${marker} ${category.name}: ${display}`)
|
||||
}
|
||||
details.push("")
|
||||
details.push("● = user override, ○ = provider fallback")
|
||||
|
||||
return details
|
||||
}
|
||||
|
||||
export async function checkModelResolution(): Promise<CheckResult> {
|
||||
const config = loadConfig() ?? {}
|
||||
const config = loadOmoConfig() ?? {}
|
||||
const info = getModelResolutionInfoWithOverrides(config)
|
||||
const available = loadAvailableModels()
|
||||
const available = loadAvailableModelsFromCache()
|
||||
|
||||
const agentCount = info.agents.length
|
||||
const categoryCount = info.categories.length
|
||||
@@ -308,7 +84,7 @@ export async function checkModelResolution(): Promise<CheckResult> {
|
||||
name: CHECK_NAMES[CHECK_IDS.MODEL_RESOLUTION],
|
||||
status: available.cacheExists ? "pass" : "warn",
|
||||
message: `${agentCount} agents, ${categoryCount} categories${overrideNote}${cacheNote}`,
|
||||
details: buildDetailsArray(info, available, config),
|
||||
details: buildModelResolutionDetails({ info, available, config }),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
130
src/cli/run/event-formatting.ts
Normal file
130
src/cli/run/event-formatting.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import pc from "picocolors"
|
||||
import type {
|
||||
RunContext,
|
||||
EventPayload,
|
||||
MessageUpdatedProps,
|
||||
MessagePartUpdatedProps,
|
||||
ToolExecuteProps,
|
||||
ToolResultProps,
|
||||
SessionErrorProps,
|
||||
} from "./types"
|
||||
|
||||
export function serializeError(error: unknown): string {
|
||||
if (!error) return "Unknown error"
|
||||
|
||||
if (error instanceof Error) {
|
||||
const parts = [error.message]
|
||||
if (error.cause) {
|
||||
parts.push(`Cause: ${serializeError(error.cause)}`)
|
||||
}
|
||||
return parts.join(" | ")
|
||||
}
|
||||
|
||||
if (typeof error === "string") {
|
||||
return error
|
||||
}
|
||||
|
||||
if (typeof error === "object") {
|
||||
const obj = error as Record<string, unknown>
|
||||
|
||||
const messagePaths = [
|
||||
obj.message,
|
||||
obj.error,
|
||||
(obj.data as Record<string, unknown>)?.message,
|
||||
(obj.data as Record<string, unknown>)?.error,
|
||||
(obj.error as Record<string, unknown>)?.message,
|
||||
]
|
||||
|
||||
for (const msg of messagePaths) {
|
||||
if (typeof msg === "string" && msg.length > 0) {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.stringify(error, null, 2)
|
||||
if (json !== "{}") {
|
||||
return json
|
||||
}
|
||||
} catch (_) {
|
||||
void _
|
||||
}
|
||||
}
|
||||
|
||||
return String(error)
|
||||
}
|
||||
|
||||
function getSessionTag(ctx: RunContext, payload: EventPayload): string {
|
||||
const props = payload.properties as Record<string, unknown> | undefined
|
||||
const info = props?.info as Record<string, unknown> | undefined
|
||||
const sessionID = props?.sessionID ?? info?.sessionID
|
||||
const isMainSession = sessionID === ctx.sessionID
|
||||
if (isMainSession) return pc.green("[MAIN]")
|
||||
if (sessionID) return pc.yellow(`[${String(sessionID).slice(0, 8)}]`)
|
||||
return pc.dim("[system]")
|
||||
}
|
||||
|
||||
export function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
|
||||
const sessionTag = getSessionTag(ctx, payload)
|
||||
const props = payload.properties as Record<string, unknown> | undefined
|
||||
|
||||
switch (payload.type) {
|
||||
case "session.idle":
|
||||
case "session.status": {
|
||||
const status = (props?.status as { type?: string })?.type ?? "idle"
|
||||
console.error(pc.dim(`${sessionTag} ${payload.type}: ${status}`))
|
||||
break
|
||||
}
|
||||
|
||||
case "message.part.updated": {
|
||||
const partProps = props as MessagePartUpdatedProps | undefined
|
||||
const part = partProps?.part
|
||||
if (part?.type === "tool-invocation") {
|
||||
const toolPart = part as { toolName?: string; state?: string }
|
||||
console.error(pc.dim(`${sessionTag} message.part (tool): ${toolPart.toolName} [${toolPart.state}]`))
|
||||
} else if (part?.type === "text" && part.text) {
|
||||
const preview = part.text.slice(0, 80).replace(/\n/g, "\\n")
|
||||
console.error(pc.dim(`${sessionTag} message.part (text): "${preview}${part.text.length > 80 ? "..." : ""}"`))
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "message.updated": {
|
||||
const msgProps = props as MessageUpdatedProps | undefined
|
||||
const role = msgProps?.info?.role ?? "unknown"
|
||||
const model = msgProps?.info?.modelID
|
||||
const agent = msgProps?.info?.agent
|
||||
const details = [role, agent, model].filter(Boolean).join(", ")
|
||||
console.error(pc.dim(`${sessionTag} message.updated (${details})`))
|
||||
break
|
||||
}
|
||||
|
||||
case "tool.execute": {
|
||||
const toolProps = props as ToolExecuteProps | undefined
|
||||
const toolName = toolProps?.name ?? "unknown"
|
||||
const input = toolProps?.input ?? {}
|
||||
const inputStr = JSON.stringify(input).slice(0, 150)
|
||||
console.error(pc.cyan(`${sessionTag} TOOL.EXECUTE: ${pc.bold(toolName)}`))
|
||||
console.error(pc.dim(` input: ${inputStr}${inputStr.length >= 150 ? "..." : ""}`))
|
||||
break
|
||||
}
|
||||
|
||||
case "tool.result": {
|
||||
const resultProps = props as ToolResultProps | undefined
|
||||
const output = resultProps?.output ?? ""
|
||||
const preview = output.slice(0, 200).replace(/\n/g, "\\n")
|
||||
console.error(pc.green(`${sessionTag} TOOL.RESULT: "${preview}${output.length > 200 ? "..." : ""}"`))
|
||||
break
|
||||
}
|
||||
|
||||
case "session.error": {
|
||||
const errorProps = props as SessionErrorProps | undefined
|
||||
const errorMsg = serializeError(errorProps?.error)
|
||||
console.error(pc.red(`${sessionTag} SESSION.ERROR: ${errorMsg}`))
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(pc.dim(`${sessionTag} ${payload.type}`))
|
||||
}
|
||||
}
|
||||
120
src/cli/run/event-handlers.ts
Normal file
120
src/cli/run/event-handlers.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import pc from "picocolors"
|
||||
import type {
|
||||
RunContext,
|
||||
EventPayload,
|
||||
SessionIdleProps,
|
||||
SessionStatusProps,
|
||||
SessionErrorProps,
|
||||
MessageUpdatedProps,
|
||||
MessagePartUpdatedProps,
|
||||
ToolExecuteProps,
|
||||
ToolResultProps,
|
||||
} from "./types"
|
||||
import type { EventState } from "./event-state"
|
||||
import { serializeError } from "./event-formatting"
|
||||
|
||||
export function handleSessionIdle(ctx: RunContext, payload: EventPayload, state: EventState): void {
|
||||
if (payload.type !== "session.idle") return
|
||||
|
||||
const props = payload.properties as SessionIdleProps | undefined
|
||||
if (props?.sessionID === ctx.sessionID) {
|
||||
state.mainSessionIdle = true
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSessionStatus(ctx: RunContext, payload: EventPayload, state: EventState): void {
|
||||
if (payload.type !== "session.status") return
|
||||
|
||||
const props = payload.properties as SessionStatusProps | undefined
|
||||
if (props?.sessionID === ctx.sessionID && props?.status?.type === "busy") {
|
||||
state.mainSessionIdle = false
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSessionError(ctx: RunContext, payload: EventPayload, state: EventState): void {
|
||||
if (payload.type !== "session.error") return
|
||||
|
||||
const props = payload.properties as SessionErrorProps | undefined
|
||||
if (props?.sessionID === ctx.sessionID) {
|
||||
state.mainSessionError = true
|
||||
state.lastError = serializeError(props?.error)
|
||||
console.error(pc.red(`\n[session.error] ${state.lastError}`))
|
||||
}
|
||||
}
|
||||
|
||||
export 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.hasReceivedMeaningfulWork = true
|
||||
}
|
||||
state.lastPartText = part.text
|
||||
}
|
||||
}
|
||||
|
||||
export function handleMessageUpdated(ctx: RunContext, payload: EventPayload, state: EventState): void {
|
||||
if (payload.type !== "message.updated") return
|
||||
|
||||
const props = payload.properties as MessageUpdatedProps | undefined
|
||||
if (props?.info?.sessionID !== ctx.sessionID) return
|
||||
if (props?.info?.role !== "assistant") return
|
||||
|
||||
state.hasReceivedMeaningfulWork = true
|
||||
state.messageCount++
|
||||
}
|
||||
|
||||
export 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))}`
|
||||
}
|
||||
}
|
||||
|
||||
state.hasReceivedMeaningfulWork = true
|
||||
process.stdout.write(`\n${pc.cyan(">")} ${pc.bold(toolName)}${inputPreview}\n`)
|
||||
}
|
||||
|
||||
export 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 = ""
|
||||
}
|
||||
25
src/cli/run/event-state.ts
Normal file
25
src/cli/run/event-state.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export interface EventState {
|
||||
mainSessionIdle: boolean
|
||||
mainSessionError: boolean
|
||||
lastError: string | null
|
||||
lastOutput: string
|
||||
lastPartText: string
|
||||
currentTool: string | null
|
||||
/** Set to true when the main session has produced meaningful work (text, tool call, or tool result) */
|
||||
hasReceivedMeaningfulWork: boolean
|
||||
/** Count of assistant messages for the main session */
|
||||
messageCount: number
|
||||
}
|
||||
|
||||
export function createEventState(): EventState {
|
||||
return {
|
||||
mainSessionIdle: false,
|
||||
mainSessionError: false,
|
||||
lastError: null,
|
||||
lastOutput: "",
|
||||
lastPartText: "",
|
||||
currentTool: null,
|
||||
hasReceivedMeaningfulWork: false,
|
||||
messageCount: 0,
|
||||
}
|
||||
}
|
||||
43
src/cli/run/event-stream-processor.ts
Normal file
43
src/cli/run/event-stream-processor.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import pc from "picocolors"
|
||||
import type { RunContext, EventPayload } from "./types"
|
||||
import type { EventState } from "./event-state"
|
||||
import { logEventVerbose } from "./event-formatting"
|
||||
import {
|
||||
handleSessionError,
|
||||
handleSessionIdle,
|
||||
handleSessionStatus,
|
||||
handleMessagePartUpdated,
|
||||
handleMessageUpdated,
|
||||
handleToolExecute,
|
||||
handleToolResult,
|
||||
} from "./event-handlers"
|
||||
|
||||
export async function processEvents(
|
||||
ctx: RunContext,
|
||||
stream: AsyncIterable<unknown>,
|
||||
state: EventState
|
||||
): Promise<void> {
|
||||
for await (const event of stream) {
|
||||
if (ctx.abortController.signal.aborted) break
|
||||
|
||||
try {
|
||||
const payload = event as EventPayload
|
||||
if (!payload?.type) {
|
||||
console.error(pc.dim(`[event] no type: ${JSON.stringify(event)}`))
|
||||
continue
|
||||
}
|
||||
|
||||
logEventVerbose(ctx, payload)
|
||||
|
||||
handleSessionError(ctx, payload, state)
|
||||
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 (err) {
|
||||
console.error(pc.red(`[event error] ${err}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,329 +1,4 @@
|
||||
import pc from "picocolors"
|
||||
import type {
|
||||
RunContext,
|
||||
EventPayload,
|
||||
SessionIdleProps,
|
||||
SessionStatusProps,
|
||||
SessionErrorProps,
|
||||
MessageUpdatedProps,
|
||||
MessagePartUpdatedProps,
|
||||
ToolExecuteProps,
|
||||
ToolResultProps,
|
||||
} from "./types"
|
||||
|
||||
export function serializeError(error: unknown): string {
|
||||
if (!error) return "Unknown error"
|
||||
|
||||
if (error instanceof Error) {
|
||||
const parts = [error.message]
|
||||
if (error.cause) {
|
||||
parts.push(`Cause: ${serializeError(error.cause)}`)
|
||||
}
|
||||
return parts.join(" | ")
|
||||
}
|
||||
|
||||
if (typeof error === "string") {
|
||||
return error
|
||||
}
|
||||
|
||||
if (typeof error === "object") {
|
||||
const obj = error as Record<string, unknown>
|
||||
|
||||
const messagePaths = [
|
||||
obj.message,
|
||||
obj.error,
|
||||
(obj.data as Record<string, unknown>)?.message,
|
||||
(obj.data as Record<string, unknown>)?.error,
|
||||
(obj.error as Record<string, unknown>)?.message,
|
||||
]
|
||||
|
||||
for (const msg of messagePaths) {
|
||||
if (typeof msg === "string" && msg.length > 0) {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const json = JSON.stringify(error, null, 2)
|
||||
if (json !== "{}") {
|
||||
return json
|
||||
}
|
||||
} catch (_) {
|
||||
void _
|
||||
}
|
||||
}
|
||||
|
||||
return String(error)
|
||||
}
|
||||
|
||||
export interface EventState {
|
||||
mainSessionIdle: boolean
|
||||
mainSessionError: boolean
|
||||
lastError: string | null
|
||||
lastOutput: string
|
||||
lastPartText: string
|
||||
currentTool: string | null
|
||||
/** Set to true when the main session has produced meaningful work (text, tool call, or tool result) */
|
||||
hasReceivedMeaningfulWork: boolean
|
||||
/** Count of assistant messages for the main session */
|
||||
messageCount: number
|
||||
}
|
||||
|
||||
export function createEventState(): EventState {
|
||||
return {
|
||||
mainSessionIdle: false,
|
||||
mainSessionError: false,
|
||||
lastError: null,
|
||||
lastOutput: "",
|
||||
lastPartText: "",
|
||||
currentTool: null,
|
||||
hasReceivedMeaningfulWork: false,
|
||||
messageCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export async function processEvents(
|
||||
ctx: RunContext,
|
||||
stream: AsyncIterable<unknown>,
|
||||
state: EventState
|
||||
): Promise<void> {
|
||||
for await (const event of stream) {
|
||||
if (ctx.abortController.signal.aborted) break
|
||||
|
||||
try {
|
||||
const payload = event as EventPayload
|
||||
if (!payload?.type) {
|
||||
console.error(pc.dim(`[event] no type: ${JSON.stringify(event)}`))
|
||||
continue
|
||||
}
|
||||
|
||||
logEventVerbose(ctx, payload)
|
||||
|
||||
handleSessionError(ctx, payload, state)
|
||||
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 (err) {
|
||||
console.error(pc.red(`[event error] ${err}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function logEventVerbose(ctx: RunContext, payload: EventPayload): void {
|
||||
const props = payload.properties as Record<string, unknown> | undefined
|
||||
const info = props?.info as Record<string, unknown> | undefined
|
||||
const sessionID = props?.sessionID ?? info?.sessionID
|
||||
const isMainSession = sessionID === ctx.sessionID
|
||||
const sessionTag = isMainSession
|
||||
? pc.green("[MAIN]")
|
||||
: sessionID
|
||||
? pc.yellow(`[${String(sessionID).slice(0, 8)}]`)
|
||||
: pc.dim("[system]")
|
||||
|
||||
switch (payload.type) {
|
||||
case "session.idle":
|
||||
case "session.status": {
|
||||
const status = (props?.status as { type?: string })?.type ?? "idle"
|
||||
console.error(pc.dim(`${sessionTag} ${payload.type}: ${status}`))
|
||||
break
|
||||
}
|
||||
|
||||
case "message.part.updated": {
|
||||
const partProps = props as MessagePartUpdatedProps | undefined
|
||||
const part = partProps?.part
|
||||
if (part?.type === "tool-invocation") {
|
||||
const toolPart = part as { toolName?: string; state?: string }
|
||||
console.error(
|
||||
pc.dim(`${sessionTag} message.part (tool): ${toolPart.toolName} [${toolPart.state}]`)
|
||||
)
|
||||
} else if (part?.type === "text" && part.text) {
|
||||
const preview = part.text.slice(0, 80).replace(/\n/g, "\\n")
|
||||
console.error(
|
||||
pc.dim(`${sessionTag} message.part (text): "${preview}${part.text.length > 80 ? "..." : ""}"`)
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "message.updated": {
|
||||
const msgProps = props as MessageUpdatedProps | undefined
|
||||
const role = msgProps?.info?.role ?? "unknown"
|
||||
const model = msgProps?.info?.modelID
|
||||
const agent = msgProps?.info?.agent
|
||||
const details = [role, agent, model].filter(Boolean).join(", ")
|
||||
console.error(pc.dim(`${sessionTag} message.updated (${details})`))
|
||||
break
|
||||
}
|
||||
|
||||
case "tool.execute": {
|
||||
const toolProps = props as ToolExecuteProps | undefined
|
||||
const toolName = toolProps?.name ?? "unknown"
|
||||
const input = toolProps?.input ?? {}
|
||||
const inputStr = JSON.stringify(input).slice(0, 150)
|
||||
console.error(
|
||||
pc.cyan(`${sessionTag} TOOL.EXECUTE: ${pc.bold(toolName)}`)
|
||||
)
|
||||
console.error(pc.dim(` input: ${inputStr}${inputStr.length >= 150 ? "..." : ""}`))
|
||||
break
|
||||
}
|
||||
|
||||
case "tool.result": {
|
||||
const resultProps = props as ToolResultProps | undefined
|
||||
const output = resultProps?.output ?? ""
|
||||
const preview = output.slice(0, 200).replace(/\n/g, "\\n")
|
||||
console.error(
|
||||
pc.green(`${sessionTag} TOOL.RESULT: "${preview}${output.length > 200 ? "..." : ""}"`)
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case "session.error": {
|
||||
const errorProps = props as SessionErrorProps | undefined
|
||||
const errorMsg = serializeError(errorProps?.error)
|
||||
console.error(pc.red(`${sessionTag} SESSION.ERROR: ${errorMsg}`))
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
console.error(pc.dim(`${sessionTag} ${payload.type}`))
|
||||
}
|
||||
}
|
||||
|
||||
function handleSessionIdle(
|
||||
ctx: RunContext,
|
||||
payload: EventPayload,
|
||||
state: EventState
|
||||
): void {
|
||||
if (payload.type !== "session.idle") return
|
||||
|
||||
const props = payload.properties as SessionIdleProps | undefined
|
||||
if (props?.sessionID === ctx.sessionID) {
|
||||
state.mainSessionIdle = true
|
||||
}
|
||||
}
|
||||
|
||||
function handleSessionStatus(
|
||||
ctx: RunContext,
|
||||
payload: EventPayload,
|
||||
state: EventState
|
||||
): void {
|
||||
if (payload.type !== "session.status") return
|
||||
|
||||
const props = payload.properties as SessionStatusProps | undefined
|
||||
if (props?.sessionID === ctx.sessionID && props?.status?.type === "busy") {
|
||||
state.mainSessionIdle = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSessionError(
|
||||
ctx: RunContext,
|
||||
payload: EventPayload,
|
||||
state: EventState
|
||||
): void {
|
||||
if (payload.type !== "session.error") return
|
||||
|
||||
const props = payload.properties as SessionErrorProps | undefined
|
||||
if (props?.sessionID === ctx.sessionID) {
|
||||
state.mainSessionError = true
|
||||
state.lastError = serializeError(props?.error)
|
||||
console.error(pc.red(`\n[session.error] ${state.lastError}`))
|
||||
}
|
||||
}
|
||||
|
||||
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.hasReceivedMeaningfulWork = true
|
||||
}
|
||||
state.lastPartText = part.text
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessageUpdated(
|
||||
ctx: RunContext,
|
||||
payload: EventPayload,
|
||||
state: EventState
|
||||
): void {
|
||||
if (payload.type !== "message.updated") return
|
||||
|
||||
const props = payload.properties as MessageUpdatedProps | undefined
|
||||
if (props?.info?.sessionID !== ctx.sessionID) return
|
||||
if (props?.info?.role !== "assistant") return
|
||||
|
||||
state.hasReceivedMeaningfulWork = true
|
||||
state.messageCount++
|
||||
}
|
||||
|
||||
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))}`
|
||||
}
|
||||
}
|
||||
|
||||
state.hasReceivedMeaningfulWork = true
|
||||
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 = ""
|
||||
}
|
||||
export type { EventState } from "./event-state"
|
||||
export { createEventState } from "./event-state"
|
||||
export { serializeError } from "./event-formatting"
|
||||
export { processEvents } from "./event-stream-processor"
|
||||
|
||||
@@ -4,4 +4,6 @@ export { createServerConnection } from "./server-connection"
|
||||
export { resolveSession } from "./session-resolver"
|
||||
export { createJsonOutputManager } from "./json-output"
|
||||
export { executeOnCompleteHook } from "./on-complete-hook"
|
||||
export { createEventState, processEvents, serializeError } from "./events"
|
||||
export type { EventState } from "./events"
|
||||
export type { RunOptions, RunContext, RunResult, ServerConnection } from "./types"
|
||||
|
||||
Reference in New Issue
Block a user