mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-28 18:33:37 +00:00
fix: restore green build after upstream API drift
This commit is contained in:
@@ -56,7 +56,7 @@ describe("compaction retry integration", () => {
|
||||
} as unknown as NonNullable<ExtensionContext["model"]>;
|
||||
|
||||
const invokeGenerateSummary = (signal = new AbortController().signal) =>
|
||||
mockGenerateSummary(testMessages, testModel, 1000, "test-api-key", undefined, signal);
|
||||
mockGenerateSummary(testMessages, testModel, 1000, "test-api-key", signal);
|
||||
|
||||
const runSummaryRetry = (options: Parameters<typeof retryAsync>[1]) =>
|
||||
retryAsync(() => invokeGenerateSummary(), options);
|
||||
|
||||
@@ -212,7 +212,6 @@ async function summarizeChunks(params: {
|
||||
messages: AgentMessage[];
|
||||
model: NonNullable<ExtensionContext["model"]>;
|
||||
apiKey: string;
|
||||
headers?: Record<string, string>;
|
||||
signal: AbortSignal;
|
||||
reserveTokens: number;
|
||||
maxChunkTokens: number;
|
||||
@@ -240,7 +239,6 @@ async function summarizeChunks(params: {
|
||||
params.model,
|
||||
params.reserveTokens,
|
||||
params.apiKey,
|
||||
params.headers,
|
||||
params.signal,
|
||||
effectiveInstructions,
|
||||
summary,
|
||||
@@ -267,7 +265,6 @@ export async function summarizeWithFallback(params: {
|
||||
messages: AgentMessage[];
|
||||
model: NonNullable<ExtensionContext["model"]>;
|
||||
apiKey: string;
|
||||
headers?: Record<string, string>;
|
||||
signal: AbortSignal;
|
||||
reserveTokens: number;
|
||||
maxChunkTokens: number;
|
||||
@@ -337,7 +334,6 @@ export async function summarizeInStages(params: {
|
||||
messages: AgentMessage[];
|
||||
model: NonNullable<ExtensionContext["model"]>;
|
||||
apiKey: string;
|
||||
headers?: Record<string, string>;
|
||||
signal: AbortSignal;
|
||||
reserveTokens: number;
|
||||
maxChunkTokens: number;
|
||||
|
||||
@@ -614,18 +614,8 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||
return { cancel: true };
|
||||
}
|
||||
|
||||
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
||||
if (!auth.ok) {
|
||||
log.warn(
|
||||
`Compaction safeguard: failed to resolve auth; cancelling compaction to preserve history. ${auth.error}`,
|
||||
);
|
||||
setCompactionSafeguardCancelReason(
|
||||
ctx.sessionManager,
|
||||
`Compaction safeguard could not resolve request auth for ${model.provider}/${model.id}: ${auth.error}`,
|
||||
);
|
||||
return { cancel: true };
|
||||
}
|
||||
if (!auth.apiKey && !auth.headers) {
|
||||
const apiKey = await ctx.modelRegistry.getApiKey(model);
|
||||
if (!apiKey) {
|
||||
log.warn(
|
||||
"Compaction safeguard: no request auth available; cancelling compaction to preserve history.",
|
||||
);
|
||||
@@ -635,8 +625,6 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||
);
|
||||
return { cancel: true };
|
||||
}
|
||||
const apiKey = auth.apiKey ?? "";
|
||||
const headers = auth.headers;
|
||||
|
||||
try {
|
||||
const modelContextWindow = resolveContextWindowTokens(model);
|
||||
@@ -700,7 +688,6 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||
messages: pruned.droppedMessagesList,
|
||||
model,
|
||||
apiKey,
|
||||
headers,
|
||||
signal,
|
||||
reserveTokens: Math.max(1, Math.floor(preparation.settings.reserveTokens)),
|
||||
maxChunkTokens: droppedMaxChunkTokens,
|
||||
@@ -772,7 +759,6 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||
messages: messagesToSummarize,
|
||||
model,
|
||||
apiKey,
|
||||
headers,
|
||||
signal,
|
||||
reserveTokens,
|
||||
maxChunkTokens,
|
||||
@@ -789,7 +775,6 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
||||
messages: turnPrefixMessages,
|
||||
model,
|
||||
apiKey,
|
||||
headers,
|
||||
signal,
|
||||
reserveTokens,
|
||||
maxChunkTokens,
|
||||
|
||||
@@ -60,13 +60,7 @@ function buildEntry(name: string): SkillEntry {
|
||||
description: `${name} test skill`,
|
||||
filePath: path.join(skillDir, "SKILL.md"),
|
||||
baseDir: skillDir,
|
||||
sourceInfo: {
|
||||
path: path.join(skillDir, "SKILL.md"),
|
||||
source: "openclaw-workspace",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: skillDir,
|
||||
},
|
||||
source: "openclaw-workspace",
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
||||
@@ -444,9 +444,9 @@ export async function installSkill(params: SkillInstallRequest): Promise<SkillIn
|
||||
// Warn when install is triggered from a non-bundled source.
|
||||
// Workspace/project/personal agent skills can contain attacker-controlled metadata.
|
||||
const trustedInstallSources = new Set(["openclaw-bundled", "openclaw-managed", "openclaw-extra"]);
|
||||
if (!trustedInstallSources.has(entry.skill.sourceInfo.source)) {
|
||||
if (!trustedInstallSources.has(entry.skill.source)) {
|
||||
warnings.push(
|
||||
`WARNING: Skill "${params.skillName}" install triggered from non-bundled source "${entry.skill.sourceInfo.source}". Verify the install recipe is trusted.`,
|
||||
`WARNING: Skill "${params.skillName}" install triggered from non-bundled source "${entry.skill.source}". Verify the install recipe is trusted.`,
|
||||
);
|
||||
}
|
||||
if (!spec) {
|
||||
|
||||
@@ -17,13 +17,7 @@ describe("buildWorkspaceSkillStatus", () => {
|
||||
description: "test",
|
||||
filePath: "/tmp/os-scoped",
|
||||
baseDir: "/tmp",
|
||||
sourceInfo: {
|
||||
path: "/tmp/os-scoped",
|
||||
source: "test",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: "/tmp",
|
||||
},
|
||||
source: "test",
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
||||
@@ -189,7 +189,7 @@ function buildSkillStatus(
|
||||
const bundled =
|
||||
bundledNames && bundledNames.size > 0
|
||||
? bundledNames.has(entry.skill.name)
|
||||
: entry.skill.sourceInfo.source === "openclaw-bundled";
|
||||
: entry.skill.source === "openclaw-bundled";
|
||||
|
||||
const { emoji, homepage, required, missing, requirementsSatisfied, configChecks } =
|
||||
evaluateEntryRequirementsForCurrentPlatform({
|
||||
@@ -205,7 +205,7 @@ function buildSkillStatus(
|
||||
return {
|
||||
name: entry.skill.name,
|
||||
description: entry.skill.description,
|
||||
source: entry.skill.sourceInfo.source,
|
||||
source: entry.skill.source,
|
||||
bundled,
|
||||
filePath: entry.skill.filePath,
|
||||
baseDir: entry.skill.baseDir,
|
||||
|
||||
@@ -24,13 +24,7 @@ function makeEntry(params: {
|
||||
description: `desc:${params.name}`,
|
||||
filePath: `/tmp/${params.name}/SKILL.md`,
|
||||
baseDir: `/tmp/${params.name}`,
|
||||
sourceInfo: {
|
||||
path: `/tmp/${params.name}/SKILL.md`,
|
||||
source: params.source ?? "openclaw-workspace",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: `/tmp/${params.name}`,
|
||||
},
|
||||
source: params.source ?? "openclaw-workspace",
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
||||
@@ -17,13 +17,7 @@ describe("resolveSkillsPromptForRun", () => {
|
||||
description: "Demo",
|
||||
filePath: "/app/skills/demo-skill/SKILL.md",
|
||||
baseDir: "/app/skills/demo-skill",
|
||||
sourceInfo: {
|
||||
path: "/app/skills/demo-skill/SKILL.md",
|
||||
source: "openclaw-bundled",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: "/app/skills/demo-skill",
|
||||
},
|
||||
source: "openclaw-bundled",
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
||||
@@ -14,13 +14,7 @@ function makeSkill(name: string, desc = "A skill", filePath = `/skills/${name}/S
|
||||
description: desc,
|
||||
filePath,
|
||||
baseDir: `/skills/${name}`,
|
||||
sourceInfo: {
|
||||
path: filePath,
|
||||
source: "workspace",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: `/skills/${name}`,
|
||||
},
|
||||
source: "workspace",
|
||||
disableModelInvocation: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ function normalizeAllowlist(input: unknown): string[] | undefined {
|
||||
const BUNDLED_SOURCES = new Set(["openclaw-bundled"]);
|
||||
|
||||
function isBundledSkill(entry: SkillEntry): boolean {
|
||||
return BUNDLED_SOURCES.has(entry.skill.sourceInfo.source);
|
||||
return BUNDLED_SOURCES.has(entry.skill.source);
|
||||
}
|
||||
|
||||
export function resolveBundledAllowlist(config?: OpenClawConfig): string[] | undefined {
|
||||
|
||||
@@ -38,13 +38,7 @@ describe("skills-cli (e2e)", () => {
|
||||
description: "Capture UI screenshots",
|
||||
filePath: path.join(baseDir, "SKILL.md"),
|
||||
baseDir,
|
||||
sourceInfo: {
|
||||
path: path.join(baseDir, "SKILL.md"),
|
||||
source: "openclaw-bundled",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir,
|
||||
},
|
||||
source: "openclaw-bundled",
|
||||
disableModelInvocation: false,
|
||||
} as SkillEntry["skill"],
|
||||
frontmatter: {},
|
||||
|
||||
@@ -1261,7 +1261,7 @@ export async function collectInstalledSkillsCodeSafetyFindings(params: {
|
||||
for (const workspaceDir of workspaceDirs) {
|
||||
const entries = loadWorkspaceSkillEntries(workspaceDir, { config: params.cfg });
|
||||
for (const entry of entries) {
|
||||
if (entry.skill.sourceInfo.source === "openclaw-bundled") {
|
||||
if (entry.skill.source === "openclaw-bundled") {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,8 +109,9 @@ function renderCronFilterIcon(hiddenCount: number) {
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
${hiddenCount > 0
|
||||
? html`<span
|
||||
${
|
||||
hiddenCount > 0
|
||||
? html`<span
|
||||
style="
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
@@ -125,7 +126,8 @@ function renderCronFilterIcon(hiddenCount: number) {
|
||||
"
|
||||
>${hiddenCount}</span
|
||||
>`
|
||||
: ""}
|
||||
: ""
|
||||
}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
@@ -311,11 +313,13 @@ export function renderChatControls(state: AppViewState) {
|
||||
state.sessionsHideCron = !hideCron;
|
||||
}}
|
||||
aria-pressed=${hideCron}
|
||||
title=${hideCron
|
||||
? hiddenCronCount > 0
|
||||
? t("chat.showCronSessionsHidden", { count: String(hiddenCronCount) })
|
||||
: t("chat.showCronSessions")
|
||||
: t("chat.hideCronSessions")}
|
||||
title=${
|
||||
hideCron
|
||||
? hiddenCronCount > 0
|
||||
? t("chat.showCronSessionsHidden", { count: String(hiddenCronCount) })
|
||||
: t("chat.showCronSessions")
|
||||
: t("chat.hideCronSessions")
|
||||
}
|
||||
>
|
||||
${renderCronFilterIcon(hiddenCronCount)}
|
||||
</button>
|
||||
@@ -941,9 +945,9 @@ export function renderTopbarThemeModeToggle(state: AppViewState) {
|
||||
(opt) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="topbar-theme-mode__btn ${opt.id === state.themeMode
|
||||
? "topbar-theme-mode__btn--active"
|
||||
: ""}"
|
||||
class="topbar-theme-mode__btn ${
|
||||
opt.id === state.themeMode ? "topbar-theme-mode__btn--active" : ""
|
||||
}"
|
||||
title=${opt.label}
|
||||
aria-label="Color mode: ${opt.label}"
|
||||
aria-pressed=${opt.id === state.themeMode}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -177,9 +177,11 @@ export function renderMessageGroup(
|
||||
<span class="chat-group-timestamp">${timestamp}</span>
|
||||
${renderMessageMeta(meta)}
|
||||
${normalizedRole === "assistant" && isTtsSupported() ? renderTtsButton(group) : nothing}
|
||||
${opts.onDelete
|
||||
? renderDeleteButton(opts.onDelete, normalizedRole === "user" ? "left" : "right")
|
||||
: nothing}
|
||||
${
|
||||
opts.onDelete
|
||||
? renderDeleteButton(opts.onDelete, normalizedRole === "user" ? "left" : "right")
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -695,70 +697,84 @@ function renderGroupedMessage(
|
||||
|
||||
return html`
|
||||
<div class="${bubbleClasses}">
|
||||
${hasActions
|
||||
? html`<div class="chat-bubble-actions">
|
||||
${
|
||||
hasActions
|
||||
? html`<div class="chat-bubble-actions">
|
||||
${canExpand ? renderExpandButton(markdown!, onOpenSidebar!) : nothing}
|
||||
${canCopyMarkdown ? renderCopyAsMarkdownButton(markdown!) : nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
${isToolMessage
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isToolMessage
|
||||
? html`
|
||||
<details class="chat-tool-msg-collapse">
|
||||
<summary class="chat-tool-msg-summary">
|
||||
<span class="chat-tool-msg-summary__icon">${icons.zap}</span>
|
||||
<span class="chat-tool-msg-summary__label">Tool output</span>
|
||||
${toolSummaryLabel
|
||||
? html`<span class="chat-tool-msg-summary__names">${toolSummaryLabel}</span>`
|
||||
: toolPreview
|
||||
? html`<span class="chat-tool-msg-summary__preview">${toolPreview}</span>`
|
||||
: nothing}
|
||||
${
|
||||
toolSummaryLabel
|
||||
? html`<span class="chat-tool-msg-summary__names">${toolSummaryLabel}</span>`
|
||||
: toolPreview
|
||||
? html`<span class="chat-tool-msg-summary__preview">${toolPreview}</span>`
|
||||
: nothing
|
||||
}
|
||||
</summary>
|
||||
<div class="chat-tool-msg-body">
|
||||
${renderMessageImages(images)}
|
||||
${reasoningMarkdown
|
||||
? html`<div class="chat-thinking">
|
||||
${
|
||||
reasoningMarkdown
|
||||
? html`<div class="chat-thinking">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(reasoningMarkdown))}
|
||||
</div>`
|
||||
: nothing}
|
||||
${jsonResult
|
||||
? html`<details class="chat-json-collapse">
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
jsonResult
|
||||
? html`<details class="chat-json-collapse">
|
||||
<summary class="chat-json-summary">
|
||||
<span class="chat-json-badge">JSON</span>
|
||||
<span class="chat-json-label">${jsonSummaryLabel(jsonResult.parsed)}</span>
|
||||
</summary>
|
||||
<pre class="chat-json-content"><code>${jsonResult.pretty}</code></pre>
|
||||
</details>`
|
||||
: markdown
|
||||
? html`<div class="chat-text" dir="${detectTextDirection(markdown)}">
|
||||
: markdown
|
||||
? html`<div class="chat-text" dir="${detectTextDirection(markdown)}">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(markdown))}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${hasToolCards ? renderCollapsedToolCards(toolCards, onOpenSidebar) : nothing}
|
||||
</div>
|
||||
</details>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
${renderMessageImages(images)}
|
||||
${reasoningMarkdown
|
||||
? html`<div class="chat-thinking">
|
||||
${
|
||||
reasoningMarkdown
|
||||
? html`<div class="chat-thinking">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(reasoningMarkdown))}
|
||||
</div>`
|
||||
: nothing}
|
||||
${jsonResult
|
||||
? html`<details class="chat-json-collapse">
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
jsonResult
|
||||
? html`<details class="chat-json-collapse">
|
||||
<summary class="chat-json-summary">
|
||||
<span class="chat-json-badge">JSON</span>
|
||||
<span class="chat-json-label">${jsonSummaryLabel(jsonResult.parsed)}</span>
|
||||
</summary>
|
||||
<pre class="chat-json-content"><code>${jsonResult.pretty}</code></pre>
|
||||
</details>`
|
||||
: markdown
|
||||
? html`<div class="chat-text" dir="${detectTextDirection(markdown)}">
|
||||
: markdown
|
||||
? html`<div class="chat-text" dir="${detectTextDirection(markdown)}">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(markdown))}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${hasToolCards ? renderCollapsedToolCards(toolCards, onOpenSidebar) : nothing}
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -81,35 +81,49 @@ export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content:
|
||||
@click=${handleClick}
|
||||
role=${canClick ? "button" : nothing}
|
||||
tabindex=${canClick ? "0" : nothing}
|
||||
@keydown=${canClick
|
||||
? (e: KeyboardEvent) => {
|
||||
if (e.key !== "Enter" && e.key !== " ") {
|
||||
return;
|
||||
@keydown=${
|
||||
canClick
|
||||
? (e: KeyboardEvent) => {
|
||||
if (e.key !== "Enter" && e.key !== " ") {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
handleClick?.();
|
||||
}
|
||||
e.preventDefault();
|
||||
handleClick?.();
|
||||
}
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
>
|
||||
<div class="chat-tool-card__header">
|
||||
<div class="chat-tool-card__title">
|
||||
<span class="chat-tool-card__icon">${icons[display.icon]}</span>
|
||||
<span>${display.label}</span>
|
||||
</div>
|
||||
${canClick
|
||||
? html`<span class="chat-tool-card__action"
|
||||
${
|
||||
canClick
|
||||
? html`<span class="chat-tool-card__action"
|
||||
>${hasText ? "View" : ""} ${icons.check}</span
|
||||
>`
|
||||
: nothing}
|
||||
${isEmpty && !canClick
|
||||
? html`<span class="chat-tool-card__status">${icons.check}</span>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isEmpty && !canClick
|
||||
? html`<span class="chat-tool-card__status">${icons.check}</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${detail ? html`<div class="chat-tool-card__detail">${detail}</div>` : nothing}
|
||||
${isEmpty ? html` <div class="chat-tool-card__status-text muted">Completed</div> ` : nothing}
|
||||
${showCollapsed
|
||||
? html`<div class="chat-tool-card__preview mono">${getTruncatedPreview(card.text!)}</div>`
|
||||
: nothing}
|
||||
${
|
||||
isEmpty
|
||||
? html`
|
||||
<div class="chat-tool-card__status-text muted">Completed</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
showCollapsed
|
||||
? html`<div class="chat-tool-card__preview mono">${getTruncatedPreview(card.text!)}</div>`
|
||||
: nothing
|
||||
}
|
||||
${showInline ? html`<div class="chat-tool-card__inline mono">${card.text}</div>` : nothing}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -121,7 +121,9 @@ export const icons = {
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
`,
|
||||
check: html` <svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5" /></svg> `,
|
||||
check: html`
|
||||
<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5" /></svg>
|
||||
`,
|
||||
arrowDown: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M12 5v14" />
|
||||
@@ -142,12 +144,8 @@ export const icons = {
|
||||
`,
|
||||
brain: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"
|
||||
/>
|
||||
<path
|
||||
d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"
|
||||
/>
|
||||
<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" />
|
||||
<path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" />
|
||||
<path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" />
|
||||
<path d="M17.599 6.5a3 3 0 0 0 .399-1.375" />
|
||||
<path d="M6.003 5.125A3 3 0 0 0 6.401 6.5" />
|
||||
@@ -238,7 +236,9 @@ export const icons = {
|
||||
<path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z" />
|
||||
</svg>
|
||||
`,
|
||||
circle: html` <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /></svg> `,
|
||||
circle: html`
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /></svg>
|
||||
`,
|
||||
puzzle: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -286,7 +286,9 @@ export const icons = {
|
||||
<path d="M22 2 11 13" />
|
||||
</svg>
|
||||
`,
|
||||
stop: html` <svg viewBox="0 0 24 24"><rect width="14" height="14" x="5" y="5" rx="1" /></svg> `,
|
||||
stop: html`
|
||||
<svg viewBox="0 0 24 24"><rect width="14" height="14" x="5" y="5" rx="1" /></svg>
|
||||
`,
|
||||
pin: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<line x1="12" x2="12" y1="17" y2="22" />
|
||||
|
||||
@@ -115,13 +115,13 @@ export function renderAgentOverview(params: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${configDirty
|
||||
? html`
|
||||
<div class="callout warn" style="margin-top: 16px">
|
||||
You have unsaved config changes.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
configDirty
|
||||
? html`
|
||||
<div class="callout warn" style="margin-top: 16px">You have unsaved config changes.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="agent-model-select" style="margin-top: 20px;">
|
||||
<div class="label">Model Selection</div>
|
||||
@@ -134,13 +134,17 @@ export function renderAgentOverview(params: {
|
||||
@change=${(e: Event) =>
|
||||
onModelChange(agent.id, (e.target as HTMLSelectElement).value || null)}
|
||||
>
|
||||
${isDefault
|
||||
? html` <option value="">Not set</option> `
|
||||
: html`
|
||||
${
|
||||
isDefault
|
||||
? html`
|
||||
<option value="">Not set</option>
|
||||
`
|
||||
: html`
|
||||
<option value="">
|
||||
${defaultPrimary ? `Inherit default (${defaultPrimary})` : "Inherit default"}
|
||||
</option>
|
||||
`}
|
||||
`
|
||||
}
|
||||
${buildModelOptions(configForm, effectivePrimary ?? undefined, params.modelCatalog)}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
@@ -179,19 +179,24 @@ export function renderAgentChannels(params: {
|
||||
</button>
|
||||
</div>
|
||||
<div class="muted" style="margin-top: 8px;">Last refresh: ${lastSuccessLabel}</div>
|
||||
${params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing}
|
||||
${!params.snapshot
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load channels to see live status.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${entries.length === 0
|
||||
? html` <div class="muted" style="margin-top: 16px">No channels found.</div> `
|
||||
: html`
|
||||
${
|
||||
params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
!params.snapshot
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">Load channels to see live status.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
entries.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">No channels found.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${entries.map((entry) => {
|
||||
const summary = summarizeChannelAccounts(entry.accounts);
|
||||
@@ -217,28 +222,33 @@ export function renderAgentChannels(params: {
|
||||
<div>${status}</div>
|
||||
<div>${configLabel}</div>
|
||||
<div>${enabled}</div>
|
||||
${summary.configured === 0
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href="https://docs.openclaw.ai/channels"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="color: var(--accent); font-size: 12px"
|
||||
>Setup guide</a
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${extras.length > 0
|
||||
? extras.map((extra) => html`<div>${extra.label}: ${extra.value}</div>`)
|
||||
: nothing}
|
||||
${
|
||||
summary.configured === 0
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href="https://docs.openclaw.ai/channels"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="color: var(--accent); font-size: 12px"
|
||||
>Setup guide</a
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
extras.length > 0
|
||||
? extras.map((extra) => html`<div>${extra.label}: ${extra.value}</div>`)
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
</section>
|
||||
`;
|
||||
@@ -289,26 +299,33 @@ export function renderAgentCron(params: {
|
||||
<div class="stat-value">${formatNextRun(params.status?.nextWakeAtMs ?? null)}</div>
|
||||
</div>
|
||||
</div>
|
||||
${params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing}
|
||||
${
|
||||
params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
</section>
|
||||
</section>
|
||||
<section class="card">
|
||||
<div class="card-title">Agent Cron Jobs</div>
|
||||
<div class="card-sub">Scheduled jobs targeting this agent.</div>
|
||||
${jobs.length === 0
|
||||
? html` <div class="muted" style="margin-top: 16px">No jobs assigned.</div> `
|
||||
: html`
|
||||
${
|
||||
jobs.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">No jobs assigned.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${jobs.map(
|
||||
(job) => html`
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
<div class="list-title">${job.name}</div>
|
||||
${job.description
|
||||
? html`<div class="list-sub">${job.description}</div>`
|
||||
: nothing}
|
||||
${
|
||||
job.description
|
||||
? html`<div class="list-sub">${job.description}</div>`
|
||||
: nothing
|
||||
}
|
||||
<div class="chip-row" style="margin-top: 6px;">
|
||||
<span class="chip">${formatCronSchedule(job)}</span>
|
||||
<span class="chip ${job.enabled ? "chip-ok" : "chip-warn"}">
|
||||
@@ -333,7 +350,8 @@ export function renderAgentCron(params: {
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
@@ -376,46 +394,60 @@ export function renderAgentFiles(params: {
|
||||
${params.agentFilesLoading ? "Loading…" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
${list
|
||||
? html`<div class="muted mono" style="margin-top: 8px;">
|
||||
${
|
||||
list
|
||||
? html`<div class="muted mono" style="margin-top: 8px;">
|
||||
Workspace: <span>${list.workspace}</span>
|
||||
</div>`
|
||||
: nothing}
|
||||
${params.agentFilesError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
params.agentFilesError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${params.agentFilesError}
|
||||
</div>`
|
||||
: nothing}
|
||||
${!list
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the agent workspace files to edit core instructions.
|
||||
</div>
|
||||
`
|
||||
: files.length === 0
|
||||
? html` <div class="muted" style="margin-top: 16px">No files found.</div> `
|
||||
: html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
!list
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the agent workspace files to edit core instructions.
|
||||
</div>
|
||||
`
|
||||
: files.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">No files found.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-tabs" style="margin-top: 14px;">
|
||||
${files.map((file) => {
|
||||
const isActive = active === file.name;
|
||||
const label = file.name.replace(/\.md$/i, "");
|
||||
return html`
|
||||
<button
|
||||
class="agent-tab ${isActive ? "active" : ""} ${file.missing
|
||||
? "agent-tab--missing"
|
||||
: ""}"
|
||||
class="agent-tab ${isActive ? "active" : ""} ${
|
||||
file.missing ? "agent-tab--missing" : ""
|
||||
}"
|
||||
@click=${() => params.onSelectFile(file.name)}
|
||||
>
|
||||
${label}${file.missing
|
||||
? html` <span class="agent-tab-badge">missing</span> `
|
||||
: nothing}
|
||||
${label}${
|
||||
file.missing
|
||||
? html`
|
||||
<span class="agent-tab-badge">missing</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</button>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${!activeEntry
|
||||
? html` <div class="muted" style="margin-top: 16px">Select a file to edit.</div> `
|
||||
: html`
|
||||
${
|
||||
!activeEntry
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">Select a file to edit.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-file-header" style="margin-top: 14px;">
|
||||
<div>
|
||||
<div class="agent-file-sub mono">${activeEntry.path}</div>
|
||||
@@ -450,13 +482,15 @@ export function renderAgentFiles(params: {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${activeEntry.missing
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 10px">
|
||||
This file is missing. Saving will create it in the agent workspace.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
activeEntry.missing
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 10px">
|
||||
This file is missing. Saving will create it in the agent workspace.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<label class="field agent-file-field" style="margin-top: 12px;">
|
||||
<span>Content</span>
|
||||
<textarea
|
||||
@@ -536,8 +570,10 @@ export function renderAgentFiles(params: {
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
`}
|
||||
`}
|
||||
`
|
||||
}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -200,41 +200,49 @@ export function renderAgentTools(params: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${!params.configForm
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the gateway config to adjust tool profiles.
|
||||
</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.
|
||||
</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.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${params.toolsCatalogLoading && !params.toolsCatalogResult && !params.toolsCatalogError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">Loading runtime tool catalog…</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.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
!params.configForm
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the gateway config to adjust tool profiles.
|
||||
</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.
|
||||
</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.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
params.toolsCatalogLoading && !params.toolsCatalogResult && !params.toolsCatalogError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">Loading runtime tool catalog…</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.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="agent-tools-meta" style="margin-top: 16px;">
|
||||
<div class="agent-kv">
|
||||
@@ -245,14 +253,16 @@ export function renderAgentTools(params: {
|
||||
<div class="label">Source</div>
|
||||
<div>${profileSource}</div>
|
||||
</div>
|
||||
${params.configDirty
|
||||
? html`
|
||||
<div class="agent-kv">
|
||||
<div class="label">Status</div>
|
||||
<div class="mono">unsaved</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
params.configDirty
|
||||
? html`
|
||||
<div class="agent-kv">
|
||||
<div class="label">Status</div>
|
||||
<div class="mono">unsaved</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 18px;">
|
||||
@@ -261,31 +271,32 @@ export function renderAgentTools(params: {
|
||||
What this agent can use in the current chat session.
|
||||
<span class="mono">${params.runtimeSessionKey || "no session"}</span>
|
||||
</div>
|
||||
${!params.runtimeSessionMatchesSelectedAgent
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Switch chat to this agent to view its live runtime tools.
|
||||
</div>
|
||||
`
|
||||
: params.toolsEffectiveLoading &&
|
||||
!params.toolsEffectiveResult &&
|
||||
!params.toolsEffectiveError
|
||||
${
|
||||
!params.runtimeSessionMatchesSelectedAgent
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">Loading available tools…</div>
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Switch chat to this agent to view its live runtime tools.
|
||||
</div>
|
||||
`
|
||||
: params.toolsEffectiveError
|
||||
: params.toolsEffectiveLoading &&
|
||||
!params.toolsEffectiveResult &&
|
||||
!params.toolsEffectiveError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Could not load available tools for this session.
|
||||
</div>
|
||||
<div class="callout info" style="margin-top: 12px">Loading available tools…</div>
|
||||
`
|
||||
: (params.toolsEffectiveResult?.groups?.length ?? 0) === 0
|
||||
: params.toolsEffectiveError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
No tools are available for this session right now.
|
||||
Could not load available tools for this session.
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
: (params.toolsEffectiveResult?.groups?.length ?? 0) === 0
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
No tools are available for this session right now.
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-tools-grid" style="margin-top: 16px;">
|
||||
${params.toolsEffectiveResult?.groups.map(
|
||||
(group) => html`
|
||||
@@ -314,7 +325,8 @@ export function renderAgentTools(params: {
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="agent-tools-presets" style="margin-top: 16px;">
|
||||
@@ -347,11 +359,13 @@ export function renderAgentTools(params: {
|
||||
<div class="agent-tools-section">
|
||||
<div class="agent-tools-header">
|
||||
${section.label}
|
||||
${section.source === "plugin" && section.pluginId
|
||||
? html`<span class="agent-pill" style="margin-left: 8px;"
|
||||
${
|
||||
section.source === "plugin" && section.pluginId
|
||||
? html`<span class="agent-pill" style="margin-left: 8px;"
|
||||
>plugin:${section.pluginId}</span
|
||||
>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="agent-tools-list">
|
||||
${section.tools.map((tool) => {
|
||||
@@ -430,9 +444,11 @@ export function renderAgentSkills(params: {
|
||||
<div class="card-title">Skills</div>
|
||||
<div class="card-sub">
|
||||
Per-agent skill allowlist and workspace skills.
|
||||
${totalCount > 0
|
||||
? html`<span class="mono">${enabledCount}/${totalCount}</span>`
|
||||
: nothing}
|
||||
${
|
||||
totalCount > 0
|
||||
? html`<span class="mono">${enabledCount}/${totalCount}</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="gap: 8px; flex-wrap: wrap;">
|
||||
@@ -483,34 +499,40 @@ export function renderAgentSkills(params: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${!params.configForm
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the gateway config to set per-agent skills.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${usingAllowlist
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
This agent uses a custom skill allowlist.
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
All skills are enabled. Disabling any skill will create a per-agent allowlist.
|
||||
</div>
|
||||
`}
|
||||
${!reportReady && !params.loading
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load skills for this agent to view workspace-specific entries.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing}
|
||||
${
|
||||
!params.configForm
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the gateway config to set per-agent skills.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
usingAllowlist
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">This agent uses a custom skill allowlist.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
All skills are enabled. Disabling any skill will create a per-agent allowlist.
|
||||
</div>
|
||||
`
|
||||
}
|
||||
${
|
||||
!reportReady && !params.loading
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load skills for this agent to view workspace-specific entries.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="filters" style="margin-top: 14px;">
|
||||
<label class="field" style="flex: 1;">
|
||||
@@ -526,9 +548,12 @@ export function renderAgentSkills(params: {
|
||||
<div class="muted">${filtered.length} shown</div>
|
||||
</div>
|
||||
|
||||
${filtered.length === 0
|
||||
? html` <div class="muted" style="margin-top: 16px">No skills found.</div> `
|
||||
: html`
|
||||
${
|
||||
filtered.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">No skills found.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-skills-groups" style="margin-top: 16px;">
|
||||
${groups.map((group) =>
|
||||
renderAgentSkillGroup(group, {
|
||||
@@ -540,7 +565,8 @@ export function renderAgentSkills(params: {
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
@@ -596,12 +622,16 @@ function renderAgentSkillRow(
|
||||
<div class="list-title">${skill.emoji ? `${skill.emoji} ` : ""}${skill.name}</div>
|
||||
<div class="list-sub">${skill.description}</div>
|
||||
${renderSkillStatusChips({ skill })}
|
||||
${missing.length > 0
|
||||
? html`<div class="muted" style="margin-top: 6px;">Missing: ${missing.join(", ")}</div>`
|
||||
: nothing}
|
||||
${reasons.length > 0
|
||||
? html`<div class="muted" style="margin-top: 6px;">Reason: ${reasons.join(", ")}</div>`
|
||||
: nothing}
|
||||
${
|
||||
missing.length > 0
|
||||
? html`<div class="muted" style="margin-top: 6px;">Missing: ${missing.join(", ")}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
reasons.length > 0
|
||||
? html`<div class="muted" style="margin-top: 6px;">Reason: ${reasons.join(", ")}</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<label class="cfg-toggle">
|
||||
|
||||
@@ -154,22 +154,29 @@ export function renderAgents(props: AgentsProps) {
|
||||
?disabled=${props.loading || agents.length === 0}
|
||||
@change=${(e: Event) => props.onSelectAgent((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
${agents.length === 0
|
||||
? html` <option value="">No agents</option> `
|
||||
: agents.map(
|
||||
(agent) => html`
|
||||
${
|
||||
agents.length === 0
|
||||
? html`
|
||||
<option value="">No agents</option>
|
||||
`
|
||||
: agents.map(
|
||||
(agent) => html`
|
||||
<option value=${agent.id} ?selected=${agent.id === selectedId}>
|
||||
${normalizeAgentLabel(agent)}${agentBadgeText(agent.id, defaultId)
|
||||
? ` (${agentBadgeText(agent.id, defaultId)})`
|
||||
: ""}
|
||||
${normalizeAgentLabel(agent)}${
|
||||
agentBadgeText(agent.id, defaultId)
|
||||
? ` (${agentBadgeText(agent.id, defaultId)})`
|
||||
: ""
|
||||
}
|
||||
</option>
|
||||
`,
|
||||
)}
|
||||
)
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="agents-toolbar-actions">
|
||||
${selectedAgent
|
||||
? html`
|
||||
${
|
||||
selectedAgent
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--sm btn--ghost"
|
||||
@@ -183,14 +190,17 @@ export function renderAgents(props: AgentsProps) {
|
||||
class="btn btn--sm btn--ghost"
|
||||
?disabled=${Boolean(defaultId && selectedAgent.id === defaultId)}
|
||||
@click=${() => props.onSetDefault(selectedAgent.id)}
|
||||
title=${defaultId && selectedAgent.id === defaultId
|
||||
? "Already the default agent"
|
||||
: "Set as the default agent"}
|
||||
title=${
|
||||
defaultId && selectedAgent.id === defaultId
|
||||
? "Already the default agent"
|
||||
: "Set as the default agent"
|
||||
}
|
||||
>
|
||||
${defaultId && selectedAgent.id === defaultId ? "Default" : "Set Default"}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<button
|
||||
class="btn btn--sm agents-refresh-btn"
|
||||
?disabled=${props.loading}
|
||||
@@ -200,142 +210,158 @@ export function renderAgents(props: AgentsProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${props.error
|
||||
? html`<div class="callout danger" style="margin-top: 8px;">${props.error}</div>`
|
||||
: nothing}
|
||||
${
|
||||
props.error
|
||||
? html`<div class="callout danger" style="margin-top: 8px;">${props.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
</section>
|
||||
<section class="agents-main">
|
||||
${!selectedAgent
|
||||
? html`
|
||||
<div class="card">
|
||||
<div class="card-title">Select an agent</div>
|
||||
<div class="card-sub">Pick an agent to inspect its workspace and tools.</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
${
|
||||
!selectedAgent
|
||||
? html`
|
||||
<div class="card">
|
||||
<div class="card-title">Select an agent</div>
|
||||
<div class="card-sub">Pick an agent to inspect its workspace and tools.</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
${renderAgentTabs(
|
||||
props.activePanel,
|
||||
(panel) => props.onSelectPanel(panel),
|
||||
tabCounts,
|
||||
)}
|
||||
${props.activePanel === "overview"
|
||||
? renderAgentOverview({
|
||||
agent: selectedAgent,
|
||||
basePath: props.basePath,
|
||||
defaultId,
|
||||
configForm: props.config.form,
|
||||
agentFilesList: props.agentFiles.list,
|
||||
agentIdentity: props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
agentIdentityError: props.agentIdentityError,
|
||||
agentIdentityLoading: props.agentIdentityLoading,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
modelCatalog: props.modelCatalog,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
onModelChange: props.onModelChange,
|
||||
onModelFallbacksChange: props.onModelFallbacksChange,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing}
|
||||
${props.activePanel === "files"
|
||||
? renderAgentFiles({
|
||||
agentId: selectedAgent.id,
|
||||
agentFilesList: props.agentFiles.list,
|
||||
agentFilesLoading: props.agentFiles.loading,
|
||||
agentFilesError: props.agentFiles.error,
|
||||
agentFileActive: props.agentFiles.active,
|
||||
agentFileContents: props.agentFiles.contents,
|
||||
agentFileDrafts: props.agentFiles.drafts,
|
||||
agentFileSaving: props.agentFiles.saving,
|
||||
onLoadFiles: props.onLoadFiles,
|
||||
onSelectFile: props.onSelectFile,
|
||||
onFileDraftChange: props.onFileDraftChange,
|
||||
onFileReset: props.onFileReset,
|
||||
onFileSave: props.onFileSave,
|
||||
})
|
||||
: nothing}
|
||||
${props.activePanel === "tools"
|
||||
? renderAgentTools({
|
||||
agentId: selectedAgent.id,
|
||||
configForm: props.config.form,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
toolsCatalogLoading: props.toolsCatalog.loading,
|
||||
toolsCatalogError: props.toolsCatalog.error,
|
||||
toolsCatalogResult: props.toolsCatalog.result,
|
||||
toolsEffectiveLoading: props.toolsEffective.loading,
|
||||
toolsEffectiveError: props.toolsEffective.error,
|
||||
toolsEffectiveResult: props.toolsEffective.result,
|
||||
runtimeSessionKey: props.runtimeSessionKey,
|
||||
runtimeSessionMatchesSelectedAgent: props.runtimeSessionMatchesSelectedAgent,
|
||||
onProfileChange: props.onToolsProfileChange,
|
||||
onOverridesChange: props.onToolsOverridesChange,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
})
|
||||
: nothing}
|
||||
${props.activePanel === "skills"
|
||||
? renderAgentSkills({
|
||||
agentId: selectedAgent.id,
|
||||
report: props.agentSkills.report,
|
||||
loading: props.agentSkills.loading,
|
||||
error: props.agentSkills.error,
|
||||
activeAgentId: props.agentSkills.agentId,
|
||||
configForm: props.config.form,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
filter: props.agentSkills.filter,
|
||||
onFilterChange: props.onSkillsFilterChange,
|
||||
onRefresh: props.onSkillsRefresh,
|
||||
onToggle: props.onAgentSkillToggle,
|
||||
onClear: props.onAgentSkillsClear,
|
||||
onDisableAll: props.onAgentSkillsDisableAll,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
})
|
||||
: nothing}
|
||||
${props.activePanel === "channels"
|
||||
? renderAgentChannels({
|
||||
context: buildAgentContext(
|
||||
selectedAgent,
|
||||
props.config.form,
|
||||
props.agentFiles.list,
|
||||
${
|
||||
props.activePanel === "overview"
|
||||
? renderAgentOverview({
|
||||
agent: selectedAgent,
|
||||
basePath: props.basePath,
|
||||
defaultId,
|
||||
props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
),
|
||||
configForm: props.config.form,
|
||||
snapshot: props.channels.snapshot,
|
||||
loading: props.channels.loading,
|
||||
error: props.channels.error,
|
||||
lastSuccess: props.channels.lastSuccess,
|
||||
onRefresh: props.onChannelsRefresh,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing}
|
||||
${props.activePanel === "cron"
|
||||
? renderAgentCron({
|
||||
context: buildAgentContext(
|
||||
selectedAgent,
|
||||
props.config.form,
|
||||
props.agentFiles.list,
|
||||
defaultId,
|
||||
props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
),
|
||||
agentId: selectedAgent.id,
|
||||
jobs: props.cron.jobs,
|
||||
status: props.cron.status,
|
||||
loading: props.cron.loading,
|
||||
error: props.cron.error,
|
||||
onRefresh: props.onCronRefresh,
|
||||
onRunNow: props.onCronRunNow,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing}
|
||||
`}
|
||||
configForm: props.config.form,
|
||||
agentFilesList: props.agentFiles.list,
|
||||
agentIdentity: props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
agentIdentityError: props.agentIdentityError,
|
||||
agentIdentityLoading: props.agentIdentityLoading,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
modelCatalog: props.modelCatalog,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
onModelChange: props.onModelChange,
|
||||
onModelFallbacksChange: props.onModelFallbacksChange,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.activePanel === "files"
|
||||
? renderAgentFiles({
|
||||
agentId: selectedAgent.id,
|
||||
agentFilesList: props.agentFiles.list,
|
||||
agentFilesLoading: props.agentFiles.loading,
|
||||
agentFilesError: props.agentFiles.error,
|
||||
agentFileActive: props.agentFiles.active,
|
||||
agentFileContents: props.agentFiles.contents,
|
||||
agentFileDrafts: props.agentFiles.drafts,
|
||||
agentFileSaving: props.agentFiles.saving,
|
||||
onLoadFiles: props.onLoadFiles,
|
||||
onSelectFile: props.onSelectFile,
|
||||
onFileDraftChange: props.onFileDraftChange,
|
||||
onFileReset: props.onFileReset,
|
||||
onFileSave: props.onFileSave,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.activePanel === "tools"
|
||||
? renderAgentTools({
|
||||
agentId: selectedAgent.id,
|
||||
configForm: props.config.form,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
toolsCatalogLoading: props.toolsCatalog.loading,
|
||||
toolsCatalogError: props.toolsCatalog.error,
|
||||
toolsCatalogResult: props.toolsCatalog.result,
|
||||
toolsEffectiveLoading: props.toolsEffective.loading,
|
||||
toolsEffectiveError: props.toolsEffective.error,
|
||||
toolsEffectiveResult: props.toolsEffective.result,
|
||||
runtimeSessionKey: props.runtimeSessionKey,
|
||||
runtimeSessionMatchesSelectedAgent: props.runtimeSessionMatchesSelectedAgent,
|
||||
onProfileChange: props.onToolsProfileChange,
|
||||
onOverridesChange: props.onToolsOverridesChange,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.activePanel === "skills"
|
||||
? renderAgentSkills({
|
||||
agentId: selectedAgent.id,
|
||||
report: props.agentSkills.report,
|
||||
loading: props.agentSkills.loading,
|
||||
error: props.agentSkills.error,
|
||||
activeAgentId: props.agentSkills.agentId,
|
||||
configForm: props.config.form,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
filter: props.agentSkills.filter,
|
||||
onFilterChange: props.onSkillsFilterChange,
|
||||
onRefresh: props.onSkillsRefresh,
|
||||
onToggle: props.onAgentSkillToggle,
|
||||
onClear: props.onAgentSkillsClear,
|
||||
onDisableAll: props.onAgentSkillsDisableAll,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.activePanel === "channels"
|
||||
? renderAgentChannels({
|
||||
context: buildAgentContext(
|
||||
selectedAgent,
|
||||
props.config.form,
|
||||
props.agentFiles.list,
|
||||
defaultId,
|
||||
props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
),
|
||||
configForm: props.config.form,
|
||||
snapshot: props.channels.snapshot,
|
||||
loading: props.channels.loading,
|
||||
error: props.channels.error,
|
||||
lastSuccess: props.channels.lastSuccess,
|
||||
onRefresh: props.onChannelsRefresh,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.activePanel === "cron"
|
||||
? renderAgentCron({
|
||||
context: buildAgentContext(
|
||||
selectedAgent,
|
||||
props.config.form,
|
||||
props.agentFiles.list,
|
||||
defaultId,
|
||||
props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
),
|
||||
agentId: selectedAgent.id,
|
||||
jobs: props.cron.jobs,
|
||||
status: props.cron.status,
|
||||
loading: props.cron.loading,
|
||||
error: props.cron.error,
|
||||
onRefresh: props.onCronRefresh,
|
||||
onRunNow: props.onCronRunNow,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
@@ -363,9 +389,11 @@ function renderAgentTabs(
|
||||
type="button"
|
||||
@click=${() => onSelect(tab.id)}
|
||||
>
|
||||
${tab.label}${counts[tab.id] != null
|
||||
? html`<span class="agent-tab-count">${counts[tab.id]}</span>`
|
||||
: nothing}
|
||||
${tab.label}${
|
||||
counts[tab.id] != null
|
||||
? html`<span class="agent-tab-count">${counts[tab.id]}</span>`
|
||||
: nothing
|
||||
}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
|
||||
@@ -86,11 +86,15 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) {
|
||||
const analysis = analyzeConfigSchema(props.schema);
|
||||
const normalized = analysis.schema;
|
||||
if (!normalized) {
|
||||
return html` <div class="callout danger">Schema unavailable. Use Raw.</div> `;
|
||||
return html`
|
||||
<div class="callout danger">Schema unavailable. Use Raw.</div>
|
||||
`;
|
||||
}
|
||||
const node = resolveSchemaNode(normalized, ["channels", props.channelId]);
|
||||
if (!node) {
|
||||
return html` <div class="callout danger">Channel config schema unavailable.</div> `;
|
||||
return html`
|
||||
<div class="callout danger">Channel config schema unavailable.</div>
|
||||
`;
|
||||
}
|
||||
const configValue = props.configValue ?? {};
|
||||
const value = resolveChannelValue(configValue, props.channelId);
|
||||
@@ -116,16 +120,20 @@ export function renderChannelConfigSection(params: { channelId: string; props: C
|
||||
const disabled = props.configSaving || props.configSchemaLoading;
|
||||
return html`
|
||||
<div style="margin-top: 16px;">
|
||||
${props.configSchemaLoading
|
||||
? html` <div class="muted">Loading config schema…</div> `
|
||||
: renderChannelConfigForm({
|
||||
channelId,
|
||||
configValue: props.configForm,
|
||||
schema: props.configSchema,
|
||||
uiHints: props.configUiHints,
|
||||
disabled,
|
||||
onPatch: props.onConfigPatch,
|
||||
})}
|
||||
${
|
||||
props.configSchemaLoading
|
||||
? html`
|
||||
<div class="muted">Loading config schema…</div>
|
||||
`
|
||||
: renderChannelConfigForm({
|
||||
channelId,
|
||||
configValue: props.configForm,
|
||||
schema: props.configSchema,
|
||||
uiHints: props.configUiHints,
|
||||
disabled,
|
||||
onPatch: props.onConfigPatch,
|
||||
})
|
||||
}
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button
|
||||
class="btn primary"
|
||||
|
||||
@@ -108,16 +108,20 @@ export function renderNostrProfileForm(params: {
|
||||
}}
|
||||
?disabled=${state.saving}
|
||||
></textarea>
|
||||
${help
|
||||
? html`<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
||||
${
|
||||
help
|
||||
? html`<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
||||
${help}
|
||||
</div>`
|
||||
: nothing}
|
||||
${error
|
||||
? html`<div style="font-size: 12px; color: var(--danger-color); margin-top: 2px;">
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
error
|
||||
? html`<div style="font-size: 12px; color: var(--danger-color); margin-top: 2px;">
|
||||
${error}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -140,16 +144,20 @@ export function renderNostrProfileForm(params: {
|
||||
}}
|
||||
?disabled=${state.saving}
|
||||
/>
|
||||
${help
|
||||
? html`<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
||||
${
|
||||
help
|
||||
? html`<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
||||
${help}
|
||||
</div>`
|
||||
: nothing}
|
||||
${error
|
||||
? html`<div style="font-size: 12px; color: var(--danger-color); margin-top: 2px;">
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
error
|
||||
? html`<div style="font-size: 12px; color: var(--danger-color); margin-top: 2px;">
|
||||
${error}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
@@ -191,12 +199,16 @@ export function renderNostrProfileForm(params: {
|
||||
<div style="font-size: 12px; color: var(--text-muted);">Account: ${accountId}</div>
|
||||
</div>
|
||||
|
||||
${state.error
|
||||
? html`<div class="callout danger" style="margin-bottom: 12px;">${state.error}</div>`
|
||||
: nothing}
|
||||
${state.success
|
||||
? html`<div class="callout success" style="margin-bottom: 12px;">${state.success}</div>`
|
||||
: nothing}
|
||||
${
|
||||
state.error
|
||||
? html`<div class="callout danger" style="margin-bottom: 12px;">${state.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
state.success
|
||||
? html`<div class="callout success" style="margin-bottom: 12px;">${state.success}</div>`
|
||||
: nothing
|
||||
}
|
||||
${renderPicturePreview()}
|
||||
${renderField("name", "Username", {
|
||||
placeholder: "satoshi",
|
||||
@@ -219,8 +231,9 @@ export function renderNostrProfileForm(params: {
|
||||
placeholder: "https://example.com/avatar.jpg",
|
||||
help: "HTTPS URL to your profile picture",
|
||||
})}
|
||||
${state.showAdvanced
|
||||
? html`
|
||||
${
|
||||
state.showAdvanced
|
||||
? html`
|
||||
<div
|
||||
style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;"
|
||||
>
|
||||
@@ -248,7 +261,8 @@ export function renderNostrProfileForm(params: {
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div style="display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap;">
|
||||
<button
|
||||
@@ -274,13 +288,15 @@ export function renderNostrProfileForm(params: {
|
||||
<button class="btn" @click=${callbacks.onCancel} ?disabled=${state.saving}>Cancel</button>
|
||||
</div>
|
||||
|
||||
${isDirty
|
||||
? html`
|
||||
<div style="font-size: 12px; color: var(--warning-color); margin-top: 8px">
|
||||
You have unsaved changes
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${
|
||||
isDirty
|
||||
? html`
|
||||
<div style="font-size: 12px; color: var(--warning-color); margin-top: 8px">
|
||||
You have unsaved changes
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -80,14 +80,16 @@ export function renderNostrCard(params: {
|
||||
<div>
|
||||
<span class="label">Last inbound</span>
|
||||
<span
|
||||
>${account.lastInboundAt
|
||||
? formatRelativeTimestamp(account.lastInboundAt)
|
||||
: "n/a"}</span
|
||||
>${
|
||||
account.lastInboundAt ? formatRelativeTimestamp(account.lastInboundAt) : "n/a"
|
||||
}</span
|
||||
>
|
||||
</div>
|
||||
${account.lastError
|
||||
? html` <div class="account-card-error">${account.lastError}</div> `
|
||||
: nothing}
|
||||
${
|
||||
account.lastError
|
||||
? html` <div class="account-card-error">${account.lastError}</div> `
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -128,8 +130,9 @@ export function renderNostrCard(params: {
|
||||
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"
|
||||
>
|
||||
<div style="font-weight: 500;">Profile</div>
|
||||
${summaryConfigured
|
||||
? html`
|
||||
${
|
||||
summaryConfigured
|
||||
? html`
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
@click=${onEditProfile}
|
||||
@@ -138,13 +141,16 @@ export function renderNostrCard(params: {
|
||||
Edit Profile
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${hasAnyProfileData
|
||||
? html`
|
||||
${
|
||||
hasAnyProfileData
|
||||
? html`
|
||||
<div class="status-list">
|
||||
${picture
|
||||
? html`
|
||||
${
|
||||
picture
|
||||
? html`
|
||||
<div style="margin-bottom: 8px;">
|
||||
<img
|
||||
src=${picture}
|
||||
@@ -156,33 +162,43 @@ export function renderNostrCard(params: {
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${name
|
||||
? html`<div><span class="label">Name</span><span>${name}</span></div>`
|
||||
: nothing}
|
||||
${displayName
|
||||
? html`<div>
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
name
|
||||
? html`<div><span class="label">Name</span><span>${name}</span></div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
displayName
|
||||
? html`<div>
|
||||
<span class="label">Display Name</span><span>${displayName}</span>
|
||||
</div>`
|
||||
: nothing}
|
||||
${about
|
||||
? html`<div>
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
about
|
||||
? html`<div>
|
||||
<span class="label">About</span
|
||||
><span style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;"
|
||||
>${about}</span
|
||||
>
|
||||
</div>`
|
||||
: nothing}
|
||||
${nip05
|
||||
? html`<div><span class="label">NIP-05</span><span>${nip05}</span></div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
nip05
|
||||
? html`<div><span class="label">NIP-05</span><span>${nip05}</span></div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div style="color: var(--text-muted); font-size: 13px">
|
||||
No profile set. Click "Edit Profile" to add your name, bio, and avatar.
|
||||
</div>
|
||||
`}
|
||||
: html`
|
||||
<div style="color: var(--text-muted); font-size: 13px">
|
||||
No profile set. Click "Edit Profile" to add your name, bio, and avatar.
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
@@ -192,13 +208,14 @@ export function renderNostrCard(params: {
|
||||
<div class="card-title">Nostr</div>
|
||||
<div class="card-sub">Decentralized DMs via Nostr relays (NIP-04).</div>
|
||||
${accountCountLabel}
|
||||
${hasMultipleAccounts
|
||||
? html`
|
||||
${
|
||||
hasMultipleAccounts
|
||||
? html`
|
||||
<div class="account-card-list">
|
||||
${nostrAccounts.map((account) => renderAccountCard(account))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
@@ -221,10 +238,13 @@ export function renderNostrCard(params: {
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
${summaryLastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${summaryLastError}</div>`
|
||||
: nothing}
|
||||
`
|
||||
}
|
||||
${
|
||||
summaryLastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${summaryLastError}</div>`
|
||||
: nothing
|
||||
}
|
||||
${renderProfileSection()} ${renderChannelConfigSection({ channelId: "nostr", props })}
|
||||
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
|
||||
@@ -120,9 +120,11 @@ export function renderSingleAccountChannelCard(params: {
|
||||
)}
|
||||
</div>
|
||||
|
||||
${params.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.lastError}</div>`
|
||||
: nothing}
|
||||
${
|
||||
params.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.lastError}</div>`
|
||||
: nothing
|
||||
}
|
||||
${params.secondaryCallout ?? nothing} ${params.extraContent ?? nothing}
|
||||
${params.configSection} ${params.footer ?? nothing}
|
||||
</div>
|
||||
|
||||
@@ -41,14 +41,16 @@ export function renderTelegramCard(params: {
|
||||
<div>
|
||||
<span class="label">Last inbound</span>
|
||||
<span
|
||||
>${account.lastInboundAt
|
||||
? formatRelativeTimestamp(account.lastInboundAt)
|
||||
: "n/a"}</span
|
||||
>${
|
||||
account.lastInboundAt ? formatRelativeTimestamp(account.lastInboundAt) : "n/a"
|
||||
}</span
|
||||
>
|
||||
</div>
|
||||
${account.lastError
|
||||
? html` <div class="account-card-error">${account.lastError}</div> `
|
||||
: nothing}
|
||||
${
|
||||
account.lastError
|
||||
? html` <div class="account-card-error">${account.lastError}</div> `
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -65,15 +67,19 @@ export function renderTelegramCard(params: {
|
||||
${telegramAccounts.map((account) => renderAccountCard(account))}
|
||||
</div>
|
||||
|
||||
${telegram?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${telegram.lastError}</div>`
|
||||
: nothing}
|
||||
${telegram?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${
|
||||
telegram?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${telegram.lastError}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
telegram?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${telegram.probe.ok ? "ok" : "failed"} · ${telegram.probe.status ?? ""}
|
||||
${telegram.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${renderChannelConfigSection({ channelId: "telegram", props })}
|
||||
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
|
||||
@@ -82,9 +82,11 @@ export function renderChannels(props: ChannelsProps) {
|
||||
${props.lastSuccessAt ? formatRelativeTimestamp(props.lastSuccessAt) : "n/a"}
|
||||
</div>
|
||||
</div>
|
||||
${props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.lastError}</div>`
|
||||
: nothing}
|
||||
${
|
||||
props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.lastError}</div>`
|
||||
: nothing
|
||||
}
|
||||
<pre class="code-block" style="margin-top: 12px;">
|
||||
${props.snapshot ? JSON.stringify(props.snapshot, null, 2) : "No snapshot yet."}
|
||||
</pre
|
||||
@@ -198,13 +200,14 @@ function renderGenericChannelCard(
|
||||
<div class="card-title">${label}</div>
|
||||
<div class="card-sub">Channel status and configuration.</div>
|
||||
${accountCountLabel}
|
||||
${accounts.length > 0
|
||||
? html`
|
||||
${
|
||||
accounts.length > 0
|
||||
? html`
|
||||
<div class="account-card-list">
|
||||
${accounts.map((account) => renderGenericAccount(account))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
@@ -219,10 +222,13 @@ function renderGenericChannelCard(
|
||||
<span>${formatNullableBoolean(displayState.connected)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`}
|
||||
${lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${lastError}</div>`
|
||||
: nothing}
|
||||
`
|
||||
}
|
||||
${
|
||||
lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${lastError}</div>`
|
||||
: nothing
|
||||
}
|
||||
${renderChannelConfigSection({ channelId: key, props })}
|
||||
</div>
|
||||
`;
|
||||
@@ -305,9 +311,11 @@ function renderGenericAccount(account: ChannelAccountSnapshot) {
|
||||
>${account.lastInboundAt ? formatRelativeTimestamp(account.lastInboundAt) : "n/a"}</span
|
||||
>
|
||||
</div>
|
||||
${account.lastError
|
||||
? html` <div class="account-card-error">${account.lastError}</div> `
|
||||
: nothing}
|
||||
${
|
||||
account.lastError
|
||||
? html` <div class="account-card-error">${account.lastError}</div> `
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -43,14 +43,18 @@ export function renderWhatsAppCard(params: {
|
||||
],
|
||||
lastError: whatsapp?.lastError,
|
||||
extraContent: html`
|
||||
${props.whatsappMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">${props.whatsappMessage}</div>`
|
||||
: nothing}
|
||||
${props.whatsappQrDataUrl
|
||||
? html`<div class="qr-wrap">
|
||||
${
|
||||
props.whatsappMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">${props.whatsappMessage}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.whatsappQrDataUrl
|
||||
? html`<div class="qr-wrap">
|
||||
<img src=${props.whatsappQrDataUrl} alt="WhatsApp QR" />
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
`,
|
||||
configSection: renderChannelConfigSection({ channelId: "whatsapp", props }),
|
||||
footer: html`<div class="row" style="margin-top: 14px; flex-wrap: wrap;">
|
||||
|
||||
@@ -642,15 +642,17 @@ function renderWelcomeState(props: ChatProps): TemplateResult {
|
||||
return html`
|
||||
<div class="agent-chat__welcome" style="--agent-color: var(--accent)">
|
||||
<div class="agent-chat__welcome-glow"></div>
|
||||
${avatar
|
||||
? html`<img
|
||||
${
|
||||
avatar
|
||||
? html`<img
|
||||
src=${avatar}
|
||||
alt=${name}
|
||||
style="width:56px; height:56px; border-radius:50%; object-fit:cover;"
|
||||
/>`
|
||||
: html`<div class="agent-chat__avatar agent-chat__avatar--logo">
|
||||
: html`<div class="agent-chat__avatar agent-chat__avatar--logo">
|
||||
<img src=${logoUrl} alt="OpenClaw" />
|
||||
</div>`}
|
||||
</div>`
|
||||
}
|
||||
<h2>${name}</h2>
|
||||
<div class="agent-chat__badges">
|
||||
<span class="agent-chat__badge"><img src=${logoUrl} alt="" /> Ready to chat</span>
|
||||
@@ -741,8 +743,9 @@ function renderPinnedSection(
|
||||
>${icons.chevronDown}</span
|
||||
>
|
||||
</button>
|
||||
${vs.pinnedExpanded
|
||||
? html`
|
||||
${
|
||||
vs.pinnedExpanded
|
||||
? html`
|
||||
<div class="agent-chat__pinned-list">
|
||||
${entries.map(
|
||||
({ index, text, role }) => html`
|
||||
@@ -768,7 +771,8 @@ function renderPinnedSection(
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -801,9 +805,11 @@ function renderSlashMenu(
|
||||
requestUpdate();
|
||||
}}
|
||||
>
|
||||
${vs.slashMenuCommand?.icon
|
||||
? html`<span class="slash-menu-icon">${icons[vs.slashMenuCommand.icon]}</span>`
|
||||
: nothing}
|
||||
${
|
||||
vs.slashMenuCommand?.icon
|
||||
? html`<span class="slash-menu-icon">${icons[vs.slashMenuCommand.icon]}</span>`
|
||||
: nothing
|
||||
}
|
||||
<span class="slash-menu-name">${arg}</span>
|
||||
<span class="slash-menu-desc">/${vs.slashMenuCommand?.name} ${arg}</span>
|
||||
</div>
|
||||
@@ -845,9 +851,9 @@ function renderSlashMenu(
|
||||
${entries.map(
|
||||
({ cmd, globalIdx }) => html`
|
||||
<div
|
||||
class="slash-menu-item ${globalIdx === vs.slashMenuIndex
|
||||
? "slash-menu-item--active"
|
||||
: ""}"
|
||||
class="slash-menu-item ${
|
||||
globalIdx === vs.slashMenuIndex ? "slash-menu-item--active" : ""
|
||||
}"
|
||||
role="option"
|
||||
aria-selected=${globalIdx === vs.slashMenuIndex}
|
||||
@click=${() => selectSlashCommand(cmd, props, requestUpdate)}
|
||||
@@ -860,11 +866,15 @@ function renderSlashMenu(
|
||||
<span class="slash-menu-name">/${cmd.name}</span>
|
||||
${cmd.args ? html`<span class="slash-menu-args">${cmd.args}</span>` : nothing}
|
||||
<span class="slash-menu-desc">${cmd.description}</span>
|
||||
${cmd.argOptions?.length
|
||||
? html`<span class="slash-menu-badge">${cmd.argOptions.length} options</span>`
|
||||
: cmd.executeLocal && !cmd.args
|
||||
? html` <span class="slash-menu-badge">instant</span> `
|
||||
: nothing}
|
||||
${
|
||||
cmd.argOptions?.length
|
||||
? html`<span class="slash-menu-badge">${cmd.argOptions.length} options</span>`
|
||||
: cmd.executeLocal && !cmd.args
|
||||
? html`
|
||||
<span class="slash-menu-badge">instant</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
@@ -944,49 +954,46 @@ export function renderChat(props: ChatProps) {
|
||||
@click=${handleCodeBlockCopy}
|
||||
>
|
||||
<div class="chat-thread-inner">
|
||||
${props.loading
|
||||
? html`
|
||||
<div class="chat-loading-skeleton" aria-label="Loading chat">
|
||||
<div class="chat-line assistant">
|
||||
<div class="chat-msg">
|
||||
<div class="chat-bubble">
|
||||
<div
|
||||
class="skeleton skeleton-line skeleton-line--long"
|
||||
style="margin-bottom: 8px"
|
||||
></div>
|
||||
<div
|
||||
class="skeleton skeleton-line skeleton-line--medium"
|
||||
style="margin-bottom: 8px"
|
||||
></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--short"></div>
|
||||
${
|
||||
props.loading
|
||||
? html`
|
||||
<div class="chat-loading-skeleton" aria-label="Loading chat">
|
||||
<div class="chat-line assistant">
|
||||
<div class="chat-msg">
|
||||
<div class="chat-bubble">
|
||||
<div class="skeleton skeleton-line skeleton-line--long" style="margin-bottom: 8px"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--medium" style="margin-bottom: 8px"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--short"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-line user" style="margin-top: 12px">
|
||||
<div class="chat-msg">
|
||||
<div class="chat-bubble">
|
||||
<div class="skeleton skeleton-line skeleton-line--medium"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-line assistant" style="margin-top: 12px">
|
||||
<div class="chat-msg">
|
||||
<div class="chat-bubble">
|
||||
<div class="skeleton skeleton-line skeleton-line--long" style="margin-bottom: 8px"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--short"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-line user" style="margin-top: 12px">
|
||||
<div class="chat-msg">
|
||||
<div class="chat-bubble">
|
||||
<div class="skeleton skeleton-line skeleton-line--medium"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-line assistant" style="margin-top: 12px">
|
||||
<div class="chat-msg">
|
||||
<div class="chat-bubble">
|
||||
<div
|
||||
class="skeleton skeleton-line skeleton-line--long"
|
||||
style="margin-bottom: 8px"
|
||||
></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--short"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${isEmpty && !vs.searchOpen ? renderWelcomeState(props) : nothing}
|
||||
${isEmpty && vs.searchOpen
|
||||
? html` <div class="agent-chat__empty">No matching messages</div> `
|
||||
: nothing}
|
||||
${
|
||||
isEmpty && vs.searchOpen
|
||||
? html`
|
||||
<div class="agent-chat__empty">No matching messages</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${repeat(
|
||||
chatItems,
|
||||
(item) => item.key,
|
||||
@@ -1164,8 +1171,9 @@ export function renderChat(props: ChatProps) {
|
||||
>
|
||||
${props.disabledReason ? html`<div class="callout">${props.disabledReason}</div>` : nothing}
|
||||
${props.error ? html`<div class="callout danger">${props.error}</div>` : nothing}
|
||||
${props.focusMode
|
||||
? html`
|
||||
${
|
||||
props.focusMode
|
||||
? html`
|
||||
<button
|
||||
class="chat-focus-exit"
|
||||
type="button"
|
||||
@@ -1176,7 +1184,8 @@ export function renderChat(props: ChatProps) {
|
||||
${icons.x}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${renderSearchBar(requestUpdate)} ${renderPinnedSection(props, pinned, requestUpdate)}
|
||||
|
||||
<div class="chat-split-container ${sidebarOpen ? "chat-split-container--open" : ""}">
|
||||
@@ -1187,8 +1196,9 @@ export function renderChat(props: ChatProps) {
|
||||
${thread}
|
||||
</div>
|
||||
|
||||
${sidebarOpen
|
||||
? html`
|
||||
${
|
||||
sidebarOpen
|
||||
? html`
|
||||
<resizable-divider
|
||||
.splitRatio=${splitRatio}
|
||||
@resize=${(e: CustomEvent) => props.onSplitRatioChange?.(e.detail.splitRatio)}
|
||||
@@ -1207,11 +1217,13 @@ export function renderChat(props: ChatProps) {
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
|
||||
${props.queue.length
|
||||
? html`
|
||||
${
|
||||
props.queue.length
|
||||
? html`
|
||||
<div class="chat-queue" role="status" aria-live="polite">
|
||||
<div class="chat-queue__title">Queued (${props.queue.length})</div>
|
||||
<div class="chat-queue__list">
|
||||
@@ -1219,8 +1231,10 @@ export function renderChat(props: ChatProps) {
|
||||
(item) => html`
|
||||
<div class="chat-queue__item">
|
||||
<div class="chat-queue__text">
|
||||
${item.text ||
|
||||
(item.attachments?.length ? `Image (${item.attachments.length})` : "")}
|
||||
${
|
||||
item.text ||
|
||||
(item.attachments?.length ? `Image (${item.attachments.length})` : "")
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
class="btn chat-queue__remove"
|
||||
@@ -1236,17 +1250,20 @@ export function renderChat(props: ChatProps) {
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${renderFallbackIndicator(props.fallbackStatus)}
|
||||
${renderCompactionIndicator(props.compactionStatus)}
|
||||
${renderContextNotice(activeSession, props.sessions?.defaults?.contextTokens ?? null)}
|
||||
${props.showNewMessages
|
||||
? html`
|
||||
${
|
||||
props.showNewMessages
|
||||
? html`
|
||||
<button class="chat-new-messages" type="button" @click=${props.onScrollToBottom}>
|
||||
${icons.arrowDown} New messages
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<!-- Input bar -->
|
||||
<div class="agent-chat__input">
|
||||
@@ -1260,9 +1277,11 @@ export function renderChat(props: ChatProps) {
|
||||
@change=${(e: Event) => handleFileSelect(e, props)}
|
||||
/>
|
||||
|
||||
${vs.sttRecording && vs.sttInterimText
|
||||
? html`<div class="agent-chat__stt-interim">${vs.sttInterimText}</div>`
|
||||
: nothing}
|
||||
${
|
||||
vs.sttRecording && vs.sttInterimText
|
||||
? html`<div class="agent-chat__stt-interim">${vs.sttInterimText}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<textarea
|
||||
${ref((el) => el && adjustTextareaHeight(el as HTMLTextAreaElement))}
|
||||
@@ -1290,12 +1309,13 @@ export function renderChat(props: ChatProps) {
|
||||
${icons.paperclip}
|
||||
</button>
|
||||
|
||||
${isSttSupported()
|
||||
? html`
|
||||
${
|
||||
isSttSupported()
|
||||
? html`
|
||||
<button
|
||||
class="agent-chat__input-btn ${vs.sttRecording
|
||||
? "agent-chat__input-btn--recording"
|
||||
: ""}"
|
||||
class="agent-chat__input-btn ${
|
||||
vs.sttRecording ? "agent-chat__input-btn--recording" : ""
|
||||
}"
|
||||
@click=${() => {
|
||||
if (vs.sttRecording) {
|
||||
stopStt();
|
||||
@@ -1342,15 +1362,17 @@ export function renderChat(props: ChatProps) {
|
||||
${vs.sttRecording ? icons.micOff : icons.mic}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${tokens ? html`<span class="agent-chat__token-count">${tokens}</span>` : nothing}
|
||||
</div>
|
||||
|
||||
<div class="agent-chat__toolbar-right">
|
||||
${nothing /* search hidden for now */}
|
||||
${canAbort
|
||||
? nothing
|
||||
: html`
|
||||
${
|
||||
canAbort
|
||||
? nothing
|
||||
: html`
|
||||
<button
|
||||
class="btn btn--ghost"
|
||||
@click=${props.onNewSession}
|
||||
@@ -1359,7 +1381,8 @@ export function renderChat(props: ChatProps) {
|
||||
>
|
||||
${icons.plus}
|
||||
</button>
|
||||
`}
|
||||
`
|
||||
}
|
||||
<button
|
||||
class="btn btn--ghost"
|
||||
@click=${() => exportMarkdown(props)}
|
||||
@@ -1370,8 +1393,9 @@ export function renderChat(props: ChatProps) {
|
||||
${icons.download}
|
||||
</button>
|
||||
|
||||
${canAbort && (isBusy || props.sending)
|
||||
? html`
|
||||
${
|
||||
canAbort && (isBusy || props.sending)
|
||||
? html`
|
||||
<button
|
||||
class="chat-send-btn chat-send-btn--stop"
|
||||
@click=${props.onAbort}
|
||||
@@ -1381,7 +1405,7 @@ export function renderChat(props: ChatProps) {
|
||||
${icons.stop}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<button
|
||||
class="chat-send-btn"
|
||||
@click=${() => {
|
||||
@@ -1396,7 +1420,8 @@ export function renderChat(props: ChatProps) {
|
||||
>
|
||||
${icons.send}
|
||||
</button>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -220,15 +220,16 @@ export function renderCommandPalette(props: CommandPaletteProps) {
|
||||
}}
|
||||
/>
|
||||
<div class="cmd-palette__results">
|
||||
${grouped.length === 0
|
||||
? html`<div class="cmd-palette__empty">
|
||||
${
|
||||
grouped.length === 0
|
||||
? html`<div class="cmd-palette__empty">
|
||||
<span class="nav-item__icon" style="opacity:0.3;width:20px;height:20px"
|
||||
>${icons.search}</span
|
||||
>
|
||||
<span>${t("overview.palette.noResults")}</span>
|
||||
</div>`
|
||||
: grouped.map(
|
||||
([category, groupedItems]) => html`
|
||||
: grouped.map(
|
||||
([category, groupedItems]) => html`
|
||||
<div class="cmd-palette__group-label">
|
||||
${CATEGORY_LABELS[category] ?? category}
|
||||
</div>
|
||||
@@ -246,16 +247,19 @@ export function renderCommandPalette(props: CommandPaletteProps) {
|
||||
>
|
||||
<span class="nav-item__icon">${icons[item.icon]}</span>
|
||||
<span>${item.label}</span>
|
||||
${item.description
|
||||
? html`<span class="cmd-palette__item-desc muted"
|
||||
${
|
||||
item.description
|
||||
? html`<span class="cmd-palette__item-desc muted"
|
||||
>${item.description}</span
|
||||
>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
`,
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div class="cmd-palette__footer">
|
||||
<span><kbd>↑↓</kbd> navigate</span>
|
||||
|
||||
@@ -79,9 +79,7 @@ const icons = {
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path
|
||||
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
|
||||
></path>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
</svg>
|
||||
`,
|
||||
edit: html`
|
||||
@@ -153,16 +151,20 @@ function renderSensitiveToggleButton(params: {
|
||||
type="button"
|
||||
class="btn btn--icon ${state.isRevealed ? "active" : ""}"
|
||||
style="width:28px;height:28px;padding:0;"
|
||||
title=${state.canReveal
|
||||
? state.isRevealed
|
||||
? "Hide value"
|
||||
: "Reveal value"
|
||||
: "Disable stream mode to reveal value"}
|
||||
aria-label=${state.canReveal
|
||||
? state.isRevealed
|
||||
? "Hide value"
|
||||
: "Reveal value"
|
||||
: "Disable stream mode to reveal value"}
|
||||
title=${
|
||||
state.canReveal
|
||||
? state.isRevealed
|
||||
? "Hide value"
|
||||
: "Reveal value"
|
||||
: "Disable stream mode to reveal value"
|
||||
}
|
||||
aria-label=${
|
||||
state.canReveal
|
||||
? state.isRevealed
|
||||
? "Hide value"
|
||||
: "Reveal value"
|
||||
: "Disable stream mode to reveal value"
|
||||
}
|
||||
aria-pressed=${state.isRevealed}
|
||||
?disabled=${params.disabled || !state.canReveal}
|
||||
@click=${() => params.onToggleSensitivePath?.(params.path)}
|
||||
@@ -537,10 +539,9 @@ export function renderNode(params: {
|
||||
(opt) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-segmented__btn ${opt === resolvedValue ||
|
||||
String(opt) === String(resolvedValue)
|
||||
? "active"
|
||||
: ""}"
|
||||
class="cfg-segmented__btn ${
|
||||
opt === resolvedValue || String(opt) === String(resolvedValue) ? "active" : ""
|
||||
}"
|
||||
?disabled=${disabled}
|
||||
@click=${() => onPatch(path, opt)}
|
||||
>
|
||||
@@ -693,8 +694,9 @@ function renderTextInput(params: {
|
||||
disabled,
|
||||
onToggleSensitivePath: params.onToggleSensitivePath,
|
||||
})}
|
||||
${schema.default !== undefined
|
||||
? html`
|
||||
${
|
||||
schema.default !== undefined
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-input__reset"
|
||||
@@ -705,7 +707,8 @@ function renderTextInput(params: {
|
||||
↺
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -947,22 +950,24 @@ function renderObject(params: {
|
||||
onPatch,
|
||||
}),
|
||||
)}
|
||||
${allowExtra
|
||||
? renderMapField({
|
||||
schema: additional,
|
||||
value: obj,
|
||||
path,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
reservedKeys: reserved,
|
||||
searchCriteria: childSearchCriteria,
|
||||
revealSensitive,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath,
|
||||
onPatch,
|
||||
})
|
||||
: nothing}
|
||||
${
|
||||
allowExtra
|
||||
? renderMapField({
|
||||
schema: additional,
|
||||
value: obj,
|
||||
path,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
reservedKeys: reserved,
|
||||
searchCriteria: childSearchCriteria,
|
||||
revealSensitive,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath,
|
||||
onPatch,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
|
||||
// For top-level, don't wrap in collapsible
|
||||
@@ -1059,9 +1064,12 @@ function renderArray(params: {
|
||||
</button>
|
||||
</div>
|
||||
${help ? html`<div class="cfg-array__help">${help}</div>` : nothing}
|
||||
${arr.length === 0
|
||||
? html` <div class="cfg-array__empty">No items yet. Click "Add" to create one.</div> `
|
||||
: html`
|
||||
${
|
||||
arr.length === 0
|
||||
? html`
|
||||
<div class="cfg-array__empty">No items yet. Click "Add" to create one.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="cfg-array__items">
|
||||
${arr.map(
|
||||
(item, idx) => html`
|
||||
@@ -1102,7 +1110,8 @@ function renderArray(params: {
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -1175,9 +1184,12 @@ function renderMapField(params: {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${visibleEntries.length === 0
|
||||
? html` <div class="cfg-map__empty">No custom entries.</div> `
|
||||
: html`
|
||||
${
|
||||
visibleEntries.length === 0
|
||||
? html`
|
||||
<div class="cfg-map__empty">No custom entries.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="cfg-map__items">
|
||||
${visibleEntries.map(([key, entryValue]) => {
|
||||
const valuePath = [...path, key];
|
||||
@@ -1229,16 +1241,17 @@ function renderMapField(params: {
|
||||
</button>
|
||||
</div>
|
||||
<div class="cfg-map__item-value">
|
||||
${anySchema
|
||||
? html`
|
||||
${
|
||||
anySchema
|
||||
? html`
|
||||
<div class="cfg-input-wrap">
|
||||
<textarea
|
||||
class="cfg-textarea cfg-textarea--sm${sensitiveState.isRedacted
|
||||
? " cfg-textarea--redacted"
|
||||
: ""}"
|
||||
placeholder=${sensitiveState.isRedacted
|
||||
? REDACTED_PLACEHOLDER
|
||||
: "JSON value"}
|
||||
class="cfg-textarea cfg-textarea--sm${
|
||||
sensitiveState.isRedacted ? " cfg-textarea--redacted" : ""
|
||||
}"
|
||||
placeholder=${
|
||||
sensitiveState.isRedacted ? REDACTED_PLACEHOLDER : "JSON value"
|
||||
}
|
||||
rows="2"
|
||||
.value=${sensitiveState.isRedacted ? "" : fallback}
|
||||
?disabled=${disabled}
|
||||
@@ -1273,26 +1286,28 @@ function renderMapField(params: {
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: renderNode({
|
||||
schema,
|
||||
value: entryValue,
|
||||
path: valuePath,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
searchCriteria,
|
||||
showLabel: false,
|
||||
revealSensitive,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath,
|
||||
onPatch,
|
||||
})}
|
||||
: renderNode({
|
||||
schema,
|
||||
value: entryValue,
|
||||
path: valuePath,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
searchCriteria,
|
||||
showLabel: false,
|
||||
revealSensitive,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath,
|
||||
onPatch,
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -360,12 +360,16 @@ function matchesSearch(params: {
|
||||
|
||||
export function renderConfigForm(props: ConfigFormProps) {
|
||||
if (!props.schema) {
|
||||
return html` <div class="muted">Schema unavailable.</div> `;
|
||||
return html`
|
||||
<div class="muted">Schema unavailable.</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">Unsupported schema. Use Raw.</div>
|
||||
`;
|
||||
}
|
||||
const unsupported = new Set(props.unsupportedPaths ?? []);
|
||||
const properties = schema.properties;
|
||||
@@ -445,9 +449,11 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
<span class="config-section-card__icon">${getSectionIcon(params.sectionKey)}</span>
|
||||
<div class="config-section-card__titles">
|
||||
<h3 class="config-section-card__title">${params.label}</h3>
|
||||
${params.description
|
||||
? html`<p class="config-section-card__desc">${params.description}</p>`
|
||||
: nothing}
|
||||
${
|
||||
params.description
|
||||
? html`<p class="config-section-card__desc">${params.description}</p>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-section-card__content">
|
||||
@@ -471,43 +477,45 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
|
||||
return html`
|
||||
<div class="config-form config-form--modern">
|
||||
${subsectionContext
|
||||
? (() => {
|
||||
const { sectionKey, subsectionKey, schema: node } = subsectionContext;
|
||||
const hint = hintForPath([sectionKey, subsectionKey], props.uiHints);
|
||||
const label = hint?.label ?? node.title ?? humanize(subsectionKey);
|
||||
const description = hint?.help ?? node.description ?? "";
|
||||
const sectionValue = value[sectionKey];
|
||||
const scopedValue =
|
||||
sectionValue && typeof sectionValue === "object"
|
||||
? (sectionValue as Record<string, unknown>)[subsectionKey]
|
||||
: undefined;
|
||||
return renderSectionCard({
|
||||
id: `config-section-${sectionKey}-${subsectionKey}`,
|
||||
sectionKey,
|
||||
label,
|
||||
description,
|
||||
node,
|
||||
nodeValue: scopedValue,
|
||||
path: [sectionKey, subsectionKey],
|
||||
});
|
||||
})()
|
||||
: filteredEntries.map(([key, node]) => {
|
||||
const meta = SECTION_META[key] ?? {
|
||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
description: node.description ?? "",
|
||||
};
|
||||
${
|
||||
subsectionContext
|
||||
? (() => {
|
||||
const { sectionKey, subsectionKey, schema: node } = subsectionContext;
|
||||
const hint = hintForPath([sectionKey, subsectionKey], props.uiHints);
|
||||
const label = hint?.label ?? node.title ?? humanize(subsectionKey);
|
||||
const description = hint?.help ?? node.description ?? "";
|
||||
const sectionValue = value[sectionKey];
|
||||
const scopedValue =
|
||||
sectionValue && typeof sectionValue === "object"
|
||||
? (sectionValue as Record<string, unknown>)[subsectionKey]
|
||||
: undefined;
|
||||
return renderSectionCard({
|
||||
id: `config-section-${sectionKey}-${subsectionKey}`,
|
||||
sectionKey,
|
||||
label,
|
||||
description,
|
||||
node,
|
||||
nodeValue: scopedValue,
|
||||
path: [sectionKey, subsectionKey],
|
||||
});
|
||||
})()
|
||||
: filteredEntries.map(([key, node]) => {
|
||||
const meta = SECTION_META[key] ?? {
|
||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
description: node.description ?? "",
|
||||
};
|
||||
|
||||
return renderSectionCard({
|
||||
id: `config-section-${key}`,
|
||||
sectionKey: key,
|
||||
label: meta.label,
|
||||
description: meta.description,
|
||||
node,
|
||||
nodeValue: value[key],
|
||||
path: [key],
|
||||
});
|
||||
})}
|
||||
return renderSectionCard({
|
||||
id: `config-section-${key}`,
|
||||
sectionKey: key,
|
||||
label: meta.label,
|
||||
description: meta.description,
|
||||
node,
|
||||
nodeValue: value[key],
|
||||
path: [key],
|
||||
});
|
||||
})
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -578,9 +578,9 @@ function renderAppearanceSection(props: ConfigProps) {
|
||||
${THEME_OPTIONS.map(
|
||||
(opt) => html`
|
||||
<button
|
||||
class="settings-theme-card ${opt.id === props.theme
|
||||
? "settings-theme-card--active"
|
||||
: ""}"
|
||||
class="settings-theme-card ${
|
||||
opt.id === props.theme ? "settings-theme-card--active" : ""
|
||||
}"
|
||||
title=${opt.description}
|
||||
@click=${(e: Event) => {
|
||||
if (opt.id !== props.theme) {
|
||||
@@ -593,11 +593,13 @@ function renderAppearanceSection(props: ConfigProps) {
|
||||
>
|
||||
<span class="settings-theme-card__icon" aria-hidden="true">${opt.icon}</span>
|
||||
<span class="settings-theme-card__label">${opt.label}</span>
|
||||
${opt.id === props.theme
|
||||
? html`<span class="settings-theme-card__check" aria-hidden="true"
|
||||
${
|
||||
opt.id === props.theme
|
||||
? html`<span class="settings-theme-card__check" aria-hidden="true"
|
||||
>${icons.check}</span
|
||||
>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
@@ -644,14 +646,16 @@ function renderAppearanceSection(props: ConfigProps) {
|
||||
${props.connected ? "Connected" : "Offline"}
|
||||
</span>
|
||||
</div>
|
||||
${props.assistantName
|
||||
? html`
|
||||
${
|
||||
props.assistantName
|
||||
? html`
|
||||
<div class="settings-info-row">
|
||||
<span class="settings-info-row__label">Assistant</span>
|
||||
<span class="settings-info-row__value">${props.assistantName}</span>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -786,8 +790,9 @@ export function renderConfig(props: ConfigProps) {
|
||||
<main class="config-main">
|
||||
<div class="config-actions">
|
||||
<div class="config-actions__left">
|
||||
${showModeToggle
|
||||
? html`
|
||||
${
|
||||
showModeToggle
|
||||
? html`
|
||||
<div class="config-mode-toggle">
|
||||
<button
|
||||
class="config-mode-toggle__btn ${formMode === "form" ? "active" : ""}"
|
||||
@@ -805,20 +810,28 @@ export function renderConfig(props: ConfigProps) {
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${hasChanges
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
hasChanges
|
||||
? html`
|
||||
<span class="config-changes-badge"
|
||||
>${formMode === "raw"
|
||||
? "Unsaved changes"
|
||||
: `${diff.length} unsaved change${diff.length !== 1 ? "s" : ""}`}</span
|
||||
>${
|
||||
formMode === "raw"
|
||||
? "Unsaved changes"
|
||||
: `${diff.length} unsaved change${diff.length !== 1 ? "s" : ""}`
|
||||
}</span
|
||||
>
|
||||
`
|
||||
: html` <span class="config-status muted">No changes</span> `}
|
||||
: html`
|
||||
<span class="config-status muted">No changes</span>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
<div class="config-actions__right">
|
||||
${props.onOpenFile
|
||||
? html`
|
||||
${
|
||||
props.onOpenFile
|
||||
? html`
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
title=${props.configPath ? `Open ${props.configPath}` : "Open config file"}
|
||||
@@ -827,7 +840,8 @@ export function renderConfig(props: ConfigProps) {
|
||||
${icons.fileText} Open
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<button class="btn btn--sm" ?disabled=${props.loading} @click=${props.onReload}>
|
||||
${props.loading ? "Loading…" : "Reload"}
|
||||
</button>
|
||||
@@ -844,8 +858,9 @@ export function renderConfig(props: ConfigProps) {
|
||||
</div>
|
||||
|
||||
<div class="config-top-tabs">
|
||||
${formMode === "form"
|
||||
? html`
|
||||
${
|
||||
formMode === "form"
|
||||
? html`
|
||||
<div class="config-search config-search--top">
|
||||
<div class="config-search__input-row">
|
||||
<svg
|
||||
@@ -867,8 +882,9 @@ export function renderConfig(props: ConfigProps) {
|
||||
@input=${(e: Event) =>
|
||||
props.onSearchChange((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
${props.searchQuery
|
||||
? html`
|
||||
${
|
||||
props.searchQuery
|
||||
? html`
|
||||
<button
|
||||
class="config-search__clear"
|
||||
aria-label="Clear search"
|
||||
@@ -877,11 +893,13 @@ export function renderConfig(props: ConfigProps) {
|
||||
×
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="config-top-tabs__scroller" role="tablist" aria-label="Settings sections">
|
||||
${topTabs.map(
|
||||
@@ -900,8 +918,9 @@ export function renderConfig(props: ConfigProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${validity === "invalid" && !cvs.validityDismissed
|
||||
? html`
|
||||
${
|
||||
validity === "invalid" && !cvs.validityDismissed
|
||||
? html`
|
||||
<div class="config-validity-warning">
|
||||
<svg
|
||||
class="config-validity-warning__icon"
|
||||
@@ -934,11 +953,13 @@ export function renderConfig(props: ConfigProps) {
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<!-- Diff panel (form mode only - raw mode doesn't have granular diff) -->
|
||||
${hasChanges && formMode === "form"
|
||||
? html`
|
||||
${
|
||||
hasChanges && formMode === "form"
|
||||
? html`
|
||||
<details class="config-diff">
|
||||
<summary class="config-diff__summary">
|
||||
<span>View ${diff.length} pending change${diff.length !== 1 ? "s" : ""}</span>
|
||||
@@ -972,27 +993,32 @@ export function renderConfig(props: ConfigProps) {
|
||||
</div>
|
||||
</details>
|
||||
`
|
||||
: nothing}
|
||||
${activeSectionMeta && formMode === "form"
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
activeSectionMeta && formMode === "form"
|
||||
? html`
|
||||
<div class="config-section-hero">
|
||||
<div class="config-section-hero__icon">
|
||||
${getSectionIcon(props.activeSection ?? "")}
|
||||
</div>
|
||||
<div class="config-section-hero__text">
|
||||
<div class="config-section-hero__title">${activeSectionMeta.label}</div>
|
||||
${activeSectionMeta.description
|
||||
? html`<div class="config-section-hero__desc">
|
||||
${
|
||||
activeSectionMeta.description
|
||||
? html`<div class="config-section-hero__desc">
|
||||
${activeSectionMeta.description}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${props.activeSection === "env"
|
||||
? html`
|
||||
${
|
||||
props.activeSection === "env"
|
||||
? html`
|
||||
<button
|
||||
class="config-env-peek-btn ${envSensitiveVisible
|
||||
? "config-env-peek-btn--active"
|
||||
: ""}"
|
||||
class="config-env-peek-btn ${
|
||||
envSensitiveVisible ? "config-env-peek-btn--active" : ""
|
||||
}"
|
||||
title=${envSensitiveVisible ? "Hide env values" : "Reveal env values"}
|
||||
@click=${() => {
|
||||
cvs.envRevealed = !cvs.envRevealed;
|
||||
@@ -1015,75 +1041,83 @@ export function renderConfig(props: ConfigProps) {
|
||||
Peek
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<!-- Form content -->
|
||||
<div class="config-content">
|
||||
${props.activeSection === "__appearance__"
|
||||
? includeVirtualSections
|
||||
? renderAppearanceSection(props)
|
||||
: nothing
|
||||
: formMode === "form"
|
||||
? html`
|
||||
${
|
||||
props.activeSection === "__appearance__"
|
||||
? includeVirtualSections
|
||||
? renderAppearanceSection(props)
|
||||
: nothing
|
||||
: formMode === "form"
|
||||
? html`
|
||||
${showAppearanceOnRoot ? renderAppearanceSection(props) : nothing}
|
||||
${props.schemaLoading
|
||||
? html`
|
||||
<div class="config-loading">
|
||||
<div class="config-loading__spinner"></div>
|
||||
<span>Loading schema…</span>
|
||||
</div>
|
||||
`
|
||||
: renderConfigForm({
|
||||
schema: analysis.schema,
|
||||
uiHints: props.uiHints,
|
||||
value: props.formValue,
|
||||
disabled: props.loading || !props.formValue,
|
||||
unsupportedPaths: analysis.unsupportedPaths,
|
||||
onPatch: props.onFormPatch,
|
||||
searchQuery: props.searchQuery,
|
||||
activeSection: props.activeSection,
|
||||
activeSubsection: effectiveSubsection,
|
||||
revealSensitive:
|
||||
props.activeSection === "env" ? envSensitiveVisible : false,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath: (path) => {
|
||||
toggleSensitivePathReveal(path);
|
||||
requestUpdate();
|
||||
},
|
||||
})}
|
||||
`
|
||||
: (() => {
|
||||
const sensitiveCount = countSensitiveConfigValues(
|
||||
props.formValue,
|
||||
[],
|
||||
props.uiHints,
|
||||
);
|
||||
const blurred = sensitiveCount > 0 && !cvs.rawRevealed;
|
||||
return html`
|
||||
${formUnsafe
|
||||
${
|
||||
props.schemaLoading
|
||||
? html`
|
||||
<div class="callout info" style="margin-bottom: 12px">
|
||||
Your config contains fields the form editor can't safely represent. Use
|
||||
Raw mode to edit those entries.
|
||||
<div class="config-loading">
|
||||
<div class="config-loading__spinner"></div>
|
||||
<span>Loading schema…</span>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: renderConfigForm({
|
||||
schema: analysis.schema,
|
||||
uiHints: props.uiHints,
|
||||
value: props.formValue,
|
||||
disabled: props.loading || !props.formValue,
|
||||
unsupportedPaths: analysis.unsupportedPaths,
|
||||
onPatch: props.onFormPatch,
|
||||
searchQuery: props.searchQuery,
|
||||
activeSection: props.activeSection,
|
||||
activeSubsection: effectiveSubsection,
|
||||
revealSensitive:
|
||||
props.activeSection === "env" ? envSensitiveVisible : false,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath: (path) => {
|
||||
toggleSensitivePathReveal(path);
|
||||
requestUpdate();
|
||||
},
|
||||
})
|
||||
}
|
||||
`
|
||||
: (() => {
|
||||
const sensitiveCount = countSensitiveConfigValues(
|
||||
props.formValue,
|
||||
[],
|
||||
props.uiHints,
|
||||
);
|
||||
const blurred = sensitiveCount > 0 && !cvs.rawRevealed;
|
||||
return html`
|
||||
${
|
||||
formUnsafe
|
||||
? html`
|
||||
<div class="callout info" style="margin-bottom: 12px">
|
||||
Your config contains fields the form editor can't safely represent. Use Raw mode to edit those
|
||||
entries.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div class="field config-raw-field">
|
||||
<span style="display:flex;align-items:center;gap:8px;">
|
||||
Raw config (JSON/JSON5)
|
||||
${sensitiveCount > 0
|
||||
? html`
|
||||
${
|
||||
sensitiveCount > 0
|
||||
? html`
|
||||
<span class="pill pill--sm"
|
||||
>${sensitiveCount} secret${sensitiveCount === 1 ? "" : "s"}
|
||||
${blurred ? "redacted" : "visible"}</span
|
||||
>
|
||||
<button
|
||||
class="btn btn--icon config-raw-toggle ${blurred ? "" : "active"}"
|
||||
title=${blurred
|
||||
? "Reveal sensitive values"
|
||||
: "Hide sensitive values"}
|
||||
title=${
|
||||
blurred ? "Reveal sensitive values" : "Hide sensitive values"
|
||||
}
|
||||
aria-label="Toggle raw config redaction"
|
||||
aria-pressed=${!blurred}
|
||||
@click=${() => {
|
||||
@@ -1094,16 +1128,18 @@ export function renderConfig(props: ConfigProps) {
|
||||
${blurred ? icons.eyeOff : icons.eye}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</span>
|
||||
${blurred
|
||||
? html`
|
||||
${
|
||||
blurred
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
${sensitiveCount} sensitive value${sensitiveCount === 1 ? "" : "s"}
|
||||
hidden. Use the reveal button above to edit the raw config.
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<textarea
|
||||
placeholder="Raw config (JSON/JSON5)"
|
||||
.value=${props.raw}
|
||||
@@ -1111,17 +1147,21 @@ export function renderConfig(props: ConfigProps) {
|
||||
props.onRawChange((e.target as HTMLTextAreaElement).value);
|
||||
}}
|
||||
></textarea>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
})()}
|
||||
})()
|
||||
}
|
||||
</div>
|
||||
|
||||
${props.issues.length > 0
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${
|
||||
props.issues.length > 0
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
<pre class="code-block">${JSON.stringify(props.issues, null, 2)}</pre>
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -341,12 +341,14 @@ function focusFormField(id: string) {
|
||||
function renderFieldLabel(text: string, required = false) {
|
||||
return html`<span>
|
||||
${text}
|
||||
${required
|
||||
? html`
|
||||
${
|
||||
required
|
||||
? html`
|
||||
<span class="cron-required-marker" aria-hidden="true">*</span>
|
||||
<span class="cron-required-sr">${t("cron.form.requiredSr")}</span>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
@@ -400,11 +402,13 @@ export function renderCron(props: CronProps) {
|
||||
<div class="cron-summary-label">${t("cron.summary.enabled")}</div>
|
||||
<div class="cron-summary-value">
|
||||
<span class=${`chip ${props.status?.enabled ? "chip-ok" : "chip-danger"}`}>
|
||||
${props.status
|
||||
? props.status.enabled
|
||||
? t("cron.summary.yes")
|
||||
: t("cron.summary.no")
|
||||
: t("common.na")}
|
||||
${
|
||||
props.status
|
||||
? props.status.enabled
|
||||
? t("cron.summary.yes")
|
||||
: t("cron.summary.no")
|
||||
: t("common.na")
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -543,15 +547,18 @@ export function renderCron(props: CronProps) {
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
${props.jobs.length === 0
|
||||
? html` <div class="muted" style="margin-top: 12px">${t("cron.jobs.noMatching")}</div> `
|
||||
: html`
|
||||
${
|
||||
props.jobs.length === 0
|
||||
? html` <div class="muted" style="margin-top: 12px">${t("cron.jobs.noMatching")}</div> `
|
||||
: html`
|
||||
<div class="list" style="margin-top: 12px;">
|
||||
${props.jobs.map((job) => renderJob(job, props))}
|
||||
</div>
|
||||
`}
|
||||
${props.jobsHasMore
|
||||
? html`
|
||||
`
|
||||
}
|
||||
${
|
||||
props.jobsHasMore
|
||||
? html`
|
||||
<div class="row" style="margin-top: 12px">
|
||||
<button
|
||||
class="btn"
|
||||
@@ -562,7 +569,8 @@ export function renderCron(props: CronProps) {
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
@@ -573,9 +581,11 @@ export function renderCron(props: CronProps) {
|
||||
<div>
|
||||
<div class="card-title">${t("cron.runs.title")}</div>
|
||||
<div class="card-sub">
|
||||
${props.runsScope === "all"
|
||||
? t("cron.runs.subtitleAll")
|
||||
: t("cron.runs.subtitleJob", { title: selectedRunTitle })}
|
||||
${
|
||||
props.runsScope === "all"
|
||||
? t("cron.runs.subtitleAll")
|
||||
: t("cron.runs.subtitleJob", { title: selectedRunTitle })
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="muted">
|
||||
@@ -666,21 +676,24 @@ export function renderCron(props: CronProps) {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
${props.runsScope === "job" && props.runsJobId == null
|
||||
? html`
|
||||
${
|
||||
props.runsScope === "job" && props.runsJobId == null
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px">${t("cron.runs.selectJobHint")}</div>
|
||||
`
|
||||
: runs.length === 0
|
||||
? html`
|
||||
: runs.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px">${t("cron.runs.noMatching")}</div>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 12px;">
|
||||
${runs.map((entry) => renderRun(entry, props.basePath, props.onNavigateToChat))}
|
||||
</div>
|
||||
`}
|
||||
${(props.runsScope === "all" || props.runsJobId != null) && props.runsHasMore
|
||||
? html`
|
||||
`
|
||||
}
|
||||
${
|
||||
(props.runsScope === "all" || props.runsJobId != null) && props.runsHasMore
|
||||
? html`
|
||||
<div class="row" style="margin-top: 12px">
|
||||
<button
|
||||
class="btn"
|
||||
@@ -691,7 +704,8 @@ export function renderCron(props: CronProps) {
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -833,13 +847,16 @@ export function renderCron(props: CronProps) {
|
||||
<option value="agentTurn">${t("cron.form.agentTurn")}</option>
|
||||
</select>
|
||||
<div class="cron-help">
|
||||
${props.form.payloadKind === "systemEvent"
|
||||
? t("cron.form.systemEventHelp")
|
||||
: t("cron.form.agentTurnHelp")}
|
||||
${
|
||||
props.form.payloadKind === "systemEvent"
|
||||
? t("cron.form.systemEventHelp")
|
||||
: t("cron.form.agentTurnHelp")
|
||||
}
|
||||
</div>
|
||||
</label>
|
||||
${isAgentTurn
|
||||
? html`
|
||||
${
|
||||
isAgentTurn
|
||||
? html`
|
||||
<label class="field">
|
||||
${renderFieldLabel(t("cron.form.timeoutSeconds"))}
|
||||
<input
|
||||
@@ -864,7 +881,8 @@ export function renderCron(props: CronProps) {
|
||||
)}
|
||||
</label>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<label class="field cron-span-2">
|
||||
${renderFieldLabel(
|
||||
@@ -905,16 +923,19 @@ export function renderCron(props: CronProps) {
|
||||
.value as CronFormState["deliveryMode"],
|
||||
})}
|
||||
>
|
||||
${supportsAnnounce
|
||||
? html` <option value="announce">${t("cron.form.announceDefault")}</option> `
|
||||
: nothing}
|
||||
${
|
||||
supportsAnnounce
|
||||
? html` <option value="announce">${t("cron.form.announceDefault")}</option> `
|
||||
: nothing
|
||||
}
|
||||
<option value="webhook">${t("cron.form.webhookPost")}</option>
|
||||
<option value="none">${t("cron.form.noneInternal")}</option>
|
||||
</select>
|
||||
<div class="cron-help">${t("cron.form.deliveryHelp")}</div>
|
||||
</label>
|
||||
${selectedDeliveryMode !== "none"
|
||||
? html`
|
||||
${
|
||||
selectedDeliveryMode !== "none"
|
||||
? html`
|
||||
<label class="field ${selectedDeliveryMode === "webhook" ? "cron-span-2" : ""}">
|
||||
${renderFieldLabel(
|
||||
selectedDeliveryMode === "webhook"
|
||||
@@ -922,8 +943,9 @@ export function renderCron(props: CronProps) {
|
||||
: t("cron.form.channel"),
|
||||
selectedDeliveryMode === "webhook",
|
||||
)}
|
||||
${selectedDeliveryMode === "webhook"
|
||||
? html`
|
||||
${
|
||||
selectedDeliveryMode === "webhook"
|
||||
? html`
|
||||
<input
|
||||
id="cron-delivery-to"
|
||||
.value=${props.form.deliveryTo}
|
||||
@@ -941,7 +963,7 @@ export function renderCron(props: CronProps) {
|
||||
placeholder=${t("cron.form.webhookPlaceholder")}
|
||||
/>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<select
|
||||
id="cron-delivery-channel"
|
||||
.value=${props.form.deliveryChannel || "last"}
|
||||
@@ -957,13 +979,17 @@ export function renderCron(props: CronProps) {
|
||||
</option>`,
|
||||
)}
|
||||
</select>
|
||||
`}
|
||||
${selectedDeliveryMode === "announce"
|
||||
? html` <div class="cron-help">${t("cron.form.channelHelp")}</div> `
|
||||
: html` <div class="cron-help">${t("cron.form.webhookHelp")}</div> `}
|
||||
`
|
||||
}
|
||||
${
|
||||
selectedDeliveryMode === "announce"
|
||||
? html` <div class="cron-help">${t("cron.form.channelHelp")}</div> `
|
||||
: html` <div class="cron-help">${t("cron.form.webhookHelp")}</div> `
|
||||
}
|
||||
</label>
|
||||
${selectedDeliveryMode === "announce"
|
||||
? html`
|
||||
${
|
||||
selectedDeliveryMode === "announce"
|
||||
? html`
|
||||
<label class="field cron-span-2">
|
||||
${renderFieldLabel(t("cron.form.to"))}
|
||||
<input
|
||||
@@ -979,15 +1005,19 @@ export function renderCron(props: CronProps) {
|
||||
<div class="cron-help">${t("cron.form.toHelp")}</div>
|
||||
</label>
|
||||
`
|
||||
: nothing}
|
||||
${selectedDeliveryMode === "webhook"
|
||||
? renderFieldError(
|
||||
props.fieldErrors.deliveryTo,
|
||||
errorIdForField("deliveryTo"),
|
||||
)
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
selectedDeliveryMode === "webhook"
|
||||
? renderFieldError(
|
||||
props.fieldErrors.deliveryTo,
|
||||
errorIdForField("deliveryTo"),
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1032,8 +1062,9 @@ export function renderCron(props: CronProps) {
|
||||
/>
|
||||
<div class="cron-help">Optional routing key for job delivery and wake routing.</div>
|
||||
</label>
|
||||
${isCronSchedule
|
||||
? html`
|
||||
${
|
||||
isCronSchedule
|
||||
? html`
|
||||
<label class="field checkbox cron-checkbox cron-span-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -1087,9 +1118,11 @@ export function renderCron(props: CronProps) {
|
||||
</label>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${isAgentTurn
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isAgentTurn
|
||||
? html`
|
||||
<label class="field cron-span-2">
|
||||
${renderFieldLabel("Account ID")}
|
||||
<input
|
||||
@@ -1150,9 +1183,11 @@ export function renderCron(props: CronProps) {
|
||||
<div class="cron-help">${t("cron.form.thinkingHelp")}</div>
|
||||
</label>
|
||||
`
|
||||
: nothing}
|
||||
${isAgentTurn
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isAgentTurn
|
||||
? html`
|
||||
<label class="field cron-span-2">
|
||||
${renderFieldLabel("Failure alerts")}
|
||||
<select
|
||||
@@ -1171,8 +1206,9 @@ export function renderCron(props: CronProps) {
|
||||
Control when this job sends repeated-failure alerts.
|
||||
</div>
|
||||
</label>
|
||||
${props.form.failureAlertMode === "custom"
|
||||
? html`
|
||||
${
|
||||
props.form.failureAlertMode === "custom"
|
||||
? html`
|
||||
<label class="field">
|
||||
${renderFieldLabel("Alert after")}
|
||||
<input
|
||||
@@ -1201,9 +1237,9 @@ export function renderCron(props: CronProps) {
|
||||
<input
|
||||
id="cron-failure-alert-cooldown-seconds"
|
||||
.value=${props.form.failureAlertCooldownSeconds}
|
||||
aria-invalid=${props.fieldErrors.failureAlertCooldownSeconds
|
||||
? "true"
|
||||
: "false"}
|
||||
aria-invalid=${
|
||||
props.fieldErrors.failureAlertCooldownSeconds ? "true" : "false"
|
||||
}
|
||||
aria-describedby=${ifDefined(
|
||||
props.fieldErrors.failureAlertCooldownSeconds
|
||||
? errorIdForField("failureAlertCooldownSeconds")
|
||||
@@ -1279,11 +1315,14 @@ export function renderCron(props: CronProps) {
|
||||
/>
|
||||
</label>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: nothing}
|
||||
${selectedDeliveryMode !== "none"
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
selectedDeliveryMode !== "none"
|
||||
? html`
|
||||
<label class="field checkbox cron-checkbox cron-span-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -1299,12 +1338,14 @@ export function renderCron(props: CronProps) {
|
||||
<div class="cron-help">${t("cron.form.bestEffortHelp")}</div>
|
||||
</label>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
${blockedByValidation
|
||||
? html`
|
||||
${
|
||||
blockedByValidation
|
||||
? html`
|
||||
<div class="cron-form-status" role="status" aria-live="polite">
|
||||
<div class="cron-form-status__title">${t("cron.form.cantAddYet")}</div>
|
||||
<div class="cron-help">${t("cron.form.fillRequired")}</div>
|
||||
@@ -1325,29 +1366,36 @@ export function renderCron(props: CronProps) {
|
||||
</ul>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<div class="row cron-form-actions">
|
||||
<button
|
||||
class="btn primary"
|
||||
?disabled=${props.busy || !props.canSubmit}
|
||||
@click=${props.onAdd}
|
||||
>
|
||||
${props.busy
|
||||
? t("cron.form.saving")
|
||||
: isEditing
|
||||
? t("cron.form.saveChanges")
|
||||
: t("cron.form.addJob")}
|
||||
${
|
||||
props.busy
|
||||
? t("cron.form.saving")
|
||||
: isEditing
|
||||
? t("cron.form.saveChanges")
|
||||
: t("cron.form.addJob")
|
||||
}
|
||||
</button>
|
||||
${submitDisabledReason
|
||||
? html`<div class="cron-submit-reason" aria-live="polite">${submitDisabledReason}</div>`
|
||||
: nothing}
|
||||
${isEditing
|
||||
? html`
|
||||
${
|
||||
submitDisabledReason
|
||||
? html`<div class="cron-submit-reason" aria-live="polite">${submitDisabledReason}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isEditing
|
||||
? html`
|
||||
<button class="btn" ?disabled=${props.busy} @click=${props.onCancelEdit}>
|
||||
${t("cron.form.cancel")}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
@@ -1474,11 +1522,13 @@ function renderJob(job: CronJob, props: CronProps) {
|
||||
<div class="list-title">${job.name}</div>
|
||||
<div class="list-sub">${formatCronSchedule(job)}</div>
|
||||
${renderJobPayload(job)}
|
||||
${job.agentId
|
||||
? html`<div class="muted cron-job-agent">
|
||||
${
|
||||
job.agentId
|
||||
? html`<div class="muted cron-job-agent">
|
||||
${t("cron.jobDetail.agent")}: ${job.agentId}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="list-meta">${renderJobState(job)}</div>
|
||||
<div class="cron-job-footer">
|
||||
@@ -1589,12 +1639,14 @@ function renderJobPayload(job: CronJob) {
|
||||
<span class="cron-job-detail-label">${t("cron.jobDetail.prompt")}</span>
|
||||
<span class="muted cron-job-detail-value">${job.payload.message}</span>
|
||||
</div>
|
||||
${delivery
|
||||
? html`<div class="cron-job-detail">
|
||||
${
|
||||
delivery
|
||||
? html`<div class="cron-job-detail">
|
||||
<span class="cron-job-detail-label">${t("cron.jobDetail.delivery")}</span>
|
||||
<span class="muted cron-job-detail-value">${delivery.mode}${deliveryTarget}</span>
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1718,15 +1770,20 @@ function renderRun(
|
||||
</div>
|
||||
<div class="list-meta cron-run-entry__meta">
|
||||
<div>${formatMs(entry.ts)}</div>
|
||||
${typeof entry.runAtMs === "number"
|
||||
? html`<div class="muted">${t("cron.runEntry.runAt")} ${formatMs(entry.runAtMs)}</div>`
|
||||
: nothing}
|
||||
${
|
||||
typeof entry.runAtMs === "number"
|
||||
? html`<div class="muted">${t("cron.runEntry.runAt")} ${formatMs(entry.runAtMs)}</div>`
|
||||
: nothing
|
||||
}
|
||||
<div class="muted">${entry.durationMs ?? 0}ms</div>
|
||||
${typeof entry.nextRunAtMs === "number"
|
||||
? html`<div class="muted">${formatRunNextLabel(entry.nextRunAtMs)}</div>`
|
||||
: nothing}
|
||||
${chatUrl
|
||||
? html`<div>
|
||||
${
|
||||
typeof entry.nextRunAtMs === "number"
|
||||
? html`<div class="muted">${formatRunNextLabel(entry.nextRunAtMs)}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
chatUrl
|
||||
? html`<div>
|
||||
<a
|
||||
class="session-link"
|
||||
href=${chatUrl}
|
||||
@@ -1749,7 +1806,8 @@ function renderRun(
|
||||
>${t("cron.runEntry.openRunChat")}</a
|
||||
>
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${entry.error ? html`<div class="muted">${entry.error}</div>` : nothing}
|
||||
${entry.deliveryError ? html`<div class="muted">${entry.deliveryError}</div>` : nothing}
|
||||
</div>
|
||||
|
||||
@@ -48,12 +48,14 @@ export function renderDebug(props: DebugProps) {
|
||||
<div class="stack" style="margin-top: 12px;">
|
||||
<div>
|
||||
<div class="muted">Status</div>
|
||||
${securitySummary
|
||||
? html`<div class="callout ${securityTone}" style="margin-top: 8px;">
|
||||
${
|
||||
securitySummary
|
||||
? html`<div class="callout ${securityTone}" style="margin-top: 8px;">
|
||||
Security audit: ${securityLabel}${info > 0 ? ` · ${info} info` : ""}. Run
|
||||
<span class="mono">openclaw security audit --deep</span> for details.
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<pre class="code-block">${JSON.stringify(props.status ?? {}, null, 2)}</pre>
|
||||
</div>
|
||||
<div>
|
||||
@@ -78,9 +80,13 @@ export function renderDebug(props: DebugProps) {
|
||||
@change=${(e: Event) =>
|
||||
props.onCallMethodChange((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
${!props.callMethod
|
||||
? html` <option value="" disabled>Select a method…</option> `
|
||||
: nothing}
|
||||
${
|
||||
!props.callMethod
|
||||
? html`
|
||||
<option value="" disabled>Select a method…</option>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${props.methods.map((m) => html`<option value=${m}>${m}</option>`)}
|
||||
</select>
|
||||
</label>
|
||||
@@ -97,12 +103,16 @@ export function renderDebug(props: DebugProps) {
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn primary" @click=${props.onCall}>Call</button>
|
||||
</div>
|
||||
${props.callError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.callError}</div>`
|
||||
: nothing}
|
||||
${props.callResult
|
||||
? html`<pre class="code-block" style="margin-top: 12px;">${props.callResult}</pre>`
|
||||
: nothing}
|
||||
${
|
||||
props.callError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.callError}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.callResult
|
||||
? html`<pre class="code-block" style="margin-top: 12px;">${props.callResult}</pre>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -117,9 +127,12 @@ ${JSON.stringify(props.models ?? [], null, 2)}</pre
|
||||
<section class="card" style="margin-top: 18px;">
|
||||
<div class="card-title">Event Log</div>
|
||||
<div class="card-sub">Latest gateway events.</div>
|
||||
${props.eventLog.length === 0
|
||||
? html` <div class="muted" style="margin-top: 12px">No events yet.</div> `
|
||||
: html`
|
||||
${
|
||||
props.eventLog.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px">No events yet.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list debug-event-log" style="margin-top: 12px;">
|
||||
${props.eventLog.map(
|
||||
(evt) => html`
|
||||
@@ -137,7 +150,8 @@ ${formatEventPayload(evt.payload)}</pre
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -39,9 +39,11 @@ export function renderExecApprovalPrompt(state: AppViewState) {
|
||||
<div class="exec-approval-title">Exec approval needed</div>
|
||||
<div class="exec-approval-sub">${remaining}</div>
|
||||
</div>
|
||||
${queueCount > 1
|
||||
? html`<div class="exec-approval-queue">${queueCount} pending</div>`
|
||||
: nothing}
|
||||
${
|
||||
queueCount > 1
|
||||
? html`<div class="exec-approval-queue">${queueCount} pending</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="exec-approval-command mono">${request.command}</div>
|
||||
<div class="exec-approval-meta">
|
||||
@@ -50,9 +52,11 @@ export function renderExecApprovalPrompt(state: AppViewState) {
|
||||
${renderMetaRow("Resolved", request.resolvedPath)}
|
||||
${renderMetaRow("Security", request.security)} ${renderMetaRow("Ask", request.ask)}
|
||||
</div>
|
||||
${state.execApprovalError
|
||||
? html`<div class="exec-approval-error">${state.execApprovalError}</div>`
|
||||
: nothing}
|
||||
${
|
||||
state.execApprovalError
|
||||
? html`<div class="exec-approval-error">${state.execApprovalError}</div>`
|
||||
: nothing
|
||||
}
|
||||
<div class="exec-approval-actions">
|
||||
<button
|
||||
class="btn primary"
|
||||
|
||||
@@ -42,16 +42,24 @@ export function renderInstances(props: InstancesProps) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.lastError}</div>`
|
||||
: nothing}
|
||||
${props.statusMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">${props.statusMessage}</div>`
|
||||
: nothing}
|
||||
${
|
||||
props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.lastError}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.statusMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">${props.statusMessage}</div>`
|
||||
: nothing
|
||||
}
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${props.entries.length === 0
|
||||
? html` <div class="muted">No instances reported yet.</div> `
|
||||
: props.entries.map((entry) => renderEntry(entry, masked))}
|
||||
${
|
||||
props.entries.length === 0
|
||||
? html`
|
||||
<div class="muted">No instances reported yet.</div>
|
||||
`
|
||||
: props.entries.map((entry) => renderEntry(entry, masked))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
@@ -86,9 +94,11 @@ function renderEntry(entry: PresenceEntry, masked: boolean) {
|
||||
${scopesLabel ? html`<span class="chip">${scopesLabel}</span>` : nothing}
|
||||
${entry.platform ? html`<span class="chip">${entry.platform}</span>` : nothing}
|
||||
${entry.deviceFamily ? html`<span class="chip">${entry.deviceFamily}</span>` : nothing}
|
||||
${entry.modelIdentifier
|
||||
? html`<span class="chip">${entry.modelIdentifier}</span>`
|
||||
: nothing}
|
||||
${
|
||||
entry.modelIdentifier
|
||||
? html`<span class="chip">${entry.modelIdentifier}</span>`
|
||||
: nothing
|
||||
}
|
||||
${entry.version ? html`<span class="chip">${entry.version}</span>` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,11 +99,13 @@ export function renderLoginGate(state: AppViewState) {
|
||||
${t("common.connect")}
|
||||
</button>
|
||||
</div>
|
||||
${state.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 14px;">
|
||||
${
|
||||
state.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 14px;">
|
||||
<div>${state.lastError}</div>
|
||||
</div>`
|
||||
: ""}
|
||||
: ""
|
||||
}
|
||||
<div class="login-gate__help">
|
||||
<div class="login-gate__help-title">${t("overview.connection.title")}</div>
|
||||
<ol class="login-gate__steps">
|
||||
|
||||
@@ -114,25 +114,32 @@ export function renderLogs(props: LogsProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
${props.file
|
||||
? html`<div class="muted" style="margin-top: 10px;">File: ${props.file}</div>`
|
||||
: nothing}
|
||||
${props.truncated
|
||||
? html`
|
||||
<div class="callout" style="margin-top: 10px">
|
||||
Log output truncated; showing latest chunk.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${props.error
|
||||
? html`<div class="callout danger" style="margin-top: 10px;">${props.error}</div>`
|
||||
: nothing}
|
||||
${
|
||||
props.file
|
||||
? html`<div class="muted" style="margin-top: 10px;">File: ${props.file}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.truncated
|
||||
? html`
|
||||
<div class="callout" style="margin-top: 10px">Log output truncated; showing latest chunk.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.error
|
||||
? html`<div class="callout danger" style="margin-top: 10px;">${props.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="log-stream" style="margin-top: 12px;" @scroll=${props.onScroll}>
|
||||
${filtered.length === 0
|
||||
? html` <div class="muted" style="padding: 12px">No log entries.</div> `
|
||||
: filtered.map(
|
||||
(entry) => html`
|
||||
${
|
||||
filtered.length === 0
|
||||
? html`
|
||||
<div class="muted" style="padding: 12px">No log entries.</div>
|
||||
`
|
||||
: filtered.map(
|
||||
(entry) => html`
|
||||
<div class="log-row">
|
||||
<div class="log-time mono">${formatTime(entry.time)}</div>
|
||||
<div class="log-level ${entry.level ?? ""}">${entry.level ?? ""}</div>
|
||||
@@ -140,7 +147,8 @@ export function renderLogs(props: LogsProps) {
|
||||
<div class="log-message mono">${entry.message ?? entry.raw}</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
@@ -18,18 +18,22 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
||||
<button @click=${props.onClose} class="btn" title="Close sidebar">${icons.x}</button>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
${props.error
|
||||
? html`
|
||||
${
|
||||
props.error
|
||||
? html`
|
||||
<div class="callout danger">${props.error}</div>
|
||||
<button @click=${props.onViewRawText} class="btn" style="margin-top: 12px;">
|
||||
View Raw Text
|
||||
</button>
|
||||
`
|
||||
: props.content
|
||||
? html`<div class="sidebar-markdown">
|
||||
: props.content
|
||||
? html`<div class="sidebar-markdown">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(props.content))}
|
||||
</div>`
|
||||
: html` <div class="muted">No content available</div> `}
|
||||
: html`
|
||||
<div class="muted">No content available</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -211,19 +211,23 @@ export function renderExecApprovals(state: ExecApprovalsState) {
|
||||
</div>
|
||||
|
||||
${renderExecApprovalsTarget(state)}
|
||||
${!ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
${
|
||||
!ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
<div class="muted">Load exec approvals to edit allowlists.</div>
|
||||
<button class="btn" ?disabled=${state.loading || !targetReady} @click=${state.onLoad}>
|
||||
${state.loading ? "Loading…" : "Load approvals"}
|
||||
</button>
|
||||
</div>`
|
||||
: html`
|
||||
: html`
|
||||
${renderExecApprovalsTabs(state)} ${renderExecApprovalsPolicy(state)}
|
||||
${state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE
|
||||
? nothing
|
||||
: renderExecApprovalsAllowlist(state)}
|
||||
`}
|
||||
${
|
||||
state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE
|
||||
? nothing
|
||||
: renderExecApprovalsAllowlist(state)
|
||||
}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
@@ -258,8 +262,9 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
||||
<option value="node" ?selected=${state.target === "node"}>Node</option>
|
||||
</select>
|
||||
</label>
|
||||
${state.target === "node"
|
||||
? html`
|
||||
${
|
||||
state.target === "node"
|
||||
? html`
|
||||
<label class="field">
|
||||
<span>Node</span>
|
||||
<select
|
||||
@@ -280,12 +285,17 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
||||
</select>
|
||||
</label>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
${state.target === "node" && !hasNodes
|
||||
? html` <div class="muted">No nodes advertise exec approvals yet.</div> `
|
||||
: nothing}
|
||||
${
|
||||
state.target === "node" && !hasNodes
|
||||
? html`
|
||||
<div class="muted">No nodes advertise exec approvals yet.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -296,9 +306,9 @@ function renderExecApprovalsTabs(state: ExecApprovalsState) {
|
||||
<span class="label">Scope</span>
|
||||
<div class="row" style="gap: 8px; flex-wrap: wrap;">
|
||||
<button
|
||||
class="btn btn--sm ${state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE
|
||||
? "active"
|
||||
: ""}"
|
||||
class="btn btn--sm ${
|
||||
state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE ? "active" : ""
|
||||
}"
|
||||
@click=${() => state.onSelectScope(EXEC_APPROVALS_DEFAULT_SCOPE)}
|
||||
>
|
||||
Defaults
|
||||
@@ -359,11 +369,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
${!isDefaults
|
||||
? html`<option value="__default__" ?selected=${securityValue === "__default__"}>
|
||||
${
|
||||
!isDefaults
|
||||
? html`<option value="__default__" ?selected=${securityValue === "__default__"}>
|
||||
Use default (${defaults.security})
|
||||
</option>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${SECURITY_OPTIONS.map(
|
||||
(option) =>
|
||||
html`<option value=${option.value} ?selected=${securityValue === option.value}>
|
||||
@@ -397,11 +409,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
${!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askValue === "__default__"}>
|
||||
${
|
||||
!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askValue === "__default__"}>
|
||||
Use default (${defaults.ask})
|
||||
</option>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${ASK_OPTIONS.map(
|
||||
(option) =>
|
||||
html`<option value=${option.value} ?selected=${askValue === option.value}>
|
||||
@@ -417,9 +431,11 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
<div class="list-main">
|
||||
<div class="list-title">Ask fallback</div>
|
||||
<div class="list-sub">
|
||||
${isDefaults
|
||||
? "Applied when the UI prompt is unavailable."
|
||||
: `Default: ${defaults.askFallback}.`}
|
||||
${
|
||||
isDefaults
|
||||
? "Applied when the UI prompt is unavailable."
|
||||
: `Default: ${defaults.askFallback}.`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
@@ -437,11 +453,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
${!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askFallbackValue === "__default__"}>
|
||||
${
|
||||
!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askFallbackValue === "__default__"}>
|
||||
Use default (${defaults.askFallback})
|
||||
</option>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${SECURITY_OPTIONS.map(
|
||||
(option) =>
|
||||
html`<option value=${option.value} ?selected=${askFallbackValue === option.value}>
|
||||
@@ -457,11 +475,13 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
<div class="list-main">
|
||||
<div class="list-title">Auto-allow skill CLIs</div>
|
||||
<div class="list-sub">
|
||||
${isDefaults
|
||||
? "Allow skill executables listed by the Gateway."
|
||||
: autoIsDefault
|
||||
? `Using default (${defaults.autoAllowSkills ? "on" : "off"}).`
|
||||
: `Override (${autoEffective ? "on" : "off"}).`}
|
||||
${
|
||||
isDefaults
|
||||
? "Allow skill executables listed by the Gateway."
|
||||
: autoIsDefault
|
||||
? `Using default (${defaults.autoAllowSkills ? "on" : "off"}).`
|
||||
: `Override (${autoEffective ? "on" : "off"}).`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
@@ -477,15 +497,17 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
${!isDefaults && !autoIsDefault
|
||||
? html`<button
|
||||
${
|
||||
!isDefaults && !autoIsDefault
|
||||
? html`<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${state.disabled}
|
||||
@click=${() => state.onRemove([...basePath, "autoAllowSkills"])}
|
||||
>
|
||||
Use default
|
||||
</button>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -513,9 +535,13 @@ function renderExecApprovalsAllowlist(state: ExecApprovalsState) {
|
||||
</button>
|
||||
</div>
|
||||
<div class="list" style="margin-top: 12px;">
|
||||
${entries.length === 0
|
||||
? html` <div class="muted">No allowlist entries yet.</div> `
|
||||
: entries.map((entry, index) => renderAllowlistEntry(state, entry, index))}
|
||||
${
|
||||
entries.length === 0
|
||||
? html`
|
||||
<div class="muted">No allowlist entries yet.</div>
|
||||
`
|
||||
: entries.map((entry, index) => renderAllowlistEntry(state, entry, index))
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -62,9 +62,13 @@ export function renderNodes(props: NodesProps) {
|
||||
</button>
|
||||
</div>
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${props.nodes.length === 0
|
||||
? html` <div class="muted">No nodes found.</div> `
|
||||
: props.nodes.map((n) => renderNode(n))}
|
||||
${
|
||||
props.nodes.length === 0
|
||||
? html`
|
||||
<div class="muted">No nodes found.</div>
|
||||
`
|
||||
: props.nodes.map((n) => renderNode(n))
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
@@ -85,25 +89,35 @@ function renderDevices(props: NodesProps) {
|
||||
${props.devicesLoading ? "Loading…" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
${props.devicesError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.devicesError}</div>`
|
||||
: nothing}
|
||||
${
|
||||
props.devicesError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.devicesError}</div>`
|
||||
: nothing
|
||||
}
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${pending.length > 0
|
||||
? html`
|
||||
${
|
||||
pending.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-bottom: 8px;">Pending</div>
|
||||
${pending.map((req) => renderPendingDevice(req, props))}
|
||||
`
|
||||
: nothing}
|
||||
${paired.length > 0
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
paired.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px; margin-bottom: 8px;">Paired</div>
|
||||
${paired.map((device) => renderPairedDevice(device, props))}
|
||||
`
|
||||
: nothing}
|
||||
${pending.length === 0 && paired.length === 0
|
||||
? html` <div class="muted">No paired devices.</div> `
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
pending.length === 0 && paired.length === 0
|
||||
? html`
|
||||
<div class="muted">No paired devices.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
@@ -151,14 +165,18 @@ function renderPairedDevice(device: PairedDevice, props: NodesProps) {
|
||||
<div class="list-title">${name}</div>
|
||||
<div class="list-sub">${device.deviceId}${ip}</div>
|
||||
<div class="muted" style="margin-top: 6px;">${roles} · ${scopes}</div>
|
||||
${tokens.length === 0
|
||||
? html` <div class="muted" style="margin-top: 6px">Tokens: none</div> `
|
||||
: html`
|
||||
${
|
||||
tokens.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 6px">Tokens: none</div>
|
||||
`
|
||||
: html`
|
||||
<div class="muted" style="margin-top: 10px;">Tokens</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; margin-top: 6px;">
|
||||
${tokens.map((token) => renderTokenRow(device.deviceId, token, props))}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -180,16 +198,18 @@ function renderTokenRow(deviceId: string, token: DeviceTokenSummary, props: Node
|
||||
>
|
||||
Rotate
|
||||
</button>
|
||||
${token.revokedAtMs
|
||||
? nothing
|
||||
: html`
|
||||
${
|
||||
token.revokedAtMs
|
||||
? nothing
|
||||
: html`
|
||||
<button
|
||||
class="btn btn--sm danger"
|
||||
@click=${() => props.onDeviceRevoke(deviceId, token.role)}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -265,21 +285,24 @@ function renderBindings(state: BindingState) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${state.formMode === "raw"
|
||||
? html`
|
||||
<div class="callout warn" style="margin-top: 12px">
|
||||
Switch the Config tab to <strong>Form</strong> mode to edit bindings here.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${!state.ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
${
|
||||
state.formMode === "raw"
|
||||
? html`
|
||||
<div class="callout warn" style="margin-top: 12px">
|
||||
Switch the Config tab to <strong>Form</strong> mode to edit bindings here.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
!state.ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
<div class="muted">Load config to edit bindings.</div>
|
||||
<button class="btn" ?disabled=${state.configLoading} @click=${state.onLoadConfig}>
|
||||
${state.configLoading ? "Loading…" : "Load config"}
|
||||
</button>
|
||||
</div>`
|
||||
: html`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
@@ -306,17 +329,26 @@ function renderBindings(state: BindingState) {
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
${!supportsBinding
|
||||
? html` <div class="muted">No nodes with system.run available.</div> `
|
||||
: nothing}
|
||||
${
|
||||
!supportsBinding
|
||||
? html`
|
||||
<div class="muted">No nodes with system.run available.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${state.agents.length === 0
|
||||
? html` <div class="muted">No agents found.</div> `
|
||||
: state.agents.map((agent) => renderAgentBinding(agent, state))}
|
||||
${
|
||||
state.agents.length === 0
|
||||
? html`
|
||||
<div class="muted">No agents found.</div>
|
||||
`
|
||||
: state.agents.map((agent) => renderAgentBinding(agent, state))
|
||||
}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
@@ -331,9 +363,11 @@ function renderAgentBinding(agent: BindingAgent, state: BindingState) {
|
||||
<div class="list-title">${label}</div>
|
||||
<div class="list-sub">
|
||||
${agent.isDefault ? "default agent" : "agent"} ·
|
||||
${bindingValue === "__default__"
|
||||
? `uses default (${state.defaultBinding ?? "any"})`
|
||||
: `override: ${agent.binding}`}
|
||||
${
|
||||
bindingValue === "__default__"
|
||||
? `uses default (${state.defaultBinding ?? "any"})`
|
||||
: `override: ${agent.binding}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
|
||||
@@ -42,15 +42,17 @@ export function renderOverviewAttention(props: OverviewAttentionProps) {
|
||||
<div class="ov-attention-title">${item.title}</div>
|
||||
<div class="muted">${item.description}</div>
|
||||
</div>
|
||||
${item.href
|
||||
? html`<a
|
||||
${
|
||||
item.href
|
||||
? html`<a
|
||||
class="ov-attention-link"
|
||||
href=${item.href}
|
||||
target=${item.external ? EXTERNAL_LINK_TARGET : nothing}
|
||||
rel=${item.external ? buildExternalLinkRel() : nothing}
|
||||
>${t("common.docs")}</a
|
||||
>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
|
||||
@@ -136,8 +136,9 @@ export function renderOverviewCards(props: OverviewCardsProps) {
|
||||
return html`
|
||||
<section class="ov-cards">${cards.map((c) => renderStatCard(c, props.onNavigate))}</section>
|
||||
|
||||
${sessions.length > 0
|
||||
? html`
|
||||
${
|
||||
sessions.length > 0
|
||||
? html`
|
||||
<section class="ov-recent">
|
||||
<h3 class="ov-recent__title">${t("overview.cards.recentSessions")}</h3>
|
||||
<ul class="ov-recent__list">
|
||||
@@ -157,6 +158,7 @@ export function renderOverviewCards(props: OverviewCardsProps) {
|
||||
</ul>
|
||||
</section>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -28,11 +28,13 @@ export function renderOverviewEventLog(props: OverviewEventLogProps) {
|
||||
<div class="ov-event-log-entry">
|
||||
<span class="ov-event-log-ts">${new Date(entry.ts).toLocaleTimeString()}</span>
|
||||
<span class="ov-event-log-name">${entry.event}</span>
|
||||
${entry.payload
|
||||
? html`<span class="ov-event-log-payload muted"
|
||||
${
|
||||
entry.payload
|
||||
? html`<span class="ov-event-log-payload muted"
|
||||
>${formatEventPayload(entry.payload).slice(0, 120)}</span
|
||||
>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
|
||||
@@ -215,9 +215,10 @@ export function renderOverview(props: OverviewProps) {
|
||||
placeholder="ws://100.x.y.z:18789"
|
||||
/>
|
||||
</label>
|
||||
${isTrustedProxy
|
||||
? ""
|
||||
: html`
|
||||
${
|
||||
isTrustedProxy
|
||||
? ""
|
||||
: html`
|
||||
<label class="field">
|
||||
<span>${t("overview.access.token")}</span>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
@@ -272,7 +273,8 @@ export function renderOverview(props: OverviewProps) {
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
`}
|
||||
`
|
||||
}
|
||||
<label class="field">
|
||||
<span>${t("overview.access.sessionKey")}</span>
|
||||
<input
|
||||
@@ -306,13 +308,14 @@ export function renderOverview(props: OverviewProps) {
|
||||
<button class="btn" @click=${() => props.onConnect()}>${t("common.connect")}</button>
|
||||
<button class="btn" @click=${() => props.onRefresh()}>${t("common.refresh")}</button>
|
||||
<span class="muted"
|
||||
>${isTrustedProxy
|
||||
? t("overview.access.trustedProxy")
|
||||
: t("overview.access.connectHint")}</span
|
||||
>${
|
||||
isTrustedProxy ? t("overview.access.trustedProxy") : t("overview.access.connectHint")
|
||||
}</span
|
||||
>
|
||||
</div>
|
||||
${!props.connected
|
||||
? html`
|
||||
${
|
||||
!props.connected
|
||||
? html`
|
||||
<div class="login-gate__help" style="margin-top: 16px;">
|
||||
<div class="login-gate__help-title">${t("overview.connection.title")}</div>
|
||||
<ol class="login-gate__steps">
|
||||
@@ -339,7 +342,8 @@ export function renderOverview(props: OverviewProps) {
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -363,22 +367,26 @@ export function renderOverview(props: OverviewProps) {
|
||||
<div class="stat">
|
||||
<div class="stat-label">${t("overview.snapshot.lastChannelsRefresh")}</div>
|
||||
<div class="stat-value">
|
||||
${props.lastChannelsRefresh
|
||||
? formatRelativeTimestamp(props.lastChannelsRefresh)
|
||||
: t("common.na")}
|
||||
${
|
||||
props.lastChannelsRefresh
|
||||
? formatRelativeTimestamp(props.lastChannelsRefresh)
|
||||
: t("common.na")
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 14px;">
|
||||
${
|
||||
props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 14px;">
|
||||
<div>${props.lastError}</div>
|
||||
${pairingHint ?? ""} ${authHint ?? ""} ${insecureContextHint ?? ""}
|
||||
</div>`
|
||||
: html`
|
||||
: html`
|
||||
<div class="callout" style="margin-top: 14px">
|
||||
${t("overview.snapshot.channelsHint")}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -216,9 +216,11 @@ export function renderSessions(props: SessionsProps) {
|
||||
<div>
|
||||
<div class="card-title">Sessions</div>
|
||||
<div class="card-sub">
|
||||
${props.result
|
||||
? `Store: ${props.result.path}`
|
||||
: "Active session keys and per-session overrides."}
|
||||
${
|
||||
props.result
|
||||
? `Store: ${props.result.path}`
|
||||
: "Active session keys and per-session overrides."
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn" ?disabled=${props.loading} @click=${props.onRefresh}>
|
||||
@@ -286,9 +288,11 @@ export function renderSessions(props: SessionsProps) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
${props.error
|
||||
? html`<div class="callout danger" style="margin-bottom: 12px;">${props.error}</div>`
|
||||
: nothing}
|
||||
${
|
||||
props.error
|
||||
? html`<div class="callout danger" style="margin-bottom: 12px;">${props.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-toolbar">
|
||||
@@ -302,8 +306,9 @@ export function renderSessions(props: SessionsProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${props.selectedKeys.size > 0
|
||||
? html`
|
||||
${
|
||||
props.selectedKeys.size > 0
|
||||
? html`
|
||||
<div class="data-table-bulk-bar">
|
||||
<span>${props.selectedKeys.size} selected</span>
|
||||
<button class="btn btn--sm" @click=${props.onDeselectAll}>Unselect</button>
|
||||
@@ -316,20 +321,26 @@ export function renderSessions(props: SessionsProps) {
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="data-table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="data-table-checkbox-col">
|
||||
${paginated.length > 0
|
||||
? html`<input
|
||||
${
|
||||
paginated.length > 0
|
||||
? html`<input
|
||||
type="checkbox"
|
||||
.checked=${paginated.length > 0 &&
|
||||
paginated.every((r) => props.selectedKeys.has(r.key))}
|
||||
.indeterminate=${paginated.some((r) => props.selectedKeys.has(r.key)) &&
|
||||
!paginated.every((r) => props.selectedKeys.has(r.key))}
|
||||
.checked=${
|
||||
paginated.length > 0 &&
|
||||
paginated.every((r) => props.selectedKeys.has(r.key))
|
||||
}
|
||||
.indeterminate=${
|
||||
paginated.some((r) => props.selectedKeys.has(r.key)) &&
|
||||
!paginated.every((r) => props.selectedKeys.has(r.key))
|
||||
}
|
||||
@change=${() => {
|
||||
const allSelected = paginated.every((r) => props.selectedKeys.has(r.key));
|
||||
if (allSelected) {
|
||||
@@ -340,7 +351,8 @@ export function renderSessions(props: SessionsProps) {
|
||||
}}
|
||||
aria-label="Select all on page"
|
||||
/>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</th>
|
||||
${sortHeader("key", "Key", "data-table-key-col")}
|
||||
<th>Label</th>
|
||||
@@ -353,34 +365,34 @@ export function renderSessions(props: SessionsProps) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${paginated.length === 0
|
||||
? html`
|
||||
<tr>
|
||||
<td
|
||||
colspan="10"
|
||||
style="text-align: center; padding: 48px 16px; color: var(--muted)"
|
||||
>
|
||||
No sessions found.
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: paginated.map((row) =>
|
||||
renderRow(
|
||||
row,
|
||||
props.basePath,
|
||||
props.onPatch,
|
||||
props.selectedKeys.has(row.key),
|
||||
props.onToggleSelect,
|
||||
props.loading,
|
||||
props.onNavigateToChat,
|
||||
),
|
||||
)}
|
||||
${
|
||||
paginated.length === 0
|
||||
? html`
|
||||
<tr>
|
||||
<td colspan="10" style="text-align: center; padding: 48px 16px; color: var(--muted)">
|
||||
No sessions found.
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: paginated.map((row) =>
|
||||
renderRow(
|
||||
row,
|
||||
props.basePath,
|
||||
props.onPatch,
|
||||
props.selectedKeys.has(row.key),
|
||||
props.onToggleSelect,
|
||||
props.loading,
|
||||
props.onNavigateToChat,
|
||||
),
|
||||
)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
${totalRows > 0
|
||||
? html`
|
||||
${
|
||||
totalRows > 0
|
||||
? html`
|
||||
<div class="data-table-pagination">
|
||||
<div class="data-table-pagination__info">
|
||||
${page * props.pageSize + 1}-${Math.min((page + 1) * props.pageSize, totalRows)}
|
||||
@@ -407,7 +419,8 @@ export function renderSessions(props: SessionsProps) {
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
@@ -467,8 +480,9 @@ function renderRow(
|
||||
</td>
|
||||
<td class="data-table-key-col">
|
||||
<div class="mono session-key-cell">
|
||||
${canLink
|
||||
? html`<a
|
||||
${
|
||||
canLink
|
||||
? html`<a
|
||||
href=${chatUrl}
|
||||
class="session-link"
|
||||
@click=${(e: MouseEvent) => {
|
||||
@@ -489,10 +503,13 @@ function renderRow(
|
||||
}}
|
||||
>${row.key}</a
|
||||
>`
|
||||
: row.key}
|
||||
${showDisplayName
|
||||
? html`<span class="muted session-key-display-name">${displayName}</span>`
|
||||
: nothing}
|
||||
: row.key
|
||||
}
|
||||
${
|
||||
showDisplayName
|
||||
? html`<span class="muted session-key-display-name">${displayName}</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -30,11 +30,23 @@ export function renderSkillStatusChips(params: {
|
||||
return html`
|
||||
<div class="chip-row" style="margin-top: 6px;">
|
||||
<span class="chip">${skill.source}</span>
|
||||
${showBundledBadge ? html` <span class="chip">bundled</span> ` : nothing}
|
||||
${
|
||||
showBundledBadge
|
||||
? html`
|
||||
<span class="chip">bundled</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<span class="chip ${skill.eligible ? "chip-ok" : "chip-warn"}">
|
||||
${skill.eligible ? "eligible" : "blocked"}
|
||||
</span>
|
||||
${skill.disabled ? html` <span class="chip chip-warn">disabled</span> ` : nothing}
|
||||
${
|
||||
skill.disabled
|
||||
? html`
|
||||
<span class="chip chip-warn">disabled</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -159,18 +159,21 @@ export function renderSkills(props: SkillsProps) {
|
||||
<div class="muted">${filtered.length} shown</div>
|
||||
</div>
|
||||
|
||||
${props.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
||||
: nothing}
|
||||
${filtered.length === 0
|
||||
? html`
|
||||
${
|
||||
props.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
filtered.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">
|
||||
${!props.connected && !props.report
|
||||
? "Not connected to gateway."
|
||||
: "No skills found."}
|
||||
${
|
||||
!props.connected && !props.report ? "Not connected to gateway." : "No skills found."
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<div class="agent-skills-groups" style="margin-top: 16px;">
|
||||
${groups.map((group) => {
|
||||
return html`
|
||||
@@ -186,7 +189,8 @@ export function renderSkills(props: SkillsProps) {
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</section>
|
||||
|
||||
${detailSkill ? renderSkillDetail(detailSkill, props) : nothing}
|
||||
@@ -267,8 +271,9 @@ function renderSkillDetail(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
${renderSkillStatusChips({ skill, showBundledBadge })}
|
||||
</div>
|
||||
|
||||
${missing.length > 0
|
||||
? html`
|
||||
${
|
||||
missing.length > 0
|
||||
? html`
|
||||
<div
|
||||
class="callout"
|
||||
style="border-color: var(--warn-subtle); background: var(--warn-subtle); color: var(--warn);"
|
||||
@@ -277,12 +282,15 @@ function renderSkillDetail(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
<div>${missing.join(", ")}</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${reasons.length > 0
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
reasons.length > 0
|
||||
? html`
|
||||
<div class="muted" style="font-size: 13px;">Reason: ${reasons.join(", ")}</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<label class="skill-toggle-wrap">
|
||||
@@ -297,24 +305,29 @@ function renderSkillDetail(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
<span style="font-size: 13px; font-weight: 500;">
|
||||
${skill.disabled ? "Disabled" : "Enabled"}
|
||||
</span>
|
||||
${canInstall
|
||||
? html`<button
|
||||
${
|
||||
canInstall
|
||||
? html`<button
|
||||
class="btn"
|
||||
?disabled=${busy}
|
||||
@click=${() => props.onInstall(skill.skillKey, skill.name, skill.install[0].id)}
|
||||
>
|
||||
${busy ? "Installing\u2026" : skill.install[0].label}
|
||||
</button>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
|
||||
${message
|
||||
? html`<div class="callout ${message.kind === "error" ? "danger" : "success"}">
|
||||
${
|
||||
message
|
||||
? html`<div class="callout ${message.kind === "error" ? "danger" : "success"}">
|
||||
${message.message}
|
||||
</div>`
|
||||
: nothing}
|
||||
${skill.primaryEnv
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
skill.primaryEnv
|
||||
? html`
|
||||
<div style="display: grid; gap: 8px;">
|
||||
<div class="field">
|
||||
<span
|
||||
@@ -350,7 +363,8 @@ function renderSkillDetail(skill: SkillStatusEntry, props: SkillsProps) {
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div
|
||||
style="border-top: 1px solid var(--border); padding-top: 12px; display: grid; gap: 6px; font-size: 12px; color: var(--muted);"
|
||||
|
||||
@@ -114,11 +114,13 @@ function renderSessionSummary(
|
||||
})) ?? [];
|
||||
|
||||
return html`
|
||||
${badges.length > 0
|
||||
? html`<div class="usage-badges">
|
||||
${
|
||||
badges.length > 0
|
||||
? html`<div class="usage-badges">
|
||||
${badges.map((b) => html`<span class="usage-badge">${b}</span>`)}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<div class="session-summary-grid">
|
||||
<div class="stat session-summary-card">
|
||||
<div class="session-summary-title">${t("usage.overview.messages")}</div>
|
||||
@@ -143,8 +145,10 @@ function renderSessionSummary(
|
||||
<div class="stat session-summary-card">
|
||||
<div class="session-summary-title">${t("usage.details.duration")}</div>
|
||||
<div class="stat-value session-summary-value">
|
||||
${formatDurationCompact(usage.durationMs, { spaced: true }) ??
|
||||
t("usage.common.emptyValue")}
|
||||
${
|
||||
formatDurationCompact(usage.durationMs, { spaced: true }) ??
|
||||
t("usage.common.emptyValue")
|
||||
}
|
||||
</div>
|
||||
<div class="session-summary-meta">
|
||||
${formatTs(usage.firstActivity)} → ${formatTs(usage.lastActivity)}
|
||||
@@ -271,14 +275,17 @@ function renderSessionDetailPanel(
|
||||
<div class="session-detail-header-left">
|
||||
<div class="session-detail-title">
|
||||
${displayLabel}
|
||||
${cursorIndicator
|
||||
? html`<span class="session-detail-indicator">${cursorIndicator}</span>`
|
||||
: nothing}
|
||||
${
|
||||
cursorIndicator
|
||||
? html`<span class="session-detail-indicator">${cursorIndicator}</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-detail-stats">
|
||||
${usage
|
||||
? html`
|
||||
${
|
||||
usage
|
||||
? html`
|
||||
<span
|
||||
><strong>${formatTokens(headerStats.totalTokens)}</strong> ${t(
|
||||
"usage.metrics.tokens",
|
||||
@@ -286,7 +293,8 @@ function renderSessionDetailPanel(
|
||||
>
|
||||
<span><strong>${formatCost(headerStats.totalCost)}</strong>${cursorIndicator}</span>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
class="btn btn--sm btn--ghost"
|
||||
@@ -480,8 +488,9 @@ function renderTimeSeriesCompact(
|
||||
<div class="timeseries-header-row">
|
||||
<div class="card-title usage-section-title">${t("usage.details.usageOverTime")}</div>
|
||||
<div class="timeseries-controls">
|
||||
${hasSelection
|
||||
? html`
|
||||
${
|
||||
hasSelection
|
||||
? html`
|
||||
<div class="chart-toggle small">
|
||||
<button
|
||||
class="btn btn--sm toggle-btn active"
|
||||
@@ -491,7 +500,8 @@ function renderTimeSeriesCompact(
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<div class="chart-toggle small">
|
||||
<button
|
||||
class="btn btn--sm toggle-btn ${!isCumulative ? "active" : ""}"
|
||||
@@ -506,8 +516,9 @@ function renderTimeSeriesCompact(
|
||||
${t("usage.details.cumulative")}
|
||||
</button>
|
||||
</div>
|
||||
${!isCumulative
|
||||
? html`
|
||||
${
|
||||
!isCumulative
|
||||
? html`
|
||||
<div class="chart-toggle small">
|
||||
<button
|
||||
class="btn btn--sm toggle-btn ${breakdownMode === "total" ? "active" : ""}"
|
||||
@@ -523,7 +534,8 @@ function renderTimeSeriesCompact(
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeseries-chart-wrapper">
|
||||
@@ -562,12 +574,14 @@ function renderTimeSeriesCompact(
|
||||
0
|
||||
</text>
|
||||
<!-- X axis labels (first and last) -->
|
||||
${points.length > 0
|
||||
? svg`
|
||||
${
|
||||
points.length > 0
|
||||
? svg`
|
||||
<text x="${padding.left}" y="${padding.top + chartHeight + 10}" text-anchor="start" class="ts-axis-label">${new Date(points[0].timestamp).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}</text>
|
||||
<text x="${width - padding.right}" y="${padding.top + chartHeight + 10}" text-anchor="end" class="ts-axis-label">${new Date(points[points.length - 1].timestamp).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}</text>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<!-- Bars -->
|
||||
${points.map((p, i) => {
|
||||
const val = barTotals[i];
|
||||
@@ -721,8 +735,9 @@ function renderTimeSeriesCompact(
|
||||
})()}
|
||||
</div>
|
||||
<div class="timeseries-summary">
|
||||
${hasSelection
|
||||
? html`
|
||||
${
|
||||
hasSelection
|
||||
? html`
|
||||
<span class="timeseries-summary__range">
|
||||
${t("usage.details.turnRange", {
|
||||
start: String(rangeStartIdx + 1),
|
||||
@@ -744,11 +759,13 @@ function renderTimeSeriesCompact(
|
||||
)}
|
||||
· ${formatCost(filteredPoints.reduce((s, p) => s + (p.cost || 0), 0))}
|
||||
`
|
||||
: html`${points.length} ${t("usage.overview.messagesAbbrev")} · ${formatTokens(cumTokens)}
|
||||
· ${formatCost(cumCost)}`}
|
||||
: html`${points.length} ${t("usage.overview.messagesAbbrev")} · ${formatTokens(cumTokens)}
|
||||
· ${formatCost(cumCost)}`
|
||||
}
|
||||
</div>
|
||||
${breakdownByType
|
||||
? html`
|
||||
${
|
||||
breakdownByType
|
||||
? html`
|
||||
<div class="timeseries-breakdown">
|
||||
<div class="card-title usage-section-title">${t("usage.breakdown.tokensByType")}</div>
|
||||
<div class="cost-breakdown-bar cost-breakdown-bar--compact">
|
||||
@@ -792,7 +809,8 @@ function renderTimeSeriesCompact(
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -851,11 +869,13 @@ function renderContextPanel(
|
||||
<div class="card-title usage-section-title">
|
||||
${t("usage.details.systemPromptBreakdown")}
|
||||
</div>
|
||||
${hasMore
|
||||
? html`<button class="btn btn--sm" @click=${onToggleExpanded}>
|
||||
${
|
||||
hasMore
|
||||
? html`<button class="btn btn--sm" @click=${onToggleExpanded}>
|
||||
${showAll ? t("usage.details.collapse") : t("usage.details.expandAll")}
|
||||
</button>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<p class="context-weight-desc">${contextPct || t("usage.details.baseContextPerMessage")}</p>
|
||||
<div class="context-stacked-bar">
|
||||
@@ -902,10 +922,11 @@ function renderContextPanel(
|
||||
${t("usage.breakdown.total")}: ~${formatTokens(totalContextTokens)}
|
||||
</div>
|
||||
<div class="context-breakdown-grid">
|
||||
${skillsList.length > 0
|
||||
? (() => {
|
||||
const more = skillsList.length - skillsTop.length;
|
||||
return html`
|
||||
${
|
||||
skillsList.length > 0
|
||||
? (() => {
|
||||
const more = skillsList.length - skillsTop.length;
|
||||
return html`
|
||||
<div class="context-breakdown-card">
|
||||
<div class="context-breakdown-title">
|
||||
${t("usage.details.skills")} (${skillsList.length})
|
||||
@@ -920,21 +941,25 @@ function renderContextPanel(
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
${more > 0
|
||||
? html`
|
||||
${
|
||||
more > 0
|
||||
? html`
|
||||
<div class="context-breakdown-more">
|
||||
${t("usage.sessions.more", { count: String(more) })}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
})()
|
||||
: nothing}
|
||||
${toolsList.length > 0
|
||||
? (() => {
|
||||
const more = toolsList.length - toolsTop.length;
|
||||
return html`
|
||||
})()
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
toolsList.length > 0
|
||||
? (() => {
|
||||
const more = toolsList.length - toolsTop.length;
|
||||
return html`
|
||||
<div class="context-breakdown-card">
|
||||
<div class="context-breakdown-title">
|
||||
${t("usage.details.tools")} (${toolsList.length})
|
||||
@@ -951,21 +976,25 @@ function renderContextPanel(
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
${more > 0
|
||||
? html`
|
||||
${
|
||||
more > 0
|
||||
? html`
|
||||
<div class="context-breakdown-more">
|
||||
${t("usage.sessions.more", { count: String(more) })}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
})()
|
||||
: nothing}
|
||||
${filesList.length > 0
|
||||
? (() => {
|
||||
const more = filesList.length - filesTop.length;
|
||||
return html`
|
||||
})()
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
filesList.length > 0
|
||||
? (() => {
|
||||
const more = filesList.length - filesTop.length;
|
||||
return html`
|
||||
<div class="context-breakdown-card">
|
||||
<div class="context-breakdown-title">
|
||||
${t("usage.details.files")} (${filesList.length})
|
||||
@@ -982,17 +1011,20 @@ function renderContextPanel(
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
${more > 0
|
||||
? html`
|
||||
${
|
||||
more > 0
|
||||
? html`
|
||||
<div class="context-breakdown-more">
|
||||
${t("usage.sessions.more", { count: String(more) })}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
})()
|
||||
: nothing}
|
||||
})()
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1177,8 +1209,9 @@ function renderSessionLogsCompact(
|
||||
${log.tokens ? html`<span>${formatTokens(log.tokens)}</span>` : nothing}
|
||||
</div>
|
||||
<div class="session-log-content">${cleanContent}</div>
|
||||
${toolInfo.tools.length > 0
|
||||
? html`
|
||||
${
|
||||
toolInfo.tools.length > 0
|
||||
? html`
|
||||
<details class="session-log-tools" ?open=${expandedAll}>
|
||||
<summary>${toolInfo.summary}</summary>
|
||||
<div class="session-log-tools-list">
|
||||
@@ -1190,17 +1223,20 @@ function renderSessionLogsCompact(
|
||||
</div>
|
||||
</details>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
${filteredEntries.length === 0
|
||||
? html`
|
||||
${
|
||||
filteredEntries.length === 0
|
||||
? html`
|
||||
<div class="usage-empty-block usage-empty-block--compact">
|
||||
${t("usage.details.noMessagesMatch")}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -93,8 +93,9 @@ function renderFilterChips(
|
||||
|
||||
return html`
|
||||
<div class="active-filters">
|
||||
${selectedDays.length > 0
|
||||
? html`
|
||||
${
|
||||
selectedDays.length > 0
|
||||
? html`
|
||||
<div class="filter-chip">
|
||||
<span class="filter-chip-label">${t("usage.filters.days")}: ${daysLabel}</span>
|
||||
<button
|
||||
@@ -107,9 +108,11 @@ function renderFilterChips(
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${selectedHours.length > 0
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
selectedHours.length > 0
|
||||
? html`
|
||||
<div class="filter-chip">
|
||||
<span class="filter-chip-label">${t("usage.filters.hours")}: ${hoursLabel}</span>
|
||||
<button
|
||||
@@ -122,9 +125,11 @@ function renderFilterChips(
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${selectedSessions.length > 0
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
selectedSessions.length > 0
|
||||
? html`
|
||||
<div class="filter-chip" title="${sessionsFullName}">
|
||||
<span class="filter-chip-label">${t("usage.filters.session")}: ${sessionsLabel}</span>
|
||||
<button
|
||||
@@ -137,14 +142,17 @@ function renderFilterChips(
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${(selectedDays.length > 0 || selectedHours.length > 0) && selectedSessions.length > 0
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
(selectedDays.length > 0 || selectedHours.length > 0) && selectedSessions.length > 0
|
||||
? html`
|
||||
<button class="btn btn--sm" @click=${onClearFilters}>
|
||||
${t("usage.filters.clearAll")}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -259,8 +267,9 @@ function renderDailyChartCompact(
|
||||
class="daily-bar-wrapper ${isSelected ? "selected" : ""}"
|
||||
@click=${(e: MouseEvent) => onSelectDay(d.date, e.shiftKey)}
|
||||
>
|
||||
${dailyChartMode === "by-type"
|
||||
? html`
|
||||
${
|
||||
dailyChartMode === "by-type"
|
||||
? html`
|
||||
<div
|
||||
class="daily-bar daily-bar--stacked"
|
||||
style="height: ${heightPx.toFixed(0)}px;"
|
||||
@@ -278,16 +287,19 @@ function renderDailyChartCompact(
|
||||
})()}
|
||||
</div>
|
||||
`
|
||||
: html` <div class="daily-bar" style="height: ${heightPx.toFixed(0)}px"></div> `}
|
||||
: html` <div class="daily-bar" style="height: ${heightPx.toFixed(0)}px"></div> `
|
||||
}
|
||||
${showTotals ? html`<div class="daily-bar-total">${totalLabel}</div>` : nothing}
|
||||
<div class="${labelClass}">${shortLabel}</div>
|
||||
<div class="daily-bar-tooltip">
|
||||
<strong>${formatFullDate(d.date)}</strong><br />
|
||||
${formatTokens(d.totalTokens)} ${t("usage.metrics.tokens").toLowerCase()}<br />
|
||||
${formatCost(d.totalCost)}
|
||||
${breakdownLines.length
|
||||
? html`${breakdownLines.map((line) => html`<div>${line}</div>`)}`
|
||||
: nothing}
|
||||
${
|
||||
breakdownLines.length
|
||||
? html`${breakdownLines.map((line) => html`<div>${line}</div>`)}`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -318,34 +330,34 @@ function renderCostBreakdownCompact(totals: UsageTotals, mode: "tokens" | "cost"
|
||||
<div
|
||||
class="cost-segment output"
|
||||
style="width: ${(isTokenMode ? tokenPcts.output : breakdown.output.pct).toFixed(1)}%"
|
||||
title="${t("usage.breakdown.output")}: ${isTokenMode
|
||||
? formatTokens(totals.output)
|
||||
: formatCost(breakdown.output.cost)}"
|
||||
title="${t("usage.breakdown.output")}: ${
|
||||
isTokenMode ? formatTokens(totals.output) : formatCost(breakdown.output.cost)
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="cost-segment input"
|
||||
style="width: ${(isTokenMode ? tokenPcts.input : breakdown.input.pct).toFixed(1)}%"
|
||||
title="${t("usage.breakdown.input")}: ${isTokenMode
|
||||
? formatTokens(totals.input)
|
||||
: formatCost(breakdown.input.cost)}"
|
||||
title="${t("usage.breakdown.input")}: ${
|
||||
isTokenMode ? formatTokens(totals.input) : formatCost(breakdown.input.cost)
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="cost-segment cache-write"
|
||||
style="width: ${(isTokenMode ? tokenPcts.cacheWrite : breakdown.cacheWrite.pct).toFixed(
|
||||
1,
|
||||
)}%"
|
||||
title="${t("usage.breakdown.cacheWrite")}: ${isTokenMode
|
||||
? formatTokens(totals.cacheWrite)
|
||||
: formatCost(breakdown.cacheWrite.cost)}"
|
||||
title="${t("usage.breakdown.cacheWrite")}: ${
|
||||
isTokenMode ? formatTokens(totals.cacheWrite) : formatCost(breakdown.cacheWrite.cost)
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
class="cost-segment cache-read"
|
||||
style="width: ${(isTokenMode ? tokenPcts.cacheRead : breakdown.cacheRead.pct).toFixed(
|
||||
1,
|
||||
)}%"
|
||||
title="${t("usage.breakdown.cacheRead")}: ${isTokenMode
|
||||
? formatTokens(totals.cacheRead)
|
||||
: formatCost(breakdown.cacheRead.cost)}"
|
||||
title="${t("usage.breakdown.cacheRead")}: ${
|
||||
isTokenMode ? formatTokens(totals.cacheRead) : formatCost(breakdown.cacheRead.cost)
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="cost-breakdown-legend">
|
||||
@@ -359,15 +371,15 @@ function renderCostBreakdownCompact(totals: UsageTotals, mode: "tokens" | "cost"
|
||||
>
|
||||
<span class="legend-item"
|
||||
><span class="legend-dot cache-write"></span>${t("usage.breakdown.cacheWrite")}
|
||||
${isTokenMode
|
||||
? formatTokens(totals.cacheWrite)
|
||||
: formatCost(breakdown.cacheWrite.cost)}</span
|
||||
${
|
||||
isTokenMode ? formatTokens(totals.cacheWrite) : formatCost(breakdown.cacheWrite.cost)
|
||||
}</span
|
||||
>
|
||||
<span class="legend-item"
|
||||
><span class="legend-dot cache-read"></span>${t("usage.breakdown.cacheRead")}
|
||||
${isTokenMode
|
||||
? formatTokens(totals.cacheRead)
|
||||
: formatCost(breakdown.cacheRead.cost)}</span
|
||||
${
|
||||
isTokenMode ? formatTokens(totals.cacheRead) : formatCost(breakdown.cacheRead.cost)
|
||||
}</span
|
||||
>
|
||||
</div>
|
||||
<div class="cost-breakdown-total">
|
||||
@@ -386,9 +398,10 @@ function renderInsightList(
|
||||
return html`
|
||||
<div class="usage-insight-card">
|
||||
<div class="usage-insight-title">${title}</div>
|
||||
${items.length === 0
|
||||
? html`<div class="muted">${emptyLabel}</div>`
|
||||
: html`
|
||||
${
|
||||
items.length === 0
|
||||
? html`<div class="muted">${emptyLabel}</div>`
|
||||
: html`
|
||||
<div class="usage-list">
|
||||
${items.map(
|
||||
(item) => html`
|
||||
@@ -402,7 +415,8 @@ function renderInsightList(
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -421,9 +435,10 @@ function renderPeakErrorList(
|
||||
return html`
|
||||
<div class=${cardClass}>
|
||||
<div class="usage-insight-title">${title}</div>
|
||||
${items.length === 0
|
||||
? html`<div class="muted">${emptyLabel}</div>`
|
||||
: html`
|
||||
${
|
||||
items.length === 0
|
||||
? html`<div class="muted">${emptyLabel}</div>`
|
||||
: html`
|
||||
<div class=${listClass}>
|
||||
${items.map(
|
||||
(item) => html`
|
||||
@@ -435,7 +450,8 @@ function renderPeakErrorList(
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -800,9 +816,11 @@ function renderSessionsCard(
|
||||
>
|
||||
<div class="session-bar-label">
|
||||
<div class="session-bar-title">${displayLabel}</div>
|
||||
${meta.length > 0
|
||||
? html`<div class="session-bar-meta">${meta.join(" · ")}</div>`
|
||||
: nothing}
|
||||
${
|
||||
meta.length > 0
|
||||
? html`<div class="session-bar-meta">${meta.join(" · ")}</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="session-bar-actions">
|
||||
<button
|
||||
@@ -837,9 +855,11 @@ function renderSessionsCard(
|
||||
<div class="card-title">${t("usage.sessions.title")}</div>
|
||||
<div class="sessions-card-count">
|
||||
${t("usage.sessions.shown", { count: String(sessions.length) })}
|
||||
${totalSessions !== sessions.length
|
||||
? ` · ${t("usage.sessions.total", { count: String(totalSessions) })}`
|
||||
: ""}
|
||||
${
|
||||
totalSessions !== sessions.length
|
||||
? ` · ${t("usage.sessions.total", { count: String(totalSessions) })}`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sessions-card-meta">
|
||||
@@ -890,46 +910,55 @@ function renderSessionsCard(
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
@click=${() => onSessionSortDirChange(sessionSortDir === "desc" ? "asc" : "desc")}
|
||||
title=${sessionSortDir === "desc"
|
||||
? t("usage.sessions.descending")
|
||||
: t("usage.sessions.ascending")}
|
||||
title=${
|
||||
sessionSortDir === "desc"
|
||||
? t("usage.sessions.descending")
|
||||
: t("usage.sessions.ascending")
|
||||
}
|
||||
>
|
||||
${sessionSortDir === "desc" ? "↓" : "↑"}
|
||||
</button>
|
||||
${selectedCount > 0
|
||||
? html`
|
||||
${
|
||||
selectedCount > 0
|
||||
? html`
|
||||
<button class="btn btn--sm" @click=${onClearSessions}>
|
||||
${t("usage.sessions.clearSelection")}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${sessionsTab === "recent"
|
||||
? recentEntries.length === 0
|
||||
? html` <div class="usage-empty-block">${t("usage.sessions.noRecent")}</div> `
|
||||
: html`
|
||||
${
|
||||
sessionsTab === "recent"
|
||||
? recentEntries.length === 0
|
||||
? html` <div class="usage-empty-block">${t("usage.sessions.noRecent")}</div> `
|
||||
: html`
|
||||
<div class="session-bars session-bars--recent">
|
||||
${recentEntries.map((s) => renderSessionBarRow(s, selectedSet.has(s.key)))}
|
||||
</div>
|
||||
`
|
||||
: sessions.length === 0
|
||||
? html` <div class="usage-empty-block">${t("usage.sessions.noneInRange")}</div> `
|
||||
: html`
|
||||
: sessions.length === 0
|
||||
? html` <div class="usage-empty-block">${t("usage.sessions.noneInRange")}</div> `
|
||||
: html`
|
||||
<div class="session-bars">
|
||||
${sortedWithDir
|
||||
.slice(0, 50)
|
||||
.map((s) => renderSessionBarRow(s, selectedSet.has(s.key)))}
|
||||
${sessions.length > 50
|
||||
? html`
|
||||
${
|
||||
sessions.length > 50
|
||||
? html`
|
||||
<div class="usage-more-sessions">
|
||||
${t("usage.sessions.more", { count: String(sessions.length - 50) })}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`}
|
||||
${selectedCount > 1
|
||||
? html`
|
||||
`
|
||||
}
|
||||
${
|
||||
selectedCount > 1
|
||||
? html`
|
||||
<div class="sessions-selected-group">
|
||||
<div class="sessions-card-count">
|
||||
${t("usage.sessions.selected", { count: String(selectedCount) })}
|
||||
@@ -939,7 +968,8 @@ function renderSessionsCard(
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -376,9 +376,11 @@ export function renderUsage(props: UsageProps) {
|
||||
>
|
||||
<summary>
|
||||
<span>${label}</span>
|
||||
${selectedCount > 0
|
||||
? html`<span class="usage-filter-badge">${selectedCount}</span>`
|
||||
: html` <span class="usage-filter-badge">${t("usage.filters.all")}</span> `}
|
||||
${
|
||||
selectedCount > 0
|
||||
? html`<span class="usage-filter-badge">${selectedCount}</span>`
|
||||
: html` <span class="usage-filter-badge">${t("usage.filters.all")}</span> `
|
||||
}
|
||||
</summary>
|
||||
<div class="usage-filter-popover">
|
||||
<div class="usage-filter-actions">
|
||||
@@ -447,16 +449,21 @@ export function renderUsage(props: UsageProps) {
|
||||
<div class="usage-header-row">
|
||||
<div class="usage-header-title">
|
||||
<div class="card-title usage-section-title">${t("usage.filters.title")}</div>
|
||||
${data.loading
|
||||
? html`<span class="usage-refresh-indicator">${t("usage.loading.badge")}</span>`
|
||||
: nothing}
|
||||
${isEmpty
|
||||
? html`<span class="usage-query-hint">${t("usage.empty.hint")}</span>`
|
||||
: nothing}
|
||||
${
|
||||
data.loading
|
||||
? html`<span class="usage-refresh-indicator">${t("usage.loading.badge")}</span>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isEmpty
|
||||
? html`<span class="usage-query-hint">${t("usage.empty.hint")}</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="usage-header-metrics">
|
||||
${displayTotals
|
||||
? html`
|
||||
${
|
||||
displayTotals
|
||||
? html`
|
||||
<span class="usage-metric-badge">
|
||||
<strong>${formatTokens(displayTotals.totalTokens)}</strong>
|
||||
${t("usage.metrics.tokens")}
|
||||
@@ -467,12 +474,15 @@ export function renderUsage(props: UsageProps) {
|
||||
</span>
|
||||
<span class="usage-metric-badge">
|
||||
<strong>${displaySessionCount}</strong>
|
||||
${displaySessionCount === 1
|
||||
? t("usage.metrics.session")
|
||||
: t("usage.metrics.sessions")}
|
||||
${
|
||||
displaySessionCount === 1
|
||||
? t("usage.metrics.session")
|
||||
: t("usage.metrics.sessions")
|
||||
}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<button
|
||||
class="btn btn--sm usage-pin-btn ${display.headerPinned ? "active" : ""}"
|
||||
title=${display.headerPinned ? t("usage.filters.unpin") : t("usage.filters.pin")}
|
||||
@@ -654,20 +664,24 @@ export function renderUsage(props: UsageProps) {
|
||||
>
|
||||
${t("usage.query.apply")}
|
||||
</button>
|
||||
${hasDraftQuery || hasQuery
|
||||
? html`
|
||||
${
|
||||
hasDraftQuery || hasQuery
|
||||
? html`
|
||||
<button class="btn btn--sm" @click=${filterActions.onClearQuery}>
|
||||
${t("usage.filters.clear")}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
<span class="usage-query-hint">
|
||||
${hasQuery
|
||||
? t("usage.query.matching", {
|
||||
shown: String(filteredSessions.length),
|
||||
total: String(totalSessions),
|
||||
})
|
||||
: t("usage.query.inRange", { total: String(totalSessions) })}
|
||||
${
|
||||
hasQuery
|
||||
? t("usage.query.matching", {
|
||||
shown: String(filteredSessions.length),
|
||||
total: String(totalSessions),
|
||||
})
|
||||
: t("usage.query.inRange", { total: String(totalSessions) })
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -679,8 +693,9 @@ export function renderUsage(props: UsageProps) {
|
||||
${renderFilterSelect("tool", t("usage.filters.tool"), toolOptions)}
|
||||
<span class="usage-query-hint">${t("usage.query.tip")}</span>
|
||||
</div>
|
||||
${queryTerms.length > 0
|
||||
? html`
|
||||
${
|
||||
queryTerms.length > 0
|
||||
? html`
|
||||
<div class="usage-query-chips">
|
||||
${queryTerms.map((term) => {
|
||||
const label = term.raw;
|
||||
@@ -701,9 +716,11 @@ export function renderUsage(props: UsageProps) {
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${querySuggestions.length > 0
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
querySuggestions.length > 0
|
||||
? html`
|
||||
<div class="usage-query-suggestions">
|
||||
${querySuggestions.map(
|
||||
(suggestion) => html`
|
||||
@@ -720,29 +737,35 @@ export function renderUsage(props: UsageProps) {
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${queryWarnings.length > 0
|
||||
? html`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
queryWarnings.length > 0
|
||||
? html`
|
||||
<div class="callout warning usage-callout usage-callout--tight">
|
||||
${queryWarnings.join(" · ")}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
|
||||
${data.error
|
||||
? html`<div class="callout danger usage-callout">${data.error}</div>`
|
||||
: nothing}
|
||||
${data.sessionsLimitReached
|
||||
? html`
|
||||
${
|
||||
data.error ? html`<div class="callout danger usage-callout">${data.error}</div>` : nothing
|
||||
}
|
||||
${
|
||||
data.sessionsLimitReached
|
||||
? html`
|
||||
<div class="callout warning usage-callout">${t("usage.sessions.limitReached")}</div>
|
||||
`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</section>
|
||||
|
||||
${isEmpty
|
||||
? renderUsageEmptyState(filterActions.onRefresh)
|
||||
: html`
|
||||
${
|
||||
isEmpty
|
||||
? renderUsageEmptyState(filterActions.onRefresh)
|
||||
: html`
|
||||
${renderUsageInsights(
|
||||
displayTotals,
|
||||
activeAggregates,
|
||||
@@ -770,9 +793,11 @@ export function renderUsage(props: UsageProps) {
|
||||
displayActions.onDailyChartModeChange,
|
||||
filterActions.onSelectDay,
|
||||
)}
|
||||
${displayTotals
|
||||
? renderCostBreakdownCompact(displayTotals, display.chartMode)
|
||||
: nothing}
|
||||
${
|
||||
displayTotals
|
||||
? renderCostBreakdownCompact(displayTotals, display.chartMode)
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${renderSessionsCard(
|
||||
filteredSessions,
|
||||
@@ -792,8 +817,9 @@ export function renderUsage(props: UsageProps) {
|
||||
filterActions.onClearSessions,
|
||||
)}
|
||||
</div>
|
||||
${primarySelectedEntry
|
||||
? html`<div class="usage-grid-column">
|
||||
${
|
||||
primarySelectedEntry
|
||||
? html`<div class="usage-grid-column">
|
||||
${renderSessionDetailPanel(
|
||||
primarySelectedEntry,
|
||||
detail.timeSeries,
|
||||
@@ -823,9 +849,11 @@ export function renderUsage(props: UsageProps) {
|
||||
filterActions.onClearSessions,
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user