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:
parent
0c5759b9d9
commit
cc74171b62
@ -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
|
||||
|
||||
@ -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: "沒有可用的內容",
|
||||
},
|
||||
|
||||
// 錯誤訊息
|
||||
|
||||
@ -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 ?? {}) };
|
||||
|
||||
@ -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>
|
||||
`;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
`;
|
||||
|
||||
@ -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>
|
||||
`;
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user