mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +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:
@@ -28,7 +28,7 @@ interface PermissionResponse {
|
||||
reason?: string;
|
||||
mode?: PermissionMode;
|
||||
allowTools?: string[];
|
||||
answers?: Record<string, string[]>;
|
||||
answers?: Record<string, string[]> | Record<string, { answers: string[] }>;
|
||||
receivedAt?: number;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,25 @@ function isAskUserQuestionToolName(toolName: string): boolean {
|
||||
return toolName === 'AskUserQuestion' || toolName === 'ask_user_question';
|
||||
}
|
||||
|
||||
function formatAskUserQuestionAnswers(answers: Record<string, string[]>, input: unknown): string {
|
||||
function isRequestUserInputToolName(toolName: string): boolean {
|
||||
return toolName === 'request_user_input';
|
||||
}
|
||||
|
||||
function isQuestionToolName(toolName: string): boolean {
|
||||
return isAskUserQuestionToolName(toolName) || isRequestUserInputToolName(toolName);
|
||||
}
|
||||
|
||||
function formatAskUserQuestionAnswers(answers: Record<string, string[]> | Record<string, { answers: string[] }>, input: unknown): string {
|
||||
// Normalize nested format to flat format for display
|
||||
const flatAnswers: Record<string, string[]> = {};
|
||||
for (const [key, value] of Object.entries(answers)) {
|
||||
if (Array.isArray(value)) {
|
||||
flatAnswers[key] = value;
|
||||
} else if (value && typeof value === 'object' && 'answers' in value) {
|
||||
flatAnswers[key] = value.answers;
|
||||
}
|
||||
}
|
||||
|
||||
const questions = (() => {
|
||||
if (!isObject(input)) return null;
|
||||
const raw = input.questions;
|
||||
@@ -46,7 +64,7 @@ function formatAskUserQuestionAnswers(answers: Record<string, string[]>, input:
|
||||
return raw.filter((q) => isObject(q));
|
||||
})();
|
||||
|
||||
const keys = Object.keys(answers).sort((a, b) => {
|
||||
const keys = Object.keys(flatAnswers).sort((a, b) => {
|
||||
const aNum = Number.parseInt(a, 10);
|
||||
const bNum = Number.parseInt(b, 10);
|
||||
if (Number.isFinite(aNum) && Number.isFinite(bNum)) return aNum - bNum;
|
||||
@@ -63,7 +81,7 @@ function formatAskUserQuestionAnswers(answers: Record<string, string[]>, input:
|
||||
: Number.isFinite(idx)
|
||||
? `Question ${idx + 1}`
|
||||
: `Question ${key}`;
|
||||
const value = answers[key] ?? [];
|
||||
const value = flatAnswers[key] ?? [];
|
||||
const joined = value.map((v) => String(v)).filter((v) => v.trim().length > 0).join(', ');
|
||||
return `${header}: ${joined || '(no answer)'}`;
|
||||
});
|
||||
@@ -82,7 +100,32 @@ function formatAskUserQuestionAnswers(answers: Record<string, string[]>, input:
|
||||
: `User answered:\n${body}`;
|
||||
}
|
||||
|
||||
function buildAskUserQuestionUpdatedInput(input: unknown, answers: Record<string, string[]>): Record<string, unknown> {
|
||||
function buildAskUserQuestionUpdatedInput(input: unknown, answers: Record<string, string[]> | Record<string, { answers: string[] }>): Record<string, unknown> {
|
||||
// Normalize to flat format for AskUserQuestion
|
||||
const flatAnswers: Record<string, string[]> = {};
|
||||
for (const [key, value] of Object.entries(answers)) {
|
||||
if (Array.isArray(value)) {
|
||||
flatAnswers[key] = value;
|
||||
} else if (value && typeof value === 'object' && 'answers' in value) {
|
||||
flatAnswers[key] = value.answers;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isObject(input)) {
|
||||
return { answers: flatAnswers };
|
||||
}
|
||||
|
||||
return {
|
||||
...input,
|
||||
answers: flatAnswers
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build updated input for request_user_input tool
|
||||
* The answers format is nested: { answers: { [id]: { answers: string[] } } }
|
||||
*/
|
||||
function buildRequestUserInputUpdatedInput(input: unknown, answers: unknown): Record<string, unknown> {
|
||||
if (!isObject(input)) {
|
||||
return { answers };
|
||||
}
|
||||
@@ -138,7 +181,7 @@ export class PermissionHandler extends BasePermissionHandler<PermissionResponse,
|
||||
// Update allowed tools
|
||||
if (response.allowTools && response.allowTools.length > 0) {
|
||||
response.allowTools.forEach(tool => {
|
||||
if (isAskUserQuestionToolName(tool)) {
|
||||
if (isQuestionToolName(tool)) {
|
||||
return;
|
||||
}
|
||||
if (tool.startsWith('Bash(') || tool === 'Bash') {
|
||||
@@ -171,6 +214,22 @@ export class PermissionHandler extends BasePermissionHandler<PermissionResponse,
|
||||
return completion;
|
||||
}
|
||||
|
||||
// Handle request_user_input
|
||||
if (isRequestUserInputToolName(pending.toolName)) {
|
||||
const answers = response.answers ?? {};
|
||||
if (Object.keys(answers).length === 0) {
|
||||
pending.resolve({ behavior: 'deny', message: 'No answers were provided.' });
|
||||
completion.status = 'denied';
|
||||
completion.reason = completion.reason ?? 'No answers were provided.';
|
||||
} else {
|
||||
pending.resolve({
|
||||
behavior: 'allow',
|
||||
updatedInput: buildRequestUserInputUpdatedInput(pending.input, answers)
|
||||
});
|
||||
}
|
||||
return completion;
|
||||
}
|
||||
|
||||
if (pending.toolName === 'exit_plan_mode' || pending.toolName === 'ExitPlanMode') {
|
||||
// Handle exit_plan_mode specially
|
||||
logger.debug('Plan mode result received', response);
|
||||
@@ -202,10 +261,10 @@ export class PermissionHandler extends BasePermissionHandler<PermissionResponse,
|
||||
* Creates the canCallTool callback for the SDK
|
||||
*/
|
||||
handleToolCall = async (toolName: string, input: unknown, mode: EnhancedMode, options: { signal: AbortSignal }): Promise<PermissionResult> => {
|
||||
const isAskUserQuestion = isAskUserQuestionToolName(toolName);
|
||||
const isQuestionTool = isQuestionToolName(toolName);
|
||||
|
||||
// Check if tool is explicitly allowed
|
||||
if (!isAskUserQuestion && toolName === 'Bash') {
|
||||
if (!isQuestionTool && toolName === 'Bash') {
|
||||
const inputObj = input as { command?: string };
|
||||
if (inputObj?.command) {
|
||||
// Check literal matches
|
||||
@@ -219,7 +278,7 @@ export class PermissionHandler extends BasePermissionHandler<PermissionResponse,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!isAskUserQuestion && this.allowedTools.has(toolName)) {
|
||||
} else if (!isQuestionTool && this.allowedTools.has(toolName)) {
|
||||
return { behavior: 'allow', updatedInput: input as Record<string, unknown> };
|
||||
}
|
||||
|
||||
@@ -230,11 +289,11 @@ export class PermissionHandler extends BasePermissionHandler<PermissionResponse,
|
||||
// Handle special cases
|
||||
//
|
||||
|
||||
if (!isAskUserQuestion && this.permissionMode === 'bypassPermissions') {
|
||||
if (!isQuestionTool && this.permissionMode === 'bypassPermissions') {
|
||||
return { behavior: 'allow', updatedInput: input as Record<string, unknown> };
|
||||
}
|
||||
|
||||
if (!isAskUserQuestion && this.permissionMode === 'acceptEdits' && descriptor.edit) {
|
||||
if (!isQuestionTool && this.permissionMode === 'acceptEdits' && descriptor.edit) {
|
||||
return { behavior: 'allow', updatedInput: input as Record<string, unknown> };
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export type PermissionCompletion = {
|
||||
mode?: string;
|
||||
decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort';
|
||||
allowTools?: string[];
|
||||
answers?: Record<string, string[]>;
|
||||
answers?: Record<string, string[]> | Record<string, { answers: string[] }>;
|
||||
};
|
||||
|
||||
export type CancelPendingRequestOptions = {
|
||||
|
||||
Reference in New Issue
Block a user