blob: ed40dca3982177ffadc951c543e50241c8d1772b [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { UiLocale } from './locale-helpers.js';
import { redactSecrets } from './redact.js';
import { getToolActivityCopy } from './tool-activity/copy.js';
/** Locale-aware display name for the tool-discovery connector. */
export function loadToolDisplayName(locale: UiLocale): string {
return getToolActivityCopy(locale).loadTools.displayName;
}
export type LoadToolGroupKind =
| 'browser'
| 'computer_use'
| 'mcp'
| 'rive'
| 'agent'
| 'settings'
| 'generic';
export interface LoadToolResultDescription {
kind: LoadToolGroupKind;
actionLabel: string;
title: string;
description: string;
label: string;
countLabel: string;
groupId?: string;
toolIds: string[];
}
/**
* Turn a `tool_search` result or historical `load_tools` result into friendly,
* locale-aware card copy. Returns `null` for unexpected shapes.
*/
export function describeLoadToolResult(
args: unknown,
value: unknown,
locale: UiLocale,
): LoadToolResultDescription | null {
const record = value as { activated?: unknown; loaded?: unknown } | null | undefined;
const loaded = record?.activated ?? record?.loaded;
if (!Array.isArray(loaded) || !loaded.every((name) => typeof name === 'string')) {
return null;
}
const tools = (loaded as string[]).map(safeDisplayText).filter(Boolean);
const argRecord = args as { group?: unknown; namespace?: unknown } | null | undefined;
const rawGroup = argRecord?.group ?? argRecord?.namespace;
const resultGroup = (value as { group?: unknown }).group;
const groupRecord =
resultGroup && typeof resultGroup === 'object'
? resultGroup as { id?: unknown; label?: unknown; description?: unknown }
: undefined;
const groupId = firstSafeText(groupRecord?.id, rawGroup);
const suppliedLabel = firstSafeText(groupRecord?.label);
const suppliedDescription = firstSafeText(groupRecord?.description);
const kind = loadToolGroupKind(groupId, suppliedLabel, tools);
const n = tools.length;
const copy = getToolActivityCopy(locale).loadTools;
if (kind !== 'generic') {
const groupCopy = copy.groups[kind];
return {
kind,
actionLabel: groupCopy.action,
title: groupCopy.title,
description: groupCopy.description,
label: groupCopy.label,
countLabel: copy.count(n),
...(groupId ? { groupId } : {}),
toolIds: tools,
};
}
const label = suppliedLabel ?? copy.fallbackLabel;
return {
kind,
actionLabel: suppliedLabel ? copy.namedAction(suppliedLabel) : copy.genericAction,
title: suppliedLabel ? copy.namedTitle(suppliedLabel) : copy.genericTitle,
description: suppliedDescription ?? copy.genericDescription,
label,
countLabel: copy.count(n),
...(groupId ? { groupId } : {}),
toolIds: tools,
};
}
function firstSafeText(...values: unknown[]): string | undefined {
for (const value of values) {
if (typeof value !== 'string') continue;
const safe = safeDisplayText(value);
if (safe) return safe;
}
return undefined;
}
function safeDisplayText(value: string): string {
return redactSecrets(value.replace(/[\u0000-\u001f\u007f-\u009f]+/g, ' ').replace(/\s+/g, ' ').trim());
}
function loadToolGroupKind(
groupId: string | undefined,
label: string | undefined,
tools: readonly string[],
): LoadToolGroupKind {
const id = groupId?.toLowerCase() ?? '';
const normalizedLabel = label?.toLowerCase() ?? '';
const names = tools.map((name) => name.toLowerCase());
const hasTool = (name: string) =>
names.some((candidate) => candidate === name || candidate.endsWith(`__${name}`));
if (
id === 'computer_use'
|| id.endsWith('_desktop_computer_use')
|| normalizedLabel === 'computer use'
|| hasTool('maka_computer')
) return 'computer_use';
if (
id === 'browser'
|| id.endsWith('_desktop_browser')
|| normalizedLabel === 'browser'
|| hasTool('browser_navigate')
) return 'browser';
if (
id === 'rive'
|| id.endsWith('_desktop_rive')
|| normalizedLabel === 'rive'
|| hasTool('riveworkflow')
) return 'rive';
if (
id === 'agent'
|| normalizedLabel === 'agent'
|| hasTool('agent_spawn')
) return 'agent';
if (
id.endsWith('_desktop_settings')
|| normalizedLabel === 'client settings'
|| hasTool('makasettingsget')
) return 'settings';
if (id.endsWith('_desktop_mcp') || normalizedLabel === 'mcp') return 'mcp';
return 'generic';
}
export function formatRedactedJson(value: unknown): string {
try {
return redactSecrets(JSON.stringify(value, null, 2));
} catch {
return redactSecrets(String(value));
}
}
export function formatToolIntent(intent: string): string {
const safe = redactSecrets(intent.replace(/\s+/g, ' ').trim());
return safe.length > 240 ? `${safe.slice(0, 240)}…` : safe;
}