Files
qwen-code/packages/web-shell/client/components/managed/managed-approval.ts
T
jinyeandQwen-Coder 32d22f950c feat(managed-agent): support durable structured user questions (#16433)
Connect Hosted question actions, structured answers, durable continuation, and the WebShell question form. Cover canonical response replay and pending-question recovery across Harness and Java context restarts.

Co-authored-by: Qwen-Coder <[email protected]>
2026-11-06 09:23:44 +00:00

120 lines
3.9 KiB
TypeScript

import { escapePreviewText } from '../../adapters/transcriptAdapter';
import type {
ACPToolCall,
Message,
PermissionOptionKind,
PermissionRequest,
} from '../../adapters/types';
import type { ManagedAgentPendingAction } from './managed-agent-provider';
// The service documents allow and deny as stable option IDs. Mapping them to
// the ordinary option kinds lets the shared approval card localize the labels.
const OPTION_KINDS: Readonly<Record<string, PermissionOptionKind>> = {
allow: 'allow_once',
deny: 'reject_once',
};
/**
* Finds the transcript tool that a pending Action asks about. Managed tool
* rows are keyed `${turnId}:${toolCallId}`, and the Action's functionCallId is
* that tool call ID; without a resolved Turn only the call ID can match.
*/
export function findManagedApprovalTool(
messages: readonly Message[],
action: ManagedAgentPendingAction,
): ACPToolCall | undefined {
const exact = action.turnId
? `${action.turnId}:${action.functionCallId}`
: undefined;
const suffix = `:${action.functionCallId}`;
let found: ACPToolCall | undefined;
for (const message of messages) {
if (message.role !== 'tool_group') continue;
for (const tool of message.tools) {
// Rows keyed by a Java itemId keep the call ID separately.
const matches =
tool.toolCallId !== undefined
? tool.toolCallId === action.functionCallId &&
(!action.turnId || tool.callId.startsWith(`${action.turnId}:`))
: exact
? tool.callId === exact
: tool.callId.endsWith(suffix);
if (matches) found = tool;
}
}
return found;
}
/** Presents a pending Hosted approval through the shared approval card. */
export function toManagedPermissionRequest(
action: ManagedAgentPendingAction,
messages: readonly Message[],
): PermissionRequest {
const tool = findManagedApprovalTool(messages, action);
const inputPreview =
action.kind === 'question' ? undefined : action.inputPreview;
let rawInput = tool?.args;
if (
!rawInput &&
action.toolName === 'exit_plan_mode' &&
inputPreview &&
!inputPreview.truncated
) {
try {
const preview: unknown = JSON.parse(inputPreview.text);
if (preview && typeof preview === 'object' && !Array.isArray(preview)) {
const input = (preview as Record<string, unknown>)['input'];
if (
input &&
typeof input === 'object' &&
!Array.isArray(input) &&
typeof (input as Record<string, unknown>)['plan'] === 'string'
)
rawInput = input as Record<string, unknown>;
}
} catch {
/* An incomplete preview remains explicitly marked as such. */
}
}
const toolCallId =
tool?.callId ??
(action.turnId ? `${action.turnId}:${action.functionCallId}` : undefined);
return {
id: action.actionId,
sessionId: action.sessionId,
...(toolCallId ? { toolCallId } : {}),
toolName: action.toolName,
...(action.toolName === 'exit_plan_mode'
? { toolKind: 'switch_mode' as const }
: {}),
title: tool?.title ?? action.toolName,
content: rawInput
? [
{
type: 'text',
text: escapePreviewText(
action.toolName === 'exit_plan_mode' &&
typeof rawInput['plan'] === 'string'
? rawInput['plan']
: JSON.stringify(rawInput, null, 2),
),
},
]
: inputPreview
? [{ type: 'text', text: escapePreviewText(inputPreview.text) }]
: [],
...(rawInput
? { rawInput, contentIsInput: true }
: inputPreview
? { contentIsInput: true }
: {}),
options: (action.kind === 'question' ? [] : action.options).map(
(option) => ({
id: option.id,
label: option.label,
...(OPTION_KINDS[option.id] ? { kind: OPTION_KINDS[option.id] } : {}),
}),
),
};
}