|
|
|
|
Quelle agents-panels-status-files.ts
Sprache: JAVA
|
|
Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
import { applyPreviewTheme } from "@create-markdown/preview";
import DOMPurify from "dompurify";
import { html, nothing } from "lit";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { marked } from "marked";
import { t } from "../../i18n/index.ts";
import { formatRelativeTimestamp } from "../format.ts";
import { icons } from "../icons.ts";
import {
formatCronPayload,
formatCronSchedule,
formatCronState,
formatNextRun,
} from "../presenter.ts";
import type {
AgentsFilesListResult,
ChannelAccountSnapshot,
ChannelsStatusSnapshot,
CronJob,
CronStatus,
} from "../types.ts";
import { type AgentContext } from "./agents-utils.ts";
import type { AgentsPanel } from "./agents.types.ts";
import { resolveChannelExtras as resolveChannelExtrasFromConfig } from "./channel-confi g-extras.ts";
function renderAgentContextCard(
context: AgentContext,
subtitle: string,
onSelectPanel: (panel: AgentsPanel) => void,
) {
return html`
<section class="card">
<div class="card-title">Agent Context</div>
<div class="card-sub">${subtitle}</div>
<div class="agents-overview-grid" style="margin-top: 16px;">
<div class="agent-kv">
<div class="label">Workspace</div>
<div>
<button
type="button"
class="workspace-link mono"
@click=${() => onSelectPanel("files")}
title="Open Files tab"
>
${context.workspace}
</button>
</div>
</div>
<div class="agent-kv">
<div class="label">Primary Model</div>
<div class="mono">${context.model}</div>
</div>
<div class="agent-kv">
<div class="label">Identity Name</div>
<div>${context.identityName}</div>
</div>
<div class="agent-kv">
<div class="label">Identity Avatar</div>
<div>${context.identityAvatar}</div>
</div>
<div class="agent-kv">
<div class="label">Skills Filter</div>
<div>${context.skillsLabel}</div>
</div>
<div class="agent-kv">
<div class="label">Default</div>
<div>${context.isDefault ? "yes" : "no"}</div>
</div>
</div>
</section>
`;
}
type ChannelSummaryEntry = {
id: string;
label: string;
accounts: ChannelAccountSnapshot[];
};
function resolveChannelLabel(snapshot: ChannelsStatusSnapshot, id: string) {
const meta = snapshot.channelMeta?.find((entry) => entry.id === id);
if (meta?.label) {
return meta.label;
}
return snapshot.channelLabels?.[id] ?? id;
}
function resolveChannelEntries(snapshot: ChannelsStatusSnapshot | null): ChannelSummaryEntry[] {
if (!snapshot) {
return [];
}
const ids = new Set<string>();
for (const id of snapshot.channelOrder ?? []) {
ids.add(id);
}
for (const entry of snapshot.channelMeta ?? []) {
ids.add(entry.id);
}
for (const id of Object.keys(snapshot.channelAccounts ?? {})) {
ids.add(id);
}
const ordered: string[] = [];
const seed = snapshot.channelOrder?.length ? snapshot.channelOrder : Array.from(ids);
for (const id of seed) {
if (!ids.has(id)) {
continue;
}
ordered.push(id);
ids.delete(id);
}
for (const id of ids) {
ordered.push(id);
}
return ordered.map((id) => ({
id,
label: resolveChannelLabel(snapshot, id),
accounts: snapshot.channelAccounts?.[id] ?? [],
}));
}
const CHANNEL_EXTRA_FIELDS = ["groupPolicy", "streamMode", "dmPolicy"] as const;
function summarizeChannelAccounts(accounts: ChannelAccountSnapshot[]) {
let connected = 0;
let configured = 0;
let enabled = 0;
for (const account of accounts) {
const probeOk =
account.probe && typeof account.probe === "object" && "ok" in account.probe
? Boolean((account.probe as { ok?: unknown }).ok)
: false;
const isConnected = account.connected === true || account.running === true || probeOk;
if (isConnected) {
connected += 1;
}
if (account.configured) {
configured += 1;
}
if (account.enabled) {
enabled += 1;
}
}
return {
total: accounts.length,
connected,
configured,
enabled,
};
}
export function renderAgentChannels(params: {
context: AgentContext;
configForm: Record<string, unknown> | null;
snapshot: ChannelsStatusSnapshot | null;
loading: boolean;
error: string | null;
lastSuccess: number | null;
onRefresh: () => void;
onSelectPanel: (panel: AgentsPanel) => void;
}) {
const entries = resolveChannelEntries(params.snapshot);
const lastSuccessLabel = params.lastSuccess
? formatRelativeTimestamp(params.lastSuccess)
: "never";
return html`
<section class="grid grid-cols-2">
${renderAgentContextCard(
params.context,
"Workspace, identity, and model configuration.",
params.onSelectPanel,
)}
<section class="card">
<div class="row" style="justify-content: space-between;">
<div>
<div class="card-title">Channels</div>
<div class="card-sub">Gateway-wide channel status snapshot.</div>
</div>
<button class="btn btn--sm" ?disabled=${params.loading} @click=${params.onRefresh}>
${params.loading ? t("common.refreshing") : t("common.refresh")}
</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`
<div class="list" style="margin-top: 16px;">
${entries.map((entry) => {
const summary = summarizeChannelAccounts(entry.accounts);
const status = summary.total
? `${summary.connected}/${summary.total} connected`
: "no accounts";
const configLabel = summary.configured
? `${summary.configured} configured`
: "not configured";
const enabled = summary.total ? `${summary.enabled} enabled` : "disabled";
const extras = resolveChannelExtrasFromConfig({
configForm: params.configForm,
channelId: entry.id,
fields: CHANNEL_EXTRA_FIELDS,
});
return html`
<div class="list-item">
<div class="list-main">
<div class="list-title">${entry.label}</div>
<div class="list-sub mono">${entry.id}</div>
</div>
<div class="list-meta">
<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}
</div>
</div>
`;
})}
</div>
`}
</section>
</section>
`;
}
export function renderAgentCron(params: {
context: AgentContext;
agentId: string;
jobs: CronJob[];
status: CronStatus | null;
loading: boolean;
error: string | null;
onRefresh: () => void;
onRunNow: (jobId: string) => void;
onSelectPanel: (panel: AgentsPanel) => void;
}) {
const jobs = params.jobs.filter((job) => job.agentId === params.agentId);
return html`
<section class="grid grid-cols-2">
${renderAgentContextCard(
params.context,
"Workspace and scheduling targets.",
params.onSelectPanel,
)}
<section class="card">
<div class="row" style="justify-content: space-between;">
<div>
<div class="card-title">Scheduler</div>
<div class="card-sub">Gateway cron status.</div>
</div>
<button class="btn btn--sm" ?disabled=${params.loading} @click=${params.onRefresh}>
${params.loading ? t("common.refreshing") : t("common.refresh")}
</button>
</div>
<div class="stat-grid" style="margin-top: 16px;">
<div class="stat">
<div class="stat-label">${t("common.enabled")}</div>
<div class="stat-value">
${params.status
? params.status.enabled
? t("common.yes")
: t("common.no")
: t("common.na")}
</div>
</div>
<div class="stat">
<div class="stat-label">Jobs</div>
<div class="stat-value">${params.status?.jobs ?? t("common.na")}</div>
</div>
<div class="stat">
<div class="stat-label">Next wake</div>
<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}
</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`
<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}
<div class="chip-row" style="margin-top: 6px;">
<span class="chip">${formatCronSchedule(job)}</span>
<span class="chip ${job.enabled ? "chip-ok" : "chip-warn"}">
${job.enabled ? "enabled" : "disabled"}
</span>
<span class="chip">${job.sessionTarget}</span>
</div>
</div>
<div class="list-meta">
<div class="mono">${formatCronState(job)}</div>
<div class="muted">${formatCronPayload(job)}</div>
<button
class="btn btn--sm"
style="margin-top: 6px;"
?disabled=${!job.enabled}
@click=${() => params.onRunNow(job.id)}
>
Run Now
</button>
</div>
</div>
`,
)}
</div>
`}
</section>
`;
}
export function renderAgentFiles(params: {
agentId: string;
agentFilesList: AgentsFilesListResult | null;
agentFilesLoading: boolean;
agentFilesError: string | null;
agentFileActive: string | null;
agentFileContents: Record<string, string>;
agentFileDrafts: Record<string, string>;
agentFileSaving: boolean;
onLoadFiles: (agentId: string) => void;
onSelectFile: (name: string) => void;
onFileDraftChange: (name: string, content: string) => void;
onFileReset: (name: string) => void;
onFileSave: (name: string) => void;
}) {
const list = params.agentFilesList?.agentId === params.agentId ? params.agentFilesList : null;
const files = list?.files ?? [];
const active = params.agentFileActive ?? null;
const activeEntry = active ? (files.find((file) => file.name === active) ?? null) : null;
const baseContent = active ? (params.agentFileContents[active] ?? "") : "";
const draft = active ? (params.agentFileDrafts[active] ?? baseContent) : "";
const isDirty = active ? draft !== baseContent : false;
return html`
<section class="card">
<div class="row" style="justify-content: space-between;">
<div>
<div class="card-title">Core Files</div>
<div class="card-sub">Bootstrap persona, identity, and tool guidance.</div>
</div>
<button
class="btn btn--sm"
?disabled=${params.agentFilesLoading}
@click=${() => params.onLoadFiles(params.agentId)}
>
${params.agentFilesLoading ? t("common.loading") : t("common.refresh")}
</button>
</div>
${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;">
${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`
<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"
: ""}"
@click=${() => params.onSelectFile(file.name)}
>
${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`
<div class="agent-file-header" style="margin-top: 14px;">
<div>
<div class="agent-file-sub mono">${activeEntry.path}</div>
</div>
<div class="agent-file-actions">
<button
class="btn btn--sm"
title="Preview rendered markdown"
@click=${(e: Event) => {
const btn = e.currentTarget as HTMLElement;
const dialog = btn.closest(".card")?.querySelector("dialog");
if (dialog) {
dialog.showModal();
}
}}
>
${icons.eye} Preview
</button>
<button
class="btn btn--sm"
?disabled=${!isDirty}
@click=${() => params.onFileReset(activeEntry.name)}
>
Reset
</button>
<button
class="btn btn--sm primary"
?disabled=${params.agentFileSaving || !isDirty}
@click=${() => params.onFileSave(activeEntry.name)}
>
${params.agentFileSaving ? "Saving…" : "Save"}
</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}
<label class="field agent-file-field" style="margin-top: 12px;">
<span>Content</span>
<textarea
class="agent-file-textarea"
.value=${draft}
@input=${(e: Event) =>
params.onFileDraftChange(
activeEntry.name,
(e.target as HTMLTextAreaElement).value,
)}
></textarea>
</label>
<dialog
class="md-preview-dialog"
@click=${(e: Event) => {
const dialog = e.currentTarget as HTMLDialogElement;
if (e.target === dialog) {
dialog.close();
}
}}
@close=${(e: Event) => {
const dialog = e.currentTarget as HTMLElement;
dialog
.querySelector(".md-preview-dialog__panel")
?.classList.remove("fullscreen");
}}
>
<div class="md-preview-dialog__panel">
<div class="md-preview-dialog__header">
<div class="md-preview-dialog__title mono">${activeEntry.name}</div>
<div class="md-preview-dialog__actions">
<button
class="btn btn--sm md-preview-expand-btn"
title="Toggle fullscreen"
@click=${(e: Event) => {
const btn = e.currentTarget as HTMLElement;
const panel = btn.closest(".md-preview-dialog__panel");
if (!panel) {
return;
}
const isFullscreen = panel.classList.toggle("fullscreen");
btn.classList.toggle("is-fullscreen", isFullscreen);
}}
>
<span class="when-normal">${icons.maximize} Expand</span
><span class="when-fullscreen">${icons.minimize} Collapse</span>
</button>
<button
class="btn btn--sm"
title="Edit file"
@click=${(e: Event) => {
(e.currentTarget as HTMLElement).closest("dialog")?.close();
const textarea =
document.querySelector<HTMLElement>(".agent-file-textarea");
textarea?.focus();
}}
>
${icons.edit} Editor
</button>
<button
class="btn btn--sm"
@click=${(e: Event) => {
(e.currentTarget as HTMLElement).closest("dialog")?.close();
}}
>
${icons.x} Close
</button>
</div>
</div>
<div class="md-preview-dialog__body">
${unsafeHTML(
applyPreviewTheme(
marked.parse(draft, { gfm: true, breaks: true }) as string,
{ sanitize: (h: string) => DOMPurify.sanitize(h) },
),
)}
</div>
</div>
</dialog>
`}
`}
</section>
`;
}
¤ Dauer der Verarbeitung: 0.21 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland
|
|
2026-05-26
|
|
|
|
|