qwen-code/packages/web-shell/client/adapters/transcriptAdapter.ts
jinye 31281f6a7d
feat(acp): dedicated agent permission dialog via _meta.toolName (follow-up to #5085) (#5105)
* feat(acp): carry _meta.toolName on permission frame; agent drawer (vscode)

WIP: producer mirrors _meta.toolName onto session/request_permission;
webui PermissionDrawer + vscode webview map it to render 'Launch this
agent?' for the Agent tool without a protocol kind. Daemon/web-shell
surface + tests follow.

* feat(web-shell): dedicated agent permission prompt via _meta.toolName

Thread the canonical tool name from the permission frame's _meta.toolName
through web-shell's PermissionRequest so ToolApproval renders 'Launch this
agent?' for the Agent tool, mirroring the vscode PermissionDrawer. Add
tests for the toolName extraction and the agent drawer title.

* fix(acp): mirror _meta.toolName on second producer path + address review

Address @wenshao's review on #5105:

- [Critical] SubAgentTracker's approval handler builds its own
  RequestPermissionRequest (the second producer path, for nested sub-agent
  tool calls) and was missing `_meta: { toolName }`. Session.ts adds it on
  the primary path; mirror it here so nested agents (and any future tool
  relying on _meta.toolName for specialized UI) don't fall back to the
  generic prompt. Locked with a _meta assertion in the approval test.

- Dedupe the three hardcoded 'agent' string matches behind a single shared
  `AGENT_TOOL_NAME` / `isAgentTool` in @qwen-code/webui (re-exported via
  daemon-react-sdk, same pattern as DAEMON_APPROVAL_MODES), consumed by
  PermissionDrawer (webui) and ToolApproval (web-shell).

- Move the agent check to the top of PermissionDrawer.getTitle() so it wins
  over kind-based checks, matching ToolApproval's isAgent-first ordering
  across the two surfaces.

- Extract the _meta.toolName lifting logic in useWebViewMessages into a
  testable `liftToolNameFromMeta` helper and cover the three cases wenshao
  flagged: lift onto toolName, preserve a pre-existing toolName, no-op when
  _meta is absent (plus undefined-toolCall guard).

🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
2026-06-14 23:27:38 +08:00

97 lines
2.6 KiB
TypeScript

import type { DaemonTranscriptBlock } from '@qwen-code/webui/daemon-react-sdk';
import type { PermissionRequest, PermissionOptionKind } from './types';
type PermissionTranscriptBlock = Extract<
DaemonTranscriptBlock,
{ kind: 'permission' }
>;
export function extractPendingPermission(
blocks: readonly DaemonTranscriptBlock[],
): PermissionRequest | null {
for (const block of blocks) {
if (!isPermissionBlock(block)) continue;
const perm = block;
if (perm.resolved) continue;
const toolCallRecord = getRecord(perm.toolCall);
const toolCallId =
typeof toolCallRecord?.['toolCallId'] === 'string'
? toolCallRecord['toolCallId']
: typeof toolCallRecord?.['id'] === 'string'
? toolCallRecord['id']
: undefined;
const toolKind =
typeof toolCallRecord?.['kind'] === 'string'
? toolCallRecord['kind']
: undefined;
const metaRecord = getRecord(toolCallRecord?.['_meta']);
const toolName =
typeof metaRecord?.['toolName'] === 'string'
? metaRecord['toolName']
: undefined;
return {
id: perm.requestId,
sessionId: perm.sessionId,
toolCallId,
title: perm.title,
toolKind,
toolName,
content: [
{
type: 'text',
text: perm.title || 'Tool permission',
},
],
options: perm.options.map((opt) => ({
id: opt.optionId,
label: opt.label,
kind: getPermissionOptionKind(opt.raw),
})),
rawInput: getPermissionRawInput(perm.toolCall),
};
}
return null;
}
function isPermissionBlock(
block: DaemonTranscriptBlock,
): block is PermissionTranscriptBlock {
return block.kind === 'permission';
}
function getPermissionRawInput(
toolCall: unknown,
): Record<string, unknown> | undefined {
const record = getRecord(toolCall);
if (!record) {
return undefined;
}
const nested =
getRecord(record['rawInput']) ??
getRecord(record['input']) ??
getRecord(record['args']);
return nested ?? record;
}
function getRecord(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return undefined;
}
return value as Record<string, unknown>;
}
function getPermissionOptionKind(
raw: unknown,
): PermissionOptionKind | undefined {
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
return undefined;
}
const kind = (raw as Record<string, unknown>).kind;
return kind === 'allow_once' ||
kind === 'allow_always' ||
kind === 'reject_once' ||
kind === 'reject_always'
? kind
: undefined;
}