feat(ui): complete i18n coverage for remaining UI components

- Add i18n support to exec-approval.ts, gateway-url-confirmation.ts,
  markdown-sidebar.ts, instances.ts
- Convert config-form.render.ts SECTION_META to use dynamic i18n
- Update config-form.node.ts button titles to use i18n
- Convert nodes.ts dropdown options (SECURITY_OPTIONS, ASK_OPTIONS) to
  use translated labels
- Convert sessions.ts VERBOSE_LEVELS to use translated labels
- Add new translation keys for all components in both en-US and zh-TW

https://claude.ai/code/session_01PxfjMoSvvLonZpx1vtL1d2
This commit is contained in:
Claude 2026-01-29 16:02:22 +00:00
parent 0c5759b9d9
commit cc74171b62
No known key found for this signature in database
10 changed files with 267 additions and 91 deletions

View File

@ -602,7 +602,57 @@ export const enUS = {
tools: "Tools",
gateway: "Gateway",
wizard: "Setup Wizard",
meta: "Metadata",
logging: "Logging",
browser: "Browser",
ui: "UI",
models: "Models",
bindings: "Bindings",
broadcast: "Broadcast",
audio: "Audio",
session: "Session",
cron: "Cron",
web: "Web",
discovery: "Discovery",
canvasHost: "Canvas Host",
talk: "Talk",
plugins: "Plugins",
},
sectionDescriptions: {
env: "Environment variables passed to the gateway process",
update: "Auto-update settings and release channel",
agents: "Agent configurations, models, and identities",
auth: "API keys and authentication profiles",
channels: "Messaging channels (Telegram, Discord, Slack, etc.)",
messages: "Message handling and routing settings",
commands: "Custom slash commands",
hooks: "Webhooks and event hooks",
skills: "Skill packs and capabilities",
tools: "Tool configurations (browser, search, etc.)",
gateway: "Gateway server settings (port, auth, binding)",
wizard: "Setup wizard state and history",
meta: "Gateway metadata and version information",
logging: "Log levels and output configuration",
browser: "Browser automation settings",
ui: "User interface preferences",
models: "AI model configurations and providers",
bindings: "Key bindings and shortcuts",
broadcast: "Broadcast and notification settings",
audio: "Audio input/output settings",
session: "Session management and persistence",
cron: "Scheduled tasks and automation",
web: "Web server and API settings",
discovery: "Service discovery and networking",
canvasHost: "Canvas rendering and display",
talk: "Voice and speech settings",
plugins: "Plugin management and extensions",
},
removeItem: "Remove item",
removeEntry: "Remove entry",
noSettingsMatch: "No settings match \"{{query}}\"",
noSettingsInSection: "No settings in this section",
schemaUnavailable: "Schema unavailable.",
unsupportedSchema: "Unsupported schema. Use Raw.",
},
// Debug page
@ -663,22 +713,42 @@ export const enUS = {
// Instances page
instances: {
title: "Instances",
cardTitle: "Connected Instances",
desc: "Presence beacons from connected gateways and nodes.",
cardDesc: "Presence beacons from the gateway and clients.",
noInstances: "No presence beacons found.",
noInstancesYet: "No instances reported yet.",
id: "ID",
type: "Type",
version: "Version",
lastSeen: "Last Seen",
lastInput: "Last input",
reason: "Reason",
unknownHost: "unknown host",
unknown: "unknown",
scopes: "scopes",
scopesCount: "{{count}} scopes",
secondsAgo: "{{count}}s ago",
},
// Exec approval prompt
execApproval: {
title: "Execution Approval Required",
titleShort: "Exec approval needed",
command: "Command",
agent: "Agent",
session: "Session",
host: "Host",
cwd: "CWD",
resolved: "Resolved",
security: "Security",
ask: "Ask",
allowOnce: "Allow Once",
allowAlways: "Allow Always",
deny: "Deny",
expiresIn: "expires in {{time}}",
expired: "expired",
pending: "{{count}} pending",
},
// Theme
@ -704,6 +774,10 @@ export const enUS = {
// Gateway connection
gateway: {
disconnected: "Disconnected from gateway.",
changeUrl: "Change Gateway URL",
changeUrlDesc: "This will reconnect to a different gateway server",
changeUrlWarning: "Only confirm if you trust this URL. Malicious URLs can compromise your system.",
confirm: "Confirm",
},
// Nostr profile messages
@ -727,9 +801,13 @@ export const enUS = {
// Markdown sidebar
sidebar: {
title: "Tool Output",
close: "Close",
closeSidebar: "Close sidebar",
viewRaw: "View raw",
viewRawText: "View Raw Text",
error: "Error loading content",
noContent: "No content available",
},
// Errors

View File

@ -609,7 +609,57 @@ export const zhTW = {
tools: "工具",
gateway: "閘道器",
wizard: "設定精靈",
meta: "中繼資料",
logging: "日誌",
browser: "瀏覽器",
ui: "使用者介面",
models: "模型",
bindings: "綁定",
broadcast: "廣播",
audio: "音訊",
session: "工作階段",
cron: "排程任務",
web: "網頁",
discovery: "服務探索",
canvasHost: "Canvas Host",
talk: "語音",
plugins: "外掛",
},
sectionDescriptions: {
env: "傳遞給閘道器程序的環境變數",
update: "自動更新設定與發行通道",
agents: "代理設定、模型與身分識別",
auth: "API 金鑰與認證設定檔",
channels: "訊息頻道Telegram、Discord、Slack 等)",
messages: "訊息處理與路由設定",
commands: "自訂斜線指令",
hooks: "Webhooks 與事件鉤子",
skills: "Skills 套件與功能",
tools: "工具設定(瀏覽器、搜尋等)",
gateway: "閘道器伺服器設定(連接埠、認證、綁定)",
wizard: "設定精靈狀態與歷史記錄",
meta: "閘道器中繼資料與版本資訊",
logging: "日誌等級與輸出設定",
browser: "瀏覽器自動化設定",
ui: "使用者介面偏好設定",
models: "AI 模型設定與提供者",
bindings: "按鍵綁定與快捷鍵",
broadcast: "廣播與通知設定",
audio: "音訊輸入/輸出設定",
session: "工作階段管理與持久化",
cron: "排程任務與自動化",
web: "網頁伺服器與 API 設定",
discovery: "服務探索與網路設定",
canvasHost: "Canvas 渲染與顯示",
talk: "語音與語音設定",
plugins: "外掛管理與擴充功能",
},
removeItem: "移除項目",
removeEntry: "移除項目",
noSettingsMatch: "找不到符合「{{query}}」的設定",
noSettingsInSection: "此區塊沒有設定",
schemaUnavailable: "結構定義不可用。",
unsupportedSchema: "不支援的結構定義。請使用原始模式。",
},
// 除錯頁面
@ -670,22 +720,42 @@ export const zhTW = {
// 實例頁面
instances: {
title: "實例",
cardTitle: "已連線的實例",
desc: "來自已連線閘道器與節點的存在訊號。",
cardDesc: "來自閘道器與用戶端的存在訊號。",
noInstances: "找不到存在訊號。",
noInstancesYet: "尚無實例回報。",
id: "識別碼",
type: "類型",
version: "版本",
lastSeen: "上次出現",
lastInput: "上次輸入",
reason: "原因",
unknownHost: "未知主機",
unknown: "未知",
scopes: "範圍",
scopesCount: "{{count}} 個範圍",
secondsAgo: "{{count}} 秒前",
},
// 執行核准提示
execApproval: {
title: "需要執行核准",
titleShort: "需要執行核准",
command: "指令",
agent: "代理",
session: "工作階段",
host: "主機",
cwd: "工作目錄",
resolved: "解析路徑",
security: "安全性",
ask: "詢問",
allowOnce: "允許一次",
allowAlways: "永久允許",
deny: "拒絕",
expiresIn: "{{time}} 後過期",
expired: "已過期",
pending: "{{count}} 個待處理",
},
// 主題
@ -711,6 +781,10 @@ export const zhTW = {
// 閘道器連線
gateway: {
disconnected: "已與閘道器斷線。",
changeUrl: "變更閘道器網址",
changeUrlDesc: "這將重新連線到不同的閘道器伺服器",
changeUrlWarning: "僅在您信任此網址時才確認。惡意網址可能會危害您的系統。",
confirm: "確認",
},
// Nostr 個人檔案訊息
@ -734,9 +808,13 @@ export const zhTW = {
// Markdown 側邊欄
sidebar: {
title: "工具輸出",
close: "關閉",
closeSidebar: "關閉側邊欄",
viewRaw: "檢視原始內容",
viewRawText: "檢視原始文字",
error: "載入內容時發生錯誤",
noContent: "沒有可用的內容",
},
// 錯誤訊息

View File

@ -1,4 +1,6 @@
import { html, nothing, type TemplateResult } from "lit";
import { t } from "../../i18n";
import type { ConfigUiHints } from "../types";
import {
defaultValue,
@ -533,7 +535,7 @@ function renderArray(params: {
<button
type="button"
class="cfg-array__item-remove"
title="Remove item"
title="${t("config.removeItem")}"
?disabled=${disabled}
@click=${() => {
const next = [...arr];
@ -668,7 +670,7 @@ function renderMapField(params: {
<button
type="button"
class="cfg-map__item-remove"
title="Remove entry"
title="${t("config.removeEntry")}"
?disabled=${disabled}
@click=${() => {
const next = { ...(value ?? {}) };

View File

@ -1,4 +1,6 @@
import { html, nothing } from "lit";
import { t } from "../../i18n";
import type { ConfigUiHints } from "../types";
import { icons } from "../icons";
import {
@ -54,37 +56,40 @@ const sectionIcons = {
default: html`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
};
// Section metadata
export const SECTION_META: Record<string, { label: string; description: string }> = {
env: { label: "Environment Variables", description: "Environment variables passed to the gateway process" },
update: { label: "Updates", description: "Auto-update settings and release channel" },
agents: { label: "Agents", description: "Agent configurations, models, and identities" },
auth: { label: "Authentication", description: "API keys and authentication profiles" },
channels: { label: "Channels", description: "Messaging channels (Telegram, Discord, Slack, etc.)" },
messages: { label: "Messages", description: "Message handling and routing settings" },
commands: { label: "Commands", description: "Custom slash commands" },
hooks: { label: "Hooks", description: "Webhooks and event hooks" },
skills: { label: "Skills", description: "Skill packs and capabilities" },
tools: { label: "Tools", description: "Tool configurations (browser, search, etc.)" },
gateway: { label: "Gateway", description: "Gateway server settings (port, auth, binding)" },
wizard: { label: "Setup Wizard", description: "Setup wizard state and history" },
// Additional sections
meta: { label: "Metadata", description: "Gateway metadata and version information" },
logging: { label: "Logging", description: "Log levels and output configuration" },
browser: { label: "Browser", description: "Browser automation settings" },
ui: { label: "UI", description: "User interface preferences" },
models: { label: "Models", description: "AI model configurations and providers" },
bindings: { label: "Bindings", description: "Key bindings and shortcuts" },
broadcast: { label: "Broadcast", description: "Broadcast and notification settings" },
audio: { label: "Audio", description: "Audio input/output settings" },
session: { label: "Session", description: "Session management and persistence" },
cron: { label: "Cron", description: "Scheduled tasks and automation" },
web: { label: "Web", description: "Web server and API settings" },
discovery: { label: "Discovery", description: "Service discovery and networking" },
canvasHost: { label: "Canvas Host", description: "Canvas rendering and display" },
talk: { label: "Talk", description: "Voice and speech settings" },
plugins: { label: "Plugins", description: "Plugin management and extensions" },
};
// Known section keys for i18n
const SECTION_KEYS = [
"env", "update", "agents", "auth", "channels", "messages", "commands",
"hooks", "skills", "tools", "gateway", "wizard", "meta", "logging",
"browser", "ui", "models", "bindings", "broadcast", "audio", "session",
"cron", "web", "discovery", "canvasHost", "talk", "plugins",
] as const;
type SectionKey = (typeof SECTION_KEYS)[number];
// Get localized section metadata
export function getSectionMeta(key: string): { label: string; description: string } {
if (SECTION_KEYS.includes(key as SectionKey)) {
return {
label: t(`config.sections.${key}`),
description: t(`config.sectionDescriptions.${key}`),
};
}
// Fallback for unknown sections
return {
label: key.charAt(0).toUpperCase() + key.slice(1),
description: "",
};
}
// Legacy export for backward compatibility (deprecated, use getSectionMeta instead)
export const SECTION_META: Record<string, { label: string; description: string }> = new Proxy(
{} as Record<string, { label: string; description: string }>,
{
get(_target, prop: string) {
return getSectionMeta(prop);
},
}
);
function getSectionIcon(key: string) {
return sectionIcons[key as keyof typeof sectionIcons] ?? sectionIcons.default;
@ -142,12 +147,12 @@ function schemaMatches(schema: JsonSchema, query: string): boolean {
export function renderConfigForm(props: ConfigFormProps) {
if (!props.schema) {
return html`<div class="muted">Schema unavailable.</div>`;
return html`<div class="muted">${t("config.schemaUnavailable")}</div>`;
}
const schema = props.schema;
const value = props.value ?? {};
if (schemaType(schema) !== "object" || !schema.properties) {
return html`<div class="callout danger">Unsupported schema. Use Raw.</div>`;
return html`<div class="callout danger">${t("config.unsupportedSchema")}</div>`;
}
const unsupported = new Set(props.unsupportedPaths ?? []);
const properties = schema.properties;
@ -193,8 +198,8 @@ export function renderConfigForm(props: ConfigFormProps) {
<div class="config-empty__icon">${icons.search}</div>
<div class="config-empty__text">
${searchQuery
? `No settings match "${searchQuery}"`
: "No settings in this section"}
? t("config.noSettingsMatch", { query: searchQuery })
: t("config.noSettingsInSection")}
</div>
</div>
`;

View File

@ -1,5 +1,6 @@
import { html, nothing } from "lit";
import { t } from "../../i18n";
import type { AppViewState } from "../app-view-state";
function formatRemaining(ms: number): string {
@ -22,29 +23,32 @@ export function renderExecApprovalPrompt(state: AppViewState) {
if (!active) return nothing;
const request = active.request;
const remainingMs = active.expiresAtMs - Date.now();
const remaining = remainingMs > 0 ? `expires in ${formatRemaining(remainingMs)}` : "expired";
const remaining =
remainingMs > 0
? t("execApproval.expiresIn", { time: formatRemaining(remainingMs) })
: t("execApproval.expired");
const queueCount = state.execApprovalQueue.length;
return html`
<div class="exec-approval-overlay" role="dialog" aria-live="polite">
<div class="exec-approval-card">
<div class="exec-approval-header">
<div>
<div class="exec-approval-title">Exec approval needed</div>
<div class="exec-approval-title">${t("execApproval.titleShort")}</div>
<div class="exec-approval-sub">${remaining}</div>
</div>
${queueCount > 1
? html`<div class="exec-approval-queue">${queueCount} pending</div>`
? html`<div class="exec-approval-queue">${t("execApproval.pending", { count: queueCount })}</div>`
: nothing}
</div>
<div class="exec-approval-command mono">${request.command}</div>
<div class="exec-approval-meta">
${renderMetaRow("Host", request.host)}
${renderMetaRow("Agent", request.agentId)}
${renderMetaRow("Session", request.sessionKey)}
${renderMetaRow("CWD", request.cwd)}
${renderMetaRow("Resolved", request.resolvedPath)}
${renderMetaRow("Security", request.security)}
${renderMetaRow("Ask", request.ask)}
${renderMetaRow(t("execApproval.host"), request.host)}
${renderMetaRow(t("execApproval.agent"), request.agentId)}
${renderMetaRow(t("execApproval.session"), request.sessionKey)}
${renderMetaRow(t("execApproval.cwd"), request.cwd)}
${renderMetaRow(t("execApproval.resolved"), request.resolvedPath)}
${renderMetaRow(t("execApproval.security"), request.security)}
${renderMetaRow(t("execApproval.ask"), request.ask)}
</div>
${state.execApprovalError
? html`<div class="exec-approval-error">${state.execApprovalError}</div>`
@ -55,21 +59,21 @@ export function renderExecApprovalPrompt(state: AppViewState) {
?disabled=${state.execApprovalBusy}
@click=${() => state.handleExecApprovalDecision("allow-once")}
>
Allow once
${t("execApproval.allowOnce")}
</button>
<button
class="btn"
?disabled=${state.execApprovalBusy}
@click=${() => state.handleExecApprovalDecision("allow-always")}
>
Always allow
${t("execApproval.allowAlways")}
</button>
<button
class="btn danger"
?disabled=${state.execApprovalBusy}
@click=${() => state.handleExecApprovalDecision("deny")}
>
Deny
${t("execApproval.deny")}
</button>
</div>
</div>

View File

@ -1,5 +1,6 @@
import { html, nothing } from "lit";
import { t } from "../../i18n";
import type { AppViewState } from "../app-view-state";
export function renderGatewayUrlConfirmation(state: AppViewState) {
@ -11,26 +12,26 @@ export function renderGatewayUrlConfirmation(state: AppViewState) {
<div class="exec-approval-card">
<div class="exec-approval-header">
<div>
<div class="exec-approval-title">Change Gateway URL</div>
<div class="exec-approval-sub">This will reconnect to a different gateway server</div>
<div class="exec-approval-title">${t("gateway.changeUrl")}</div>
<div class="exec-approval-sub">${t("gateway.changeUrlDesc")}</div>
</div>
</div>
<div class="exec-approval-command mono">${pendingGatewayUrl}</div>
<div class="callout danger" style="margin-top: 12px;">
Only confirm if you trust this URL. Malicious URLs can compromise your system.
${t("gateway.changeUrlWarning")}
</div>
<div class="exec-approval-actions">
<button
class="btn primary"
@click=${() => state.handleGatewayUrlConfirm()}
>
Confirm
${t("gateway.confirm")}
</button>
<button
class="btn"
@click=${() => state.handleGatewayUrlCancel()}
>
Cancel
${t("common.cancel")}
</button>
</div>
</div>

View File

@ -1,5 +1,6 @@
import { html, nothing } from "lit";
import { t } from "../../i18n";
import { formatPresenceAge, formatPresenceSummary } from "../presenter";
import type { PresenceEntry } from "../types";
@ -16,11 +17,11 @@ export function renderInstances(props: InstancesProps) {
<section class="card">
<div class="row" style="justify-content: space-between;">
<div>
<div class="card-title">Connected Instances</div>
<div class="card-sub">Presence beacons from the gateway and clients.</div>
<div class="card-title">${t("instances.cardTitle")}</div>
<div class="card-sub">${t("instances.cardDesc")}</div>
</div>
<button class="btn" ?disabled=${props.loading} @click=${props.onRefresh}>
${props.loading ? "Loading…" : "Refresh"}
${props.loading ? t("common.loading") : t("common.refresh")}
</button>
</div>
${props.lastError
@ -35,7 +36,7 @@ export function renderInstances(props: InstancesProps) {
: nothing}
<div class="list" style="margin-top: 16px;">
${props.entries.length === 0
? html`<div class="muted">No instances reported yet.</div>`
? html`<div class="muted">${t("instances.noInstancesYet")}</div>`
: props.entries.map((entry) => renderEntry(entry))}
</div>
</section>
@ -45,21 +46,21 @@ export function renderInstances(props: InstancesProps) {
function renderEntry(entry: PresenceEntry) {
const lastInput =
entry.lastInputSeconds != null
? `${entry.lastInputSeconds}s ago`
: "n/a";
const mode = entry.mode ?? "unknown";
? t("instances.secondsAgo", { count: entry.lastInputSeconds })
: t("common.na");
const mode = entry.mode ?? t("instances.unknown");
const roles = Array.isArray(entry.roles) ? entry.roles.filter(Boolean) : [];
const scopes = Array.isArray(entry.scopes) ? entry.scopes.filter(Boolean) : [];
const scopesLabel =
scopes.length > 0
? scopes.length > 3
? `${scopes.length} scopes`
: `scopes: ${scopes.join(", ")}`
? t("instances.scopesCount", { count: scopes.length })
: `${t("instances.scopes")}: ${scopes.join(", ")}`
: null;
return html`
<div class="list-item">
<div class="list-main">
<div class="list-title">${entry.host ?? "unknown host"}</div>
<div class="list-title">${entry.host ?? t("instances.unknownHost")}</div>
<div class="list-sub">${formatPresenceSummary(entry)}</div>
<div class="chip-row">
<span class="chip">${mode}</span>
@ -77,8 +78,8 @@ function renderEntry(entry: PresenceEntry) {
</div>
<div class="list-meta">
<div>${formatPresenceAge(entry)}</div>
<div class="muted">Last input ${lastInput}</div>
<div class="muted">Reason ${entry.reason ?? ""}</div>
<div class="muted">${t("instances.lastInput")} ${lastInput}</div>
<div class="muted">${t("instances.reason")} ${entry.reason ?? ""}</div>
</div>
</div>
`;

View File

@ -1,6 +1,7 @@
import { html, nothing } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { t } from "../../i18n";
import { icons } from "../icons";
import { toSanitizedMarkdownHtml } from "../markdown";
@ -15,8 +16,8 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
return html`
<div class="sidebar-panel">
<div class="sidebar-header">
<div class="sidebar-title">Tool Output</div>
<button @click=${props.onClose} class="btn" title="Close sidebar">
<div class="sidebar-title">${t("sidebar.title")}</div>
<button @click=${props.onClose} class="btn" title="${t("sidebar.closeSidebar")}">
${icons.x}
</button>
</div>
@ -25,12 +26,12 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
? html`
<div class="callout danger">${props.error}</div>
<button @click=${props.onViewRawText} class="btn" style="margin-top: 12px;">
View Raw Text
${t("sidebar.viewRawText")}
</button>
`
: props.content
? html`<div class="sidebar-markdown">${unsafeHTML(toSanitizedMarkdownHtml(props.content))}</div>`
: html`<div class="muted">No content available</div>`}
: html`<div class="muted">${t("sidebar.noContent")}</div>`}
</div>
</div>
`;

View File

@ -274,17 +274,21 @@ type ExecApprovalsState = {
const EXEC_APPROVALS_DEFAULT_SCOPE = "__defaults__";
const SECURITY_OPTIONS: Array<{ value: ExecSecurity; label: string }> = [
{ value: "deny", label: "Deny" },
{ value: "allowlist", label: "Allowlist" },
{ value: "full", label: "Full" },
];
function getSecurityOptions(): Array<{ value: ExecSecurity; label: string }> {
return [
{ value: "deny", label: t("nodes.approvals.deny") },
{ value: "allowlist", label: t("nodes.approvals.allowlist") },
{ value: "full", label: t("nodes.approvals.full") },
];
}
const ASK_OPTIONS: Array<{ value: ExecAsk; label: string }> = [
{ value: "off", label: "Off" },
{ value: "on-miss", label: "On miss" },
{ value: "always", label: "Always" },
];
function getAskOptions(): Array<{ value: ExecAsk; label: string }> {
return [
{ value: "off", label: t("nodes.approvals.off") },
{ value: "on-miss", label: t("nodes.approvals.onMiss") },
{ value: "always", label: t("nodes.approvals.always") },
];
}
function resolveBindingsState(props: NodesProps): BindingState {
const config = props.configForm;
@ -696,7 +700,7 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
Use default (${defaults.security})
</option>`
: nothing}
${SECURITY_OPTIONS.map(
${getSecurityOptions().map(
(option) =>
html`<option
value=${option.value}
@ -737,7 +741,7 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
Use default (${defaults.ask})
</option>`
: nothing}
${ASK_OPTIONS.map(
${getAskOptions().map(
(option) =>
html`<option
value=${option.value}
@ -780,7 +784,7 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
Use default (${defaults.askFallback})
</option>`
: nothing}
${SECURITY_OPTIONS.map(
${getSecurityOptions().map(
(option) =>
html`<option
value=${option.value}
@ -878,7 +882,7 @@ function renderAllowlistEntry(
return html`
<div class="list-item">
<div class="list-main">
<div class="list-title">${entry.pattern?.trim() ? entry.pattern : "New pattern"}</div>
<div class="list-title">${entry.pattern?.trim() ? entry.pattern : t("nodes.approvals.newPattern")}</div>
<div class="list-sub">Last used: ${lastUsed}</div>
${lastCommand ? html`<div class="list-sub mono">${lastCommand}</div>` : nothing}
${lastPath ? html`<div class="list-sub mono">${lastPath}</div>` : nothing}

View File

@ -36,11 +36,13 @@ export type SessionsProps = {
const THINK_LEVELS = ["", "off", "minimal", "low", "medium", "high"] as const;
const BINARY_THINK_LEVELS = ["", "off", "on"] as const;
const VERBOSE_LEVELS = [
{ value: "", label: "inherit" },
{ value: "off", label: "off (explicit)" },
{ value: "on", label: "on" },
] as const;
function getVerboseLevels() {
return [
{ value: "", label: t("common.inherit") },
{ value: "off", label: t("sessions.levels.offExplicit") },
{ value: "on", label: t("sessions.levels.on") },
] as const;
}
const REASONING_LEVELS = ["", "off", "on", "stream"] as const;
function normalizeProviderId(provider?: string | null): string {
@ -236,7 +238,7 @@ function renderRow(
onPatch(row.key, { verboseLevel: value || null });
}}
>
${VERBOSE_LEVELS.map(
${getVerboseLevels().map(
(level) => html`<option value=${level.value}>${level.label}</option>`,
)}
</select>