When auto-approve is disabled, teammate tool requests arrived as permission_request JSON via SendMessage and rendered as "Raw JSON" with no way to approve/deny (#29). - Intercept permission_request in lead inbox relay, convert to ToolApprovalRequest and show in existing ToolApprovalSheet - Respond via teammate inbox (permission_response) + control_response via stdin as fallback - Show teammate name in approval header (e.g. "bob — Bash") - Compact noise label in Messages panel for permission_request/response - Proper file locking, race condition protection, idempotency checks
139 lines
3.8 KiB
TypeScript
139 lines
3.8 KiB
TypeScript
import { MESSAGE_REPLY_TAG } from '@shared/constants/agentBlocks';
|
|
|
|
type StructuredAgentMessage = Record<string, unknown>;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Reply block parsing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface ParsedMessageReply {
|
|
agentName: string;
|
|
originalText: string;
|
|
replyText: string;
|
|
}
|
|
|
|
const REPLY_BLOCK_RE = new RegExp(
|
|
'```' +
|
|
MESSAGE_REPLY_TAG +
|
|
'\\nReply on @([\\w.-]+) original message with text "([\\s\\S]*?)", here is answer: "([\\s\\S]*?)"\\n```'
|
|
);
|
|
|
|
function encodeReplyField(value: string): string {
|
|
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
}
|
|
|
|
function decodeReplyField(value: string): string {
|
|
// Backwards-compat: avoid touching legacy content that has no escapes.
|
|
if (!value.includes('\\"') && !value.includes('\\\\')) return value;
|
|
return value.replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
}
|
|
|
|
/**
|
|
* Parses a message_reply_for_agent block from content.
|
|
* Returns null if no reply block is found.
|
|
*/
|
|
export function parseMessageReply(content: string): ParsedMessageReply | null {
|
|
const match = REPLY_BLOCK_RE.exec(content);
|
|
if (!match) return null;
|
|
return {
|
|
agentName: match[1],
|
|
originalText: decodeReplyField(match[2]),
|
|
replyText: decodeReplyField(match[3]),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Builds a reply block string for sending.
|
|
*/
|
|
export function buildReplyBlock(
|
|
agentName: string,
|
|
originalText: string,
|
|
replyText: string
|
|
): string {
|
|
const tag = MESSAGE_REPLY_TAG;
|
|
return [
|
|
'```' + tag,
|
|
`Reply on @${agentName} original message with text "${encodeReplyField(originalText)}", here is answer: "${encodeReplyField(replyText)}"`,
|
|
'```',
|
|
].join('\n');
|
|
}
|
|
|
|
const NOISE_TYPES = new Set([
|
|
'idle_notification',
|
|
'shutdown_approved',
|
|
'teammate_terminated',
|
|
'shutdown_request',
|
|
]);
|
|
|
|
const TYPE_LABELS: Record<string, string> = {
|
|
idle_notification: 'Idle',
|
|
shutdown_approved: 'Shutdown confirmed',
|
|
teammate_terminated: 'Terminated',
|
|
shutdown_request: 'Shutdown requested',
|
|
shutdown_response: 'Shutdown response',
|
|
message: 'Message',
|
|
broadcast: 'Broadcast',
|
|
permission_request: 'Permission request',
|
|
permission_response: 'Permission response',
|
|
};
|
|
|
|
export function parseStructuredAgentMessage(content: string): StructuredAgentMessage | null {
|
|
const trimmed = content.trim();
|
|
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(trimmed) as unknown;
|
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
return parsed as StructuredAgentMessage;
|
|
}
|
|
} catch {
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function getMessageTypeLabel(type: string | null): string | null {
|
|
if (!type) {
|
|
return null;
|
|
}
|
|
return TYPE_LABELS[type] ?? type;
|
|
}
|
|
|
|
function getStringField(value: unknown): string | null {
|
|
return typeof value === 'string' && value.trim() !== '' ? value : null;
|
|
}
|
|
|
|
export function detectOperationalNoise(content: string, teammateId: string): string | null {
|
|
const trimmed = content.trim();
|
|
const parsed = parseStructuredAgentMessage(trimmed);
|
|
const type = getStringField(parsed?.type);
|
|
|
|
if (type && NOISE_TYPES.has(type)) {
|
|
const message = getStringField(parsed?.message);
|
|
return message ?? getMessageTypeLabel(type) ?? type;
|
|
}
|
|
|
|
if (teammateId === 'system') {
|
|
return trimmed.length < 200 ? trimmed : null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function getStructuredMessageSummary(parsed: StructuredAgentMessage): string {
|
|
const explicitSummary = getStringField(parsed.summary);
|
|
if (explicitSummary) {
|
|
return explicitSummary;
|
|
}
|
|
|
|
const message = getStringField(parsed.message);
|
|
if (message) {
|
|
return message;
|
|
}
|
|
|
|
const type = getStringField(parsed.type);
|
|
return getMessageTypeLabel(type) ?? 'Structured message';
|
|
}
|