feat(coding-agent/modes): added timeout support to hook selector with auto-selection on timeout
- Added timeout support to hook selector with auto-selection behavior on timeout. - Extended ExtensionUIDialogOptions to include timeout and tui configuration properties. - Changed hook selector timeout behavior from cancellation to auto-selecting the currently highlighted option. - Updated UIContext.select() method signature to accept timeout option for automated selection scenarios.
This commit is contained in:
@@ -50,7 +50,15 @@ export class HookSelectorComponent extends Container {
|
||||
opts.timeout,
|
||||
opts.tui,
|
||||
s => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)),
|
||||
() => this.onCancelCallback(),
|
||||
() => {
|
||||
// Auto-select current option on timeout (typically the first/recommended option)
|
||||
const selected = this.options[this.selectedIndex];
|
||||
if (selected) {
|
||||
this.onSelectCallback(selected);
|
||||
} else {
|
||||
this.onCancelCallback();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
ExtensionContextActions,
|
||||
ExtensionError,
|
||||
ExtensionUIContext,
|
||||
ExtensionUIDialogOptions,
|
||||
} from "../../extensibility/extensions";
|
||||
import { HookEditorComponent } from "../../modes/components/hook-editor";
|
||||
import { HookInputComponent } from "../../modes/components/hook-input";
|
||||
@@ -25,7 +26,7 @@ export class ExtensionUiController {
|
||||
async initHooksAndCustomTools(): Promise<void> {
|
||||
// Create and set hook & tool UI context
|
||||
const uiContext: ExtensionUIContext = {
|
||||
select: (title, options, dialogOptions) => this.showHookSelector(title, options, dialogOptions?.initialIndex),
|
||||
select: (title, options, dialogOptions) => this.showHookSelector(title, options, dialogOptions),
|
||||
confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
|
||||
input: (title, placeholder, _dialogOptions) => this.showHookInput(title, placeholder),
|
||||
notify: (message, type) => this.showHookNotify(message, type),
|
||||
@@ -484,7 +485,11 @@ export class ExtensionUiController {
|
||||
/**
|
||||
* Show a selector for hooks.
|
||||
*/
|
||||
showHookSelector(title: string, options: string[], initialIndex?: number): Promise<string | undefined> {
|
||||
showHookSelector(
|
||||
title: string,
|
||||
options: string[],
|
||||
dialogOptions?: ExtensionUIDialogOptions,
|
||||
): Promise<string | undefined> {
|
||||
const { promise, resolve } = Promise.withResolvers<string | undefined>();
|
||||
this.ctx.hookSelector = new HookSelectorComponent(
|
||||
title,
|
||||
@@ -497,7 +502,7 @@ export class ExtensionUiController {
|
||||
this.hideHookSelector();
|
||||
resolve(undefined);
|
||||
},
|
||||
{ initialIndex },
|
||||
{ initialIndex: dialogOptions?.initialIndex, timeout: dialogOptions?.timeout, tui: this.ctx.ui },
|
||||
);
|
||||
|
||||
this.ctx.editorContainer.clear();
|
||||
|
||||
@@ -20,7 +20,7 @@ import chalk from "chalk";
|
||||
import { KeybindingsManager } from "../config/keybindings";
|
||||
import { renderPromptTemplate } from "../config/prompt-templates";
|
||||
import type { SettingsManager } from "../config/settings-manager";
|
||||
import type { ExtensionUIContext } from "../extensibility/extensions";
|
||||
import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
|
||||
import type { CompactOptions } from "../extensibility/extensions/types";
|
||||
import { loadSlashCommands } from "../extensibility/slash-commands";
|
||||
import { resolvePlanUrlToPath } from "../internal-urls";
|
||||
@@ -1020,8 +1020,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
this.extensionUiController.setHookStatus(key, text);
|
||||
}
|
||||
|
||||
showHookSelector(title: string, options: string[], initialIndex?: number): Promise<string | undefined> {
|
||||
return this.extensionUiController.showHookSelector(title, options, initialIndex);
|
||||
showHookSelector(
|
||||
title: string,
|
||||
options: string[],
|
||||
dialogOptions?: ExtensionUIDialogOptions,
|
||||
): Promise<string | undefined> {
|
||||
return this.extensionUiController.showHookSelector(title, options, dialogOptions);
|
||||
}
|
||||
|
||||
hideHookSelector(): void {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-m
|
||||
import type { Component, Container, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
|
||||
import type { KeybindingsManager } from "../config/keybindings";
|
||||
import type { SettingsManager } from "../config/settings-manager";
|
||||
import type { ExtensionUIContext } from "../extensibility/extensions";
|
||||
import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
|
||||
import type { CompactOptions } from "../extensibility/extensions/types";
|
||||
import type { MCPManager } from "../mcp";
|
||||
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
||||
@@ -189,7 +189,11 @@ export interface InteractiveModeContext {
|
||||
): Promise<void>;
|
||||
setHookWidget(key: string, content: unknown): void;
|
||||
setHookStatus(key: string, text: string | undefined): void;
|
||||
showHookSelector(title: string, options: string[], initialIndex?: number): Promise<string | undefined>;
|
||||
showHookSelector(
|
||||
title: string,
|
||||
options: string[],
|
||||
dialogOptions?: ExtensionUIDialogOptions,
|
||||
): Promise<string | undefined>;
|
||||
hideHookSelector(): void;
|
||||
showHookInput(title: string, placeholder?: string): Promise<string | undefined>;
|
||||
hideHookInput(): void;
|
||||
|
||||
@@ -10,7 +10,7 @@ Ask the user a question when you need clarification or input during task executi
|
||||
</conditions>
|
||||
|
||||
<instruction>
|
||||
- Place recommended option first with " (Recommended)" suffix
|
||||
- Use `recommended: <index>` to mark the default option (0-indexed); " (Recommended)" suffix is added automatically
|
||||
- Use `questions` array for multiple related questions instead of asking one at a time
|
||||
- Set `multi: true` on a question to allow multiple selections
|
||||
</instruction>
|
||||
@@ -37,12 +37,13 @@ If you can make a reasonable inference from the user's request, **do it**. Users
|
||||
|
||||
<example name="single">
|
||||
question: "Which authentication method should this API use?"
|
||||
options: [{"label": "JWT (Recommended)"}, {"label": "OAuth2"}, {"label": "Session cookies"}]
|
||||
options: [{"label": "JWT"}, {"label": "OAuth2"}, {"label": "Session cookies"}]
|
||||
recommended: 0
|
||||
</example>
|
||||
|
||||
<example name="multi-part">
|
||||
questions: [
|
||||
{"id": "auth", "question": "Which auth method?", "options": [{"label": "JWT"}, {"label": "OAuth2"}]},
|
||||
{"id": "auth", "question": "Which auth method?", "options": [{"label": "JWT"}, {"label": "OAuth2"}], "recommended": 0},
|
||||
{"id": "cache", "question": "Enable caching?", "options": [{"label": "Yes"}, {"label": "No"}]},
|
||||
{"id": "features", "question": "Which features to include?", "options": [{"label": "Logging"}, {"label": "Metrics"}, {"label": "Tracing"}], "multi": true}
|
||||
]
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
* Usage notes:
|
||||
* - Users will always be able to select "Other" to provide custom text input
|
||||
* - Use multi: true to allow multiple answers to be selected for a question
|
||||
* - If you recommend a specific option, make that the first option in the list
|
||||
* and add "(Recommended)" at the end of the label
|
||||
* - Use recommended: <index> to mark the default option; "(Recommended)" suffix is added automatically
|
||||
* - Questions time out after 30 seconds and auto-select the recommended option
|
||||
*/
|
||||
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
@@ -39,12 +39,14 @@ const QuestionItem = Type.Object({
|
||||
question: Type.String({ description: "Question text" }),
|
||||
options: Type.Array(OptionItem, { description: "Available options" }),
|
||||
multi: Type.Optional(Type.Boolean({ description: "Allow multiple selections" })),
|
||||
recommended: Type.Optional(Type.Number({ description: "Index of recommended option (0-indexed)" })),
|
||||
});
|
||||
|
||||
const askSchema = Type.Object({
|
||||
question: Type.Optional(Type.String({ description: "Question to ask" })),
|
||||
options: Type.Optional(Type.Array(OptionItem, { description: "Available options" })),
|
||||
multi: Type.Optional(Type.Boolean({ description: "Allow multiple selections (default: false)" })),
|
||||
recommended: Type.Optional(Type.Number({ description: "Index of recommended option (0-indexed, default: 0)" })),
|
||||
questions: Type.Optional(Type.Array(QuestionItem, { description: "Multiple questions in sequence" })),
|
||||
});
|
||||
|
||||
@@ -74,10 +76,30 @@ export interface AskToolDetails {
|
||||
// =============================================================================
|
||||
|
||||
const OTHER_OPTION = "Other (type your own)";
|
||||
const RECOMMENDED_SUFFIX = " (Recommended)";
|
||||
|
||||
function getDoneOptionLabel(): string {
|
||||
return `${theme.status.success} Done selecting`;
|
||||
}
|
||||
|
||||
/** Add "(Recommended)" suffix to the option at the given index if not already present */
|
||||
function addRecommendedSuffix(labels: string[], recommendedIndex?: number): string[] {
|
||||
if (recommendedIndex === undefined || recommendedIndex < 0 || recommendedIndex >= labels.length) {
|
||||
return labels;
|
||||
}
|
||||
return labels.map((label, i) => {
|
||||
if (i === recommendedIndex && !label.endsWith(RECOMMENDED_SUFFIX)) {
|
||||
return label + RECOMMENDED_SUFFIX;
|
||||
}
|
||||
return label;
|
||||
});
|
||||
}
|
||||
|
||||
/** Strip "(Recommended)" suffix from a label */
|
||||
function stripRecommendedSuffix(label: string): string {
|
||||
return label.endsWith(RECOMMENDED_SUFFIX) ? label.slice(0, -RECOMMENDED_SUFFIX.length) : label;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Question Selection Logic
|
||||
// =============================================================================
|
||||
@@ -88,7 +110,11 @@ interface SelectionResult {
|
||||
}
|
||||
|
||||
interface UIContext {
|
||||
select(prompt: string, options: string[], options_?: { initialIndex?: number }): Promise<string | undefined>;
|
||||
select(
|
||||
prompt: string,
|
||||
options: string[],
|
||||
options_?: { initialIndex?: number; timeout?: number },
|
||||
): Promise<string | undefined>;
|
||||
input(prompt: string): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
@@ -97,6 +123,7 @@ async function askSingleQuestion(
|
||||
question: string,
|
||||
optionLabels: string[],
|
||||
multi: boolean,
|
||||
recommended?: number,
|
||||
): Promise<SelectionResult> {
|
||||
const doneLabel = getDoneOptionLabel();
|
||||
let selectedOptions: string[] = [];
|
||||
@@ -155,12 +182,16 @@ async function askSingleQuestion(
|
||||
}
|
||||
selectedOptions = Array.from(selected);
|
||||
} else {
|
||||
const choice = await ui.select(question, [...optionLabels, OTHER_OPTION]);
|
||||
const displayLabels = addRecommendedSuffix(optionLabels, recommended);
|
||||
const choice = await ui.select(question, [...displayLabels, OTHER_OPTION], {
|
||||
timeout: 30000,
|
||||
initialIndex: recommended,
|
||||
});
|
||||
if (choice === OTHER_OPTION) {
|
||||
const input = await ui.input("Enter your response:");
|
||||
if (input) customInput = input;
|
||||
} else if (choice) {
|
||||
selectedOptions = [choice];
|
||||
selectedOptions = [stripRecommendedSuffix(choice)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,11 +218,13 @@ interface AskParams {
|
||||
question?: string;
|
||||
options?: Array<{ label: string }>;
|
||||
multi?: boolean;
|
||||
recommended?: number;
|
||||
questions?: Array<{
|
||||
id: string;
|
||||
question: string;
|
||||
options: Array<{ label: string }>;
|
||||
multi?: boolean;
|
||||
recommended?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -243,6 +276,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
||||
q.question,
|
||||
optionLabels,
|
||||
q.multi ?? false,
|
||||
q.recommended,
|
||||
);
|
||||
|
||||
results.push({
|
||||
@@ -275,7 +309,13 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
||||
};
|
||||
}
|
||||
|
||||
const { selectedOptions, customInput } = await askSingleQuestion(ui, question, optionLabels, multi);
|
||||
const { selectedOptions, customInput } = await askSingleQuestion(
|
||||
ui,
|
||||
question,
|
||||
optionLabels,
|
||||
multi,
|
||||
params.recommended,
|
||||
);
|
||||
|
||||
const details: AskToolDetails = {
|
||||
question,
|
||||
|
||||
Reference in New Issue
Block a user