fix(ui): localize agent controls

This commit is contained in:
Vincent Koc
2026-07-12 13:45:11 +08:00
committed by Vincent Koc
parent eb4e3b2472
commit b9f6cae6bc
4 changed files with 356 additions and 210 deletions
+138 -43
View File
@@ -15,6 +15,7 @@ import type {
ToolsCatalogResult,
} from "../../api/types.ts";
import { controlUiPublicAssetPath } from "../../app/public-assets.ts";
import { t } from "../../i18n/index.ts";
import { resolveAgentAvatarUrl, resolveAssistantTextAvatar } from "../avatar.ts";
import { buildQualifiedChatModelValue } from "../chat/model-ref.ts";
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "../string-coerce.ts";
@@ -37,95 +38,176 @@ export type AgentToolSection = {
tools: AgentToolEntry[];
};
const FALLBACK_TOOL_SECTIONS: AgentToolSection[] = [
type FallbackToolEntry = Omit<AgentToolEntry, "description"> & {
descriptionKey: string;
};
type FallbackToolSection = Omit<AgentToolSection, "label" | "tools"> & {
labelKey: string;
tools: FallbackToolEntry[];
};
const FALLBACK_TOOL_SECTIONS: FallbackToolSection[] = [
{
id: "fs",
label: "Files",
labelKey: "agents.toolCatalog.groups.files",
tools: [
{ id: "read", label: "read", description: "Read file contents" },
{ id: "write", label: "write", description: "Create or overwrite files" },
{ id: "edit", label: "edit", description: "Make precise edits" },
{ id: "apply_patch", label: "apply_patch", description: "Patch files (OpenAI)" },
{ id: "read", label: "read", descriptionKey: "agents.toolCatalog.descriptions.read" },
{ id: "write", label: "write", descriptionKey: "agents.toolCatalog.descriptions.write" },
{ id: "edit", label: "edit", descriptionKey: "agents.toolCatalog.descriptions.edit" },
{
id: "apply_patch",
label: "apply_patch",
descriptionKey: "agents.toolCatalog.descriptions.applyPatch",
},
],
},
{
id: "runtime",
label: "Runtime",
labelKey: "agents.toolCatalog.groups.runtime",
tools: [
{ id: "exec", label: "exec", description: "Run shell commands" },
{ id: "process", label: "process", description: "Manage background processes" },
{ id: "exec", label: "exec", descriptionKey: "agents.toolCatalog.descriptions.exec" },
{
id: "process",
label: "process",
descriptionKey: "agents.toolCatalog.descriptions.process",
},
],
},
{
id: "web",
label: "Web",
labelKey: "agents.toolCatalog.groups.web",
tools: [
{ id: "web_search", label: "web_search", description: "Search the web" },
{ id: "web_fetch", label: "web_fetch", description: "Fetch web content" },
{
id: "web_search",
label: "web_search",
descriptionKey: "agents.toolCatalog.descriptions.webSearch",
},
{
id: "web_fetch",
label: "web_fetch",
descriptionKey: "agents.toolCatalog.descriptions.webFetch",
},
],
},
{
id: "memory",
label: "Memory",
labelKey: "agents.toolCatalog.groups.memory",
tools: [
{ id: "memory_search", label: "memory_search", description: "Semantic search" },
{ id: "memory_get", label: "memory_get", description: "Read memory files" },
{
id: "memory_search",
label: "memory_search",
descriptionKey: "agents.toolCatalog.descriptions.memorySearch",
},
{
id: "memory_get",
label: "memory_get",
descriptionKey: "agents.toolCatalog.descriptions.memoryGet",
},
],
},
{
id: "sessions",
label: "Sessions",
labelKey: "agents.toolCatalog.groups.sessions",
tools: [
{ id: "sessions_list", label: "sessions_list", description: "List sessions" },
{ id: "sessions_history", label: "sessions_history", description: "Session history" },
{ id: "sessions_send", label: "sessions_send", description: "Send to session" },
{ id: "sessions_spawn", label: "sessions_spawn", description: "Spawn sub-agent" },
{ id: "session_status", label: "session_status", description: "Session status" },
{
id: "sessions_list",
label: "sessions_list",
descriptionKey: "agents.toolCatalog.descriptions.sessionsList",
},
{
id: "sessions_history",
label: "sessions_history",
descriptionKey: "agents.toolCatalog.descriptions.sessionsHistory",
},
{
id: "sessions_send",
label: "sessions_send",
descriptionKey: "agents.toolCatalog.descriptions.sessionsSend",
},
{
id: "sessions_spawn",
label: "sessions_spawn",
descriptionKey: "agents.toolCatalog.descriptions.sessionsSpawn",
},
{
id: "session_status",
label: "session_status",
descriptionKey: "agents.toolCatalog.descriptions.sessionStatus",
},
],
},
{
id: "ui",
label: "UI",
labelKey: "agents.toolCatalog.groups.ui",
tools: [
{ id: "browser", label: "browser", description: "Control web browser" },
{ id: "canvas", label: "canvas", description: "Control canvases" },
{
id: "browser",
label: "browser",
descriptionKey: "agents.toolCatalog.descriptions.browser",
},
{
id: "canvas",
label: "canvas",
descriptionKey: "agents.toolCatalog.descriptions.canvas",
},
],
},
{
id: "messaging",
label: "Messaging",
tools: [{ id: "message", label: "message", description: "Send messages" }],
labelKey: "agents.toolCatalog.groups.messaging",
tools: [
{
id: "message",
label: "message",
descriptionKey: "agents.toolCatalog.descriptions.message",
},
],
},
{
id: "automation",
label: "Automation",
labelKey: "agents.toolCatalog.groups.automation",
tools: [
{ id: "cron", label: "cron", description: "Schedule tasks" },
{ id: "gateway", label: "gateway", description: "Gateway control" },
{ id: "cron", label: "cron", descriptionKey: "agents.toolCatalog.descriptions.cron" },
{
id: "gateway",
label: "gateway",
descriptionKey: "agents.toolCatalog.descriptions.gateway",
},
],
},
{
id: "nodes",
label: "Nodes",
tools: [{ id: "nodes", label: "nodes", description: "Nodes + devices" }],
labelKey: "agents.toolCatalog.groups.nodes",
tools: [
{ id: "nodes", label: "nodes", descriptionKey: "agents.toolCatalog.descriptions.nodes" },
],
},
{
id: "agents",
label: "Agents",
tools: [{ id: "agents_list", label: "agents_list", description: "List agents" }],
labelKey: "agents.toolCatalog.groups.agents",
tools: [
{
id: "agents_list",
label: "agents_list",
descriptionKey: "agents.toolCatalog.descriptions.agentsList",
},
],
},
{
id: "media",
label: "Media",
tools: [{ id: "image", label: "image", description: "Image understanding" }],
labelKey: "agents.toolCatalog.groups.media",
tools: [
{ id: "image", label: "image", descriptionKey: "agents.toolCatalog.descriptions.image" },
],
},
];
const PROFILE_OPTIONS = [
{ id: "minimal", label: "Minimal" },
{ id: "coding", label: "Coding" },
{ id: "messaging", label: "Messaging" },
{ id: "full", label: "Full" },
{ id: "minimal", labelKey: "agents.toolCatalog.profiles.minimal" },
{ id: "coding", labelKey: "agents.toolCatalog.profiles.coding" },
{ id: "messaging", labelKey: "agents.toolCatalog.profiles.messaging" },
{ id: "full", labelKey: "agents.toolCatalog.profiles.full" },
] as const;
export function resolveToolSections(
@@ -148,16 +230,27 @@ export function resolveToolSections(
})),
}));
}
return FALLBACK_TOOL_SECTIONS;
return FALLBACK_TOOL_SECTIONS.map((section) => ({
id: section.id,
label: t(section.labelKey),
tools: section.tools.map((tool) => ({
id: tool.id,
label: tool.label,
description: t(tool.descriptionKey),
})),
}));
}
export function resolveToolProfileOptions(
toolsCatalogResult: ToolsCatalogResult | null,
): readonly ToolCatalogProfile[] | typeof PROFILE_OPTIONS {
): readonly ToolCatalogProfile[] | ReadonlyArray<{ id: string; label: string }> {
if (toolsCatalogResult?.profiles?.length) {
return toolsCatalogResult.profiles;
}
return PROFILE_OPTIONS;
return PROFILE_OPTIONS.map((profile) => ({
id: profile.id,
label: t(profile.labelKey),
}));
}
type ToolPolicy = {
@@ -303,7 +396,9 @@ export function buildAgentContext(
runtime,
identityName,
identityAvatar,
skillsLabel: skillFilter ? `${skillCount} selected` : "all skills",
skillsLabel: skillFilter
? t("agents.overview.selectedSkills", { count: String(skillCount) })
: t("agents.overview.allSkills"),
isDefault: Boolean(defaultId && agent.id === defaultId),
};
}
+50 -62
View File
@@ -635,19 +635,21 @@ function renderWikiPreviewOverlay(props: DreamingProps) {
<div class="dreams-diary__preview-panel" @click=${(event: Event) => event.stopPropagation()}>
<div class="dreams-diary__preview-header">
<div>
<div class="dreams-diary__preview-title">${state.wikiPreviewTitle || "Wiki page"}</div>
<div class="dreams-diary__preview-title">
${state.wikiPreviewTitle || t("dreaming.wiki.previewFallbackTitle")}
</div>
<div class="dreams-diary__preview-meta">
${state.wikiPreviewPath}
${state.wikiPreviewUpdatedAt ? ` · ${state.wikiPreviewUpdatedAt}` : ""}
</div>
</div>
<button class="btn btn--subtle btn--sm" @click=${() => closeWikiPreview(props)}>
Close
${t("dreaming.wiki.close")}
</button>
</div>
<div class="dreams-diary__preview-body">
${state.wikiPreviewLoading
? html`<div class="dreams-diary__empty-text">Loading wiki page…</div>`
? html`<div class="dreams-diary__empty-text">${t("dreaming.wiki.loadingPage")}</div>`
: state.wikiPreviewError
? html`<div class="dreams-diary__error">${state.wikiPreviewError}</div>`
: html`
@@ -672,28 +674,11 @@ function renderWikiPreviewOverlay(props: DreamingProps) {
function renderDiarySubtabExplainer(activeDiarySubTab: DreamingViewState["activeDiarySubTab"]) {
switch (activeDiarySubTab) {
case "dreams":
return html`
<p class="dreams-diary__explainer">
This is the raw dream diary the system writes while replaying and consolidating memory;
use it to inspect what the memory system is noticing, and where it still looks noisy or
thin.
</p>
`;
return html` <p class="dreams-diary__explainer">${t("dreaming.wiki.dreamsExplainer")}</p> `;
case "insights":
return html`
<p class="dreams-diary__explainer">
These are imported insights clustered from external history; use them to review what
imports surfaced before any of it graduates into durable memory.
</p>
`;
return html` <p class="dreams-diary__explainer">${t("dreaming.wiki.insightsExplainer")}</p> `;
case "palace":
return html`
<p class="dreams-diary__explainer">
This is the compiled memory wiki surface the system can search and reason over; use it to
inspect actual memory pages, claims, open questions, and contradictions rather than raw
imported source chats.
</p>
`;
return html` <p class="dreams-diary__explainer">${t("dreaming.wiki.palaceExplainer")}</p> `;
}
return nothing;
}
@@ -883,7 +868,7 @@ function renderAdvancedSection(props: DreamingProps) {
?disabled=${props.dreamDiaryActionLoading}
@click=${() => props.onCopyDreamingArchivePath()}
>
Copy archive path
${t("dreaming.wiki.copyArchivePath")}
</button>
`
: nothing}
@@ -993,7 +978,7 @@ function renderDiaryImportsSection(props: DreamingProps) {
if (props.wikiImportInsightsLoading && clusters.length === 0) {
return html`
<div class="dreams-diary__empty">
<div class="dreams-diary__empty-text">Loading imported insights…</div>
<div class="dreams-diary__empty-text">${t("dreaming.wiki.loadingInsights")}</div>
</div>
`;
}
@@ -1001,10 +986,8 @@ function renderDiaryImportsSection(props: DreamingProps) {
if (clusters.length === 0) {
return html`
<div class="dreams-diary__empty">
<div class="dreams-diary__empty-text">No imported insights yet</div>
<div class="dreams-diary__empty-hint">
Run a ChatGPT import with apply to surface clustered imported insights here.
</div>
<div class="dreams-diary__empty-text">${t("dreaming.wiki.noInsights")}</div>
<div class="dreams-diary__empty-hint">${t("dreaming.wiki.noInsightsHint")}</div>
</div>
`;
}
@@ -1078,7 +1061,7 @@ function renderDiaryImportsSection(props: DreamingProps) {
${item.candidateSignals.length > 0
? html`
<div class="dreams-diary__insight-list">
<strong>Potentially useful signals</strong>
<strong>${t("dreaming.wiki.candidateSignals")}</strong>
${item.candidateSignals.map(
(signal) => html`<p class="dreams-diary__insight-line">• ${signal}</p>`,
)}
@@ -1088,7 +1071,7 @@ function renderDiaryImportsSection(props: DreamingProps) {
${item.correctionSignals.length > 0
? html`
<div class="dreams-diary__insight-list">
<strong>Corrections or revisions</strong>
<strong>${t("dreaming.wiki.corrections")}</strong>
${item.correctionSignals.map(
(signal) => html`<p class="dreams-diary__insight-line">• ${signal}</p>`,
)}
@@ -1098,36 +1081,40 @@ function renderDiaryImportsSection(props: DreamingProps) {
${expanded
? html`
<div class="dreams-diary__insight-list">
<strong>Import details</strong>
<strong>${t("dreaming.wiki.importDetails")}</strong>
${item.firstUserLine
? html`
<p class="dreams-diary__insight-line">
<strong>Started with:</strong> ${item.firstUserLine}
<strong>${t("dreaming.wiki.startedWith")}</strong>
${item.firstUserLine}
</p>
`
: nothing}
${item.lastUserLine && item.lastUserLine !== item.firstUserLine
? html`
<p class="dreams-diary__insight-line">
<strong>Ended on:</strong> ${item.lastUserLine}
<strong>${t("dreaming.wiki.endedOn")}</strong>
${item.lastUserLine}
</p>
`
: nothing}
<p class="dreams-diary__insight-line">
<strong>Messages:</strong> ${item.userMessageCount} user ·
${item.assistantMessageCount} assistant
<strong>${t("dreaming.wiki.messages")}</strong>
${item.userMessageCount} user · ${item.assistantMessageCount} assistant
</p>
${item.riskReasons.length > 0
? html`
<p class="dreams-diary__insight-line">
<strong>Risk reasons:</strong> ${item.riskReasons.join(", ")}
<strong>${t("dreaming.wiki.riskReasons")}</strong>
${item.riskReasons.join(", ")}
</p>
`
: nothing}
${item.labels.length > 0
? html`
<p class="dreams-diary__insight-line">
<strong>Labels:</strong> ${item.labels.join(", ")}
<strong>${t("dreaming.wiki.labels")}</strong>
${item.labels.join(", ")}
</p>
`
: nothing}
@@ -1165,7 +1152,7 @@ function renderDiaryImportsSection(props: DreamingProps) {
void openWikiPreview(item.pagePath, props);
}}
>
Open source page
${t("dreaming.wiki.openSourcePage")}
</button>
</div>
</article>
@@ -1184,7 +1171,7 @@ function renderMemoryPalaceSection(props: DreamingProps) {
if (props.wikiMemoryPalaceLoading && clusters.length === 0) {
return html`
<div class="dreams-diary__empty">
<div class="dreams-diary__empty-text">Loading memory palace…</div>
<div class="dreams-diary__empty-text">${t("dreaming.wiki.loadingPalace")}</div>
</div>
`;
}
@@ -1192,11 +1179,8 @@ function renderMemoryPalaceSection(props: DreamingProps) {
if (clusters.length === 0) {
return html`
<div class="dreams-diary__empty">
<div class="dreams-diary__empty-text">Memory palace is not populated yet</div>
<div class="dreams-diary__empty-hint">
Right now the wiki mostly has raw source imports and operational reports. This tab becomes
useful once syntheses, entities, or concepts start getting written.
</div>
<div class="dreams-diary__empty-text">${t("dreaming.wiki.emptyPalace")}</div>
<div class="dreams-diary__empty-hint">${t("dreaming.wiki.emptyPalaceHint")}</div>
</div>
`;
}
@@ -1283,7 +1267,7 @@ function renderMemoryPalaceSection(props: DreamingProps) {
${item.claims.length > 0
? html`
<div class="dreams-diary__insight-list">
<strong>Claims</strong>
<strong>${t("dreaming.wiki.claims")}</strong>
${item.claims.map(
(claim) => html`<p class="dreams-diary__insight-line">• ${claim}</p>`,
)}
@@ -1293,7 +1277,7 @@ function renderMemoryPalaceSection(props: DreamingProps) {
${item.questions.length > 0
? html`
<div class="dreams-diary__insight-list">
<strong>Open questions</strong>
<strong>${t("dreaming.wiki.openQuestions")}</strong>
${item.questions.map(
(question) => html`<p class="dreams-diary__insight-line">• ${question}</p>`,
)}
@@ -1303,7 +1287,7 @@ function renderMemoryPalaceSection(props: DreamingProps) {
${item.contradictions.length > 0
? html`
<div class="dreams-diary__insight-list">
<strong>Contradictions</strong>
<strong>${t("dreaming.wiki.contradictions")}</strong>
${item.contradictions.map(
(entry) => html`<p class="dreams-diary__insight-line">• ${entry}</p>`,
)}
@@ -1313,14 +1297,16 @@ function renderMemoryPalaceSection(props: DreamingProps) {
${expanded
? html`
<div class="dreams-diary__insight-list">
<strong>Page details</strong>
<strong>${t("dreaming.wiki.pageDetails")}</strong>
<p class="dreams-diary__insight-line">
<strong>Wiki page:</strong> ${item.pagePath}
<strong>${t("dreaming.wiki.wikiPage")}</strong>
${item.pagePath}
</p>
${item.id
? html`
<p class="dreams-diary__insight-line">
<strong>Id:</strong> ${item.id}
<strong>${t("dreaming.wiki.id")}</strong>
${item.id}
</p>
`
: nothing}
@@ -1348,7 +1334,7 @@ function renderMemoryPalaceSection(props: DreamingProps) {
void openWikiPreview(item.pagePath, props);
}}
>
Open wiki page
${t("dreaming.wiki.openWikiPage")}
</button>
</div>
</article>
@@ -1461,7 +1447,7 @@ function renderDiarySection(props: DreamingProps) {
props.onViewStateChange();
}}
>
Dreams
${t("dreaming.wiki.dreamsTab")}
</button>
<button
class="dreams-diary__subtab ${activeDiarySubTab === "insights"
@@ -1474,7 +1460,7 @@ function renderDiarySection(props: DreamingProps) {
props.onViewStateChange();
}}
>
Imported Insights
${t("dreaming.wiki.insightsTab")}
</button>
<button
class="dreams-diary__subtab ${activeDiarySubTab === "palace"
@@ -1487,7 +1473,7 @@ function renderDiarySection(props: DreamingProps) {
props.onViewStateChange();
}}
>
Memory Palace
${t("dreaming.wiki.palaceTab")}
</button>
</div>
<button
@@ -1514,7 +1500,7 @@ function renderDiarySection(props: DreamingProps) {
}}
>
${memoryWikiUnavailable
? "How to enable"
? t("dreaming.wiki.howToEnable")
: activeDiarySubTab === "dreams"
? props.dreamDiaryLoading
? t("dreaming.diary.reloading")
@@ -1534,18 +1520,20 @@ function renderDiarySection(props: DreamingProps) {
${memoryWikiUnavailable
? html`
<div class="dreams-diary__empty">
<div class="dreams-diary__empty-text">Memory Wiki is not enabled</div>
<div class="dreams-diary__empty-text">${t("dreaming.wiki.unavailable")}</div>
<div class="dreams-diary__empty-hint">
Imported Insights and Memory Palace are provided by the bundled
<code>memory-wiki</code> plugin.
${t("dreaming.wiki.unavailablePluginPrefix")}
<code>memory-wiki</code> ${t("dreaming.wiki.unavailablePluginSuffix")}
</div>
<div class="dreams-diary__empty-hint">
Enable <code>plugins.entries.memory-wiki.enabled = true</code>, then reload this
tab.
${t("dreaming.wiki.enablePrefix")}
<code>plugins.entries.memory-wiki.enabled = true</code>${t(
"dreaming.wiki.enableSuffix",
)}
</div>
<div class="dreams-diary__empty-actions">
<button class="btn btn--subtle btn--sm" @click=${() => props.onOpenConfig()}>
Open Config
${t("dreaming.wiki.openConfig")}
</button>
</div>
</div>
+30 -16
View File
@@ -106,19 +106,19 @@ export function renderAgentOverview(params: {
return html`
<section class="card">
<div class="card-title">Overview</div>
<div class="card-sub">Workspace paths and identity metadata.</div>
<div class="card-title">${t("agents.overview.title")}</div>
<div class="card-sub">${t("agents.overview.subtitle")}</div>
<div class="agents-overview-grid" style="margin-top: 16px;">
<div class="agent-kv">
<div class="label">Workspace</div>
<div class="label">${t("agents.context.workspace")}</div>
<div>
<openclaw-tooltip content="Open Files tab">
<openclaw-tooltip .content=${t("agents.context.openFilesTab")}>
<button
type="button"
class="workspace-link mono"
@click=${() => onSelectPanel("files")}
aria-label="Open Files tab"
aria-label=${t("agents.context.openFilesTab")}
>
${workspace}
</button>
@@ -126,11 +126,11 @@ export function renderAgentOverview(params: {
</div>
</div>
<div class="agent-kv">
<div class="label">Primary Model</div>
<div class="label">${t("agents.context.primaryModel")}</div>
<div class="mono">${model}</div>
</div>
<div class="agent-kv">
<div class="label">Runtime</div>
<div class="label">${t("agents.context.runtime")}</div>
<div class="mono">${runtime}</div>
</div>
<div class="agent-kv">
@@ -138,24 +138,32 @@ export function renderAgentOverview(params: {
<div class="mono">${thinkingDefault}</div>
</div>
<div class="agent-kv">
<div class="label">Skills Filter</div>
<div>${skillFilter ? `${skillCount} selected` : "all skills"}</div>
<div class="label">${t("agents.context.skillsFilter")}</div>
<div>
${skillFilter
? t("agents.overview.selectedSkills", { count: String(skillCount) })
: t("agents.overview.allSkills")}
</div>
</div>
</div>
${configDirty
? html`
<div class="callout warn" style="margin-top: 16px">
You have unsaved config changes.
${t("agents.overview.unsavedConfig")}
</div>
`
: nothing}
<div class="agent-model-select" style="margin-top: 20px;">
<div class="label">Model Selection</div>
<div class="label">${t("agents.overview.modelSelection")}</div>
<div class="agent-model-fields">
<label class="field">
<span>Primary model${isDefault ? " (default)" : ""}</span>
<span>
${isDefault
? t("agents.overview.primaryModelDefault")
: t("agents.overview.primaryModel")}
</span>
<select
.value=${selectedPrimary ?? ""}
?disabled=${disabled}
@@ -163,10 +171,16 @@ export function renderAgentOverview(params: {
onModelChange(agent.id, (e.target as HTMLSelectElement).value || null)}
>
${isDefault
? html` <option value="" ?selected=${!selectedPrimary}>Not set</option> `
? html`
<option value="" ?selected=${!selectedPrimary}>
${t("agents.overview.notSet")}
</option>
`
: html`
<option value="" ?selected=${!selectedPrimary}>
${defaultPrimary ? `Inherit default (${defaultPrimary})` : "Inherit default"}
${defaultPrimary
? t("agents.overview.inheritDefaultModel", { model: defaultPrimary })
: t("agents.overview.inheritDefault")}
</option>
`}
${buildModelOptions(
@@ -178,7 +192,7 @@ export function renderAgentOverview(params: {
</select>
</label>
<div class="field">
<span>Fallbacks</span>
<span>${t("agents.overview.fallbacks")}</span>
<div
class="agent-chip-input"
@click=${(e: Event) => {
@@ -235,7 +249,7 @@ export function renderAgentOverview(params: {
?disabled=${configSaving || !configDirty}
@click=${onConfigSave}
>
${configSaving ? "Saving" : "Save"}
${configSaving ? t("common.saving") : t("common.save")}
</button>
</div>
</div>
+138 -89
View File
@@ -47,12 +47,12 @@ function buildCatalogBadgeLabels(section: AgentToolSection, tool: AgentToolEntry
const pluginId = tool.pluginId ?? section.pluginId;
const badges: string[] = [];
if (source === "plugin" && pluginId) {
badges.push(`Plugin: ${pluginId}`);
badges.push(t("agentTools.plugin", { id: pluginId }));
} else if (source === "core") {
badges.push("Built-In");
badges.push(t("agentTools.builtIn"));
}
if (tool.optional) {
badges.push("Optional");
badges.push(t("agentTools.optional"));
}
return badges;
}
@@ -64,7 +64,7 @@ function buildRowStatusBadges(params: {
}) {
const badges = buildCatalogBadgeLabels(params.section, params.tool);
if (params.activeEntry) {
badges.unshift("Live Now");
badges.unshift(t("agentTools.liveNow"));
}
return badges;
}
@@ -75,24 +75,24 @@ function formatToolPolicyState(params: {
denied: boolean;
}) {
if (params.denied) {
return "Disabled by agent override.";
return t("agentTools.disabledByOverride");
}
if (params.allowed && params.baseAllowed) {
return "Enabled by the current profile.";
return t("agentTools.enabledByProfile");
}
if (params.allowed) {
return "Enabled by agent override.";
return t("agentTools.enabledByOverride");
}
return "Not included in the current profile.";
return t("agentTools.notIncluded");
}
function formatToolSourceLabel(section: AgentToolSection, tool: AgentToolEntry) {
const source = tool.source ?? section.source;
const pluginId = tool.pluginId ?? section.pluginId;
if (source === "plugin" && pluginId) {
return `Plugin: ${pluginId}`;
return t("agentTools.plugin", { id: pluginId });
}
return "Built-In";
return t("agentTools.builtIn");
}
function formatToolAccessSummary(params: {
@@ -101,15 +101,15 @@ function formatToolAccessSummary(params: {
denied: boolean;
}) {
if (params.denied) {
return "Override Off";
return t("agentTools.overrideOff");
}
if (params.allowed && params.baseAllowed) {
return "Enabled";
return t("agentTools.enabled");
}
if (params.allowed) {
return "Override On";
return t("agentTools.overrideOn");
}
return "Profile Off";
return t("agentTools.profileOff");
}
function formatToolRuntimeSummary(params: {
@@ -117,12 +117,12 @@ function formatToolRuntimeSummary(params: {
runtimeSessionMatchesSelectedAgent: boolean;
}) {
if (params.activeEntry) {
return "Live Now";
return t("agentTools.liveNow");
}
if (params.runtimeSessionMatchesSelectedAgent) {
return "Not Live";
return t("agentTools.notLive");
}
return "Other Agent";
return t("agentTools.otherAgent");
}
function toToolAnchorId(toolId: string) {
@@ -130,10 +130,6 @@ function toToolAnchorId(toolId: string) {
return `agent-tool-${safe}`;
}
function formatCountLabel(count: number, singular: string, plural = `${singular}s`) {
return `${count} ${count === 1 ? singular : plural}`;
}
function flattenEffectiveTools(groups: ToolsEffectiveResult["groups"] | null | undefined) {
return (groups ?? []).flatMap((group) => group.tools);
}
@@ -247,10 +243,10 @@ export function renderAgentTools(params: {
const profileOptions = resolveToolProfileOptions(params.toolsCatalogResult);
const toolSections = resolveToolSections(params.toolsCatalogResult);
const profileSource = agentTools.profile
? "agent override"
? t("agentTools.profileSourceAgent")
: globalTools.profile
? "global default"
: "default";
? t("agentTools.profileSourceGlobal")
: t("agentTools.profileSourceDefault");
const hasAgentAllow = Array.isArray(agentTools.allow) && agentTools.allow.length > 0;
const hasGlobalAllow = Array.isArray(globalTools.allow) && globalTools.allow.length > 0;
const editable =
@@ -365,18 +361,23 @@ export function renderAgentTools(params: {
<section class="card">
<div class="agent-tools-header">
<div class="agent-tools-header__intro">
<div class="card-title">Tool Access</div>
<div class="card-title">${t("agentTools.title")}</div>
<div class="card-sub">
Profile + per-tool overrides for this agent.
<span class="mono">${enabledCount}/${toolIds.length}</span> enabled.
${t("agentTools.subtitle")}
<span class="mono"
>${t("agentTools.enabledSummary", {
enabled: String(enabledCount),
total: String(toolIds.length),
})}</span
>
</div>
</div>
<div class="agent-tools-header__actions">
<button class="btn btn--sm" ?disabled=${!editable} @click=${() => updateAll(true)}>
Enable All
${t("agentTools.enableAll")}
</button>
<button class="btn btn--sm" ?disabled=${!editable} @click=${() => updateAll(false)}>
Disable All
${t("agentTools.disableAll")}
</button>
<button
class="btn btn--sm"
@@ -390,43 +391,41 @@ export function renderAgentTools(params: {
?disabled=${params.configSaving || !params.configDirty}
@click=${params.onConfigSave}
>
${params.configSaving ? "Saving" : "Save"}
${params.configSaving ? t("common.saving") : t("common.save")}
</button>
</div>
</div>
${!params.configForm
? html`
<div class="callout info" style="margin-top: 12px">
Load the gateway config to adjust tool profiles.
</div>
<div class="callout info" style="margin-top: 12px">${t("agentTools.loadConfig")}</div>
`
: nothing}
${hasAgentAllow
? html`
<div class="callout info" style="margin-top: 12px">
This agent is using an explicit allowlist in config. Tool overrides are managed in the
Config tab.
${t("agentTools.explicitAllowlist")}
</div>
`
: nothing}
${hasGlobalAllow
? html`
<div class="callout info" style="margin-top: 12px">
Global tools.allow is set. Agent overrides cannot enable tools that are globally
blocked.
${t("agentTools.globalAllowlist")}
</div>
`
: nothing}
${params.toolsCatalogLoading && !params.toolsCatalogResult && !params.toolsCatalogError
? html`
<div class="callout info" style="margin-top: 12px">Loading runtime tool catalog…</div>
<div class="callout info" style="margin-top: 12px">
${t("agentTools.loadingCatalog")}
</div>
`
: nothing}
${params.toolsCatalogError
? html`
<div class="callout info" style="margin-top: 12px">
Could not load runtime tool catalog. Showing built-in fallback list instead.
${t("agentTools.catalogFallback")}
</div>
`
: nothing}
@@ -434,16 +433,16 @@ export function renderAgentTools(params: {
<div class="agent-tools-overview">
<div class="agent-tools-overview__primary">
<div class="agent-tools-pane">
<div class="label">Available Right Now</div>
<div class="label">${t("agentTools.availableNow")}</div>
<div class="card-sub">
What this agent can use in the current chat session.
<span class="mono">${params.runtimeSessionKey || "no session"}</span>
${t("agentTools.availableNowSubtitle")}
<span class="mono">${params.runtimeSessionKey || t("agentTools.noSession")}</span>
</div>
${renderEffectiveToolNotices(params.toolsEffectiveResult)}
${!params.runtimeSessionMatchesSelectedAgent
? html`
<div class="callout info" style="margin-top: 12px">
Switch chat to this agent to view its live runtime tools.
${t("agentTools.switchAgent")}
</div>
`
: params.toolsEffectiveLoading &&
@@ -451,19 +450,19 @@ export function renderAgentTools(params: {
!params.toolsEffectiveError
? html`
<div class="callout info" style="margin-top: 12px">
Loading available tools…
${t("agentTools.loadingAvailable")}
</div>
`
: params.toolsEffectiveError
? html`
<div class="callout info" style="margin-top: 12px">
Could not load available tools for this session.
${t("agentTools.availableError")}
</div>
`
: (params.toolsEffectiveResult?.groups?.length ?? 0) === 0
? html`
<div class="callout info" style="margin-top: 12px">
No tools are available for this session right now.
${t("agentTools.noAvailable")}
</div>
`
: html`
@@ -487,9 +486,13 @@ export function renderAgentTools(params: {
? html`
<span
class="agent-tools-runtime-chip agent-tools-runtime-chip--more"
title=${`${hiddenEffectiveToolCount} more live tools are available in the groups below.`}
title=${t("agentTools.moreLiveTitle", {
count: String(hiddenEffectiveToolCount),
})}
>
+${hiddenEffectiveToolCount} more live tools
${t("agentTools.moreLive", {
count: String(hiddenEffectiveToolCount),
})}
</span>
`
: nothing}
@@ -498,7 +501,7 @@ export function renderAgentTools(params: {
</div>
<div class="agent-tools-pane">
<div class="label">Quick Presets</div>
<div class="label">${t("agentTools.quickPresets")}</div>
<div class="agent-tools-buttons">
${profileOptions.map(
(option) => html`
@@ -516,7 +519,7 @@ export function renderAgentTools(params: {
?disabled=${!editable}
@click=${() => params.onProfileChange(params.agentId, null, false)}
>
Inherit
${t("agentTools.inherit")}
</button>
</div>
</div>
@@ -524,25 +527,29 @@ export function renderAgentTools(params: {
<div class="agent-tools-facts">
<div class="agent-tools-fact">
<div class="label">Profile</div>
<div class="label">${t("agentTools.profile")}</div>
<div class="mono">${profile}</div>
</div>
<div class="agent-tools-fact">
<div class="label">Source</div>
<div class="label">${t("agentTools.source")}</div>
<div>${profileSource}</div>
</div>
<div class="agent-tools-fact">
<div class="label">Enabled</div>
<div class="label">${t("agentTools.enabled")}</div>
<div class="mono">${enabledCount}/${toolIds.length}</div>
</div>
<div class="agent-tools-fact">
<div class="label">Live</div>
<div class="label">${t("agentTools.live")}</div>
<div class="mono">${liveToolCount}</div>
</div>
<div class="agent-tools-fact">
<div class="label">Status</div>
<div class="label">${t("agentTools.status")}</div>
<div class="mono">
${params.configSaving ? "saving…" : params.configDirty ? "unsaved" : "saved"}
${params.configSaving
? t("agentTools.statusSaving")
: params.configDirty
? t("agentTools.statusUnsaved")
: t("agentTools.statusSaved")}
</div>
</div>
</div>
@@ -566,10 +573,15 @@ export function renderAgentTools(params: {
<span class="agent-tools-group__title">
${section.label}
${section.source === "plugin" && section.pluginId
? html`<span class="agent-pill">Plugin: ${section.pluginId}</span>`
? html`<span class="agent-pill"
>${t("agentTools.plugin", { id: section.pluginId })}</span
>`
: nothing}
</span>
<span class="agent-tools-group__preview" aria-label="Tool preview">
<span
class="agent-tools-group__preview"
aria-label=${t("agentTools.toolPreview")}
>
${previewTools.map(
(tool) =>
html`<span class="mono" translate="no" title=${tool.label}
@@ -577,15 +589,37 @@ export function renderAgentTools(params: {
>`,
)}
${remainingPreviewCount > 0
? html`<span>+${remainingPreviewCount} more</span>`
? html`<span
>${t("agentTools.more", {
count: String(remainingPreviewCount),
})}</span
>`
: nothing}
</span>
</span>
<span class="agent-tools-group__counts">
<span>${formatCountLabel(section.tools.length, "Tool")}</span>
<span>${formatCountLabel(enabledSectionCount, "Enabled Tool")}</span>
<span
>${t(section.tools.length === 1 ? "agentTools.toolsOne" : "agentTools.tools", {
count: String(section.tools.length),
})}</span
>
<span
>${t(
enabledSectionCount === 1
? "agentTools.enabledToolsOne"
: "agentTools.enabledTools",
{ count: String(enabledSectionCount) },
)}</span
>
${activeSectionCount > 0
? html`<span>${formatCountLabel(activeSectionCount, "Live Tool")}</span>`
? html`<span
>${t(
activeSectionCount === 1
? "agentTools.liveToolsOne"
: "agentTools.liveTools",
{ count: String(activeSectionCount) },
)}</span
>`
: nothing}
</span>
</summary>
@@ -616,11 +650,11 @@ export function renderAgentTools(params: {
</div>
<dl class="agent-tool-summary__facts">
<div class="agent-tool-summary__fact">
<dt class="label">Access</dt>
<dt class="label">${t("agentTools.access")}</dt>
<dd>${accessSummary}</dd>
</div>
<div class="agent-tool-summary__fact">
<dt class="label">Session</dt>
<dt class="label">${t("agentTools.session")}</dt>
<dd>${runtimeSummary}</dd>
</div>
</dl>
@@ -636,7 +670,12 @@ export function renderAgentTools(params: {
type="checkbox"
.checked=${resolved.allowed}
?disabled=${!editable}
aria-label=${`${resolved.allowed ? "Disable" : "Enable"} ${tool.label}`}
aria-label=${t(
resolved.allowed
? "agentTools.disableNamed"
: "agentTools.enableNamed",
{ name: tool.label },
)}
@change=${(e: Event) =>
updateTool(tool.id, (e.target as HTMLInputElement).checked)}
/>
@@ -646,17 +685,17 @@ export function renderAgentTools(params: {
<div class="agent-tool-details">
<div class="agent-tool-details-strip">
<div class="agent-tool-detail agent-tool-detail--inline">
<div class="label">Access</div>
<div class="label">${t("agentTools.access")}</div>
<div>${formatToolPolicyState(resolved)}</div>
</div>
<div class="agent-tool-detail agent-tool-detail--inline">
<div class="label">Source</div>
<div class="label">${t("agentTools.source")}</div>
<div>${formatToolSourceLabel(section, tool)}</div>
</div>
${defaultProfiles.length > 0
? html`
<div class="agent-tool-detail agent-tool-detail--inline">
<div class="label">Default Presets</div>
<div class="label">${t("agentTools.defaultPresets")}</div>
<div class="agent-tool-badges">
${defaultProfiles.map(
(profileId) =>
@@ -667,16 +706,20 @@ export function renderAgentTools(params: {
`
: nothing}
<div class="agent-tool-detail agent-tool-detail--inline">
<div class="label">Current Session</div>
<div class="label">${t("agentTools.session")}</div>
<div>
${activeEntry
? `Available now via ${renderEffectiveToolBadge(activeEntry)}.`
? t("agentTools.availableVia", {
source: renderEffectiveToolBadge(activeEntry),
})
: params.runtimeSessionMatchesSelectedAgent
? "Not available in this chat session right now."
: "Switch chat to this agent to inspect live availability."}
? t("agentTools.unavailableSession")
: t("agentTools.inspectAgent")}
</div>
</div>
<a class="agent-tool-jump" href="#${anchorId}"> Link to This Tool </a>
<a class="agent-tool-jump" href="#${anchorId}">
${t("agentTools.linkTool")}
</a>
</div>
</div>
</details>
@@ -735,9 +778,9 @@ export function renderAgentSkills(params: {
<section class="card">
<div class="row" style="justify-content: space-between; flex-wrap: wrap;">
<div style="min-width: 0;">
<div class="card-title">Skills</div>
<div class="card-title">${t("agents.skillsPanel.title")}</div>
<div class="card-sub">
Per-agent skill allowlist and workspace skills.
${t("agents.skillsPanel.subtitle")}
${totalCount > 0
? html`<span class="mono">${enabledCount}/${totalCount}</span>`
: nothing}
@@ -753,21 +796,21 @@ export function renderAgentSkills(params: {
?disabled=${!editable}
@click=${() => params.onClear(params.agentId)}
>
Enable All
${t("agentTools.enableAll")}
</button>
<button
class="btn btn--sm"
?disabled=${!editable}
@click=${() => params.onDisableAll(params.agentId)}
>
Disable All
${t("agentTools.disableAll")}
</button>
<button
class="btn btn--sm"
?disabled=${!editable || !usingAllowlist}
@click=${() => params.onClear(params.agentId)}
>
Reset
${t("common.reset")}
</button>
</div>
<button
@@ -785,7 +828,7 @@ export function renderAgentSkills(params: {
?disabled=${params.configSaving || !params.configDirty}
@click=${params.onConfigSave}
>
${params.configSaving ? "Saving" : "Save"}
${params.configSaving ? t("common.saving") : t("common.save")}
</button>
</div>
</div>
@@ -793,25 +836,25 @@ export function renderAgentSkills(params: {
${!params.configForm
? html`
<div class="callout info" style="margin-top: 12px">
Load the gateway config to set per-agent skills.
${t("agents.skillsPanel.loadConfig")}
</div>
`
: nothing}
${usingAllowlist
? html`
<div class="callout info" style="margin-top: 12px">
This agent uses a custom skill allowlist.
${t("agents.skillsPanel.customAllowlist")}
</div>
`
: html`
<div class="callout info" style="margin-top: 12px">
All skills are enabled. Disabling any skill will create a per-agent allowlist.
${t("agents.skillsPanel.allEnabled")}
</div>
`}
${!reportReady && !params.loading
? html`
<div class="callout info" style="margin-top: 12px">
Load skills for this agent to view workspace-specific entries.
${t("agents.skillsPanel.loadAgent")}
</div>
`
: nothing}
@@ -821,20 +864,22 @@ export function renderAgentSkills(params: {
<div class="filters" style="margin-top: 14px;">
<label class="field" style="flex: 1;">
<span>Filter</span>
<span>${t("agents.skillsPanel.filter")}</span>
<input
.value=${params.filter}
@input=${(e: Event) => params.onFilterChange((e.target as HTMLInputElement).value)}
placeholder="Search skills"
placeholder=${t("agents.skillsPanel.searchPlaceholder")}
autocomplete="off"
name="agent-skills-filter"
/>
</label>
<div class="muted">${filtered.length} shown</div>
<div class="muted">
${t("agents.skillsPanel.shown", { count: String(filtered.length) })}
</div>
</div>
${filtered.length === 0
? html` <div class="muted" style="margin-top: 16px">No skills found.</div> `
? html` <div class="muted" style="margin-top: 16px">${t("agents.skillsPanel.empty")}</div> `
: html`
<div class="agent-skills-groups" style="margin-top: 16px;">
${groups.map((group) =>
@@ -904,10 +949,14 @@ function renderAgentSkillRow(
<div class="list-sub">${skill.description}</div>
${renderSkillStatusChips({ skill })}
${missing.length > 0
? html`<div class="muted" style="margin-top: 6px;">Missing: ${missing.join(", ")}</div>`
? html`<div class="muted" style="margin-top: 6px;">
${t("agents.skillsPanel.missing", { items: missing.join(", ") })}
</div>`
: nothing}
${reasons.length > 0
? html`<div class="muted" style="margin-top: 6px;">Reason: ${reasons.join(", ")}</div>`
? html`<div class="muted" style="margin-top: 6px;">
${t("agents.skillsPanel.reason", { items: reasons.join(", ") })}
</div>`
: nothing}
</div>
<div class="list-meta">