feat(ui): add Agents dashboard
parent
c8af8e9555
commit
2a68bcbeb3
|
|
@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
|
|
||||||
|
- Web UI: add Agents dashboard for managing agent files, tools, skills, models, channels, and cron jobs.
|
||||||
- Docs: seed zh-CN translations. (#6619) Thanks @joshp123.
|
- Docs: seed zh-CN translations. (#6619) Thanks @joshp123.
|
||||||
- Docs: expand zh-Hans navigation and fix zh-CN index asset paths. (#7242) Thanks @joshp123.
|
- Docs: expand zh-Hans navigation and fix zh-CN index asset paths. (#7242) Thanks @joshp123.
|
||||||
- Docs: add zh-CN landing notice + AI-translated image. (#7303) Thanks @joshp123.
|
- Docs: add zh-CN landing notice + AI-translated image. (#7303) Thanks @joshp123.
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ type ResolvedAgentConfig = {
|
||||||
workspace?: string;
|
workspace?: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
model?: AgentEntry["model"];
|
model?: AgentEntry["model"];
|
||||||
|
skills?: AgentEntry["skills"];
|
||||||
memorySearch?: AgentEntry["memorySearch"];
|
memorySearch?: AgentEntry["memorySearch"];
|
||||||
humanDelay?: AgentEntry["humanDelay"];
|
humanDelay?: AgentEntry["humanDelay"];
|
||||||
heartbeat?: AgentEntry["heartbeat"];
|
heartbeat?: AgentEntry["heartbeat"];
|
||||||
|
|
@ -112,6 +113,7 @@ export function resolveAgentConfig(
|
||||||
typeof entry.model === "string" || (entry.model && typeof entry.model === "object")
|
typeof entry.model === "string" || (entry.model && typeof entry.model === "object")
|
||||||
? entry.model
|
? entry.model
|
||||||
: undefined,
|
: undefined,
|
||||||
|
skills: Array.isArray(entry.skills) ? entry.skills : undefined,
|
||||||
memorySearch: entry.memorySearch,
|
memorySearch: entry.memorySearch,
|
||||||
humanDelay: entry.humanDelay,
|
humanDelay: entry.humanDelay,
|
||||||
heartbeat: entry.heartbeat,
|
heartbeat: entry.heartbeat,
|
||||||
|
|
@ -123,6 +125,18 @@ export function resolveAgentConfig(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveAgentSkillsFilter(
|
||||||
|
cfg: OpenClawConfig,
|
||||||
|
agentId: string,
|
||||||
|
): string[] | undefined {
|
||||||
|
const raw = resolveAgentConfig(cfg, agentId)?.skills;
|
||||||
|
if (!raw) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const normalized = raw.map((entry) => String(entry).trim()).filter(Boolean);
|
||||||
|
return normalized.length > 0 ? normalized : [];
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {
|
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {
|
||||||
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
const raw = resolveAgentConfig(cfg, agentId)?.model;
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
resolveAgentDir,
|
resolveAgentDir,
|
||||||
resolveAgentWorkspaceDir,
|
resolveAgentWorkspaceDir,
|
||||||
resolveSessionAgentId,
|
resolveSessionAgentId,
|
||||||
|
resolveAgentSkillsFilter,
|
||||||
} from "../../agents/agent-scope.js";
|
} from "../../agents/agent-scope.js";
|
||||||
import { resolveModelRefFromString } from "../../agents/model-selection.js";
|
import { resolveModelRefFromString } from "../../agents/model-selection.js";
|
||||||
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
||||||
|
|
@ -24,6 +25,31 @@ import { initSessionState } from "./session.js";
|
||||||
import { stageSandboxMedia } from "./stage-sandbox-media.js";
|
import { stageSandboxMedia } from "./stage-sandbox-media.js";
|
||||||
import { createTypingController } from "./typing.js";
|
import { createTypingController } from "./typing.js";
|
||||||
|
|
||||||
|
function mergeSkillFilters(channelFilter?: string[], agentFilter?: string[]): string[] | undefined {
|
||||||
|
const normalize = (list?: string[]) => {
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return list.map((entry) => String(entry).trim()).filter(Boolean);
|
||||||
|
};
|
||||||
|
const channel = normalize(channelFilter);
|
||||||
|
const agent = normalize(agentFilter);
|
||||||
|
if (!channel && !agent) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!channel) {
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
if (!agent) {
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
if (channel.length === 0 || agent.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const agentSet = new Set(agent);
|
||||||
|
return channel.filter((name) => agentSet.has(name));
|
||||||
|
}
|
||||||
|
|
||||||
export async function getReplyFromConfig(
|
export async function getReplyFromConfig(
|
||||||
ctx: MsgContext,
|
ctx: MsgContext,
|
||||||
opts?: GetReplyOptions,
|
opts?: GetReplyOptions,
|
||||||
|
|
@ -38,6 +64,12 @@ export async function getReplyFromConfig(
|
||||||
sessionKey: agentSessionKey,
|
sessionKey: agentSessionKey,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
});
|
});
|
||||||
|
const mergedSkillFilter = mergeSkillFilters(
|
||||||
|
opts?.skillFilter,
|
||||||
|
resolveAgentSkillsFilter(cfg, agentId),
|
||||||
|
);
|
||||||
|
const resolvedOpts =
|
||||||
|
mergedSkillFilter !== undefined ? { ...opts, skillFilter: mergedSkillFilter } : opts;
|
||||||
const agentCfg = cfg.agents?.defaults;
|
const agentCfg = cfg.agents?.defaults;
|
||||||
const sessionCfg = cfg.session;
|
const sessionCfg = cfg.session;
|
||||||
const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({
|
const { defaultProvider, defaultModel, aliasIndex } = resolveDefaultModel({
|
||||||
|
|
@ -164,8 +196,8 @@ export async function getReplyFromConfig(
|
||||||
provider,
|
provider,
|
||||||
model,
|
model,
|
||||||
typing,
|
typing,
|
||||||
opts,
|
opts: resolvedOpts,
|
||||||
skillFilter: opts?.skillFilter,
|
skillFilter: mergedSkillFilter,
|
||||||
});
|
});
|
||||||
if (directiveResult.kind === "reply") {
|
if (directiveResult.kind === "reply") {
|
||||||
return directiveResult.reply;
|
return directiveResult.reply;
|
||||||
|
|
@ -216,7 +248,7 @@ export async function getReplyFromConfig(
|
||||||
sessionScope,
|
sessionScope,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
isGroup,
|
isGroup,
|
||||||
opts,
|
opts: resolvedOpts,
|
||||||
typing,
|
typing,
|
||||||
allowTextCommands,
|
allowTextCommands,
|
||||||
inlineStatusRequested,
|
inlineStatusRequested,
|
||||||
|
|
@ -238,7 +270,7 @@ export async function getReplyFromConfig(
|
||||||
contextTokens,
|
contextTokens,
|
||||||
directiveAck,
|
directiveAck,
|
||||||
abortedLastRun,
|
abortedLastRun,
|
||||||
skillFilter: opts?.skillFilter,
|
skillFilter: mergedSkillFilter,
|
||||||
});
|
});
|
||||||
if (inlineActionResult.kind === "reply") {
|
if (inlineActionResult.kind === "reply") {
|
||||||
return inlineActionResult.reply;
|
return inlineActionResult.reply;
|
||||||
|
|
@ -284,7 +316,7 @@ export async function getReplyFromConfig(
|
||||||
perMessageQueueMode,
|
perMessageQueueMode,
|
||||||
perMessageQueueOptions,
|
perMessageQueueOptions,
|
||||||
typing,
|
typing,
|
||||||
opts,
|
opts: resolvedOpts,
|
||||||
defaultProvider,
|
defaultProvider,
|
||||||
defaultModel,
|
defaultModel,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
resolveAgentDir,
|
resolveAgentDir,
|
||||||
resolveAgentModelFallbacksOverride,
|
resolveAgentModelFallbacksOverride,
|
||||||
resolveAgentModelPrimary,
|
resolveAgentModelPrimary,
|
||||||
|
resolveAgentSkillsFilter,
|
||||||
resolveAgentWorkspaceDir,
|
resolveAgentWorkspaceDir,
|
||||||
} from "../agents/agent-scope.js";
|
} from "../agents/agent-scope.js";
|
||||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||||
|
|
@ -187,11 +188,13 @@ export async function agentCommand(
|
||||||
|
|
||||||
const needsSkillsSnapshot = isNewSession || !sessionEntry?.skillsSnapshot;
|
const needsSkillsSnapshot = isNewSession || !sessionEntry?.skillsSnapshot;
|
||||||
const skillsSnapshotVersion = getSkillsSnapshotVersion(workspaceDir);
|
const skillsSnapshotVersion = getSkillsSnapshotVersion(workspaceDir);
|
||||||
|
const skillFilter = resolveAgentSkillsFilter(cfg, sessionAgentId);
|
||||||
const skillsSnapshot = needsSkillsSnapshot
|
const skillsSnapshot = needsSkillsSnapshot
|
||||||
? buildWorkspaceSkillSnapshot(workspaceDir, {
|
? buildWorkspaceSkillSnapshot(workspaceDir, {
|
||||||
config: cfg,
|
config: cfg,
|
||||||
eligibility: { remote: getRemoteSkillEligibility() },
|
eligibility: { remote: getRemoteSkillEligibility() },
|
||||||
snapshotVersion: skillsSnapshotVersion,
|
snapshotVersion: skillsSnapshotVersion,
|
||||||
|
skillFilter,
|
||||||
})
|
})
|
||||||
: sessionEntry?.skillsSnapshot;
|
: sessionEntry?.skillsSnapshot;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,7 @@ const FIELD_LABELS: Record<string, string> = {
|
||||||
"diagnostics.cacheTrace.includePrompt": "Cache Trace Include Prompt",
|
"diagnostics.cacheTrace.includePrompt": "Cache Trace Include Prompt",
|
||||||
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",
|
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",
|
||||||
"agents.list.*.identity.avatar": "Identity Avatar",
|
"agents.list.*.identity.avatar": "Identity Avatar",
|
||||||
|
"agents.list.*.skills": "Agent Skill Filter",
|
||||||
"gateway.remote.url": "Remote Gateway URL",
|
"gateway.remote.url": "Remote Gateway URL",
|
||||||
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
|
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
|
||||||
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity",
|
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity",
|
||||||
|
|
@ -346,6 +347,7 @@ const FIELD_LABELS: Record<string, string> = {
|
||||||
"channels.mattermost.requireMention": "Mattermost Require Mention",
|
"channels.mattermost.requireMention": "Mattermost Require Mention",
|
||||||
"channels.signal.account": "Signal Account",
|
"channels.signal.account": "Signal Account",
|
||||||
"channels.imessage.cliPath": "iMessage CLI Path",
|
"channels.imessage.cliPath": "iMessage CLI Path",
|
||||||
|
"agents.list[].skills": "Agent Skill Filter",
|
||||||
"agents.list[].identity.avatar": "Agent Avatar",
|
"agents.list[].identity.avatar": "Agent Avatar",
|
||||||
"discovery.mdns.mode": "mDNS Discovery Mode",
|
"discovery.mdns.mode": "mDNS Discovery Mode",
|
||||||
"plugins.enabled": "Enable Plugins",
|
"plugins.enabled": "Enable Plugins",
|
||||||
|
|
@ -377,6 +379,10 @@ const FIELD_HELP: Record<string, string> = {
|
||||||
"gateway.remote.sshTarget":
|
"gateway.remote.sshTarget":
|
||||||
"Remote gateway over SSH (tunnels the gateway port to localhost). Format: user@host or user@host:port.",
|
"Remote gateway over SSH (tunnels the gateway port to localhost). Format: user@host or user@host:port.",
|
||||||
"gateway.remote.sshIdentity": "Optional SSH identity file path (passed to ssh -i).",
|
"gateway.remote.sshIdentity": "Optional SSH identity file path (passed to ssh -i).",
|
||||||
|
"agents.list.*.skills":
|
||||||
|
"Optional allowlist of skills for this agent (omit = all skills; empty = no skills).",
|
||||||
|
"agents.list[].skills":
|
||||||
|
"Optional allowlist of skills for this agent (omit = all skills; empty = no skills).",
|
||||||
"agents.list[].identity.avatar":
|
"agents.list[].identity.avatar":
|
||||||
"Avatar image path (relative to the agent workspace only) or a remote URL/data URL.",
|
"Avatar image path (relative to the agent workspace only) or a remote URL/data URL.",
|
||||||
"discovery.mdns.mode":
|
"discovery.mdns.mode":
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ export type AgentConfig = {
|
||||||
workspace?: string;
|
workspace?: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
model?: AgentModelConfig;
|
model?: AgentModelConfig;
|
||||||
|
/** Optional allowlist of skills for this agent (omit = all skills; empty = none). */
|
||||||
|
skills?: string[];
|
||||||
memorySearch?: MemorySearchConfig;
|
memorySearch?: MemorySearchConfig;
|
||||||
/** Human-like delay between block replies for this agent. */
|
/** Human-like delay between block replies for this agent. */
|
||||||
humanDelay?: HumanDelayConfig;
|
humanDelay?: HumanDelayConfig;
|
||||||
|
|
|
||||||
|
|
@ -427,6 +427,7 @@ export const AgentEntrySchema = z
|
||||||
workspace: z.string().optional(),
|
workspace: z.string().optional(),
|
||||||
agentDir: z.string().optional(),
|
agentDir: z.string().optional(),
|
||||||
model: AgentModelSchema.optional(),
|
model: AgentModelSchema.optional(),
|
||||||
|
skills: z.array(z.string()).optional(),
|
||||||
memorySearch: MemorySearchSchema,
|
memorySearch: MemorySearchSchema,
|
||||||
humanDelay: HumanDelaySchema.optional(),
|
humanDelay: HumanDelaySchema.optional(),
|
||||||
heartbeat: HeartbeatSchema,
|
heartbeat: HeartbeatSchema,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { normalizeAgentId } from "../routing/session-key.js";
|
||||||
|
|
||||||
const MAX_ASSISTANT_NAME = 50;
|
const MAX_ASSISTANT_NAME = 50;
|
||||||
const MAX_ASSISTANT_AVATAR = 200;
|
const MAX_ASSISTANT_AVATAR = 200;
|
||||||
|
const MAX_ASSISTANT_EMOJI = 16;
|
||||||
|
|
||||||
export const DEFAULT_ASSISTANT_IDENTITY: AssistantIdentity = {
|
export const DEFAULT_ASSISTANT_IDENTITY: AssistantIdentity = {
|
||||||
agentId: "main",
|
agentId: "main",
|
||||||
|
|
@ -17,6 +18,7 @@ export type AssistantIdentity = {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
name: string;
|
name: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
emoji?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function coerceIdentityValue(value: string | undefined, maxLength: number): string | undefined {
|
function coerceIdentityValue(value: string | undefined, maxLength: number): string | undefined {
|
||||||
|
|
@ -64,6 +66,33 @@ function normalizeAvatarValue(value: string | undefined): string | undefined {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeEmojiValue(value: string | undefined): string | undefined {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (trimmed.length > MAX_ASSISTANT_EMOJI) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let hasNonAscii = false;
|
||||||
|
for (let i = 0; i < trimmed.length; i += 1) {
|
||||||
|
if (trimmed.charCodeAt(i) > 127) {
|
||||||
|
hasNonAscii = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasNonAscii) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (isAvatarUrl(trimmed) || looksLikeAvatarPath(trimmed)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveAssistantIdentity(params: {
|
export function resolveAssistantIdentity(params: {
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
agentId?: string | null;
|
agentId?: string | null;
|
||||||
|
|
@ -92,5 +121,13 @@ export function resolveAssistantIdentity(params: {
|
||||||
avatarCandidates.map((candidate) => normalizeAvatarValue(candidate)).find(Boolean) ??
|
avatarCandidates.map((candidate) => normalizeAvatarValue(candidate)).find(Boolean) ??
|
||||||
DEFAULT_ASSISTANT_IDENTITY.avatar;
|
DEFAULT_ASSISTANT_IDENTITY.avatar;
|
||||||
|
|
||||||
return { agentId, name, avatar };
|
const emojiCandidates = [
|
||||||
|
coerceIdentityValue(agentIdentity?.emoji, MAX_ASSISTANT_EMOJI),
|
||||||
|
coerceIdentityValue(fileIdentity?.emoji, MAX_ASSISTANT_EMOJI),
|
||||||
|
coerceIdentityValue(agentIdentity?.avatar, MAX_ASSISTANT_EMOJI),
|
||||||
|
coerceIdentityValue(fileIdentity?.avatar, MAX_ASSISTANT_EMOJI),
|
||||||
|
];
|
||||||
|
const emoji = emojiCandidates.map((candidate) => normalizeEmojiValue(candidate)).find(Boolean);
|
||||||
|
|
||||||
|
return { agentId, name, avatar, emoji };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,20 @@ import {
|
||||||
AgentParamsSchema,
|
AgentParamsSchema,
|
||||||
type AgentSummary,
|
type AgentSummary,
|
||||||
AgentSummarySchema,
|
AgentSummarySchema,
|
||||||
|
type AgentsFileEntry,
|
||||||
|
AgentsFileEntrySchema,
|
||||||
|
type AgentsFilesGetParams,
|
||||||
|
AgentsFilesGetParamsSchema,
|
||||||
|
type AgentsFilesGetResult,
|
||||||
|
AgentsFilesGetResultSchema,
|
||||||
|
type AgentsFilesListParams,
|
||||||
|
AgentsFilesListParamsSchema,
|
||||||
|
type AgentsFilesListResult,
|
||||||
|
AgentsFilesListResultSchema,
|
||||||
|
type AgentsFilesSetParams,
|
||||||
|
AgentsFilesSetParamsSchema,
|
||||||
|
type AgentsFilesSetResult,
|
||||||
|
AgentsFilesSetResultSchema,
|
||||||
type AgentsListParams,
|
type AgentsListParams,
|
||||||
AgentsListParamsSchema,
|
AgentsListParamsSchema,
|
||||||
type AgentsListResult,
|
type AgentsListResult,
|
||||||
|
|
@ -209,6 +223,15 @@ export const validateAgentIdentityParams =
|
||||||
export const validateAgentWaitParams = ajv.compile<AgentWaitParams>(AgentWaitParamsSchema);
|
export const validateAgentWaitParams = ajv.compile<AgentWaitParams>(AgentWaitParamsSchema);
|
||||||
export const validateWakeParams = ajv.compile<WakeParams>(WakeParamsSchema);
|
export const validateWakeParams = ajv.compile<WakeParams>(WakeParamsSchema);
|
||||||
export const validateAgentsListParams = ajv.compile<AgentsListParams>(AgentsListParamsSchema);
|
export const validateAgentsListParams = ajv.compile<AgentsListParams>(AgentsListParamsSchema);
|
||||||
|
export const validateAgentsFilesListParams = ajv.compile<AgentsFilesListParams>(
|
||||||
|
AgentsFilesListParamsSchema,
|
||||||
|
);
|
||||||
|
export const validateAgentsFilesGetParams = ajv.compile<AgentsFilesGetParams>(
|
||||||
|
AgentsFilesGetParamsSchema,
|
||||||
|
);
|
||||||
|
export const validateAgentsFilesSetParams = ajv.compile<AgentsFilesSetParams>(
|
||||||
|
AgentsFilesSetParamsSchema,
|
||||||
|
);
|
||||||
export const validateNodePairRequestParams = ajv.compile<NodePairRequestParams>(
|
export const validateNodePairRequestParams = ajv.compile<NodePairRequestParams>(
|
||||||
NodePairRequestParamsSchema,
|
NodePairRequestParamsSchema,
|
||||||
);
|
);
|
||||||
|
|
@ -408,6 +431,13 @@ export {
|
||||||
WebLoginStartParamsSchema,
|
WebLoginStartParamsSchema,
|
||||||
WebLoginWaitParamsSchema,
|
WebLoginWaitParamsSchema,
|
||||||
AgentSummarySchema,
|
AgentSummarySchema,
|
||||||
|
AgentsFileEntrySchema,
|
||||||
|
AgentsFilesListParamsSchema,
|
||||||
|
AgentsFilesListResultSchema,
|
||||||
|
AgentsFilesGetParamsSchema,
|
||||||
|
AgentsFilesGetResultSchema,
|
||||||
|
AgentsFilesSetParamsSchema,
|
||||||
|
AgentsFilesSetResultSchema,
|
||||||
AgentsListParamsSchema,
|
AgentsListParamsSchema,
|
||||||
AgentsListResultSchema,
|
AgentsListResultSchema,
|
||||||
ModelsListParamsSchema,
|
ModelsListParamsSchema,
|
||||||
|
|
@ -482,6 +512,13 @@ export type {
|
||||||
WebLoginStartParams,
|
WebLoginStartParams,
|
||||||
WebLoginWaitParams,
|
WebLoginWaitParams,
|
||||||
AgentSummary,
|
AgentSummary,
|
||||||
|
AgentsFileEntry,
|
||||||
|
AgentsFilesListParams,
|
||||||
|
AgentsFilesListResult,
|
||||||
|
AgentsFilesGetParams,
|
||||||
|
AgentsFilesGetResult,
|
||||||
|
AgentsFilesSetParams,
|
||||||
|
AgentsFilesSetResult,
|
||||||
AgentsListParams,
|
AgentsListParams,
|
||||||
AgentsListResult,
|
AgentsListResult,
|
||||||
SkillsStatusParams,
|
SkillsStatusParams,
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ export const AgentIdentityResultSchema = Type.Object(
|
||||||
agentId: NonEmptyString,
|
agentId: NonEmptyString,
|
||||||
name: Type.Optional(NonEmptyString),
|
name: Type.Optional(NonEmptyString),
|
||||||
avatar: Type.Optional(NonEmptyString),
|
avatar: Type.Optional(NonEmptyString),
|
||||||
|
emoji: Type.Optional(NonEmptyString),
|
||||||
},
|
},
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,70 @@ export const AgentsListResultSchema = Type.Object(
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const AgentsFileEntrySchema = Type.Object(
|
||||||
|
{
|
||||||
|
name: NonEmptyString,
|
||||||
|
path: NonEmptyString,
|
||||||
|
missing: Type.Boolean(),
|
||||||
|
size: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||||
|
updatedAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||||
|
content: Type.Optional(Type.String()),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentsFilesListParamsSchema = Type.Object(
|
||||||
|
{
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentsFilesListResultSchema = Type.Object(
|
||||||
|
{
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
workspace: NonEmptyString,
|
||||||
|
files: Type.Array(AgentsFileEntrySchema),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentsFilesGetParamsSchema = Type.Object(
|
||||||
|
{
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
name: NonEmptyString,
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentsFilesGetResultSchema = Type.Object(
|
||||||
|
{
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
workspace: NonEmptyString,
|
||||||
|
file: AgentsFileEntrySchema,
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentsFilesSetParamsSchema = Type.Object(
|
||||||
|
{
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
name: NonEmptyString,
|
||||||
|
content: Type.String(),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentsFilesSetResultSchema = Type.Object(
|
||||||
|
{
|
||||||
|
ok: Type.Literal(true),
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
workspace: NonEmptyString,
|
||||||
|
file: AgentsFileEntrySchema,
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
export const ModelsListParamsSchema = Type.Object({}, { additionalProperties: false });
|
export const ModelsListParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||||
|
|
||||||
export const ModelsListResultSchema = Type.Object(
|
export const ModelsListResultSchema = Type.Object(
|
||||||
|
|
@ -53,7 +117,12 @@ export const ModelsListResultSchema = Type.Object(
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
export const SkillsStatusParamsSchema = Type.Object({}, { additionalProperties: false });
|
export const SkillsStatusParamsSchema = Type.Object(
|
||||||
|
{
|
||||||
|
agentId: Type.Optional(NonEmptyString),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
export const SkillsBinsParamsSchema = Type.Object({}, { additionalProperties: false });
|
export const SkillsBinsParamsSchema = Type.Object({}, { additionalProperties: false });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,13 @@ import {
|
||||||
} from "./agent.js";
|
} from "./agent.js";
|
||||||
import {
|
import {
|
||||||
AgentSummarySchema,
|
AgentSummarySchema,
|
||||||
|
AgentsFileEntrySchema,
|
||||||
|
AgentsFilesGetParamsSchema,
|
||||||
|
AgentsFilesGetResultSchema,
|
||||||
|
AgentsFilesListParamsSchema,
|
||||||
|
AgentsFilesListResultSchema,
|
||||||
|
AgentsFilesSetParamsSchema,
|
||||||
|
AgentsFilesSetResultSchema,
|
||||||
AgentsListParamsSchema,
|
AgentsListParamsSchema,
|
||||||
AgentsListResultSchema,
|
AgentsListResultSchema,
|
||||||
ModelChoiceSchema,
|
ModelChoiceSchema,
|
||||||
|
|
@ -182,6 +189,13 @@ export const ProtocolSchemas: Record<string, TSchema> = {
|
||||||
WebLoginStartParams: WebLoginStartParamsSchema,
|
WebLoginStartParams: WebLoginStartParamsSchema,
|
||||||
WebLoginWaitParams: WebLoginWaitParamsSchema,
|
WebLoginWaitParams: WebLoginWaitParamsSchema,
|
||||||
AgentSummary: AgentSummarySchema,
|
AgentSummary: AgentSummarySchema,
|
||||||
|
AgentsFileEntry: AgentsFileEntrySchema,
|
||||||
|
AgentsFilesListParams: AgentsFilesListParamsSchema,
|
||||||
|
AgentsFilesListResult: AgentsFilesListResultSchema,
|
||||||
|
AgentsFilesGetParams: AgentsFilesGetParamsSchema,
|
||||||
|
AgentsFilesGetResult: AgentsFilesGetResultSchema,
|
||||||
|
AgentsFilesSetParams: AgentsFilesSetParamsSchema,
|
||||||
|
AgentsFilesSetResult: AgentsFilesSetResultSchema,
|
||||||
AgentsListParams: AgentsListParamsSchema,
|
AgentsListParams: AgentsListParamsSchema,
|
||||||
AgentsListResult: AgentsListResultSchema,
|
AgentsListResult: AgentsListResultSchema,
|
||||||
ModelChoice: ModelChoiceSchema,
|
ModelChoice: ModelChoiceSchema,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,13 @@ import type {
|
||||||
} from "./agent.js";
|
} from "./agent.js";
|
||||||
import type {
|
import type {
|
||||||
AgentSummarySchema,
|
AgentSummarySchema,
|
||||||
|
AgentsFileEntrySchema,
|
||||||
|
AgentsFilesGetParamsSchema,
|
||||||
|
AgentsFilesGetResultSchema,
|
||||||
|
AgentsFilesListParamsSchema,
|
||||||
|
AgentsFilesListResultSchema,
|
||||||
|
AgentsFilesSetParamsSchema,
|
||||||
|
AgentsFilesSetResultSchema,
|
||||||
AgentsListParamsSchema,
|
AgentsListParamsSchema,
|
||||||
AgentsListResultSchema,
|
AgentsListResultSchema,
|
||||||
ModelChoiceSchema,
|
ModelChoiceSchema,
|
||||||
|
|
@ -171,6 +178,13 @@ export type ChannelsLogoutParams = Static<typeof ChannelsLogoutParamsSchema>;
|
||||||
export type WebLoginStartParams = Static<typeof WebLoginStartParamsSchema>;
|
export type WebLoginStartParams = Static<typeof WebLoginStartParamsSchema>;
|
||||||
export type WebLoginWaitParams = Static<typeof WebLoginWaitParamsSchema>;
|
export type WebLoginWaitParams = Static<typeof WebLoginWaitParamsSchema>;
|
||||||
export type AgentSummary = Static<typeof AgentSummarySchema>;
|
export type AgentSummary = Static<typeof AgentSummarySchema>;
|
||||||
|
export type AgentsFileEntry = Static<typeof AgentsFileEntrySchema>;
|
||||||
|
export type AgentsFilesListParams = Static<typeof AgentsFilesListParamsSchema>;
|
||||||
|
export type AgentsFilesListResult = Static<typeof AgentsFilesListResultSchema>;
|
||||||
|
export type AgentsFilesGetParams = Static<typeof AgentsFilesGetParamsSchema>;
|
||||||
|
export type AgentsFilesGetResult = Static<typeof AgentsFilesGetResultSchema>;
|
||||||
|
export type AgentsFilesSetParams = Static<typeof AgentsFilesSetParamsSchema>;
|
||||||
|
export type AgentsFilesSetResult = Static<typeof AgentsFilesSetResultSchema>;
|
||||||
export type AgentsListParams = Static<typeof AgentsListParamsSchema>;
|
export type AgentsListParams = Static<typeof AgentsListParamsSchema>;
|
||||||
export type AgentsListResult = Static<typeof AgentsListResultSchema>;
|
export type AgentsListResult = Static<typeof AgentsListResultSchema>;
|
||||||
export type ModelChoice = Static<typeof ModelChoiceSchema>;
|
export type ModelChoice = Static<typeof ModelChoiceSchema>;
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ const BASE_METHODS = [
|
||||||
"talk.mode",
|
"talk.mode",
|
||||||
"models.list",
|
"models.list",
|
||||||
"agents.list",
|
"agents.list",
|
||||||
|
"agents.files.list",
|
||||||
|
"agents.files.get",
|
||||||
|
"agents.files.set",
|
||||||
"skills.status",
|
"skills.status",
|
||||||
"skills.bins",
|
"skills.bins",
|
||||||
"skills.install",
|
"skills.install",
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,128 @@
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
import type { GatewayRequestHandlers } from "./types.js";
|
import type { GatewayRequestHandlers } from "./types.js";
|
||||||
|
import { listAgentIds, resolveAgentWorkspaceDir } from "../../agents/agent-scope.js";
|
||||||
|
import {
|
||||||
|
DEFAULT_AGENTS_FILENAME,
|
||||||
|
DEFAULT_BOOTSTRAP_FILENAME,
|
||||||
|
DEFAULT_HEARTBEAT_FILENAME,
|
||||||
|
DEFAULT_IDENTITY_FILENAME,
|
||||||
|
DEFAULT_MEMORY_ALT_FILENAME,
|
||||||
|
DEFAULT_MEMORY_FILENAME,
|
||||||
|
DEFAULT_SOUL_FILENAME,
|
||||||
|
DEFAULT_TOOLS_FILENAME,
|
||||||
|
DEFAULT_USER_FILENAME,
|
||||||
|
} from "../../agents/workspace.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { loadConfig } from "../../config/config.js";
|
||||||
|
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
errorShape,
|
errorShape,
|
||||||
formatValidationErrors,
|
formatValidationErrors,
|
||||||
|
validateAgentsFilesGetParams,
|
||||||
|
validateAgentsFilesListParams,
|
||||||
|
validateAgentsFilesSetParams,
|
||||||
validateAgentsListParams,
|
validateAgentsListParams,
|
||||||
} from "../protocol/index.js";
|
} from "../protocol/index.js";
|
||||||
import { listAgentsForGateway } from "../session-utils.js";
|
import { listAgentsForGateway } from "../session-utils.js";
|
||||||
|
|
||||||
|
const BOOTSTRAP_FILE_NAMES = [
|
||||||
|
DEFAULT_AGENTS_FILENAME,
|
||||||
|
DEFAULT_SOUL_FILENAME,
|
||||||
|
DEFAULT_TOOLS_FILENAME,
|
||||||
|
DEFAULT_IDENTITY_FILENAME,
|
||||||
|
DEFAULT_USER_FILENAME,
|
||||||
|
DEFAULT_HEARTBEAT_FILENAME,
|
||||||
|
DEFAULT_BOOTSTRAP_FILENAME,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const MEMORY_FILE_NAMES = [DEFAULT_MEMORY_FILENAME, DEFAULT_MEMORY_ALT_FILENAME] as const;
|
||||||
|
|
||||||
|
const ALLOWED_FILE_NAMES = new Set<string>([...BOOTSTRAP_FILE_NAMES, ...MEMORY_FILE_NAMES]);
|
||||||
|
|
||||||
|
type FileMeta = {
|
||||||
|
size: number;
|
||||||
|
updatedAtMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function statFile(filePath: string): Promise<FileMeta | null> {
|
||||||
|
try {
|
||||||
|
const stat = await fs.stat(filePath);
|
||||||
|
if (!stat.isFile()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
size: stat.size,
|
||||||
|
updatedAtMs: Math.floor(stat.mtimeMs),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listAgentFiles(workspaceDir: string) {
|
||||||
|
const files: Array<{
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
missing: boolean;
|
||||||
|
size?: number;
|
||||||
|
updatedAtMs?: number;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const name of BOOTSTRAP_FILE_NAMES) {
|
||||||
|
const filePath = path.join(workspaceDir, name);
|
||||||
|
const meta = await statFile(filePath);
|
||||||
|
if (meta) {
|
||||||
|
files.push({
|
||||||
|
name,
|
||||||
|
path: filePath,
|
||||||
|
missing: false,
|
||||||
|
size: meta.size,
|
||||||
|
updatedAtMs: meta.updatedAtMs,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
files.push({ name, path: filePath, missing: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryMemoryPath = path.join(workspaceDir, DEFAULT_MEMORY_FILENAME);
|
||||||
|
const primaryMeta = await statFile(primaryMemoryPath);
|
||||||
|
if (primaryMeta) {
|
||||||
|
files.push({
|
||||||
|
name: DEFAULT_MEMORY_FILENAME,
|
||||||
|
path: primaryMemoryPath,
|
||||||
|
missing: false,
|
||||||
|
size: primaryMeta.size,
|
||||||
|
updatedAtMs: primaryMeta.updatedAtMs,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const altMemoryPath = path.join(workspaceDir, DEFAULT_MEMORY_ALT_FILENAME);
|
||||||
|
const altMeta = await statFile(altMemoryPath);
|
||||||
|
if (altMeta) {
|
||||||
|
files.push({
|
||||||
|
name: DEFAULT_MEMORY_ALT_FILENAME,
|
||||||
|
path: altMemoryPath,
|
||||||
|
missing: false,
|
||||||
|
size: altMeta.size,
|
||||||
|
updatedAtMs: altMeta.updatedAtMs,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
files.push({ name: DEFAULT_MEMORY_FILENAME, path: primaryMemoryPath, missing: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAgentIdOrError(agentIdRaw: string, cfg: ReturnType<typeof loadConfig>) {
|
||||||
|
const agentId = normalizeAgentId(agentIdRaw);
|
||||||
|
const allowed = new Set(listAgentIds(cfg));
|
||||||
|
if (!allowed.has(agentId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return agentId;
|
||||||
|
}
|
||||||
|
|
||||||
export const agentsHandlers: GatewayRequestHandlers = {
|
export const agentsHandlers: GatewayRequestHandlers = {
|
||||||
"agents.list": ({ params, respond }) => {
|
"agents.list": ({ params, respond }) => {
|
||||||
if (!validateAgentsListParams(params)) {
|
if (!validateAgentsListParams(params)) {
|
||||||
|
|
@ -26,4 +141,143 @@ export const agentsHandlers: GatewayRequestHandlers = {
|
||||||
const result = listAgentsForGateway(cfg);
|
const result = listAgentsForGateway(cfg);
|
||||||
respond(true, result, undefined);
|
respond(true, result, undefined);
|
||||||
},
|
},
|
||||||
|
"agents.files.list": async ({ params, respond }) => {
|
||||||
|
if (!validateAgentsFilesListParams(params)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
`invalid agents.files.list params: ${formatValidationErrors(
|
||||||
|
validateAgentsFilesListParams.errors,
|
||||||
|
)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const agentId = resolveAgentIdOrError(String(params.agentId ?? ""), cfg);
|
||||||
|
if (!agentId) {
|
||||||
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unknown agent id"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||||
|
const files = await listAgentFiles(workspaceDir);
|
||||||
|
respond(true, { agentId, workspace: workspaceDir, files }, undefined);
|
||||||
|
},
|
||||||
|
"agents.files.get": async ({ params, respond }) => {
|
||||||
|
if (!validateAgentsFilesGetParams(params)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
`invalid agents.files.get params: ${formatValidationErrors(
|
||||||
|
validateAgentsFilesGetParams.errors,
|
||||||
|
)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const agentId = resolveAgentIdOrError(String(params.agentId ?? ""), cfg);
|
||||||
|
if (!agentId) {
|
||||||
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unknown agent id"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = String(params.name ?? "").trim();
|
||||||
|
if (!ALLOWED_FILE_NAMES.has(name)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `unsupported file "${name}"`),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||||
|
const filePath = path.join(workspaceDir, name);
|
||||||
|
const meta = await statFile(filePath);
|
||||||
|
if (!meta) {
|
||||||
|
respond(
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
agentId,
|
||||||
|
workspace: workspaceDir,
|
||||||
|
file: { name, path: filePath, missing: true },
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content = await fs.readFile(filePath, "utf-8");
|
||||||
|
respond(
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
agentId,
|
||||||
|
workspace: workspaceDir,
|
||||||
|
file: {
|
||||||
|
name,
|
||||||
|
path: filePath,
|
||||||
|
missing: false,
|
||||||
|
size: meta.size,
|
||||||
|
updatedAtMs: meta.updatedAtMs,
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
"agents.files.set": async ({ params, respond }) => {
|
||||||
|
if (!validateAgentsFilesSetParams(params)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
`invalid agents.files.set params: ${formatValidationErrors(
|
||||||
|
validateAgentsFilesSetParams.errors,
|
||||||
|
)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const agentId = resolveAgentIdOrError(String(params.agentId ?? ""), cfg);
|
||||||
|
if (!agentId) {
|
||||||
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unknown agent id"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = String(params.name ?? "").trim();
|
||||||
|
if (!ALLOWED_FILE_NAMES.has(name)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `unsupported file "${name}"`),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||||
|
await fs.mkdir(workspaceDir, { recursive: true });
|
||||||
|
const filePath = path.join(workspaceDir, name);
|
||||||
|
const content = String(params.content ?? "");
|
||||||
|
await fs.writeFile(filePath, content, "utf-8");
|
||||||
|
const meta = await statFile(filePath);
|
||||||
|
respond(
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
ok: true,
|
||||||
|
agentId,
|
||||||
|
workspace: workspaceDir,
|
||||||
|
file: {
|
||||||
|
name,
|
||||||
|
path: filePath,
|
||||||
|
missing: false,
|
||||||
|
size: meta?.size,
|
||||||
|
updatedAtMs: meta?.updatedAtMs,
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
import type { GatewayRequestHandlers } from "./types.js";
|
import type { GatewayRequestHandlers } from "./types.js";
|
||||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
import {
|
||||||
|
listAgentIds,
|
||||||
|
resolveAgentWorkspaceDir,
|
||||||
|
resolveDefaultAgentId,
|
||||||
|
} from "../../agents/agent-scope.js";
|
||||||
import { installSkill } from "../../agents/skills-install.js";
|
import { installSkill } from "../../agents/skills-install.js";
|
||||||
import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
|
import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
|
||||||
import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.js";
|
import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.js";
|
||||||
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
||||||
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
import { getRemoteSkillEligibility } from "../../infra/skills-remote.js";
|
||||||
|
import { normalizeAgentId } from "../../routing/session-key.js";
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
errorShape,
|
errorShape,
|
||||||
|
|
@ -75,7 +80,20 @@ export const skillsHandlers: GatewayRequestHandlers = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
|
const agentIdRaw = typeof params?.agentId === "string" ? params.agentId.trim() : "";
|
||||||
|
const agentId = agentIdRaw ? normalizeAgentId(agentIdRaw) : resolveDefaultAgentId(cfg);
|
||||||
|
if (agentIdRaw) {
|
||||||
|
const knownAgents = listAgentIds(cfg);
|
||||||
|
if (!knownAgents.includes(agentId)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `unknown agent id "${agentIdRaw}"`),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||||
const report = buildWorkspaceSkillStatus(workspaceDir, {
|
const report = buildWorkspaceSkillStatus(workspaceDir, {
|
||||||
config: cfg,
|
config: cfg,
|
||||||
eligibility: { remote: getRemoteSkillEligibility() },
|
eligibility: { remote: getRemoteSkillEligibility() },
|
||||||
|
|
|
||||||
|
|
@ -989,6 +989,7 @@
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
line-clamp: 3;
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
@ -1496,3 +1497,398 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
Agents
|
||||||
|
=========================================== */
|
||||||
|
|
||||||
|
.agents-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-sidebar {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-main {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--card);
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color var(--duration-fast) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-row:hover {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-row.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: var(--focus-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--secondary);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-avatar--lg {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-info {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-sub {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-pill {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
background: var(--secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-pill.warn {
|
||||||
|
color: var(--warn);
|
||||||
|
border-color: var(--warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-header-main {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-header-meta {
|
||||||
|
display: grid;
|
||||||
|
justify-items: end;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tab {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color var(--duration-fast) ease,
|
||||||
|
background var(--duration-fast) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tab.active {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agents-overview-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-kv {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-kv-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-model-select {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-model-meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-files-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-files-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-file-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--card);
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color var(--duration-fast) ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-file-row:hover {
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-file-row.active {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: var(--focus-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-file-name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-file-meta {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-files-editor {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-file-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-file-title {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-file-sub {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-file-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tools-meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tools-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tools-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tools-section {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tools-header {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tools-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px 12px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tool-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tool-title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tool-sub {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-skills-groups {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-skills-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-skills-group summary {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-skills-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-skills-header > span:last-child {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-skills-group summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-skills-group summary::marker {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-skills-header::after {
|
||||||
|
content: "▸";
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
transition: transform var(--duration-fast) ease;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-skills-group[open] .agent-skills-header::after {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-skill-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-skill-row .list-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (min-width: 900px) {
|
||||||
|
.skills-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.agents-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-header {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-header-meta {
|
||||||
|
justify-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-files-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-tools-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,10 @@
|
||||||
|
|
||||||
.shell--chat-focus .content {
|
.shell--chat-focus .content {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
gap: 0;
|
}
|
||||||
|
|
||||||
|
.shell--chat-focus .content > * + * {
|
||||||
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
|
|
@ -418,23 +421,32 @@
|
||||||
.content {
|
.content {
|
||||||
grid-area: content;
|
grid-area: content;
|
||||||
padding: 12px 16px 32px;
|
padding: 12px 16px 32px;
|
||||||
display: flex;
|
display: block;
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content > * + * {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
:root[data-theme="light"] .content {
|
:root[data-theme="light"] .content {
|
||||||
background: var(--bg-content);
|
background: var(--bg-content);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content--chat {
|
.content--chat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content--chat > * + * {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* Content header */
|
/* Content header */
|
||||||
.content-header {
|
.content-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import type { AppViewState } from "./app-view-state";
|
||||||
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
|
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
|
||||||
import { refreshChatAvatar } from "./app-chat";
|
import { refreshChatAvatar } from "./app-chat";
|
||||||
import { renderChatControls, renderTab, renderThemeToggle } from "./app-render.helpers";
|
import { renderChatControls, renderTab, renderThemeToggle } from "./app-render.helpers";
|
||||||
|
import { loadAgentFileContent, loadAgentFiles, saveAgentFile } from "./controllers/agent-files";
|
||||||
|
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity";
|
||||||
|
import { loadAgentSkills } from "./controllers/agent-skills";
|
||||||
|
import { loadAgents } from "./controllers/agents";
|
||||||
import { loadChannels } from "./controllers/channels";
|
import { loadChannels } from "./controllers/channels";
|
||||||
import { loadChatHistory } from "./controllers/chat";
|
import { loadChatHistory } from "./controllers/chat";
|
||||||
import {
|
import {
|
||||||
|
|
@ -47,6 +51,7 @@ import {
|
||||||
} from "./controllers/skills";
|
} from "./controllers/skills";
|
||||||
import { icons } from "./icons";
|
import { icons } from "./icons";
|
||||||
import { TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation";
|
import { TAB_GROUPS, subtitleForTab, titleForTab } from "./navigation";
|
||||||
|
import { renderAgents } from "./views/agents";
|
||||||
import { renderChannels } from "./views/channels";
|
import { renderChannels } from "./views/channels";
|
||||||
import { renderChat } from "./views/chat";
|
import { renderChat } from "./views/chat";
|
||||||
import { renderConfig } from "./views/config";
|
import { renderConfig } from "./views/config";
|
||||||
|
|
@ -90,6 +95,13 @@ export function renderApp(state: AppViewState) {
|
||||||
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
|
const showThinking = state.onboarding ? false : state.settings.chatShowThinking;
|
||||||
const assistantAvatarUrl = resolveAssistantAvatarUrl(state);
|
const assistantAvatarUrl = resolveAssistantAvatarUrl(state);
|
||||||
const chatAvatarUrl = state.chatAvatarUrl ?? assistantAvatarUrl ?? null;
|
const chatAvatarUrl = state.chatAvatarUrl ?? assistantAvatarUrl ?? null;
|
||||||
|
const configValue =
|
||||||
|
state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null);
|
||||||
|
const resolvedAgentId =
|
||||||
|
state.agentsSelectedId ??
|
||||||
|
state.agentsList?.defaultId ??
|
||||||
|
state.agentsList?.agents?.[0]?.id ??
|
||||||
|
null;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="shell ${isChat ? "shell--chat" : ""} ${chatFocus ? "shell--chat-focus" : ""} ${state.settings.navCollapsed ? "shell--nav-collapsed" : ""} ${state.onboarding ? "shell--onboarding" : ""}">
|
<div class="shell ${isChat ? "shell--chat" : ""} ${chatFocus ? "shell--chat-focus" : ""} ${state.settings.navCollapsed ? "shell--nav-collapsed" : ""} ${state.onboarding ? "shell--onboarding" : ""}">
|
||||||
|
|
@ -317,6 +329,352 @@ export function renderApp(state: AppViewState) {
|
||||||
: nothing
|
: nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${
|
||||||
|
state.tab === "agents"
|
||||||
|
? renderAgents({
|
||||||
|
loading: state.agentsLoading,
|
||||||
|
error: state.agentsError,
|
||||||
|
agentsList: state.agentsList,
|
||||||
|
selectedAgentId: resolvedAgentId,
|
||||||
|
activePanel: state.agentsPanel,
|
||||||
|
configForm: configValue,
|
||||||
|
configLoading: state.configLoading,
|
||||||
|
configSaving: state.configSaving,
|
||||||
|
configDirty: state.configFormDirty,
|
||||||
|
channelsLoading: state.channelsLoading,
|
||||||
|
channelsError: state.channelsError,
|
||||||
|
channelsSnapshot: state.channelsSnapshot,
|
||||||
|
channelsLastSuccess: state.channelsLastSuccess,
|
||||||
|
cronLoading: state.cronLoading,
|
||||||
|
cronStatus: state.cronStatus,
|
||||||
|
cronJobs: state.cronJobs,
|
||||||
|
cronError: state.cronError,
|
||||||
|
agentFilesLoading: state.agentFilesLoading,
|
||||||
|
agentFilesError: state.agentFilesError,
|
||||||
|
agentFilesList: state.agentFilesList,
|
||||||
|
agentFileActive: state.agentFileActive,
|
||||||
|
agentFileContents: state.agentFileContents,
|
||||||
|
agentFileDrafts: state.agentFileDrafts,
|
||||||
|
agentFileSaving: state.agentFileSaving,
|
||||||
|
agentIdentityLoading: state.agentIdentityLoading,
|
||||||
|
agentIdentityError: state.agentIdentityError,
|
||||||
|
agentIdentityById: state.agentIdentityById,
|
||||||
|
agentSkillsLoading: state.agentSkillsLoading,
|
||||||
|
agentSkillsReport: state.agentSkillsReport,
|
||||||
|
agentSkillsError: state.agentSkillsError,
|
||||||
|
agentSkillsAgentId: state.agentSkillsAgentId,
|
||||||
|
skillsFilter: state.skillsFilter,
|
||||||
|
onRefresh: async () => {
|
||||||
|
await loadAgents(state);
|
||||||
|
const agentIds = state.agentsList?.agents?.map((entry) => entry.id) ?? [];
|
||||||
|
if (agentIds.length > 0) {
|
||||||
|
void loadAgentIdentities(state, agentIds);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSelectAgent: (agentId) => {
|
||||||
|
if (state.agentsSelectedId === agentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.agentsSelectedId = agentId;
|
||||||
|
state.agentFilesList = null;
|
||||||
|
state.agentFilesError = null;
|
||||||
|
state.agentFilesLoading = false;
|
||||||
|
state.agentFileActive = null;
|
||||||
|
state.agentFileContents = {};
|
||||||
|
state.agentFileDrafts = {};
|
||||||
|
state.agentSkillsReport = null;
|
||||||
|
state.agentSkillsError = null;
|
||||||
|
state.agentSkillsAgentId = null;
|
||||||
|
void loadAgentIdentity(state, agentId);
|
||||||
|
if (state.agentsPanel === "files") {
|
||||||
|
void loadAgentFiles(state, agentId);
|
||||||
|
}
|
||||||
|
if (state.agentsPanel === "skills") {
|
||||||
|
void loadAgentSkills(state, agentId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSelectPanel: (panel) => {
|
||||||
|
state.agentsPanel = panel;
|
||||||
|
if (panel === "files" && resolvedAgentId) {
|
||||||
|
if (state.agentFilesList?.agentId !== resolvedAgentId) {
|
||||||
|
state.agentFilesList = null;
|
||||||
|
state.agentFilesError = null;
|
||||||
|
state.agentFileActive = null;
|
||||||
|
state.agentFileContents = {};
|
||||||
|
state.agentFileDrafts = {};
|
||||||
|
void loadAgentFiles(state, resolvedAgentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (panel === "skills") {
|
||||||
|
if (resolvedAgentId) {
|
||||||
|
void loadAgentSkills(state, resolvedAgentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (panel === "channels") {
|
||||||
|
void loadChannels(state, false);
|
||||||
|
}
|
||||||
|
if (panel === "cron") {
|
||||||
|
void state.loadCron();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLoadFiles: (agentId) => loadAgentFiles(state, agentId),
|
||||||
|
onSelectFile: (name) => {
|
||||||
|
state.agentFileActive = name;
|
||||||
|
if (!resolvedAgentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void loadAgentFileContent(state, resolvedAgentId, name);
|
||||||
|
},
|
||||||
|
onFileDraftChange: (name, content) => {
|
||||||
|
state.agentFileDrafts = { ...state.agentFileDrafts, [name]: content };
|
||||||
|
},
|
||||||
|
onFileReset: (name) => {
|
||||||
|
const base = state.agentFileContents[name] ?? "";
|
||||||
|
state.agentFileDrafts = { ...state.agentFileDrafts, [name]: base };
|
||||||
|
},
|
||||||
|
onFileSave: (name) => {
|
||||||
|
if (!resolvedAgentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const content =
|
||||||
|
state.agentFileDrafts[name] ?? state.agentFileContents[name] ?? "";
|
||||||
|
void saveAgentFile(state, resolvedAgentId, name, content);
|
||||||
|
},
|
||||||
|
onToolsProfileChange: (agentId, profile, clearAllow) => {
|
||||||
|
if (!configValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = (configValue as { agents?: { list?: unknown[] } }).agents?.list;
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = list.findIndex(
|
||||||
|
(entry) =>
|
||||||
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
|
"id" in entry &&
|
||||||
|
(entry as { id?: string }).id === agentId,
|
||||||
|
);
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const basePath = ["agents", "list", index, "tools"];
|
||||||
|
if (profile) {
|
||||||
|
updateConfigFormValue(state, [...basePath, "profile"], profile);
|
||||||
|
} else {
|
||||||
|
removeConfigFormValue(state, [...basePath, "profile"]);
|
||||||
|
}
|
||||||
|
if (clearAllow) {
|
||||||
|
removeConfigFormValue(state, [...basePath, "allow"]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onToolsOverridesChange: (agentId, alsoAllow, deny) => {
|
||||||
|
if (!configValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = (configValue as { agents?: { list?: unknown[] } }).agents?.list;
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = list.findIndex(
|
||||||
|
(entry) =>
|
||||||
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
|
"id" in entry &&
|
||||||
|
(entry as { id?: string }).id === agentId,
|
||||||
|
);
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const basePath = ["agents", "list", index, "tools"];
|
||||||
|
if (alsoAllow.length > 0) {
|
||||||
|
updateConfigFormValue(state, [...basePath, "alsoAllow"], alsoAllow);
|
||||||
|
} else {
|
||||||
|
removeConfigFormValue(state, [...basePath, "alsoAllow"]);
|
||||||
|
}
|
||||||
|
if (deny.length > 0) {
|
||||||
|
updateConfigFormValue(state, [...basePath, "deny"], deny);
|
||||||
|
} else {
|
||||||
|
removeConfigFormValue(state, [...basePath, "deny"]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onConfigReload: () => loadConfig(state),
|
||||||
|
onConfigSave: () => saveConfig(state),
|
||||||
|
onChannelsRefresh: () => loadChannels(state, false),
|
||||||
|
onCronRefresh: () => state.loadCron(),
|
||||||
|
onSkillsFilterChange: (next) => (state.skillsFilter = next),
|
||||||
|
onSkillsRefresh: () => {
|
||||||
|
if (resolvedAgentId) {
|
||||||
|
void loadAgentSkills(state, resolvedAgentId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAgentSkillToggle: (agentId, skillName, enabled) => {
|
||||||
|
if (!configValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = (configValue as { agents?: { list?: unknown[] } }).agents?.list;
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = list.findIndex(
|
||||||
|
(entry) =>
|
||||||
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
|
"id" in entry &&
|
||||||
|
(entry as { id?: string }).id === agentId,
|
||||||
|
);
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entry = list[index] as { skills?: unknown };
|
||||||
|
const normalizedSkill = skillName.trim();
|
||||||
|
if (!normalizedSkill) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allSkills =
|
||||||
|
state.agentSkillsReport?.skills?.map((skill) => skill.name).filter(Boolean) ??
|
||||||
|
[];
|
||||||
|
const existing = Array.isArray(entry.skills)
|
||||||
|
? entry.skills.map((name) => String(name).trim()).filter(Boolean)
|
||||||
|
: undefined;
|
||||||
|
const base = existing ?? allSkills;
|
||||||
|
const next = new Set(base);
|
||||||
|
if (enabled) {
|
||||||
|
next.add(normalizedSkill);
|
||||||
|
} else {
|
||||||
|
next.delete(normalizedSkill);
|
||||||
|
}
|
||||||
|
updateConfigFormValue(state, ["agents", "list", index, "skills"], [...next]);
|
||||||
|
},
|
||||||
|
onAgentSkillsClear: (agentId) => {
|
||||||
|
if (!configValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = (configValue as { agents?: { list?: unknown[] } }).agents?.list;
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = list.findIndex(
|
||||||
|
(entry) =>
|
||||||
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
|
"id" in entry &&
|
||||||
|
(entry as { id?: string }).id === agentId,
|
||||||
|
);
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeConfigFormValue(state, ["agents", "list", index, "skills"]);
|
||||||
|
},
|
||||||
|
onAgentSkillsDisableAll: (agentId) => {
|
||||||
|
if (!configValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = (configValue as { agents?: { list?: unknown[] } }).agents?.list;
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = list.findIndex(
|
||||||
|
(entry) =>
|
||||||
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
|
"id" in entry &&
|
||||||
|
(entry as { id?: string }).id === agentId,
|
||||||
|
);
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateConfigFormValue(state, ["agents", "list", index, "skills"], []);
|
||||||
|
},
|
||||||
|
onModelChange: (agentId, modelId) => {
|
||||||
|
if (!configValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = (configValue as { agents?: { list?: unknown[] } }).agents?.list;
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = list.findIndex(
|
||||||
|
(entry) =>
|
||||||
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
|
"id" in entry &&
|
||||||
|
(entry as { id?: string }).id === agentId,
|
||||||
|
);
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const basePath = ["agents", "list", index, "model"];
|
||||||
|
if (!modelId) {
|
||||||
|
removeConfigFormValue(state, basePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entry = list[index] as { model?: unknown };
|
||||||
|
const existing = entry?.model;
|
||||||
|
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
||||||
|
const fallbacks = (existing as { fallbacks?: unknown }).fallbacks;
|
||||||
|
const next = {
|
||||||
|
primary: modelId,
|
||||||
|
...(Array.isArray(fallbacks) ? { fallbacks } : {}),
|
||||||
|
};
|
||||||
|
updateConfigFormValue(state, basePath, next);
|
||||||
|
} else {
|
||||||
|
updateConfigFormValue(state, basePath, modelId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onModelFallbacksChange: (agentId, fallbacks) => {
|
||||||
|
if (!configValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = (configValue as { agents?: { list?: unknown[] } }).agents?.list;
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const index = list.findIndex(
|
||||||
|
(entry) =>
|
||||||
|
entry &&
|
||||||
|
typeof entry === "object" &&
|
||||||
|
"id" in entry &&
|
||||||
|
(entry as { id?: string }).id === agentId,
|
||||||
|
);
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const basePath = ["agents", "list", index, "model"];
|
||||||
|
const entry = list[index] as { model?: unknown };
|
||||||
|
const normalized = fallbacks.map((name) => name.trim()).filter(Boolean);
|
||||||
|
const existing = entry.model;
|
||||||
|
const resolvePrimary = () => {
|
||||||
|
if (typeof existing === "string") {
|
||||||
|
return existing.trim() || null;
|
||||||
|
}
|
||||||
|
if (existing && typeof existing === "object" && !Array.isArray(existing)) {
|
||||||
|
const primary = (existing as { primary?: unknown }).primary;
|
||||||
|
if (typeof primary === "string") {
|
||||||
|
const trimmed = primary.trim();
|
||||||
|
return trimmed || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const primary = resolvePrimary();
|
||||||
|
if (normalized.length === 0) {
|
||||||
|
if (primary) {
|
||||||
|
updateConfigFormValue(state, basePath, primary);
|
||||||
|
} else {
|
||||||
|
removeConfigFormValue(state, basePath);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const next = primary
|
||||||
|
? { primary, fallbacks: normalized }
|
||||||
|
: { fallbacks: normalized };
|
||||||
|
updateConfigFormValue(state, basePath, next);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: nothing
|
||||||
|
}
|
||||||
|
|
||||||
${
|
${
|
||||||
state.tab === "skills"
|
state.tab === "skills"
|
||||||
? renderSkills({
|
? renderSkills({
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ import {
|
||||||
stopDebugPolling,
|
stopDebugPolling,
|
||||||
} from "./app-polling";
|
} from "./app-polling";
|
||||||
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
|
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
|
||||||
|
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity";
|
||||||
|
import { loadAgentSkills } from "./controllers/agent-skills";
|
||||||
|
import { loadAgents } from "./controllers/agents";
|
||||||
import { loadChannels } from "./controllers/channels";
|
import { loadChannels } from "./controllers/channels";
|
||||||
import { loadConfig, loadConfigSchema } from "./controllers/config";
|
import { loadConfig, loadConfigSchema } from "./controllers/config";
|
||||||
import { loadCronJobs, loadCronStatus } from "./controllers/cron";
|
import { loadCronJobs, loadCronStatus } from "./controllers/cron";
|
||||||
|
|
@ -185,6 +188,28 @@ export async function refreshActiveTab(host: SettingsHost) {
|
||||||
if (host.tab === "skills") {
|
if (host.tab === "skills") {
|
||||||
await loadSkills(host as unknown as OpenClawApp);
|
await loadSkills(host as unknown as OpenClawApp);
|
||||||
}
|
}
|
||||||
|
if (host.tab === "agents") {
|
||||||
|
await loadAgents(host as unknown as OpenClawApp);
|
||||||
|
await loadConfig(host as unknown as OpenClawApp);
|
||||||
|
const agentIds = host.agentsList?.agents?.map((entry) => entry.id) ?? [];
|
||||||
|
if (agentIds.length > 0) {
|
||||||
|
void loadAgentIdentities(host as unknown as OpenClawApp, agentIds);
|
||||||
|
}
|
||||||
|
const agentId =
|
||||||
|
host.agentsSelectedId ?? host.agentsList?.defaultId ?? host.agentsList?.agents?.[0]?.id;
|
||||||
|
if (agentId) {
|
||||||
|
void loadAgentIdentity(host as unknown as OpenClawApp, agentId);
|
||||||
|
if (host.agentsPanel === "skills") {
|
||||||
|
void loadAgentSkills(host as unknown as OpenClawApp, agentId);
|
||||||
|
}
|
||||||
|
if (host.agentsPanel === "channels") {
|
||||||
|
void loadChannels(host as unknown as OpenClawApp, false);
|
||||||
|
}
|
||||||
|
if (host.agentsPanel === "cron") {
|
||||||
|
void loadCron(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (host.tab === "nodes") {
|
if (host.tab === "nodes") {
|
||||||
await loadNodes(host as unknown as OpenClawApp);
|
await loadNodes(host as unknown as OpenClawApp);
|
||||||
await loadDevices(host as unknown as OpenClawApp);
|
await loadDevices(host as unknown as OpenClawApp);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import type { ThemeMode } from "./theme";
|
||||||
import type { ThemeTransitionContext } from "./theme-transition";
|
import type { ThemeTransitionContext } from "./theme-transition";
|
||||||
import type {
|
import type {
|
||||||
AgentsListResult,
|
AgentsListResult,
|
||||||
|
AgentsFilesListResult,
|
||||||
|
AgentIdentityResult,
|
||||||
ChannelsStatusSnapshot,
|
ChannelsStatusSnapshot,
|
||||||
ConfigSnapshot,
|
ConfigSnapshot,
|
||||||
CronJob,
|
CronJob,
|
||||||
|
|
@ -106,6 +108,22 @@ export type AppViewState = {
|
||||||
agentsLoading: boolean;
|
agentsLoading: boolean;
|
||||||
agentsList: AgentsListResult | null;
|
agentsList: AgentsListResult | null;
|
||||||
agentsError: string | null;
|
agentsError: string | null;
|
||||||
|
agentsSelectedId: string | null;
|
||||||
|
agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron";
|
||||||
|
agentFilesLoading: boolean;
|
||||||
|
agentFilesError: string | null;
|
||||||
|
agentFilesList: AgentsFilesListResult | null;
|
||||||
|
agentFileContents: Record<string, string>;
|
||||||
|
agentFileDrafts: Record<string, string>;
|
||||||
|
agentFileActive: string | null;
|
||||||
|
agentFileSaving: boolean;
|
||||||
|
agentIdentityLoading: boolean;
|
||||||
|
agentIdentityError: string | null;
|
||||||
|
agentIdentityById: Record<string, AgentIdentityResult>;
|
||||||
|
agentSkillsLoading: boolean;
|
||||||
|
agentSkillsError: string | null;
|
||||||
|
agentSkillsReport: SkillStatusReport | null;
|
||||||
|
agentSkillsAgentId: string | null;
|
||||||
sessionsLoading: boolean;
|
sessionsLoading: boolean;
|
||||||
sessionsResult: SessionsListResult | null;
|
sessionsResult: SessionsListResult | null;
|
||||||
sessionsError: string | null;
|
sessionsError: string | null;
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,14 @@ import type { AppViewState } from "./app-view-state";
|
||||||
import type { DevicePairingList } from "./controllers/devices";
|
import type { DevicePairingList } from "./controllers/devices";
|
||||||
import type { ExecApprovalRequest } from "./controllers/exec-approval";
|
import type { ExecApprovalRequest } from "./controllers/exec-approval";
|
||||||
import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals";
|
import type { ExecApprovalsFile, ExecApprovalsSnapshot } from "./controllers/exec-approvals";
|
||||||
|
import type { SkillMessage } from "./controllers/skills";
|
||||||
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
import type { GatewayBrowserClient, GatewayHelloOk } from "./gateway";
|
||||||
import type { Tab } from "./navigation";
|
import type { Tab } from "./navigation";
|
||||||
import type { ResolvedTheme, ThemeMode } from "./theme";
|
import type { ResolvedTheme, ThemeMode } from "./theme";
|
||||||
import type {
|
import type {
|
||||||
AgentsListResult,
|
AgentsListResult,
|
||||||
|
AgentsFilesListResult,
|
||||||
|
AgentIdentityResult,
|
||||||
ConfigSnapshot,
|
ConfigSnapshot,
|
||||||
ConfigUiHints,
|
ConfigUiHints,
|
||||||
CronJob,
|
CronJob,
|
||||||
|
|
@ -197,6 +200,23 @@ export class OpenClawApp extends LitElement {
|
||||||
@state() agentsLoading = false;
|
@state() agentsLoading = false;
|
||||||
@state() agentsList: AgentsListResult | null = null;
|
@state() agentsList: AgentsListResult | null = null;
|
||||||
@state() agentsError: string | null = null;
|
@state() agentsError: string | null = null;
|
||||||
|
@state() agentsSelectedId: string | null = null;
|
||||||
|
@state() agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron" =
|
||||||
|
"overview";
|
||||||
|
@state() agentFilesLoading = false;
|
||||||
|
@state() agentFilesError: string | null = null;
|
||||||
|
@state() agentFilesList: AgentsFilesListResult | null = null;
|
||||||
|
@state() agentFileContents: Record<string, string> = {};
|
||||||
|
@state() agentFileDrafts: Record<string, string> = {};
|
||||||
|
@state() agentFileActive: string | null = null;
|
||||||
|
@state() agentFileSaving = false;
|
||||||
|
@state() agentIdentityLoading = false;
|
||||||
|
@state() agentIdentityError: string | null = null;
|
||||||
|
@state() agentIdentityById: Record<string, AgentIdentityResult> = {};
|
||||||
|
@state() agentSkillsLoading = false;
|
||||||
|
@state() agentSkillsError: string | null = null;
|
||||||
|
@state() agentSkillsReport: SkillStatusReport | null = null;
|
||||||
|
@state() agentSkillsAgentId: string | null = null;
|
||||||
|
|
||||||
@state() sessionsLoading = false;
|
@state() sessionsLoading = false;
|
||||||
@state() sessionsResult: SessionsListResult | null = null;
|
@state() sessionsResult: SessionsListResult | null = null;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { LitElement, html, css } from "lit";
|
import { LitElement, css, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -44,7 +44,7 @@ export class ResizableDivider extends LitElement {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return html``;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import type { GatewayBrowserClient } from "../gateway";
|
||||||
|
import type {
|
||||||
|
AgentFileEntry,
|
||||||
|
AgentsFilesGetResult,
|
||||||
|
AgentsFilesListResult,
|
||||||
|
AgentsFilesSetResult,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
export type AgentFilesState = {
|
||||||
|
client: GatewayBrowserClient | null;
|
||||||
|
connected: boolean;
|
||||||
|
agentFilesLoading: boolean;
|
||||||
|
agentFilesError: string | null;
|
||||||
|
agentFilesList: AgentsFilesListResult | null;
|
||||||
|
agentFileContents: Record<string, string>;
|
||||||
|
agentFileDrafts: Record<string, string>;
|
||||||
|
agentFileActive: string | null;
|
||||||
|
agentFileSaving: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function mergeFileEntry(
|
||||||
|
list: AgentsFilesListResult | null,
|
||||||
|
entry: AgentFileEntry,
|
||||||
|
): AgentsFilesListResult | null {
|
||||||
|
if (!list) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
const hasEntry = list.files.some((file) => file.name === entry.name);
|
||||||
|
const nextFiles = hasEntry
|
||||||
|
? list.files.map((file) => (file.name === entry.name ? entry : file))
|
||||||
|
: [...list.files, entry];
|
||||||
|
return { ...list, files: nextFiles };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAgentFiles(state: AgentFilesState, agentId: string) {
|
||||||
|
if (!state.client || !state.connected || state.agentFilesLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.agentFilesLoading = true;
|
||||||
|
state.agentFilesError = null;
|
||||||
|
try {
|
||||||
|
const res = await state.client.request<AgentsFilesListResult | null>("agents.files.list", {
|
||||||
|
agentId,
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
state.agentFilesList = res;
|
||||||
|
if (state.agentFileActive && !res.files.some((file) => file.name === state.agentFileActive)) {
|
||||||
|
state.agentFileActive = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
state.agentFilesError = String(err);
|
||||||
|
} finally {
|
||||||
|
state.agentFilesLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAgentFileContent(state: AgentFilesState, agentId: string, name: string) {
|
||||||
|
if (!state.client || !state.connected || state.agentFilesLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Object.hasOwn(state.agentFileContents, name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.agentFilesLoading = true;
|
||||||
|
state.agentFilesError = null;
|
||||||
|
try {
|
||||||
|
const res = await state.client.request<AgentsFilesGetResult | null>("agents.files.get", {
|
||||||
|
agentId,
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
if (res?.file) {
|
||||||
|
const content = res.file.content ?? "";
|
||||||
|
state.agentFilesList = mergeFileEntry(state.agentFilesList, res.file);
|
||||||
|
state.agentFileContents = { ...state.agentFileContents, [name]: content };
|
||||||
|
if (!Object.hasOwn(state.agentFileDrafts, name)) {
|
||||||
|
state.agentFileDrafts = { ...state.agentFileDrafts, [name]: content };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
state.agentFilesError = String(err);
|
||||||
|
} finally {
|
||||||
|
state.agentFilesLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveAgentFile(
|
||||||
|
state: AgentFilesState,
|
||||||
|
agentId: string,
|
||||||
|
name: string,
|
||||||
|
content: string,
|
||||||
|
) {
|
||||||
|
if (!state.client || !state.connected || state.agentFileSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.agentFileSaving = true;
|
||||||
|
state.agentFilesError = null;
|
||||||
|
try {
|
||||||
|
const res = await state.client.request<AgentsFilesSetResult | null>("agents.files.set", {
|
||||||
|
agentId,
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
if (res?.file) {
|
||||||
|
state.agentFilesList = mergeFileEntry(state.agentFilesList, res.file);
|
||||||
|
state.agentFileContents = { ...state.agentFileContents, [name]: content };
|
||||||
|
state.agentFileDrafts = { ...state.agentFileDrafts, [name]: content };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
state.agentFilesError = String(err);
|
||||||
|
} finally {
|
||||||
|
state.agentFileSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import type { GatewayBrowserClient } from "../gateway";
|
||||||
|
import type { AgentIdentityResult } from "../types";
|
||||||
|
|
||||||
|
export type AgentIdentityState = {
|
||||||
|
client: GatewayBrowserClient | null;
|
||||||
|
connected: boolean;
|
||||||
|
agentIdentityLoading: boolean;
|
||||||
|
agentIdentityError: string | null;
|
||||||
|
agentIdentityById: Record<string, AgentIdentityResult>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadAgentIdentity(state: AgentIdentityState, agentId: string) {
|
||||||
|
if (!state.client || !state.connected || state.agentIdentityLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.agentIdentityById[agentId]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.agentIdentityLoading = true;
|
||||||
|
state.agentIdentityError = null;
|
||||||
|
try {
|
||||||
|
const res = await state.client.request<AgentIdentityResult | null>("agent.identity.get", {
|
||||||
|
agentId,
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
state.agentIdentityById = { ...state.agentIdentityById, [agentId]: res };
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
state.agentIdentityError = String(err);
|
||||||
|
} finally {
|
||||||
|
state.agentIdentityLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadAgentIdentities(state: AgentIdentityState, agentIds: string[]) {
|
||||||
|
if (!state.client || !state.connected || state.agentIdentityLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const missing = agentIds.filter((id) => !state.agentIdentityById[id]);
|
||||||
|
if (missing.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.agentIdentityLoading = true;
|
||||||
|
state.agentIdentityError = null;
|
||||||
|
try {
|
||||||
|
for (const agentId of missing) {
|
||||||
|
const res = await state.client.request<AgentIdentityResult | null>("agent.identity.get", {
|
||||||
|
agentId,
|
||||||
|
});
|
||||||
|
if (res) {
|
||||||
|
state.agentIdentityById = { ...state.agentIdentityById, [agentId]: res };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
state.agentIdentityError = String(err);
|
||||||
|
} finally {
|
||||||
|
state.agentIdentityLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type { GatewayBrowserClient } from "../gateway";
|
||||||
|
import type { SkillStatusReport } from "../types";
|
||||||
|
|
||||||
|
export type AgentSkillsState = {
|
||||||
|
client: GatewayBrowserClient | null;
|
||||||
|
connected: boolean;
|
||||||
|
agentSkillsLoading: boolean;
|
||||||
|
agentSkillsError: string | null;
|
||||||
|
agentSkillsReport: SkillStatusReport | null;
|
||||||
|
agentSkillsAgentId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function loadAgentSkills(state: AgentSkillsState, agentId: string) {
|
||||||
|
if (!state.client || !state.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.agentSkillsLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.agentSkillsLoading = true;
|
||||||
|
state.agentSkillsError = null;
|
||||||
|
try {
|
||||||
|
const res = await state.client.request("skills.status", { agentId });
|
||||||
|
if (res) {
|
||||||
|
state.agentSkillsReport = res as SkillStatusReport;
|
||||||
|
state.agentSkillsAgentId = agentId;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
state.agentSkillsError = String(err);
|
||||||
|
} finally {
|
||||||
|
state.agentSkillsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ export type AgentsState = {
|
||||||
agentsLoading: boolean;
|
agentsLoading: boolean;
|
||||||
agentsError: string | null;
|
agentsError: string | null;
|
||||||
agentsList: AgentsListResult | null;
|
agentsList: AgentsListResult | null;
|
||||||
|
agentsSelectedId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loadAgents(state: AgentsState) {
|
export async function loadAgents(state: AgentsState) {
|
||||||
|
|
@ -22,6 +23,11 @@ export async function loadAgents(state: AgentsState) {
|
||||||
const res = await state.client.request("agents.list", {});
|
const res = await state.client.request("agents.list", {});
|
||||||
if (res) {
|
if (res) {
|
||||||
state.agentsList = res;
|
state.agentsList = res;
|
||||||
|
const selected = state.agentsSelectedId;
|
||||||
|
const known = res.agents.some((entry) => entry.id === selected);
|
||||||
|
if (!selected || !known) {
|
||||||
|
state.agentsSelectedId = res.defaultId ?? res.agents[0]?.id ?? null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
state.agentsError = String(err);
|
state.agentsError = String(err);
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,12 @@ export const TAB_GROUPS = [
|
||||||
label: "Control",
|
label: "Control",
|
||||||
tabs: ["overview", "channels", "instances", "sessions", "cron"],
|
tabs: ["overview", "channels", "instances", "sessions", "cron"],
|
||||||
},
|
},
|
||||||
{ label: "Agent", tabs: ["skills", "nodes"] },
|
{ label: "Agent", tabs: ["agents", "skills", "nodes"] },
|
||||||
{ label: "Settings", tabs: ["config", "debug", "logs"] },
|
{ label: "Settings", tabs: ["config", "debug", "logs"] },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type Tab =
|
export type Tab =
|
||||||
|
| "agents"
|
||||||
| "overview"
|
| "overview"
|
||||||
| "channels"
|
| "channels"
|
||||||
| "instances"
|
| "instances"
|
||||||
|
|
@ -24,6 +25,7 @@ export type Tab =
|
||||||
| "logs";
|
| "logs";
|
||||||
|
|
||||||
const TAB_PATHS: Record<Tab, string> = {
|
const TAB_PATHS: Record<Tab, string> = {
|
||||||
|
agents: "/agents",
|
||||||
overview: "/overview",
|
overview: "/overview",
|
||||||
channels: "/channels",
|
channels: "/channels",
|
||||||
instances: "/instances",
|
instances: "/instances",
|
||||||
|
|
@ -120,6 +122,8 @@ export function inferBasePathFromPathname(pathname: string): string {
|
||||||
|
|
||||||
export function iconForTab(tab: Tab): IconName {
|
export function iconForTab(tab: Tab): IconName {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
|
case "agents":
|
||||||
|
return "folder";
|
||||||
case "chat":
|
case "chat":
|
||||||
return "messageSquare";
|
return "messageSquare";
|
||||||
case "overview":
|
case "overview":
|
||||||
|
|
@ -149,6 +153,8 @@ export function iconForTab(tab: Tab): IconName {
|
||||||
|
|
||||||
export function titleForTab(tab: Tab) {
|
export function titleForTab(tab: Tab) {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
|
case "agents":
|
||||||
|
return "Agents";
|
||||||
case "overview":
|
case "overview":
|
||||||
return "Overview";
|
return "Overview";
|
||||||
case "channels":
|
case "channels":
|
||||||
|
|
@ -178,6 +184,8 @@ export function titleForTab(tab: Tab) {
|
||||||
|
|
||||||
export function subtitleForTab(tab: Tab) {
|
export function subtitleForTab(tab: Tab) {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
|
case "agents":
|
||||||
|
return "Manage agent workspaces, tools, and identities.";
|
||||||
case "overview":
|
case "overview":
|
||||||
return "Gateway status, entry points, and a fast health read.";
|
return "Gateway status, entry points, and a fast health read.";
|
||||||
case "channels":
|
case "channels":
|
||||||
|
|
|
||||||
|
|
@ -340,6 +340,41 @@ export type AgentsListResult = {
|
||||||
agents: GatewayAgentRow[];
|
agents: GatewayAgentRow[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AgentIdentityResult = {
|
||||||
|
agentId: string;
|
||||||
|
name: string;
|
||||||
|
avatar: string;
|
||||||
|
emoji?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgentFileEntry = {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
missing: boolean;
|
||||||
|
size?: number;
|
||||||
|
updatedAtMs?: number;
|
||||||
|
content?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgentsFilesListResult = {
|
||||||
|
agentId: string;
|
||||||
|
workspace: string;
|
||||||
|
files: AgentFileEntry[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgentsFilesGetResult = {
|
||||||
|
agentId: string;
|
||||||
|
workspace: string;
|
||||||
|
file: AgentFileEntry;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgentsFilesSetResult = {
|
||||||
|
ok: true;
|
||||||
|
agentId: string;
|
||||||
|
workspace: string;
|
||||||
|
file: AgentFileEntry;
|
||||||
|
};
|
||||||
|
|
||||||
export type GatewaySessionRow = {
|
export type GatewaySessionRow = {
|
||||||
key: string;
|
key: string;
|
||||||
kind: "direct" | "group" | "global" | "unknown";
|
kind: "direct" | "group" | "global" | "unknown";
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -63,6 +63,46 @@ function resolveChannelValue(
|
||||||
return resolved ?? {};
|
return resolved ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EXTRA_CHANNEL_FIELDS = ["groupPolicy", "streamMode", "dmPolicy"] as const;
|
||||||
|
|
||||||
|
function formatExtraValue(raw: unknown): string {
|
||||||
|
if (raw == null) {
|
||||||
|
return "n/a";
|
||||||
|
}
|
||||||
|
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean") {
|
||||||
|
return String(raw);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.stringify(raw);
|
||||||
|
} catch {
|
||||||
|
return "n/a";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderExtraChannelFields(value: Record<string, unknown>) {
|
||||||
|
const entries = EXTRA_CHANNEL_FIELDS.flatMap((field) => {
|
||||||
|
if (!(field in value)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [[field, value[field]]] as Array<[string, unknown]>;
|
||||||
|
});
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="status-list" style="margin-top: 12px;">
|
||||||
|
${entries.map(
|
||||||
|
([field, raw]) => html`
|
||||||
|
<div>
|
||||||
|
<span class="label">${field}</span>
|
||||||
|
<span>${formatExtraValue(raw)}</span>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
export function renderChannelConfigForm(props: ChannelConfigFormProps) {
|
export function renderChannelConfigForm(props: ChannelConfigFormProps) {
|
||||||
const analysis = analyzeConfigSchema(props.schema);
|
const analysis = analyzeConfigSchema(props.schema);
|
||||||
const normalized = analysis.schema;
|
const normalized = analysis.schema;
|
||||||
|
|
@ -92,6 +132,7 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) {
|
||||||
onPatch: props.onPatch,
|
onPatch: props.onPatch,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
${renderExtraChannelFields(value)}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,42 @@ import type { SkillMessageMap } from "../controllers/skills";
|
||||||
import type { SkillStatusEntry, SkillStatusReport } from "../types";
|
import type { SkillStatusEntry, SkillStatusReport } from "../types";
|
||||||
import { clampText } from "../format";
|
import { clampText } from "../format";
|
||||||
|
|
||||||
|
type SkillGroup = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
skills: SkillStatusEntry[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const SKILL_SOURCE_GROUPS: Array<{ id: string; label: string; sources: string[] }> = [
|
||||||
|
{ id: "workspace", label: "Workspace Skills", sources: ["openclaw-workspace"] },
|
||||||
|
{ id: "built-in", label: "Built-in Skills", sources: ["openclaw-bundled"] },
|
||||||
|
{ id: "installed", label: "Installed Skills", sources: ["openclaw-managed"] },
|
||||||
|
{ id: "extra", label: "Extra Skills", sources: ["openclaw-extra"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
function groupSkills(skills: SkillStatusEntry[]): SkillGroup[] {
|
||||||
|
const groups = new Map<string, SkillGroup>();
|
||||||
|
for (const def of SKILL_SOURCE_GROUPS) {
|
||||||
|
groups.set(def.id, { id: def.id, label: def.label, skills: [] });
|
||||||
|
}
|
||||||
|
const other: SkillGroup = { id: "other", label: "Other Skills", skills: [] };
|
||||||
|
for (const skill of skills) {
|
||||||
|
const match = SKILL_SOURCE_GROUPS.find((group) => group.sources.includes(skill.source));
|
||||||
|
if (match) {
|
||||||
|
groups.get(match.id)?.skills.push(skill);
|
||||||
|
} else {
|
||||||
|
other.skills.push(skill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ordered = SKILL_SOURCE_GROUPS.map((group) => groups.get(group.id)).filter(
|
||||||
|
(group): group is SkillGroup => Boolean(group && group.skills.length > 0),
|
||||||
|
);
|
||||||
|
if (other.skills.length > 0) {
|
||||||
|
ordered.push(other);
|
||||||
|
}
|
||||||
|
return ordered;
|
||||||
|
}
|
||||||
|
|
||||||
export type SkillsProps = {
|
export type SkillsProps = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
report: SkillStatusReport | null;
|
report: SkillStatusReport | null;
|
||||||
|
|
@ -27,6 +63,7 @@ export function renderSkills(props: SkillsProps) {
|
||||||
[skill.name, skill.description, skill.source].join(" ").toLowerCase().includes(filter),
|
[skill.name, skill.description, skill.source].join(" ").toLowerCase().includes(filter),
|
||||||
)
|
)
|
||||||
: skills;
|
: skills;
|
||||||
|
const groups = groupSkills(filtered);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<section class="card">
|
<section class="card">
|
||||||
|
|
@ -64,8 +101,21 @@ export function renderSkills(props: SkillsProps) {
|
||||||
<div class="muted" style="margin-top: 16px">No skills found.</div>
|
<div class="muted" style="margin-top: 16px">No skills found.</div>
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<div class="list" style="margin-top: 16px;">
|
<div class="agent-skills-groups" style="margin-top: 16px;">
|
||||||
${filtered.map((skill) => renderSkill(skill, props))}
|
${groups.map((group) => {
|
||||||
|
const collapsedByDefault = group.id === "workspace" || group.id === "built-in";
|
||||||
|
return html`
|
||||||
|
<details class="agent-skills-group" ?open=${!collapsedByDefault}>
|
||||||
|
<summary class="agent-skills-header">
|
||||||
|
<span>${group.label}</span>
|
||||||
|
<span class="muted">${group.skills.length}</span>
|
||||||
|
</summary>
|
||||||
|
<div class="list skills-grid">
|
||||||
|
${group.skills.map((skill) => renderSkill(skill, props))}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue