mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
feat: add support for Codex's request_user_input tool
Implement full support for the request_user_input tool with both Web UI and CLI
components. This includes:
- New view and footer components for request_user_input tool UI
- Tool registration in knownTools and view registries
- Nested answer format support (Record<string, { answers: string[] }>)
- Backward compatibility with flat answer format (Record<string, string[]>)
- Permission handler updates for CLI request_user_input acceptance
- Type definitions and schema updates across shared/cli/server/web packages
- Translation strings for request_user_input UI elements
- Conditional footer rendering in ToolCard for question tools
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { isObject } from '@hapi/protocol'
|
||||
|
||||
export type RequestUserInputOption = {
|
||||
label: string
|
||||
description: string | null
|
||||
}
|
||||
|
||||
export type RequestUserInputQuestion = {
|
||||
id: string
|
||||
question: string
|
||||
options: RequestUserInputOption[]
|
||||
}
|
||||
|
||||
export type RequestUserInputQuestionInfo = {
|
||||
id: string
|
||||
question: string | null
|
||||
}
|
||||
|
||||
// Nested answer format: { answers: { [id]: { answers: string[] } } }
|
||||
export type RequestUserInputAnswers = Record<string, { answers: string[] }>
|
||||
|
||||
export function isRequestUserInputToolName(toolName: string): boolean {
|
||||
return toolName === 'request_user_input'
|
||||
}
|
||||
|
||||
export function parseRequestUserInputInput(input: unknown): { questions: RequestUserInputQuestion[] } {
|
||||
if (!isObject(input)) return { questions: [] }
|
||||
|
||||
const rawQuestions = input.questions
|
||||
if (!Array.isArray(rawQuestions)) return { questions: [] }
|
||||
|
||||
const questions: RequestUserInputQuestion[] = []
|
||||
for (const raw of rawQuestions) {
|
||||
if (!isObject(raw)) continue
|
||||
|
||||
const id = typeof raw.id === 'string' ? raw.id.trim() : ''
|
||||
const question = typeof raw.question === 'string' ? raw.question.trim() : ''
|
||||
|
||||
// Skip questions without id
|
||||
if (!id) continue
|
||||
|
||||
const rawOptions = Array.isArray(raw.options) ? raw.options : []
|
||||
const options: RequestUserInputOption[] = []
|
||||
for (const opt of rawOptions) {
|
||||
if (!isObject(opt)) continue
|
||||
const label = typeof opt.label === 'string' ? opt.label.trim() : ''
|
||||
if (!label) continue
|
||||
const description = typeof opt.description === 'string' ? opt.description.trim() : null
|
||||
options.push({ label, description })
|
||||
}
|
||||
|
||||
questions.push({
|
||||
id,
|
||||
question,
|
||||
options
|
||||
})
|
||||
}
|
||||
|
||||
return { questions }
|
||||
}
|
||||
|
||||
export function extractRequestUserInputQuestionsInfo(input: unknown): RequestUserInputQuestionInfo[] | null {
|
||||
if (!isObject(input)) return null
|
||||
const raw = input.questions
|
||||
if (!Array.isArray(raw)) return null
|
||||
|
||||
const questions: RequestUserInputQuestionInfo[] = []
|
||||
for (const q of raw) {
|
||||
if (!isObject(q)) continue
|
||||
const id = typeof q.id === 'string' ? q.id.trim() : ''
|
||||
const question = typeof q.question === 'string' ? q.question.trim() : null
|
||||
if (!id) continue
|
||||
questions.push({
|
||||
id,
|
||||
question: question && question.length > 0 ? question : null
|
||||
})
|
||||
}
|
||||
return questions
|
||||
}
|
||||
|
||||
/**
|
||||
* Format answers for submission in the nested format expected by request_user_input
|
||||
* Format: { answers: { [id]: { answers: ["option", "user_note: note text"] } } }
|
||||
*/
|
||||
export function formatRequestUserInputAnswers(
|
||||
answersByQuestion: Record<string, { selected: string | null; userNote: string }>
|
||||
): { answers: RequestUserInputAnswers } {
|
||||
const answers: RequestUserInputAnswers = {}
|
||||
|
||||
for (const [id, answer] of Object.entries(answersByQuestion)) {
|
||||
const answerArray: string[] = []
|
||||
|
||||
if (answer.selected) {
|
||||
answerArray.push(answer.selected)
|
||||
}
|
||||
|
||||
const note = answer.userNote.trim()
|
||||
if (note.length > 0) {
|
||||
answerArray.push(`user_note: ${note}`)
|
||||
}
|
||||
|
||||
answers[id] = { answers: answerArray }
|
||||
}
|
||||
|
||||
return { answers }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse answers from the nested format for display
|
||||
*/
|
||||
export function parseRequestUserInputAnswers(
|
||||
answers: unknown
|
||||
): Record<string, { selected: string | null; userNote: string | null }> | null {
|
||||
if (!isObject(answers)) return null
|
||||
|
||||
// Handle nested format: { answers: { [id]: { answers: string[] } } }
|
||||
const answersObj = isObject(answers.answers) ? answers.answers : answers
|
||||
|
||||
const parsed: Record<string, { selected: string | null; userNote: string | null }> = {}
|
||||
|
||||
for (const [id, value] of Object.entries(answersObj)) {
|
||||
let answerArray: string[] = []
|
||||
|
||||
if (isObject(value) && Array.isArray(value.answers)) {
|
||||
answerArray = value.answers.filter((a): a is string => typeof a === 'string')
|
||||
} else if (Array.isArray(value)) {
|
||||
answerArray = value.filter((a): a is string => typeof a === 'string')
|
||||
}
|
||||
|
||||
let selected: string | null = null
|
||||
let userNote: string | null = null
|
||||
|
||||
for (const item of answerArray) {
|
||||
if (item.startsWith('user_note: ')) {
|
||||
userNote = item.slice('user_note: '.length).trim()
|
||||
} else if (!selected) {
|
||||
selected = item
|
||||
}
|
||||
}
|
||||
|
||||
parsed[id] = { selected, userNote }
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
Reference in New Issue
Block a user