mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-21 00:21:25 +00:00
664 lines
20 KiB
TypeScript
664 lines
20 KiB
TypeScript
/**
|
|
* AskDispatcher - Routes ask messages to appropriate handlers
|
|
*
|
|
* This dispatcher is responsible for:
|
|
* - Categorizing ask types using type guards from client module
|
|
* - Routing to the appropriate handler based on ask category
|
|
* - Coordinating between OutputManager and PromptManager
|
|
* - Tracking which asks have been handled (to avoid duplicates)
|
|
*
|
|
* Design notes:
|
|
* - Uses isIdleAsk, isInteractiveAsk, isResumableAsk, isNonBlockingAsk type guards
|
|
* - Single responsibility: Ask routing and handling only
|
|
* - Delegates output to OutputManager, input to PromptManager
|
|
* - Sends responses back through a provided callback
|
|
*/
|
|
|
|
import {
|
|
type WebviewMessage,
|
|
type ClineMessage,
|
|
type ClineAsk,
|
|
type ClineAskResponse,
|
|
isIdleAsk,
|
|
isInteractiveAsk,
|
|
isResumableAsk,
|
|
isNonBlockingAsk,
|
|
} from "@roo-code/types"
|
|
import { debugLog } from "@roo-code/core/cli"
|
|
|
|
import { FOLLOWUP_TIMEOUT_SECONDS } from "@/types/index.js"
|
|
|
|
import type { OutputManager } from "./output-manager.js"
|
|
import type { PromptManager } from "./prompt-manager.js"
|
|
|
|
// =============================================================================
|
|
// Types
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Configuration for AskDispatcher.
|
|
*/
|
|
export interface AskDispatcherOptions {
|
|
/**
|
|
* OutputManager for displaying ask-related output.
|
|
*/
|
|
outputManager: OutputManager
|
|
|
|
/**
|
|
* PromptManager for collecting user input.
|
|
*/
|
|
promptManager: PromptManager
|
|
|
|
/**
|
|
* Callback to send responses to the extension.
|
|
*/
|
|
sendMessage: (message: WebviewMessage) => void
|
|
|
|
/**
|
|
* Whether running in non-interactive mode (auto-approve).
|
|
*/
|
|
nonInteractive?: boolean
|
|
|
|
/**
|
|
* Whether to exit on API request errors instead of retrying.
|
|
*/
|
|
exitOnError?: boolean
|
|
|
|
/**
|
|
* Whether to disable ask handling (for TUI mode).
|
|
* In TUI mode, the TUI handles asks directly.
|
|
*/
|
|
disabled?: boolean
|
|
}
|
|
|
|
/**
|
|
* Result of handling an ask.
|
|
*/
|
|
export interface AskHandleResult {
|
|
/** Whether the ask was handled */
|
|
handled: boolean
|
|
/** The response sent (if any) */
|
|
response?: ClineAskResponse
|
|
/** Any error that occurred */
|
|
error?: Error
|
|
}
|
|
|
|
// =============================================================================
|
|
// AskDispatcher Class
|
|
// =============================================================================
|
|
|
|
export class AskDispatcher {
|
|
private outputManager: OutputManager
|
|
private promptManager: PromptManager
|
|
private sendMessage: (message: WebviewMessage) => void
|
|
private nonInteractive: boolean
|
|
private exitOnError: boolean
|
|
private disabled: boolean
|
|
|
|
/**
|
|
* Track which asks have been handled to avoid duplicates.
|
|
* Key: message ts
|
|
*/
|
|
private handledAsks = new Set<number>()
|
|
|
|
constructor(options: AskDispatcherOptions) {
|
|
this.outputManager = options.outputManager
|
|
this.promptManager = options.promptManager
|
|
this.sendMessage = options.sendMessage
|
|
this.nonInteractive = options.nonInteractive ?? false
|
|
this.exitOnError = options.exitOnError ?? false
|
|
this.disabled = options.disabled ?? false
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Public API
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* Handle an ask message.
|
|
* Routes to the appropriate handler based on ask type.
|
|
*
|
|
* @param message - The ClineMessage with type="ask"
|
|
* @returns Promise<AskHandleResult>
|
|
*/
|
|
async handleAsk(message: ClineMessage): Promise<AskHandleResult> {
|
|
// Disabled in TUI mode - TUI handles asks directly
|
|
if (this.disabled) {
|
|
return { handled: false }
|
|
}
|
|
|
|
const ts = message.ts
|
|
const ask = message.ask
|
|
const text = message.text || ""
|
|
|
|
// Check if already handled
|
|
if (this.handledAsks.has(ts)) {
|
|
return { handled: true }
|
|
}
|
|
|
|
// Must be an ask message
|
|
if (message.type !== "ask" || !ask) {
|
|
return { handled: false }
|
|
}
|
|
|
|
// Skip partial messages (wait for complete)
|
|
if (message.partial) {
|
|
return { handled: false }
|
|
}
|
|
|
|
// Mark as being handled
|
|
this.handledAsks.add(ts)
|
|
|
|
try {
|
|
// Route based on ask category
|
|
if (isNonBlockingAsk(ask)) {
|
|
return await this.handleNonBlockingAsk(ts, ask, text)
|
|
}
|
|
|
|
if (isIdleAsk(ask)) {
|
|
return await this.handleIdleAsk(ts, ask, text)
|
|
}
|
|
|
|
if (isResumableAsk(ask)) {
|
|
return await this.handleResumableAsk(ts, ask, text)
|
|
}
|
|
|
|
if (isInteractiveAsk(ask)) {
|
|
return await this.handleInteractiveAsk(ts, ask, text)
|
|
}
|
|
|
|
// Unknown ask type - log and handle generically
|
|
debugLog("[AskDispatcher] Unknown ask type", { ask, ts })
|
|
return await this.handleUnknownAsk(ts, ask, text)
|
|
} catch (error) {
|
|
// Re-allow handling on error
|
|
this.handledAsks.delete(ts)
|
|
return {
|
|
handled: false,
|
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if an ask has been handled.
|
|
*/
|
|
isHandled(ts: number): boolean {
|
|
return this.handledAsks.has(ts)
|
|
}
|
|
|
|
/**
|
|
* Clear handled asks (call when starting new task).
|
|
*/
|
|
clear(): void {
|
|
this.handledAsks.clear()
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Category Handlers
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* Handle non-blocking asks (command_output).
|
|
* These don't actually block the agent - just need acknowledgment.
|
|
*/
|
|
private async handleNonBlockingAsk(_ts: number, _ask: ClineAsk, _text: string): Promise<AskHandleResult> {
|
|
// command_output - output is handled by OutputManager
|
|
// Just send approval to continue
|
|
this.sendApprovalResponse(true)
|
|
return { handled: true, response: "yesButtonClicked" }
|
|
}
|
|
|
|
/**
|
|
* Handle idle asks (completion_result, api_req_failed, etc.).
|
|
* These indicate the task has stopped.
|
|
*/
|
|
private async handleIdleAsk(ts: number, ask: ClineAsk, text: string): Promise<AskHandleResult> {
|
|
switch (ask) {
|
|
case "completion_result":
|
|
// Task complete - nothing to do here, TaskCompleted event handles it
|
|
return { handled: true }
|
|
|
|
case "api_req_failed":
|
|
return await this.handleApiFailedRetry(ts, text)
|
|
|
|
case "mistake_limit_reached":
|
|
return await this.handleMistakeLimitReached(ts, text)
|
|
|
|
case "resume_completed_task":
|
|
return await this.handleResumeTask(ts, ask, text)
|
|
|
|
case "auto_approval_max_req_reached":
|
|
return await this.handleAutoApprovalMaxReached(ts, text)
|
|
|
|
default:
|
|
return { handled: false }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle resumable asks (resume_task).
|
|
*/
|
|
private async handleResumableAsk(ts: number, ask: ClineAsk, text: string): Promise<AskHandleResult> {
|
|
return await this.handleResumeTask(ts, ask, text)
|
|
}
|
|
|
|
/**
|
|
* Handle interactive asks (followup, command, tool, use_mcp_server).
|
|
* These require user approval or input.
|
|
*/
|
|
private async handleInteractiveAsk(ts: number, ask: ClineAsk, text: string): Promise<AskHandleResult> {
|
|
switch (ask) {
|
|
case "followup":
|
|
return await this.handleFollowupQuestion(ts, text)
|
|
|
|
case "command":
|
|
return await this.handleCommandApproval(ts, text)
|
|
|
|
case "tool":
|
|
return await this.handleToolApproval(ts, text)
|
|
|
|
case "use_mcp_server":
|
|
return await this.handleMcpApproval(ts, text)
|
|
|
|
default:
|
|
return { handled: false }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle unknown ask types.
|
|
*/
|
|
private async handleUnknownAsk(ts: number, ask: ClineAsk, text: string): Promise<AskHandleResult> {
|
|
if (this.nonInteractive) {
|
|
if (text) {
|
|
this.outputManager.output(`\n[${ask}]`, text)
|
|
}
|
|
return { handled: true }
|
|
}
|
|
|
|
return await this.handleGenericApproval(ts, ask, text)
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Specific Ask Handlers
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* Handle followup questions - prompt for text input with suggestions.
|
|
*/
|
|
private async handleFollowupQuestion(ts: number, text: string): Promise<AskHandleResult> {
|
|
let question = text
|
|
let suggestions: Array<{ answer: string; mode?: string | null }> = []
|
|
|
|
try {
|
|
const data = JSON.parse(text)
|
|
question = data.question || text
|
|
suggestions = Array.isArray(data.suggest) ? data.suggest : []
|
|
} catch {
|
|
// Use raw text if not JSON
|
|
}
|
|
|
|
this.outputManager.output("\n[question]", question)
|
|
|
|
if (suggestions.length > 0) {
|
|
this.outputManager.output("\nSuggested answers:")
|
|
suggestions.forEach((suggestion, index) => {
|
|
const suggestionText = suggestion.answer || String(suggestion)
|
|
const modeHint = suggestion.mode ? ` (mode: ${suggestion.mode})` : ""
|
|
this.outputManager.output(` ${index + 1}. ${suggestionText}${modeHint}`)
|
|
})
|
|
this.outputManager.output("")
|
|
}
|
|
|
|
const firstSuggestion = suggestions.length > 0 ? suggestions[0] : null
|
|
const defaultAnswer = firstSuggestion?.answer ?? ""
|
|
|
|
if (this.nonInteractive) {
|
|
// Use timeout prompt in non-interactive mode
|
|
const timeoutMs = FOLLOWUP_TIMEOUT_SECONDS * 1000
|
|
const result = await this.promptManager.promptWithTimeout(
|
|
suggestions.length > 0
|
|
? `Enter number (1-${suggestions.length}) or type your answer (auto-select in ${Math.round(timeoutMs / 1000)}s): `
|
|
: `Your answer (auto-select in ${Math.round(timeoutMs / 1000)}s): `,
|
|
timeoutMs,
|
|
defaultAnswer,
|
|
)
|
|
|
|
let responseText = result.value.trim()
|
|
responseText = this.resolveNumberedSuggestion(responseText, suggestions)
|
|
|
|
if (result.timedOut || result.cancelled) {
|
|
this.outputManager.output(`[Using default: ${defaultAnswer || "(empty)"}]`)
|
|
}
|
|
|
|
this.sendFollowupResponse(responseText)
|
|
return { handled: true, response: "messageResponse" }
|
|
}
|
|
|
|
// Interactive mode
|
|
try {
|
|
const answer = await this.promptManager.promptForInput(
|
|
suggestions.length > 0
|
|
? `Enter number (1-${suggestions.length}) or type your answer: `
|
|
: "Your answer: ",
|
|
)
|
|
|
|
let responseText = answer.trim()
|
|
responseText = this.resolveNumberedSuggestion(responseText, suggestions)
|
|
|
|
this.sendFollowupResponse(responseText)
|
|
return { handled: true, response: "messageResponse" }
|
|
} catch {
|
|
this.outputManager.output(`[Using default: ${defaultAnswer || "(empty)"}]`)
|
|
this.sendFollowupResponse(defaultAnswer)
|
|
return { handled: true, response: "messageResponse" }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle command execution approval.
|
|
*/
|
|
private async handleCommandApproval(ts: number, text: string): Promise<AskHandleResult> {
|
|
this.outputManager.output("\n[command request]")
|
|
this.outputManager.output(` Command: ${text || "(no command specified)"}`)
|
|
this.outputManager.markDisplayed(ts, text || "", false)
|
|
|
|
if (this.nonInteractive) {
|
|
// Auto-approved by extension settings
|
|
return { handled: true }
|
|
}
|
|
|
|
try {
|
|
const approved = await this.promptManager.promptForYesNo("Execute this command? (y/n): ")
|
|
this.sendApprovalResponse(approved)
|
|
return { handled: true, response: approved ? "yesButtonClicked" : "noButtonClicked" }
|
|
} catch {
|
|
this.outputManager.output("[Defaulting to: no]")
|
|
this.sendApprovalResponse(false)
|
|
return { handled: true, response: "noButtonClicked" }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle tool execution approval.
|
|
*/
|
|
private async handleToolApproval(ts: number, text: string): Promise<AskHandleResult> {
|
|
let toolName = "unknown"
|
|
let toolInfo: Record<string, unknown> = {}
|
|
|
|
try {
|
|
toolInfo = JSON.parse(text) as Record<string, unknown>
|
|
toolName = (toolInfo.tool as string) || "unknown"
|
|
} catch {
|
|
// Use raw text if not JSON
|
|
}
|
|
|
|
const isProtected = toolInfo.isProtected === true
|
|
|
|
if (isProtected) {
|
|
this.outputManager.output(`\n[Tool Request] ${toolName} [PROTECTED CONFIGURATION FILE]`)
|
|
this.outputManager.output(`⚠️ WARNING: This tool wants to modify a protected configuration file.`)
|
|
this.outputManager.output(
|
|
` Protected files include .rooignore, .roo/*, and other sensitive config files.`,
|
|
)
|
|
} else {
|
|
this.outputManager.output(`\n[Tool Request] ${toolName}`)
|
|
}
|
|
|
|
// Display tool details
|
|
for (const [key, value] of Object.entries(toolInfo)) {
|
|
if (key === "tool" || key === "isProtected") continue
|
|
|
|
let displayValue: string
|
|
if (typeof value === "string") {
|
|
displayValue = value.length > 200 ? value.substring(0, 200) + "..." : value
|
|
} else if (typeof value === "object" && value !== null) {
|
|
const json = JSON.stringify(value)
|
|
displayValue = json.length > 200 ? json.substring(0, 200) + "..." : json
|
|
} else {
|
|
displayValue = String(value)
|
|
}
|
|
|
|
this.outputManager.output(` ${key}: ${displayValue}`)
|
|
}
|
|
|
|
this.outputManager.markDisplayed(ts, text || "", false)
|
|
|
|
if (this.nonInteractive) {
|
|
// Auto-approved by extension settings (unless protected)
|
|
return { handled: true }
|
|
}
|
|
|
|
try {
|
|
const approved = await this.promptManager.promptForYesNo("Approve this action? (y/n): ")
|
|
this.sendApprovalResponse(approved)
|
|
return { handled: true, response: approved ? "yesButtonClicked" : "noButtonClicked" }
|
|
} catch {
|
|
this.outputManager.output("[Defaulting to: no]")
|
|
this.sendApprovalResponse(false)
|
|
return { handled: true, response: "noButtonClicked" }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle MCP server access approval.
|
|
*/
|
|
private async handleMcpApproval(ts: number, text: string): Promise<AskHandleResult> {
|
|
let serverName = "unknown"
|
|
let toolName = ""
|
|
let resourceUri = ""
|
|
|
|
try {
|
|
const mcpInfo = JSON.parse(text)
|
|
serverName = mcpInfo.server_name || "unknown"
|
|
|
|
if (mcpInfo.type === "use_mcp_tool") {
|
|
toolName = mcpInfo.tool_name || ""
|
|
} else if (mcpInfo.type === "access_mcp_resource") {
|
|
resourceUri = mcpInfo.uri || ""
|
|
}
|
|
} catch {
|
|
// Use raw text if not JSON
|
|
}
|
|
|
|
this.outputManager.output("\n[mcp request]")
|
|
this.outputManager.output(` Server: ${serverName}`)
|
|
if (toolName) {
|
|
this.outputManager.output(` Tool: ${toolName}`)
|
|
}
|
|
if (resourceUri) {
|
|
this.outputManager.output(` Resource: ${resourceUri}`)
|
|
}
|
|
this.outputManager.markDisplayed(ts, text || "", false)
|
|
|
|
if (this.nonInteractive) {
|
|
// Auto-approved by extension settings
|
|
return { handled: true }
|
|
}
|
|
|
|
try {
|
|
const approved = await this.promptManager.promptForYesNo("Allow MCP access? (y/n): ")
|
|
this.sendApprovalResponse(approved)
|
|
return { handled: true, response: approved ? "yesButtonClicked" : "noButtonClicked" }
|
|
} catch {
|
|
this.outputManager.output("[Defaulting to: no]")
|
|
this.sendApprovalResponse(false)
|
|
return { handled: true, response: "noButtonClicked" }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle API request failed - retry prompt.
|
|
*/
|
|
private async handleApiFailedRetry(ts: number, text: string): Promise<AskHandleResult> {
|
|
this.outputManager.output("\n[api request failed]")
|
|
this.outputManager.output(` Error: ${text || "Unknown error"}`)
|
|
this.outputManager.markDisplayed(ts, text || "", false)
|
|
|
|
if (this.exitOnError) {
|
|
console.error(`[CLI] API request failed: ${text || "Unknown error"}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
if (this.nonInteractive) {
|
|
this.outputManager.output("\n[retrying api request]")
|
|
// Auto-retry in non-interactive mode
|
|
return { handled: true }
|
|
}
|
|
|
|
try {
|
|
const retry = await this.promptManager.promptForYesNo("Retry the request? (y/n): ")
|
|
this.sendApprovalResponse(retry)
|
|
return { handled: true, response: retry ? "yesButtonClicked" : "noButtonClicked" }
|
|
} catch {
|
|
this.outputManager.output("[Defaulting to: no]")
|
|
this.sendApprovalResponse(false)
|
|
return { handled: true, response: "noButtonClicked" }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle mistake limit reached.
|
|
*/
|
|
private async handleMistakeLimitReached(ts: number, text: string): Promise<AskHandleResult> {
|
|
this.outputManager.output("\n[mistake limit reached]")
|
|
if (text) {
|
|
this.outputManager.output(` Details: ${text}`)
|
|
}
|
|
this.outputManager.markDisplayed(ts, text || "", false)
|
|
|
|
if (this.nonInteractive) {
|
|
// Auto-proceed in non-interactive mode
|
|
this.sendApprovalResponse(true)
|
|
return { handled: true, response: "yesButtonClicked" }
|
|
}
|
|
|
|
try {
|
|
const proceed = await this.promptManager.promptForYesNo("Continue anyway? (y/n): ")
|
|
this.sendApprovalResponse(proceed)
|
|
return { handled: true, response: proceed ? "yesButtonClicked" : "noButtonClicked" }
|
|
} catch {
|
|
this.outputManager.output("[Defaulting to: no]")
|
|
this.sendApprovalResponse(false)
|
|
return { handled: true, response: "noButtonClicked" }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle auto-approval max reached.
|
|
*/
|
|
private async handleAutoApprovalMaxReached(ts: number, text: string): Promise<AskHandleResult> {
|
|
this.outputManager.output("\n[auto-approval limit reached]")
|
|
if (text) {
|
|
this.outputManager.output(` Details: ${text}`)
|
|
}
|
|
this.outputManager.markDisplayed(ts, text || "", false)
|
|
|
|
if (this.nonInteractive) {
|
|
// Auto-proceed in non-interactive mode
|
|
this.sendApprovalResponse(true)
|
|
return { handled: true, response: "yesButtonClicked" }
|
|
}
|
|
|
|
try {
|
|
const proceed = await this.promptManager.promptForYesNo("Continue with manual approval? (y/n): ")
|
|
this.sendApprovalResponse(proceed)
|
|
return { handled: true, response: proceed ? "yesButtonClicked" : "noButtonClicked" }
|
|
} catch {
|
|
this.outputManager.output("[Defaulting to: no]")
|
|
this.sendApprovalResponse(false)
|
|
return { handled: true, response: "noButtonClicked" }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle task resume prompt.
|
|
*/
|
|
private async handleResumeTask(ts: number, ask: ClineAsk, text: string): Promise<AskHandleResult> {
|
|
const isCompleted = ask === "resume_completed_task"
|
|
this.outputManager.output(`\n[Resume ${isCompleted ? "Completed " : ""}Task]`)
|
|
if (text) {
|
|
this.outputManager.output(` ${text}`)
|
|
}
|
|
this.outputManager.markDisplayed(ts, text || "", false)
|
|
|
|
if (this.nonInteractive) {
|
|
this.outputManager.output("\n[continuing task]")
|
|
// Auto-resume in non-interactive mode
|
|
this.sendApprovalResponse(true)
|
|
return { handled: true, response: "yesButtonClicked" }
|
|
}
|
|
|
|
try {
|
|
const resume = await this.promptManager.promptForYesNo("Continue with this task? (y/n): ")
|
|
this.sendApprovalResponse(resume)
|
|
return { handled: true, response: resume ? "yesButtonClicked" : "noButtonClicked" }
|
|
} catch {
|
|
this.outputManager.output("[Defaulting to: no]")
|
|
this.sendApprovalResponse(false)
|
|
return { handled: true, response: "noButtonClicked" }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle generic approval prompts for unknown ask types.
|
|
*/
|
|
private async handleGenericApproval(ts: number, ask: ClineAsk, text: string): Promise<AskHandleResult> {
|
|
this.outputManager.output(`\n[${ask}]`)
|
|
if (text) {
|
|
this.outputManager.output(` ${text}`)
|
|
}
|
|
this.outputManager.markDisplayed(ts, text || "", false)
|
|
|
|
try {
|
|
const approved = await this.promptManager.promptForYesNo("Approve? (y/n): ")
|
|
this.sendApprovalResponse(approved)
|
|
return { handled: true, response: approved ? "yesButtonClicked" : "noButtonClicked" }
|
|
} catch {
|
|
this.outputManager.output("[Defaulting to: no]")
|
|
this.sendApprovalResponse(false)
|
|
return { handled: true, response: "noButtonClicked" }
|
|
}
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Response Helpers
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* Send a followup response (text answer) to the extension.
|
|
*/
|
|
private sendFollowupResponse(text: string): void {
|
|
this.sendMessage({ type: "askResponse", askResponse: "messageResponse", text })
|
|
}
|
|
|
|
/**
|
|
* Send an approval response (yes/no) to the extension.
|
|
*/
|
|
private sendApprovalResponse(approved: boolean): void {
|
|
this.sendMessage({
|
|
type: "askResponse",
|
|
askResponse: approved ? "yesButtonClicked" : "noButtonClicked",
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Resolve a numbered suggestion selection.
|
|
*/
|
|
private resolveNumberedSuggestion(
|
|
input: string,
|
|
suggestions: Array<{ answer: string; mode?: string | null }>,
|
|
): string {
|
|
const num = parseInt(input, 10)
|
|
if (!isNaN(num) && num >= 1 && num <= suggestions.length) {
|
|
const selectedSuggestion = suggestions[num - 1]
|
|
if (selectedSuggestion) {
|
|
const selected = selectedSuggestion.answer || String(selectedSuggestion)
|
|
this.outputManager.output(`Selected: ${selected}`)
|
|
return selected
|
|
}
|
|
}
|
|
return input
|
|
}
|
|
}
|