mirror of https://github.com/openclaw/openclaw.git
build: refresh deps and vitest cache lanes
This commit is contained in:
parent
b49accc273
commit
10527ff8a3
|
|
@ -92,8 +92,8 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
|
|||
- `pnpm test:max` exposes that same planner profile for a full local run.
|
||||
- On supported local Node versions, including Node 25, the normal profile can use top-level lane parallelism. `pnpm test:max` still pushes the planner harder when you want a more aggressive local run.
|
||||
- The base Vitest config marks the wrapper manifests/config files as `forceRerunTriggers` so changed-mode reruns stay correct when scheduler inputs change.
|
||||
- Vitest's filesystem module cache is now enabled by default for Node-side test reruns.
|
||||
- Opt out with `OPENCLAW_VITEST_FS_MODULE_CACHE=0` or `OPENCLAW_VITEST_FS_MODULE_CACHE=false` if you suspect stale transform cache behavior.
|
||||
- The wrapper keeps `OPENCLAW_VITEST_FS_MODULE_CACHE` enabled on supported hosts, but assigns a lane-local `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH` so concurrent Vitest processes do not race on one shared experimental cache directory.
|
||||
- Set `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH=/abs/path` if you want one explicit cache location for direct single-run profiling.
|
||||
- Perf-debug note:
|
||||
- `pnpm test:perf:imports` enables Vitest import-duration reporting plus import-breakdown output.
|
||||
- `pnpm test:perf:imports:changed` scopes the same profiling view to files changed since `origin/main`.
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ For local PR land/gate checks, run:
|
|||
If `pnpm test` flakes on a loaded host, rerun once before treating it as a regression, then isolate with `pnpm vitest run <path/to/test>`. For memory-constrained hosts, use:
|
||||
|
||||
- `OPENCLAW_TEST_PROFILE=low OPENCLAW_TEST_SERIAL_GATEWAY=1 pnpm test`
|
||||
- `OPENCLAW_VITEST_FS_MODULE_CACHE=0 pnpm test:changed`
|
||||
- `OPENCLAW_VITEST_FS_MODULE_CACHE_PATH=/tmp/openclaw-vitest-cache pnpm test:changed`
|
||||
|
||||
## Model latency bench (local keys)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,16 @@
|
|||
"description": "OpenClaw diagnostics OpenTelemetry exporter",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.213.0",
|
||||
"@opentelemetry/exporter-logs-otlp-proto": "^0.213.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.213.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.213.0",
|
||||
"@opentelemetry/resources": "^2.6.0",
|
||||
"@opentelemetry/sdk-logs": "^0.213.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.6.0",
|
||||
"@opentelemetry/sdk-node": "^0.213.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.6.0",
|
||||
"@opentelemetry/api": "^1.9.1",
|
||||
"@opentelemetry/api-logs": "^0.214.0",
|
||||
"@opentelemetry/exporter-logs-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/exporter-metrics-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.214.0",
|
||||
"@opentelemetry/resources": "^2.6.1",
|
||||
"@opentelemetry/sdk-logs": "^0.214.0",
|
||||
"@opentelemetry/sdk-metrics": "^2.6.1",
|
||||
"@opentelemetry/sdk-node": "^0.214.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.6.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.40.0"
|
||||
},
|
||||
"openclaw": {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
"build:viewer": "bun build src/viewer-client.ts --target browser --format esm --minify --outfile assets/viewer-runtime.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pierre/diffs": "1.1.3",
|
||||
"@pierre/diffs": "1.1.5",
|
||||
"@sinclair/typebox": "0.34.48",
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@larksuiteoapi/node-sdk": "^1.59.0",
|
||||
"@larksuiteoapi/node-sdk": "^1.60.0",
|
||||
"@sinclair/typebox": "0.34.48",
|
||||
"https-proxy-agent": "^8.0.0",
|
||||
"zod": "^4.3.6"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"@matrix-org/matrix-sdk-crypto-nodejs": "^0.4.0",
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"markdown-it": "14.1.1",
|
||||
"matrix-js-sdk": "41.2.0-rc.0",
|
||||
"matrix-js-sdk": "41.2.0",
|
||||
"music-metadata": "^11.12.3",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"dependencies": {
|
||||
"@lancedb/lancedb": "^0.27.1",
|
||||
"@sinclair/typebox": "0.34.48",
|
||||
"openai": "^6.32.0"
|
||||
"openai": "^6.33.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
"description": "OpenClaw Microsoft Teams channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@microsoft/teams.api": "2.0.5",
|
||||
"@microsoft/teams.apps": "2.0.5",
|
||||
"@microsoft/teams.api": "2.0.6",
|
||||
"@microsoft/teams.apps": "2.0.6",
|
||||
"express": "^5.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
"description": "OpenClaw Tlon/Urbit channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.1014.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.1014.0",
|
||||
"@aws-sdk/client-s3": "3.1018.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.1018.0",
|
||||
"@tloncorp/tlon-skill": "0.3.0",
|
||||
"@urbit/aura": "^3.0.0",
|
||||
"zod": "^4.3.6"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"description": "OpenClaw Zalo channel plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"undici": "7.24.5",
|
||||
"undici": "7.24.6",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
44
package.json
44
package.json
|
|
@ -810,18 +810,18 @@
|
|||
"ui:install": "node scripts/ui.js install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.16.1",
|
||||
"@agentclientprotocol/sdk": "0.17.0",
|
||||
"@anthropic-ai/vertex-sdk": "^0.14.4",
|
||||
"@aws-sdk/client-bedrock": "^3.1014.0",
|
||||
"@aws-sdk/client-bedrock": "^3.1018.0",
|
||||
"@clack/prompts": "^1.1.0",
|
||||
"@homebridge/ciao": "^1.3.5",
|
||||
"@line/bot-sdk": "^10.6.0",
|
||||
"@lydell/node-pty": "1.2.0-beta.3",
|
||||
"@mariozechner/pi-agent-core": "0.61.1",
|
||||
"@mariozechner/pi-ai": "0.61.1",
|
||||
"@mariozechner/pi-coding-agent": "0.61.1",
|
||||
"@mariozechner/pi-tui": "0.61.1",
|
||||
"@modelcontextprotocol/sdk": "1.27.1",
|
||||
"@mariozechner/pi-agent-core": "0.63.0",
|
||||
"@mariozechner/pi-ai": "0.63.0",
|
||||
"@mariozechner/pi-coding-agent": "0.63.0",
|
||||
"@mariozechner/pi-tui": "0.63.0",
|
||||
"@modelcontextprotocol/sdk": "1.28.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@sinclair/typebox": "0.34.48",
|
||||
"ajv": "^8.18.0",
|
||||
|
|
@ -832,9 +832,9 @@
|
|||
"croner": "^10.0.1",
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"file-type": "21.3.4",
|
||||
"file-type": "22.0.0",
|
||||
"gaxios": "7.1.4",
|
||||
"hono": "4.12.8",
|
||||
"hono": "4.12.9",
|
||||
"ipaddr.js": "^2.3.0",
|
||||
"jiti": "^2.6.1",
|
||||
"json5": "^2.2.3",
|
||||
|
|
@ -849,9 +849,9 @@
|
|||
"qrcode-terminal": "^0.12.0",
|
||||
"sharp": "^0.34.5",
|
||||
"sqlite-vec": "0.1.7",
|
||||
"tar": "7.5.12",
|
||||
"tar": "7.5.13",
|
||||
"tslog": "^4.10.2",
|
||||
"undici": "^7.24.5",
|
||||
"undici": "^7.24.6",
|
||||
"uuid": "^13.0.0",
|
||||
"ws": "^8.20.0",
|
||||
"yaml": "^2.8.3",
|
||||
|
|
@ -866,19 +866,19 @@
|
|||
"@types/node": "^25.5.0",
|
||||
"@types/qrcode-terminal": "^0.12.2",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260322.1",
|
||||
"@vitest/coverage-v8": "^4.1.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260326.1",
|
||||
"@vitest/coverage-v8": "^4.1.2",
|
||||
"jscpd": "4.0.8",
|
||||
"jsdom": "^29.0.1",
|
||||
"lit": "^3.3.2",
|
||||
"oxfmt": "0.41.0",
|
||||
"oxlint": "^1.56.0",
|
||||
"oxlint-tsgolint": "^0.17.1",
|
||||
"oxfmt": "0.42.0",
|
||||
"oxlint": "^1.57.0",
|
||||
"oxlint-tsgolint": "^0.17.4",
|
||||
"signal-utils": "0.21.1",
|
||||
"tsdown": "0.21.4",
|
||||
"tsdown": "0.21.5",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^4.1.0"
|
||||
"typescript": "^6.0.2",
|
||||
"vitest": "^4.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@napi-rs/canvas": "^0.1.89",
|
||||
|
|
@ -899,18 +899,18 @@
|
|||
"pnpm": {
|
||||
"minimumReleaseAge": 2880,
|
||||
"overrides": {
|
||||
"hono": "4.12.8",
|
||||
"hono": "4.12.9",
|
||||
"@hono/node-server": "1.19.10",
|
||||
"fast-xml-parser": "5.5.7",
|
||||
"request": "npm:@cypress/request@3.0.10",
|
||||
"request-promise": "npm:@cypress/request-promise@5.0.0",
|
||||
"file-type": "21.3.4",
|
||||
"file-type": "22.0.0",
|
||||
"form-data": "2.5.4",
|
||||
"minimatch": "10.2.4",
|
||||
"qs": "6.14.2",
|
||||
"node-domexception": "npm:@nolyfill/domexception@^1.0.28",
|
||||
"@sinclair/typebox": "0.34.48",
|
||||
"tar": "7.5.12",
|
||||
"tar": "7.5.13",
|
||||
"tough-cookie": "4.1.3",
|
||||
"yauzl": "3.2.1"
|
||||
},
|
||||
|
|
|
|||
2483
pnpm-lock.yaml
2483
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -107,6 +107,53 @@ const getShardLabel = (args) => {
|
|||
return typeof args[shardIndex + 1] === "string" ? args[shardIndex + 1] : "";
|
||||
};
|
||||
|
||||
const normalizeEnvFlag = (value) => value?.trim().toLowerCase();
|
||||
|
||||
const isEnvFlagEnabled = (value) => {
|
||||
const normalized = normalizeEnvFlag(value);
|
||||
return normalized === "1" || normalized === "true";
|
||||
};
|
||||
|
||||
const isEnvFlagDisabled = (value) => {
|
||||
const normalized = normalizeEnvFlag(value);
|
||||
return normalized === "0" || normalized === "false";
|
||||
};
|
||||
|
||||
const isWindowsEnv = (env, platform = process.platform) => {
|
||||
if (platform === "win32") {
|
||||
return true;
|
||||
}
|
||||
return normalizeEnvFlag(env.RUNNER_OS) === "windows";
|
||||
};
|
||||
|
||||
const isFsModuleCacheEnabled = (env, platform = process.platform) => {
|
||||
if (isWindowsEnv(env, platform)) {
|
||||
return isEnvFlagEnabled(env.OPENCLAW_VITEST_FS_MODULE_CACHE);
|
||||
}
|
||||
return !isEnvFlagDisabled(env.OPENCLAW_VITEST_FS_MODULE_CACHE);
|
||||
};
|
||||
|
||||
export const resolveVitestFsModuleCachePath = ({
|
||||
cwd = process.cwd(),
|
||||
env = process.env,
|
||||
platform = process.platform,
|
||||
unitId = "",
|
||||
} = {}) => {
|
||||
const explicitPath = env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH?.trim();
|
||||
if (!isFsModuleCacheEnabled(env, platform)) {
|
||||
return undefined;
|
||||
}
|
||||
if (explicitPath) {
|
||||
return explicitPath;
|
||||
}
|
||||
return path.join(
|
||||
cwd,
|
||||
"node_modules",
|
||||
".experimental-vitest-cache",
|
||||
sanitizeArtifactName(unitId || "default"),
|
||||
);
|
||||
};
|
||||
|
||||
export function formatPlanOutput(plan) {
|
||||
return [
|
||||
`runtime=${plan.runtimeCapabilities.runtimeProfileName} mode=${plan.runtimeCapabilities.mode} intent=${plan.runtimeCapabilities.intentProfile} memoryBand=${plan.runtimeCapabilities.memoryBand} loadBand=${plan.runtimeCapabilities.loadBand} vitestMaxWorkers=${String(plan.executionBudget.vitestMaxWorkers ?? "default")} topLevelParallel=${plan.topLevelParallelEnabled ? String(plan.topLevelParallelLimit) : "off"}`,
|
||||
|
|
@ -458,14 +505,24 @@ export async function executePlan(plan, options = {}) {
|
|||
);
|
||||
};
|
||||
try {
|
||||
const childEnv = {
|
||||
...env,
|
||||
...unit.env,
|
||||
VITEST_GROUP: unit.id,
|
||||
NODE_OPTIONS: resolvedNodeOptions,
|
||||
};
|
||||
const vitestFsModuleCachePath = resolveVitestFsModuleCachePath({
|
||||
env: childEnv,
|
||||
platform: process.platform,
|
||||
unitId: unit.id,
|
||||
});
|
||||
if (vitestFsModuleCachePath) {
|
||||
childEnv.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH = vitestFsModuleCachePath;
|
||||
laneLogStream.write(`[test-parallel] fsModuleCachePath=${vitestFsModuleCachePath}\n`);
|
||||
}
|
||||
child = spawn(pnpmInvocation.command, spawnArgs, {
|
||||
stdio: ["inherit", "pipe", "pipe"],
|
||||
env: {
|
||||
...env,
|
||||
...unit.env,
|
||||
VITEST_GROUP: unit.id,
|
||||
NODE_OPTIONS: resolvedNodeOptions,
|
||||
},
|
||||
env: childEnv,
|
||||
shell: false,
|
||||
});
|
||||
captureTreeSample("spawn");
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ describe("compaction identifier-preservation instructions", () => {
|
|||
}
|
||||
|
||||
function firstSummaryInstructions() {
|
||||
return mockGenerateSummary.mock.calls[0]?.[5];
|
||||
return mockGenerateSummary.mock.calls[0]?.[6];
|
||||
}
|
||||
|
||||
it("injects identifier-preservation guidance even without custom instructions", async () => {
|
||||
|
|
@ -101,7 +101,7 @@ describe("compaction identifier-preservation instructions", () => {
|
|||
|
||||
expect(mockGenerateSummary.mock.calls.length).toBeGreaterThan(1);
|
||||
for (const call of mockGenerateSummary.mock.calls) {
|
||||
expect(call[5]).toContain("Preserve all opaque identifiers exactly as written");
|
||||
expect(call[6]).toContain("Preserve all opaque identifiers exactly as written");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ describe("compaction identifier-preservation instructions", () => {
|
|||
});
|
||||
|
||||
const mergedCall = mockGenerateSummary.mock.calls.at(-1);
|
||||
const instructions = mergedCall?.[5] ?? "";
|
||||
const instructions = mergedCall?.[6] ?? "";
|
||||
expect(instructions).toContain("Merge these partial summaries into a single cohesive summary.");
|
||||
expect(instructions).toContain("Prioritize customer-visible regressions.");
|
||||
expect((instructions.match(/Additional focus:/g) ?? []).length).toBe(1);
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ describe("compaction retry integration", () => {
|
|||
} as unknown as NonNullable<ExtensionContext["model"]>;
|
||||
|
||||
const invokeGenerateSummary = (signal = new AbortController().signal) =>
|
||||
mockGenerateSummary(testMessages, testModel, 1000, "test-api-key", signal);
|
||||
mockGenerateSummary(testMessages, testModel, 1000, "test-api-key", undefined, signal);
|
||||
|
||||
const runSummaryRetry = (options: Parameters<typeof retryAsync>[1]) =>
|
||||
retryAsync(() => invokeGenerateSummary(), options);
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ async function summarizeChunks(params: {
|
|||
messages: AgentMessage[];
|
||||
model: NonNullable<ExtensionContext["model"]>;
|
||||
apiKey: string;
|
||||
headers?: Record<string, string>;
|
||||
signal: AbortSignal;
|
||||
reserveTokens: number;
|
||||
maxChunkTokens: number;
|
||||
|
|
@ -239,6 +240,7 @@ async function summarizeChunks(params: {
|
|||
params.model,
|
||||
params.reserveTokens,
|
||||
params.apiKey,
|
||||
params.headers,
|
||||
params.signal,
|
||||
effectiveInstructions,
|
||||
summary,
|
||||
|
|
@ -265,6 +267,7 @@ export async function summarizeWithFallback(params: {
|
|||
messages: AgentMessage[];
|
||||
model: NonNullable<ExtensionContext["model"]>;
|
||||
apiKey: string;
|
||||
headers?: Record<string, string>;
|
||||
signal: AbortSignal;
|
||||
reserveTokens: number;
|
||||
maxChunkTokens: number;
|
||||
|
|
@ -334,6 +337,7 @@ export async function summarizeInStages(params: {
|
|||
messages: AgentMessage[];
|
||||
model: NonNullable<ExtensionContext["model"]>;
|
||||
apiKey: string;
|
||||
headers?: Record<string, string>;
|
||||
signal: AbortSignal;
|
||||
reserveTokens: number;
|
||||
maxChunkTokens: number;
|
||||
|
|
|
|||
|
|
@ -614,17 +614,29 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
|||
return { cancel: true };
|
||||
}
|
||||
|
||||
const apiKey = await ctx.modelRegistry.getApiKey(model);
|
||||
if (!apiKey) {
|
||||
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
||||
if (!auth.ok) {
|
||||
log.warn(
|
||||
"Compaction safeguard: no API key available; cancelling compaction to preserve history.",
|
||||
`Compaction safeguard: failed to resolve auth; cancelling compaction to preserve history. ${auth.error}`,
|
||||
);
|
||||
setCompactionSafeguardCancelReason(
|
||||
ctx.sessionManager,
|
||||
`Compaction safeguard could not resolve an API key for ${model.provider}/${model.id}.`,
|
||||
`Compaction safeguard could not resolve request auth for ${model.provider}/${model.id}: ${auth.error}`,
|
||||
);
|
||||
return { cancel: true };
|
||||
}
|
||||
if (!auth.apiKey && !auth.headers) {
|
||||
log.warn(
|
||||
"Compaction safeguard: no request auth available; cancelling compaction to preserve history.",
|
||||
);
|
||||
setCompactionSafeguardCancelReason(
|
||||
ctx.sessionManager,
|
||||
`Compaction safeguard could not resolve request auth for ${model.provider}/${model.id}.`,
|
||||
);
|
||||
return { cancel: true };
|
||||
}
|
||||
const apiKey = auth.apiKey ?? "";
|
||||
const headers = auth.headers;
|
||||
|
||||
try {
|
||||
const modelContextWindow = resolveContextWindowTokens(model);
|
||||
|
|
@ -688,6 +700,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
|||
messages: pruned.droppedMessagesList,
|
||||
model,
|
||||
apiKey,
|
||||
headers,
|
||||
signal,
|
||||
reserveTokens: Math.max(1, Math.floor(preparation.settings.reserveTokens)),
|
||||
maxChunkTokens: droppedMaxChunkTokens,
|
||||
|
|
@ -759,6 +772,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
|||
messages: messagesToSummarize,
|
||||
model,
|
||||
apiKey,
|
||||
headers,
|
||||
signal,
|
||||
reserveTokens,
|
||||
maxChunkTokens,
|
||||
|
|
@ -775,6 +789,7 @@ export default function compactionSafeguardExtension(api: ExtensionAPI): void {
|
|||
messages: turnPrefixMessages,
|
||||
model,
|
||||
apiKey,
|
||||
headers,
|
||||
signal,
|
||||
reserveTokens,
|
||||
maxChunkTokens,
|
||||
|
|
|
|||
|
|
@ -58,9 +58,15 @@ function buildEntry(name: string): SkillEntry {
|
|||
skill: {
|
||||
name,
|
||||
description: `${name} test skill`,
|
||||
source: "openclaw-workspace",
|
||||
filePath: path.join(skillDir, "SKILL.md"),
|
||||
baseDir: skillDir,
|
||||
sourceInfo: {
|
||||
path: path.join(skillDir, "SKILL.md"),
|
||||
source: "openclaw-workspace",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: skillDir,
|
||||
},
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
|
|
|||
|
|
@ -444,9 +444,9 @@ export async function installSkill(params: SkillInstallRequest): Promise<SkillIn
|
|||
// Warn when install is triggered from a non-bundled source.
|
||||
// Workspace/project/personal agent skills can contain attacker-controlled metadata.
|
||||
const trustedInstallSources = new Set(["openclaw-bundled", "openclaw-managed", "openclaw-extra"]);
|
||||
if (!trustedInstallSources.has(entry.skill.source)) {
|
||||
if (!trustedInstallSources.has(entry.skill.sourceInfo.source)) {
|
||||
warnings.push(
|
||||
`WARNING: Skill "${params.skillName}" install triggered from non-bundled source "${entry.skill.source}". Verify the install recipe is trusted.`,
|
||||
`WARNING: Skill "${params.skillName}" install triggered from non-bundled source "${entry.skill.sourceInfo.source}". Verify the install recipe is trusted.`,
|
||||
);
|
||||
}
|
||||
if (!spec) {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,15 @@ describe("buildWorkspaceSkillStatus", () => {
|
|||
skill: {
|
||||
name: "os-scoped",
|
||||
description: "test",
|
||||
source: "test",
|
||||
filePath: "/tmp/os-scoped",
|
||||
baseDir: "/tmp",
|
||||
sourceInfo: {
|
||||
path: "/tmp/os-scoped",
|
||||
source: "test",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: "/tmp",
|
||||
},
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ function buildSkillStatus(
|
|||
const bundled =
|
||||
bundledNames && bundledNames.size > 0
|
||||
? bundledNames.has(entry.skill.name)
|
||||
: entry.skill.source === "openclaw-bundled";
|
||||
: entry.skill.sourceInfo.source === "openclaw-bundled";
|
||||
|
||||
const { emoji, homepage, required, missing, requirementsSatisfied, configChecks } =
|
||||
evaluateEntryRequirementsForCurrentPlatform({
|
||||
|
|
@ -205,7 +205,7 @@ function buildSkillStatus(
|
|||
return {
|
||||
name: entry.skill.name,
|
||||
description: entry.skill.description,
|
||||
source: entry.skill.source,
|
||||
source: entry.skill.sourceInfo.source,
|
||||
bundled,
|
||||
filePath: entry.skill.filePath,
|
||||
baseDir: entry.skill.baseDir,
|
||||
|
|
|
|||
|
|
@ -22,9 +22,15 @@ function makeEntry(params: {
|
|||
skill: {
|
||||
name: params.name,
|
||||
description: `desc:${params.name}`,
|
||||
source: params.source ?? "openclaw-workspace",
|
||||
filePath: `/tmp/${params.name}/SKILL.md`,
|
||||
baseDir: `/tmp/${params.name}`,
|
||||
sourceInfo: {
|
||||
path: `/tmp/${params.name}/SKILL.md`,
|
||||
source: params.source ?? "openclaw-workspace",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: `/tmp/${params.name}`,
|
||||
},
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
|
|
|||
|
|
@ -17,7 +17,13 @@ describe("resolveSkillsPromptForRun", () => {
|
|||
description: "Demo",
|
||||
filePath: "/app/skills/demo-skill/SKILL.md",
|
||||
baseDir: "/app/skills/demo-skill",
|
||||
source: "openclaw-bundled",
|
||||
sourceInfo: {
|
||||
path: "/app/skills/demo-skill/SKILL.md",
|
||||
source: "openclaw-bundled",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: "/app/skills/demo-skill",
|
||||
},
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
frontmatter: {},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,13 @@ function makeSkill(name: string, desc = "A skill", filePath = `/skills/${name}/S
|
|||
description: desc,
|
||||
filePath,
|
||||
baseDir: `/skills/${name}`,
|
||||
source: "workspace",
|
||||
sourceInfo: {
|
||||
path: filePath,
|
||||
source: "workspace",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: `/skills/${name}`,
|
||||
},
|
||||
disableModelInvocation: false,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ function normalizeAllowlist(input: unknown): string[] | undefined {
|
|||
const BUNDLED_SOURCES = new Set(["openclaw-bundled"]);
|
||||
|
||||
function isBundledSkill(entry: SkillEntry): boolean {
|
||||
return BUNDLED_SOURCES.has(entry.skill.source);
|
||||
return BUNDLED_SOURCES.has(entry.skill.sourceInfo.source);
|
||||
}
|
||||
|
||||
export function resolveBundledAllowlist(config?: OpenClawConfig): string[] | undefined {
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
|
|||
Status: formatSkillStatus(skill),
|
||||
Skill: formatSkillName(skill),
|
||||
Description: theme.muted(skill.description),
|
||||
Source: skill.source ?? "",
|
||||
Source: skill.source,
|
||||
Missing: missing ? theme.warn(missing) : "",
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -36,9 +36,16 @@ describe("skills-cli (e2e)", () => {
|
|||
skill: {
|
||||
name: "peekaboo",
|
||||
description: "Capture UI screenshots",
|
||||
source: "openclaw-bundled",
|
||||
filePath: path.join(baseDir, "SKILL.md"),
|
||||
baseDir,
|
||||
sourceInfo: {
|
||||
path: path.join(baseDir, "SKILL.md"),
|
||||
source: "openclaw-bundled",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir,
|
||||
},
|
||||
disableModelInvocation: false,
|
||||
} as SkillEntry["skill"],
|
||||
frontmatter: {},
|
||||
metadata: { emoji: "📸" },
|
||||
|
|
|
|||
|
|
@ -1261,7 +1261,7 @@ export async function collectInstalledSkillsCodeSafetyFindings(params: {
|
|||
for (const workspaceDir of workspaceDirs) {
|
||||
const entries = loadWorkspaceSkillEntries(workspaceDir, { config: params.cfg });
|
||||
for (const entry of entries) {
|
||||
if (entry.skill.source === "openclaw-bundled") {
|
||||
if (entry.skill.sourceInfo.source === "openclaw-bundled") {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
createExecutionArtifacts,
|
||||
resolvePnpmCommandInvocation,
|
||||
resolveVitestFsModuleCachePath,
|
||||
} from "../../scripts/test-planner/executor.mjs";
|
||||
import {
|
||||
buildCIExecutionManifest,
|
||||
|
|
@ -375,3 +376,42 @@ describe("resolvePnpmCommandInvocation", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveVitestFsModuleCachePath", () => {
|
||||
it("uses a lane-local cache path by default on non-Windows hosts", () => {
|
||||
expect(
|
||||
resolveVitestFsModuleCachePath({
|
||||
cwd: "/repo",
|
||||
env: {},
|
||||
platform: "linux",
|
||||
unitId: "unit-fast-1",
|
||||
}),
|
||||
).toBe("/repo/node_modules/.experimental-vitest-cache/unit-fast-1");
|
||||
});
|
||||
|
||||
it("respects an explicit cache path override", () => {
|
||||
expect(
|
||||
resolveVitestFsModuleCachePath({
|
||||
cwd: "/repo",
|
||||
env: {
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/custom-vitest-cache",
|
||||
},
|
||||
platform: "linux",
|
||||
unitId: "unit-fast-1",
|
||||
}),
|
||||
).toBe("/tmp/custom-vitest-cache");
|
||||
});
|
||||
|
||||
it("does not force a cache path when the cache is disabled", () => {
|
||||
expect(
|
||||
resolveVitestFsModuleCachePath({
|
||||
cwd: "/repo",
|
||||
env: {
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE: "0",
|
||||
},
|
||||
platform: "linux",
|
||||
unitId: "unit-fast-1",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,22 @@ describe("loadVitestExperimentalConfig", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("passes through the filesystem module cache path when provided", () => {
|
||||
expect(
|
||||
loadVitestExperimentalConfig(
|
||||
{
|
||||
OPENCLAW_VITEST_FS_MODULE_CACHE_PATH: "/tmp/openclaw-vitest-cache",
|
||||
},
|
||||
"linux",
|
||||
),
|
||||
).toEqual({
|
||||
experimental: {
|
||||
fsModuleCache: true,
|
||||
fsModuleCachePath: "/tmp/openclaw-vitest-cache",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("disables the filesystem module cache by default on Windows", () => {
|
||||
expect(loadVitestExperimentalConfig({}, "win32")).toEqual({});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@
|
|||
"marked": "^17.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/browser-playwright": "4.1.0",
|
||||
"@vitest/browser-playwright": "4.1.2",
|
||||
"jsdom": "^29.0.1",
|
||||
"playwright": "^1.58.2",
|
||||
"vite": "8.0.1",
|
||||
"vitest": "4.1.0"
|
||||
"vite": "8.0.3",
|
||||
"vitest": "4.1.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,9 +109,8 @@ function renderCronFilterIcon(hiddenCount: number) {
|
|||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
${
|
||||
hiddenCount > 0
|
||||
? html`<span
|
||||
${hiddenCount > 0
|
||||
? html`<span
|
||||
style="
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
|
|
@ -124,10 +123,9 @@ function renderCronFilterIcon(hiddenCount: number) {
|
|||
padding: 1px 3px;
|
||||
pointer-events: none;
|
||||
"
|
||||
>${hiddenCount}</span
|
||||
>${hiddenCount}</span
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
: ""}
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
|
@ -158,9 +156,7 @@ export function renderChatSessionSelect(state: AppViewState) {
|
|||
group.options,
|
||||
(entry) => entry.key,
|
||||
(entry) =>
|
||||
html`<option value=${entry.key} title=${entry.title}>
|
||||
${entry.label}
|
||||
</option>`,
|
||||
html`<option value=${entry.key} title=${entry.title}>${entry.label}</option>`,
|
||||
)}
|
||||
</optgroup>`,
|
||||
)}
|
||||
|
|
@ -315,13 +311,11 @@ export function renderChatControls(state: AppViewState) {
|
|||
state.sessionsHideCron = !hideCron;
|
||||
}}
|
||||
aria-pressed=${hideCron}
|
||||
title=${
|
||||
hideCron
|
||||
? hiddenCronCount > 0
|
||||
? t("chat.showCronSessionsHidden", { count: String(hiddenCronCount) })
|
||||
: t("chat.showCronSessions")
|
||||
: t("chat.hideCronSessions")
|
||||
}
|
||||
title=${hideCron
|
||||
? hiddenCronCount > 0
|
||||
? t("chat.showCronSessionsHidden", { count: String(hiddenCronCount) })
|
||||
: t("chat.showCronSessions")
|
||||
: t("chat.hideCronSessions")}
|
||||
>
|
||||
${renderCronFilterIcon(hiddenCronCount)}
|
||||
</button>
|
||||
|
|
@ -398,14 +392,28 @@ export function renderChatMobileToggle(state: AppViewState) {
|
|||
title="Chat settings"
|
||||
aria-label="Chat settings"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
<path
|
||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="chat-controls-dropdown" @click=${(e: Event) => {
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<div
|
||||
class="chat-controls-dropdown"
|
||||
@click=${(e: Event) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div class="chat-controls">
|
||||
<label class="field chat-controls__session">
|
||||
<select
|
||||
|
|
@ -420,9 +428,7 @@ export function renderChatMobileToggle(state: AppViewState) {
|
|||
<optgroup label=${group.label}>
|
||||
${group.options.map(
|
||||
(opt) => html`
|
||||
<option value=${opt.key} title=${opt.title}>
|
||||
${opt.label}
|
||||
</option>
|
||||
<option value=${opt.key} title=${opt.title}>${opt.label}</option>
|
||||
`,
|
||||
)}
|
||||
</optgroup>
|
||||
|
|
@ -935,7 +941,9 @@ export function renderTopbarThemeModeToggle(state: AppViewState) {
|
|||
(opt) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="topbar-theme-mode__btn ${opt.id === state.themeMode ? "topbar-theme-mode__btn--active" : ""}"
|
||||
class="topbar-theme-mode__btn ${opt.id === state.themeMode
|
||||
? "topbar-theme-mode__btn--active"
|
||||
: ""}"
|
||||
title=${opt.label}
|
||||
aria-label="Color mode: ${opt.label}"
|
||||
aria-pressed=${opt.id === state.themeMode}
|
||||
|
|
@ -1019,19 +1027,22 @@ export function renderThemeToggle(state: AppViewState) {
|
|||
aria-haspopup="menu"
|
||||
aria-expanded="false"
|
||||
@click=${toggleOpen}
|
||||
>${currentThemeIcon(state.theme)}</button>
|
||||
>
|
||||
${currentThemeIcon(state.theme)}
|
||||
</button>
|
||||
<div class="theme-orb__menu" role="menu" aria-hidden="true">
|
||||
${THEME_OPTIONS.map(
|
||||
(opt) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="theme-orb__option ${opt.id === state.theme ? "theme-orb__option--active" : ""}"
|
||||
title=${opt.label}
|
||||
role="menuitemradio"
|
||||
aria-checked=${opt.id === state.theme}
|
||||
aria-label=${opt.label}
|
||||
@click=${(e: Event) => pick(opt, e)}
|
||||
>${opt.icon}</button>`,
|
||||
(opt) => html` <button
|
||||
type="button"
|
||||
class="theme-orb__option ${opt.id === state.theme ? "theme-orb__option--active" : ""}"
|
||||
title=${opt.label}
|
||||
role="menuitemradio"
|
||||
aria-checked=${opt.id === state.theme}
|
||||
aria-label=${opt.label}
|
||||
@click=${(e: Event) => pick(opt, e)}
|
||||
>
|
||||
${opt.icon}
|
||||
</button>`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -177,11 +177,9 @@ export function renderMessageGroup(
|
|||
<span class="chat-group-timestamp">${timestamp}</span>
|
||||
${renderMessageMeta(meta)}
|
||||
${normalizedRole === "assistant" && isTtsSupported() ? renderTtsButton(group) : nothing}
|
||||
${
|
||||
opts.onDelete
|
||||
? renderDeleteButton(opts.onDelete, normalizedRole === "user" ? "left" : "right")
|
||||
: nothing
|
||||
}
|
||||
${opts.onDelete
|
||||
? renderDeleteButton(opts.onDelete, normalizedRole === "user" ? "left" : "right")
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -387,7 +385,9 @@ function renderDeleteButton(onDelete: () => void, side: DeleteConfirmSide) {
|
|||
};
|
||||
requestAnimationFrame(() => document.addEventListener("click", closeOnOutside, true));
|
||||
}}
|
||||
>${icons.trash ?? icons.x}</button>
|
||||
>
|
||||
${icons.trash ?? icons.x}
|
||||
</button>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
|
@ -565,7 +565,9 @@ function renderCollapsedToolCards(
|
|||
<details class="chat-tools-collapse">
|
||||
<summary class="chat-tools-summary">
|
||||
<span class="chat-tools-summary__icon">${icons.zap}</span>
|
||||
<span class="chat-tools-summary__count">${totalTools} tool${totalTools === 1 ? "" : "s"}</span>
|
||||
<span class="chat-tools-summary__count"
|
||||
>${totalTools} tool${totalTools === 1 ? "" : "s"}</span
|
||||
>
|
||||
<span class="chat-tools-summary__names">${summaryLabel}</span>
|
||||
</summary>
|
||||
<div class="chat-tools-collapse__body">
|
||||
|
|
@ -693,80 +695,70 @@ function renderGroupedMessage(
|
|||
|
||||
return html`
|
||||
<div class="${bubbleClasses}">
|
||||
${
|
||||
hasActions
|
||||
? html`<div class="chat-bubble-actions">
|
||||
${canExpand ? renderExpandButton(markdown!, onOpenSidebar!) : nothing}
|
||||
${canCopyMarkdown ? renderCopyAsMarkdownButton(markdown!) : nothing}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
isToolMessage
|
||||
? html`
|
||||
${hasActions
|
||||
? html`<div class="chat-bubble-actions">
|
||||
${canExpand ? renderExpandButton(markdown!, onOpenSidebar!) : nothing}
|
||||
${canCopyMarkdown ? renderCopyAsMarkdownButton(markdown!) : nothing}
|
||||
</div>`
|
||||
: nothing}
|
||||
${isToolMessage
|
||||
? html`
|
||||
<details class="chat-tool-msg-collapse">
|
||||
<summary class="chat-tool-msg-summary">
|
||||
<span class="chat-tool-msg-summary__icon">${icons.zap}</span>
|
||||
<span class="chat-tool-msg-summary__label">Tool output</span>
|
||||
${
|
||||
toolSummaryLabel
|
||||
? html`<span class="chat-tool-msg-summary__names">${toolSummaryLabel}</span>`
|
||||
: toolPreview
|
||||
? html`<span class="chat-tool-msg-summary__preview">${toolPreview}</span>`
|
||||
: nothing
|
||||
}
|
||||
${toolSummaryLabel
|
||||
? html`<span class="chat-tool-msg-summary__names">${toolSummaryLabel}</span>`
|
||||
: toolPreview
|
||||
? html`<span class="chat-tool-msg-summary__preview">${toolPreview}</span>`
|
||||
: nothing}
|
||||
</summary>
|
||||
<div class="chat-tool-msg-body">
|
||||
${renderMessageImages(images)}
|
||||
${
|
||||
reasoningMarkdown
|
||||
? html`<div class="chat-thinking">${unsafeHTML(
|
||||
toSanitizedMarkdownHtml(reasoningMarkdown),
|
||||
)}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
jsonResult
|
||||
? html`<details class="chat-json-collapse">
|
||||
<summary class="chat-json-summary">
|
||||
<span class="chat-json-badge">JSON</span>
|
||||
<span class="chat-json-label">${jsonSummaryLabel(jsonResult.parsed)}</span>
|
||||
</summary>
|
||||
<pre class="chat-json-content"><code>${jsonResult.pretty}</code></pre>
|
||||
</details>`
|
||||
: markdown
|
||||
? html`<div class="chat-text" dir="${detectTextDirection(markdown)}">${unsafeHTML(toSanitizedMarkdownHtml(markdown))}</div>`
|
||||
: nothing
|
||||
}
|
||||
${reasoningMarkdown
|
||||
? html`<div class="chat-thinking">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(reasoningMarkdown))}
|
||||
</div>`
|
||||
: nothing}
|
||||
${jsonResult
|
||||
? html`<details class="chat-json-collapse">
|
||||
<summary class="chat-json-summary">
|
||||
<span class="chat-json-badge">JSON</span>
|
||||
<span class="chat-json-label">${jsonSummaryLabel(jsonResult.parsed)}</span>
|
||||
</summary>
|
||||
<pre class="chat-json-content"><code>${jsonResult.pretty}</code></pre>
|
||||
</details>`
|
||||
: markdown
|
||||
? html`<div class="chat-text" dir="${detectTextDirection(markdown)}">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(markdown))}
|
||||
</div>`
|
||||
: nothing}
|
||||
${hasToolCards ? renderCollapsedToolCards(toolCards, onOpenSidebar) : nothing}
|
||||
</div>
|
||||
</details>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
${renderMessageImages(images)}
|
||||
${
|
||||
reasoningMarkdown
|
||||
? html`<div class="chat-thinking">${unsafeHTML(
|
||||
toSanitizedMarkdownHtml(reasoningMarkdown),
|
||||
)}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
jsonResult
|
||||
? html`<details class="chat-json-collapse">
|
||||
<summary class="chat-json-summary">
|
||||
<span class="chat-json-badge">JSON</span>
|
||||
<span class="chat-json-label">${jsonSummaryLabel(jsonResult.parsed)}</span>
|
||||
</summary>
|
||||
<pre class="chat-json-content"><code>${jsonResult.pretty}</code></pre>
|
||||
</details>`
|
||||
: markdown
|
||||
? html`<div class="chat-text" dir="${detectTextDirection(markdown)}">${unsafeHTML(toSanitizedMarkdownHtml(markdown))}</div>`
|
||||
: nothing
|
||||
}
|
||||
${reasoningMarkdown
|
||||
? html`<div class="chat-thinking">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(reasoningMarkdown))}
|
||||
</div>`
|
||||
: nothing}
|
||||
${jsonResult
|
||||
? html`<details class="chat-json-collapse">
|
||||
<summary class="chat-json-summary">
|
||||
<span class="chat-json-badge">JSON</span>
|
||||
<span class="chat-json-label">${jsonSummaryLabel(jsonResult.parsed)}</span>
|
||||
</summary>
|
||||
<pre class="chat-json-content"><code>${jsonResult.pretty}</code></pre>
|
||||
</details>`
|
||||
: markdown
|
||||
? html`<div class="chat-text" dir="${detectTextDirection(markdown)}">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(markdown))}
|
||||
</div>`
|
||||
: nothing}
|
||||
${hasToolCards ? renderCollapsedToolCards(toolCards, onOpenSidebar) : nothing}
|
||||
`
|
||||
}
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,43 +81,35 @@ export function renderToolCardSidebar(card: ToolCard, onOpenSidebar?: (content:
|
|||
@click=${handleClick}
|
||||
role=${canClick ? "button" : nothing}
|
||||
tabindex=${canClick ? "0" : nothing}
|
||||
@keydown=${
|
||||
canClick
|
||||
? (e: KeyboardEvent) => {
|
||||
if (e.key !== "Enter" && e.key !== " ") {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
handleClick?.();
|
||||
@keydown=${canClick
|
||||
? (e: KeyboardEvent) => {
|
||||
if (e.key !== "Enter" && e.key !== " ") {
|
||||
return;
|
||||
}
|
||||
: nothing
|
||||
}
|
||||
e.preventDefault();
|
||||
handleClick?.();
|
||||
}
|
||||
: nothing}
|
||||
>
|
||||
<div class="chat-tool-card__header">
|
||||
<div class="chat-tool-card__title">
|
||||
<span class="chat-tool-card__icon">${icons[display.icon]}</span>
|
||||
<span>${display.label}</span>
|
||||
</div>
|
||||
${
|
||||
canClick
|
||||
? html`<span class="chat-tool-card__action">${hasText ? "View" : ""} ${icons.check}</span>`
|
||||
: nothing
|
||||
}
|
||||
${isEmpty && !canClick ? html`<span class="chat-tool-card__status">${icons.check}</span>` : nothing}
|
||||
${canClick
|
||||
? html`<span class="chat-tool-card__action"
|
||||
>${hasText ? "View" : ""} ${icons.check}</span
|
||||
>`
|
||||
: nothing}
|
||||
${isEmpty && !canClick
|
||||
? html`<span class="chat-tool-card__status">${icons.check}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
${detail ? html`<div class="chat-tool-card__detail">${detail}</div>` : nothing}
|
||||
${
|
||||
isEmpty
|
||||
? html`
|
||||
<div class="chat-tool-card__status-text muted">Completed</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
showCollapsed
|
||||
? html`<div class="chat-tool-card__preview mono">${getTruncatedPreview(card.text!)}</div>`
|
||||
: nothing
|
||||
}
|
||||
${isEmpty ? html` <div class="chat-tool-card__status-text muted">Completed</div> ` : nothing}
|
||||
${showCollapsed
|
||||
? html`<div class="chat-tool-card__preview mono">${getTruncatedPreview(card.text!)}</div>`
|
||||
: nothing}
|
||||
${showInline ? html`<div class="chat-tool-card__inline mono">${card.text}</div>` : nothing}
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ export class DashboardHeader extends LitElement {
|
|||
<div class="dashboard-header__breadcrumb">
|
||||
<span
|
||||
class="dashboard-header__breadcrumb-link"
|
||||
@click=${() => this.dispatchEvent(new CustomEvent("navigate", { detail: "overview", bubbles: true, composed: true }))}
|
||||
@click=${() =>
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate", { detail: "overview", bubbles: true, composed: true }),
|
||||
)}
|
||||
>
|
||||
OpenClaw
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -121,9 +121,7 @@ export const icons = {
|
|||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
`,
|
||||
check: html`
|
||||
<svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5" /></svg>
|
||||
`,
|
||||
check: html` <svg viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5" /></svg> `,
|
||||
arrowDown: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M12 5v14" />
|
||||
|
|
@ -144,8 +142,12 @@ export const icons = {
|
|||
`,
|
||||
brain: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z" />
|
||||
<path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z" />
|
||||
<path
|
||||
d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"
|
||||
/>
|
||||
<path
|
||||
d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"
|
||||
/>
|
||||
<path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4" />
|
||||
<path d="M17.599 6.5a3 3 0 0 0 .399-1.375" />
|
||||
<path d="M6.003 5.125A3 3 0 0 0 6.401 6.5" />
|
||||
|
|
@ -236,9 +238,7 @@ export const icons = {
|
|||
<path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z" />
|
||||
</svg>
|
||||
`,
|
||||
circle: html`
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /></svg>
|
||||
`,
|
||||
circle: html` <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /></svg> `,
|
||||
puzzle: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
|
|
@ -286,9 +286,7 @@ export const icons = {
|
|||
<path d="M22 2 11 13" />
|
||||
</svg>
|
||||
`,
|
||||
stop: html`
|
||||
<svg viewBox="0 0 24 24"><rect width="14" height="14" x="5" y="5" rx="1" /></svg>
|
||||
`,
|
||||
stop: html` <svg viewBox="0 0 24 24"><rect width="14" height="14" x="5" y="5" rx="1" /></svg> `,
|
||||
pin: html`
|
||||
<svg viewBox="0 0 24 24">
|
||||
<line x1="12" x2="12" y1="17" y2="22" />
|
||||
|
|
|
|||
|
|
@ -100,7 +100,9 @@ export function renderAgentOverview(params: {
|
|||
class="workspace-link mono"
|
||||
@click=${() => onSelectPanel("files")}
|
||||
title="Open Files tab"
|
||||
>${workspace}</button>
|
||||
>
|
||||
${workspace}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-kv">
|
||||
|
|
@ -113,13 +115,13 @@ export function renderAgentOverview(params: {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
configDirty
|
||||
? html`
|
||||
<div class="callout warn" style="margin-top: 16px">You have unsaved config changes.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${configDirty
|
||||
? html`
|
||||
<div class="callout warn" style="margin-top: 16px">
|
||||
You have unsaved config changes.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="agent-model-select" style="margin-top: 20px;">
|
||||
<div class="label">Model Selection</div>
|
||||
|
|
@ -132,29 +134,28 @@ export function renderAgentOverview(params: {
|
|||
@change=${(e: Event) =>
|
||||
onModelChange(agent.id, (e.target as HTMLSelectElement).value || null)}
|
||||
>
|
||||
${
|
||||
isDefault
|
||||
? html`
|
||||
<option value="">Not set</option>
|
||||
`
|
||||
: html`
|
||||
<option value="">
|
||||
${defaultPrimary ? `Inherit default (${defaultPrimary})` : "Inherit default"}
|
||||
</option>
|
||||
`
|
||||
}
|
||||
${isDefault
|
||||
? html` <option value="">Not set</option> `
|
||||
: html`
|
||||
<option value="">
|
||||
${defaultPrimary ? `Inherit default (${defaultPrimary})` : "Inherit default"}
|
||||
</option>
|
||||
`}
|
||||
${buildModelOptions(configForm, effectivePrimary ?? undefined, params.modelCatalog)}
|
||||
</select>
|
||||
</label>
|
||||
<div class="field">
|
||||
<span>Fallbacks</span>
|
||||
<div class="agent-chip-input" @click=${(e: Event) => {
|
||||
const container = e.currentTarget as HTMLElement;
|
||||
const input = container.querySelector("input");
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
}}>
|
||||
<div
|
||||
class="agent-chip-input"
|
||||
@click=${(e: Event) => {
|
||||
const container = e.currentTarget as HTMLElement;
|
||||
const input = container.querySelector("input");
|
||||
if (input) {
|
||||
input.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
${fallbackChips.map(
|
||||
(chip, i) => html`
|
||||
<span class="chip">
|
||||
|
|
@ -164,7 +165,9 @@ export function renderAgentOverview(params: {
|
|||
class="chip-remove"
|
||||
?disabled=${disabled}
|
||||
@click=${() => removeChip(i)}
|
||||
>×</button>
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
`,
|
||||
)}
|
||||
|
|
@ -185,7 +188,12 @@ export function renderAgentOverview(params: {
|
|||
</div>
|
||||
</div>
|
||||
<div class="agent-model-actions">
|
||||
<button type="button" class="btn btn--sm" ?disabled=${configLoading} @click=${onConfigReload}>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--sm"
|
||||
?disabled=${configLoading}
|
||||
@click=${onConfigReload}
|
||||
>
|
||||
Reload Config
|
||||
</button>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ function renderAgentContextCard(
|
|||
class="workspace-link mono"
|
||||
@click=${() => onSelectPanel("files")}
|
||||
title="Open Files tab"
|
||||
>${context.workspace}</button>
|
||||
>
|
||||
${context.workspace}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-kv">
|
||||
|
|
@ -161,7 +163,11 @@ export function renderAgentChannels(params: {
|
|||
: "never";
|
||||
return html`
|
||||
<section class="grid grid-cols-2">
|
||||
${renderAgentContextCard(params.context, "Workspace, identity, and model configuration.", params.onSelectPanel)}
|
||||
${renderAgentContextCard(
|
||||
params.context,
|
||||
"Workspace, identity, and model configuration.",
|
||||
params.onSelectPanel,
|
||||
)}
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content: space-between;">
|
||||
<div>
|
||||
|
|
@ -172,81 +178,67 @@ export function renderAgentChannels(params: {
|
|||
${params.loading ? "Refreshing…" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
<div class="muted" style="margin-top: 8px;">
|
||||
Last refresh: ${lastSuccessLabel}
|
||||
</div>
|
||||
${
|
||||
params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
!params.snapshot
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">Load channels to see live status.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
entries.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">No channels found.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${entries.map((entry) => {
|
||||
const summary = summarizeChannelAccounts(entry.accounts);
|
||||
const status = summary.total
|
||||
? `${summary.connected}/${summary.total} connected`
|
||||
: "no accounts";
|
||||
const configLabel = summary.configured
|
||||
? `${summary.configured} configured`
|
||||
: "not configured";
|
||||
const enabled = summary.total ? `${summary.enabled} enabled` : "disabled";
|
||||
const extras = resolveChannelExtrasFromConfig({
|
||||
configForm: params.configForm,
|
||||
channelId: entry.id,
|
||||
fields: CHANNEL_EXTRA_FIELDS,
|
||||
});
|
||||
return html`
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
<div class="list-title">${entry.label}</div>
|
||||
<div class="list-sub mono">${entry.id}</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<div>${status}</div>
|
||||
<div>${configLabel}</div>
|
||||
<div>${enabled}</div>
|
||||
${
|
||||
summary.configured === 0
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href="https://docs.openclaw.ai/channels"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="color: var(--accent); font-size: 12px"
|
||||
>Setup guide</a
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
extras.length > 0
|
||||
? extras.map(
|
||||
(extra) => html`<div>${extra.label}: ${extra.value}</div>`,
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="muted" style="margin-top: 8px;">Last refresh: ${lastSuccessLabel}</div>
|
||||
${params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing}
|
||||
${!params.snapshot
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load channels to see live status.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${entries.length === 0
|
||||
? html` <div class="muted" style="margin-top: 16px">No channels found.</div> `
|
||||
: html`
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${entries.map((entry) => {
|
||||
const summary = summarizeChannelAccounts(entry.accounts);
|
||||
const status = summary.total
|
||||
? `${summary.connected}/${summary.total} connected`
|
||||
: "no accounts";
|
||||
const configLabel = summary.configured
|
||||
? `${summary.configured} configured`
|
||||
: "not configured";
|
||||
const enabled = summary.total ? `${summary.enabled} enabled` : "disabled";
|
||||
const extras = resolveChannelExtrasFromConfig({
|
||||
configForm: params.configForm,
|
||||
channelId: entry.id,
|
||||
fields: CHANNEL_EXTRA_FIELDS,
|
||||
});
|
||||
return html`
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
<div class="list-title">${entry.label}</div>
|
||||
<div class="list-sub mono">${entry.id}</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
<div class="list-meta">
|
||||
<div>${status}</div>
|
||||
<div>${configLabel}</div>
|
||||
<div>${enabled}</div>
|
||||
${summary.configured === 0
|
||||
? html`
|
||||
<div>
|
||||
<a
|
||||
href="https://docs.openclaw.ai/channels"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style="color: var(--accent); font-size: 12px"
|
||||
>Setup guide</a
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${extras.length > 0
|
||||
? extras.map((extra) => html`<div>${extra.label}: ${extra.value}</div>`)
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
</section>
|
||||
</section>
|
||||
`;
|
||||
|
|
@ -266,7 +258,11 @@ export function renderAgentCron(params: {
|
|||
const jobs = params.jobs.filter((job) => job.agentId === params.agentId);
|
||||
return html`
|
||||
<section class="grid grid-cols-2">
|
||||
${renderAgentContextCard(params.context, "Workspace and scheduling targets.", params.onSelectPanel)}
|
||||
${renderAgentContextCard(
|
||||
params.context,
|
||||
"Workspace and scheduling targets.",
|
||||
params.onSelectPanel,
|
||||
)}
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content: space-between;">
|
||||
<div>
|
||||
|
|
@ -293,57 +289,51 @@ export function renderAgentCron(params: {
|
|||
<div class="stat-value">${formatNextRun(params.status?.nextWakeAtMs ?? null)}</div>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
${params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing}
|
||||
</section>
|
||||
</section>
|
||||
<section class="card">
|
||||
<div class="card-title">Agent Cron Jobs</div>
|
||||
<div class="card-sub">Scheduled jobs targeting this agent.</div>
|
||||
${
|
||||
jobs.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">No jobs assigned.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${jobs.map(
|
||||
(job) => html`
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
<div class="list-title">${job.name}</div>
|
||||
${
|
||||
job.description
|
||||
? html`<div class="list-sub">${job.description}</div>`
|
||||
: nothing
|
||||
}
|
||||
<div class="chip-row" style="margin-top: 6px;">
|
||||
<span class="chip">${formatCronSchedule(job)}</span>
|
||||
<span class="chip ${job.enabled ? "chip-ok" : "chip-warn"}">
|
||||
${job.enabled ? "enabled" : "disabled"}
|
||||
</span>
|
||||
<span class="chip">${job.sessionTarget}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<div class="mono">${formatCronState(job)}</div>
|
||||
<div class="muted">${formatCronPayload(job)}</div>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
style="margin-top: 6px;"
|
||||
?disabled=${!job.enabled}
|
||||
@click=${() => params.onRunNow(job.id)}
|
||||
>Run Now</button>
|
||||
${jobs.length === 0
|
||||
? html` <div class="muted" style="margin-top: 16px">No jobs assigned.</div> `
|
||||
: html`
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${jobs.map(
|
||||
(job) => html`
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
<div class="list-title">${job.name}</div>
|
||||
${job.description
|
||||
? html`<div class="list-sub">${job.description}</div>`
|
||||
: nothing}
|
||||
<div class="chip-row" style="margin-top: 6px;">
|
||||
<span class="chip">${formatCronSchedule(job)}</span>
|
||||
<span class="chip ${job.enabled ? "chip-ok" : "chip-warn"}">
|
||||
${job.enabled ? "enabled" : "disabled"}
|
||||
</span>
|
||||
<span class="chip">${job.sessionTarget}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
<div class="list-meta">
|
||||
<div class="mono">${formatCronState(job)}</div>
|
||||
<div class="muted">${formatCronPayload(job)}</div>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
style="margin-top: 6px;"
|
||||
?disabled=${!job.enabled}
|
||||
@click=${() => params.onRunNow(job.id)}
|
||||
>
|
||||
Run Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
|
@ -386,166 +376,168 @@ export function renderAgentFiles(params: {
|
|||
${params.agentFilesLoading ? "Loading…" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
${
|
||||
list
|
||||
? html`<div class="muted mono" style="margin-top: 8px;">Workspace: <span>${list.workspace}</span></div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
params.agentFilesError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.agentFilesError}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
!list
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the agent workspace files to edit core instructions.
|
||||
${list
|
||||
? html`<div class="muted mono" style="margin-top: 8px;">
|
||||
Workspace: <span>${list.workspace}</span>
|
||||
</div>`
|
||||
: nothing}
|
||||
${params.agentFilesError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${params.agentFilesError}
|
||||
</div>`
|
||||
: nothing}
|
||||
${!list
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the agent workspace files to edit core instructions.
|
||||
</div>
|
||||
`
|
||||
: files.length === 0
|
||||
? html` <div class="muted" style="margin-top: 16px">No files found.</div> `
|
||||
: html`
|
||||
<div class="agent-tabs" style="margin-top: 14px;">
|
||||
${files.map((file) => {
|
||||
const isActive = active === file.name;
|
||||
const label = file.name.replace(/\.md$/i, "");
|
||||
return html`
|
||||
<button
|
||||
class="agent-tab ${isActive ? "active" : ""} ${file.missing
|
||||
? "agent-tab--missing"
|
||||
: ""}"
|
||||
@click=${() => params.onSelectFile(file.name)}
|
||||
>
|
||||
${label}${file.missing
|
||||
? html` <span class="agent-tab-badge">missing</span> `
|
||||
: nothing}
|
||||
</button>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: files.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">No files found.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-tabs" style="margin-top: 14px;">
|
||||
${files.map((file) => {
|
||||
const isActive = active === file.name;
|
||||
const label = file.name.replace(/\.md$/i, "");
|
||||
return html`
|
||||
<button
|
||||
class="agent-tab ${isActive ? "active" : ""} ${file.missing ? "agent-tab--missing" : ""}"
|
||||
@click=${() => params.onSelectFile(file.name)}
|
||||
>${label}${
|
||||
file.missing
|
||||
? html`
|
||||
<span class="agent-tab-badge">missing</span>
|
||||
`
|
||||
: nothing
|
||||
}</button>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${
|
||||
!activeEntry
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">Select a file to edit.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-file-header" style="margin-top: 14px;">
|
||||
<div>
|
||||
<div class="agent-file-sub mono">${activeEntry.path}</div>
|
||||
${!activeEntry
|
||||
? html` <div class="muted" style="margin-top: 16px">Select a file to edit.</div> `
|
||||
: html`
|
||||
<div class="agent-file-header" style="margin-top: 14px;">
|
||||
<div>
|
||||
<div class="agent-file-sub mono">${activeEntry.path}</div>
|
||||
</div>
|
||||
<div class="agent-file-actions">
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
title="Preview rendered markdown"
|
||||
@click=${(e: Event) => {
|
||||
const btn = e.currentTarget as HTMLElement;
|
||||
const dialog = btn.closest(".card")?.querySelector("dialog");
|
||||
if (dialog) {
|
||||
dialog.showModal();
|
||||
}
|
||||
}}
|
||||
>
|
||||
${icons.eye} Preview
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${!isDirty}
|
||||
@click=${() => params.onFileReset(activeEntry.name)}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm primary"
|
||||
?disabled=${params.agentFileSaving || !isDirty}
|
||||
@click=${() => params.onFileSave(activeEntry.name)}
|
||||
>
|
||||
${params.agentFileSaving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${activeEntry.missing
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 10px">
|
||||
This file is missing. Saving will create it in the agent workspace.
|
||||
</div>
|
||||
<div class="agent-file-actions">
|
||||
`
|
||||
: nothing}
|
||||
<label class="field agent-file-field" style="margin-top: 12px;">
|
||||
<span>Content</span>
|
||||
<textarea
|
||||
class="agent-file-textarea"
|
||||
.value=${draft}
|
||||
@input=${(e: Event) =>
|
||||
params.onFileDraftChange(
|
||||
activeEntry.name,
|
||||
(e.target as HTMLTextAreaElement).value,
|
||||
)}
|
||||
></textarea>
|
||||
</label>
|
||||
<dialog
|
||||
class="md-preview-dialog"
|
||||
@click=${(e: Event) => {
|
||||
const dialog = e.currentTarget as HTMLDialogElement;
|
||||
if (e.target === dialog) {
|
||||
dialog.close();
|
||||
}
|
||||
}}
|
||||
@close=${(e: Event) => {
|
||||
const dialog = e.currentTarget as HTMLElement;
|
||||
dialog
|
||||
.querySelector(".md-preview-dialog__panel")
|
||||
?.classList.remove("fullscreen");
|
||||
}}
|
||||
>
|
||||
<div class="md-preview-dialog__panel">
|
||||
<div class="md-preview-dialog__header">
|
||||
<div class="md-preview-dialog__title mono">${activeEntry.name}</div>
|
||||
<div class="md-preview-dialog__actions">
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
title="Preview rendered markdown"
|
||||
class="btn btn--sm md-preview-expand-btn"
|
||||
title="Toggle fullscreen"
|
||||
@click=${(e: Event) => {
|
||||
const btn = e.currentTarget as HTMLElement;
|
||||
const dialog = btn.closest(".card")?.querySelector("dialog");
|
||||
if (dialog) {
|
||||
dialog.showModal();
|
||||
const panel = btn.closest(".md-preview-dialog__panel");
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
const isFullscreen = panel.classList.toggle("fullscreen");
|
||||
btn.classList.toggle("is-fullscreen", isFullscreen);
|
||||
}}
|
||||
>
|
||||
${icons.eye} Preview
|
||||
<span class="when-normal">${icons.maximize} Expand</span
|
||||
><span class="when-fullscreen">${icons.minimize} Collapse</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${!isDirty}
|
||||
@click=${() => params.onFileReset(activeEntry.name)}
|
||||
title="Edit file"
|
||||
@click=${(e: Event) => {
|
||||
(e.currentTarget as HTMLElement).closest("dialog")?.close();
|
||||
const textarea =
|
||||
document.querySelector<HTMLElement>(".agent-file-textarea");
|
||||
textarea?.focus();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
${icons.edit} Editor
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm primary"
|
||||
?disabled=${params.agentFileSaving || !isDirty}
|
||||
@click=${() => params.onFileSave(activeEntry.name)}
|
||||
class="btn btn--sm"
|
||||
@click=${(e: Event) => {
|
||||
(e.currentTarget as HTMLElement).closest("dialog")?.close();
|
||||
}}
|
||||
>
|
||||
${params.agentFileSaving ? "Saving…" : "Save"}
|
||||
${icons.x} Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
activeEntry.missing
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 10px">
|
||||
This file is missing. Saving will create it in the agent workspace.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<label class="field agent-file-field" style="margin-top: 12px;">
|
||||
<span>Content</span>
|
||||
<textarea
|
||||
class="agent-file-textarea"
|
||||
.value=${draft}
|
||||
@input=${(e: Event) =>
|
||||
params.onFileDraftChange(
|
||||
activeEntry.name,
|
||||
(e.target as HTMLTextAreaElement).value,
|
||||
)}
|
||||
></textarea>
|
||||
</label>
|
||||
<dialog
|
||||
class="md-preview-dialog"
|
||||
@click=${(e: Event) => {
|
||||
const dialog = e.currentTarget as HTMLDialogElement;
|
||||
if (e.target === dialog) {
|
||||
dialog.close();
|
||||
}
|
||||
}}
|
||||
@close=${(e: Event) => {
|
||||
const dialog = e.currentTarget as HTMLElement;
|
||||
dialog
|
||||
.querySelector(".md-preview-dialog__panel")
|
||||
?.classList.remove("fullscreen");
|
||||
}}
|
||||
>
|
||||
<div class="md-preview-dialog__panel">
|
||||
<div class="md-preview-dialog__header">
|
||||
<div class="md-preview-dialog__title mono">${activeEntry.name}</div>
|
||||
<div class="md-preview-dialog__actions">
|
||||
<button
|
||||
class="btn btn--sm md-preview-expand-btn"
|
||||
title="Toggle fullscreen"
|
||||
@click=${(e: Event) => {
|
||||
const btn = e.currentTarget as HTMLElement;
|
||||
const panel = btn.closest(".md-preview-dialog__panel");
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
const isFullscreen = panel.classList.toggle("fullscreen");
|
||||
btn.classList.toggle("is-fullscreen", isFullscreen);
|
||||
}}
|
||||
><span class="when-normal">${icons.maximize} Expand</span><span class="when-fullscreen">${icons.minimize} Collapse</span></button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
title="Edit file"
|
||||
@click=${(e: Event) => {
|
||||
(e.currentTarget as HTMLElement).closest("dialog")?.close();
|
||||
const textarea =
|
||||
document.querySelector<HTMLElement>(".agent-file-textarea");
|
||||
textarea?.focus();
|
||||
}}
|
||||
>${icons.edit} Editor</button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
@click=${(e: Event) => {
|
||||
(e.currentTarget as HTMLElement).closest("dialog")?.close();
|
||||
}}
|
||||
>${icons.x} Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="md-preview-dialog__body">
|
||||
${unsafeHTML(applyPreviewTheme(marked.parse(draft, { gfm: true, breaks: true }) as string, { sanitize: (h: string) => DOMPurify.sanitize(h) }))}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
`
|
||||
}
|
||||
`
|
||||
}
|
||||
<div class="md-preview-dialog__body">
|
||||
${unsafeHTML(
|
||||
applyPreviewTheme(
|
||||
marked.parse(draft, { gfm: true, breaks: true }) as string,
|
||||
{ sanitize: (h: string) => DOMPurify.sanitize(h) },
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
`}
|
||||
`}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,7 +183,11 @@ export function renderAgentTools(params: {
|
|||
<button class="btn btn--sm" ?disabled=${!editable} @click=${() => updateAll(false)}>
|
||||
Disable All
|
||||
</button>
|
||||
<button class="btn btn--sm" ?disabled=${params.configLoading} @click=${params.onConfigReload}>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${params.configLoading}
|
||||
@click=${params.onConfigReload}
|
||||
>
|
||||
Reload Config
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -196,49 +200,41 @@ export function renderAgentTools(params: {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
!params.configForm
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the gateway config to adjust tool profiles.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
hasAgentAllow
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
This agent is using an explicit allowlist in config. Tool overrides are managed in the Config tab.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
hasGlobalAllow
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Global tools.allow is set. Agent overrides cannot enable tools that are globally blocked.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
params.toolsCatalogLoading && !params.toolsCatalogResult && !params.toolsCatalogError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">Loading runtime tool catalog…</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
params.toolsCatalogError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Could not load runtime tool catalog. Showing built-in fallback list instead.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${!params.configForm
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the gateway config to adjust tool profiles.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${hasAgentAllow
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
This agent is using an explicit allowlist in config. Tool overrides are managed in the
|
||||
Config tab.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${hasGlobalAllow
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Global tools.allow is set. Agent overrides cannot enable tools that are globally
|
||||
blocked.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${params.toolsCatalogLoading && !params.toolsCatalogResult && !params.toolsCatalogError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">Loading runtime tool catalog…</div>
|
||||
`
|
||||
: nothing}
|
||||
${params.toolsCatalogError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Could not load runtime tool catalog. Showing built-in fallback list instead.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="agent-tools-meta" style="margin-top: 16px;">
|
||||
<div class="agent-kv">
|
||||
|
|
@ -249,16 +245,14 @@ export function renderAgentTools(params: {
|
|||
<div class="label">Source</div>
|
||||
<div>${profileSource}</div>
|
||||
</div>
|
||||
${
|
||||
params.configDirty
|
||||
? html`
|
||||
<div class="agent-kv">
|
||||
<div class="label">Status</div>
|
||||
<div class="mono">unsaved</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${params.configDirty
|
||||
? html`
|
||||
<div class="agent-kv">
|
||||
<div class="label">Status</div>
|
||||
<div class="mono">unsaved</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 18px;">
|
||||
|
|
@ -267,58 +261,60 @@ export function renderAgentTools(params: {
|
|||
What this agent can use in the current chat session.
|
||||
<span class="mono">${params.runtimeSessionKey || "no session"}</span>
|
||||
</div>
|
||||
${
|
||||
!params.runtimeSessionMatchesSelectedAgent
|
||||
${!params.runtimeSessionMatchesSelectedAgent
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Switch chat to this agent to view its live runtime tools.
|
||||
</div>
|
||||
`
|
||||
: params.toolsEffectiveLoading &&
|
||||
!params.toolsEffectiveResult &&
|
||||
!params.toolsEffectiveError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Switch chat to this agent to view its live runtime tools.
|
||||
</div>
|
||||
<div class="callout info" style="margin-top: 12px">Loading available tools…</div>
|
||||
`
|
||||
: params.toolsEffectiveLoading &&
|
||||
!params.toolsEffectiveResult &&
|
||||
!params.toolsEffectiveError
|
||||
: params.toolsEffectiveError
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">Loading available tools…</div>
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Could not load available tools for this session.
|
||||
</div>
|
||||
`
|
||||
: params.toolsEffectiveError
|
||||
: (params.toolsEffectiveResult?.groups?.length ?? 0) === 0
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Could not load available tools for this session.
|
||||
No tools are available for this session right now.
|
||||
</div>
|
||||
`
|
||||
: (params.toolsEffectiveResult?.groups?.length ?? 0) === 0
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
No tools are available for this session right now.
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-tools-grid" style="margin-top: 16px;">
|
||||
${params.toolsEffectiveResult?.groups.map(
|
||||
(group) => html`
|
||||
<div class="agent-tools-section">
|
||||
<div class="agent-tools-header">${group.label}</div>
|
||||
<div class="agent-tools-list">
|
||||
${group.tools.map((tool) => {
|
||||
return html`
|
||||
<div class="agent-tool-row">
|
||||
<div>
|
||||
<div class="agent-tool-title">${tool.label}</div>
|
||||
<div class="agent-tool-sub">${tool.description}</div>
|
||||
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px;">
|
||||
<span class="agent-pill">${renderEffectiveToolBadge(tool)}</span>
|
||||
</div>
|
||||
: html`
|
||||
<div class="agent-tools-grid" style="margin-top: 16px;">
|
||||
${params.toolsEffectiveResult?.groups.map(
|
||||
(group) => html`
|
||||
<div class="agent-tools-section">
|
||||
<div class="agent-tools-header">${group.label}</div>
|
||||
<div class="agent-tools-list">
|
||||
${group.tools.map((tool) => {
|
||||
return html`
|
||||
<div class="agent-tool-row">
|
||||
<div>
|
||||
<div class="agent-tool-title">${tool.label}</div>
|
||||
<div class="agent-tool-sub">${tool.description}</div>
|
||||
<div
|
||||
style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px;"
|
||||
>
|
||||
<span class="agent-pill"
|
||||
>${renderEffectiveToolBadge(tool)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<div class="agent-tools-presets" style="margin-top: 16px;">
|
||||
|
|
@ -347,43 +343,42 @@ export function renderAgentTools(params: {
|
|||
|
||||
<div class="agent-tools-grid" style="margin-top: 20px;">
|
||||
${toolSections.map(
|
||||
(section) =>
|
||||
html`
|
||||
<div class="agent-tools-section">
|
||||
<div class="agent-tools-header">
|
||||
${section.label}
|
||||
${
|
||||
section.source === "plugin" && section.pluginId
|
||||
? html`<span class="agent-pill" style="margin-left: 8px;">plugin:${section.pluginId}</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<div class="agent-tools-list">
|
||||
${section.tools.map((tool) => {
|
||||
const { allowed } = resolveAllowed(tool.id);
|
||||
return html`
|
||||
<div class="agent-tool-row">
|
||||
<div>
|
||||
<div class="agent-tool-title mono">${tool.label}</div>
|
||||
<div class="agent-tool-sub">${tool.description}</div>
|
||||
${renderToolBadges(section, tool)}
|
||||
</div>
|
||||
<label class="cfg-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${allowed}
|
||||
?disabled=${!editable}
|
||||
@change=${(e: Event) =>
|
||||
updateTool(tool.id, (e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span class="cfg-toggle__track"></span>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
(section) => html`
|
||||
<div class="agent-tools-section">
|
||||
<div class="agent-tools-header">
|
||||
${section.label}
|
||||
${section.source === "plugin" && section.pluginId
|
||||
? html`<span class="agent-pill" style="margin-left: 8px;"
|
||||
>plugin:${section.pluginId}</span
|
||||
>`
|
||||
: nothing}
|
||||
</div>
|
||||
`,
|
||||
<div class="agent-tools-list">
|
||||
${section.tools.map((tool) => {
|
||||
const { allowed } = resolveAllowed(tool.id);
|
||||
return html`
|
||||
<div class="agent-tool-row">
|
||||
<div>
|
||||
<div class="agent-tool-title mono">${tool.label}</div>
|
||||
<div class="agent-tool-sub">${tool.description}</div>
|
||||
${renderToolBadges(section, tool)}
|
||||
</div>
|
||||
<label class="cfg-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
.checked=${allowed}
|
||||
?disabled=${!editable}
|
||||
@change=${(e: Event) =>
|
||||
updateTool(tool.id, (e.target as HTMLInputElement).checked)}
|
||||
/>
|
||||
<span class="cfg-toggle__track"></span>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -435,16 +430,21 @@ export function renderAgentSkills(params: {
|
|||
<div class="card-title">Skills</div>
|
||||
<div class="card-sub">
|
||||
Per-agent skill allowlist and workspace skills.
|
||||
${
|
||||
totalCount > 0
|
||||
? html`<span class="mono">${enabledCount}/${totalCount}</span>`
|
||||
: nothing
|
||||
}
|
||||
${totalCount > 0
|
||||
? html`<span class="mono">${enabledCount}/${totalCount}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="gap: 8px; flex-wrap: wrap;">
|
||||
<div class="row" style="gap: 4px; border: 1px solid var(--border); border-radius: var(--radius-md); padding: 2px;">
|
||||
<button class="btn btn--sm" ?disabled=${!editable} @click=${() => params.onClear(params.agentId)}>
|
||||
<div
|
||||
class="row"
|
||||
style="gap: 4px; border: 1px solid var(--border); border-radius: var(--radius-md); padding: 2px;"
|
||||
>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${!editable}
|
||||
@click=${() => params.onClear(params.agentId)}
|
||||
>
|
||||
Enable All
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -463,7 +463,11 @@ export function renderAgentSkills(params: {
|
|||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn--sm" ?disabled=${params.configLoading} @click=${params.onConfigReload}>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${params.configLoading}
|
||||
@click=${params.onConfigReload}
|
||||
>
|
||||
Reload Config
|
||||
</button>
|
||||
<button class="btn btn--sm" ?disabled=${params.loading} @click=${params.onRefresh}>
|
||||
|
|
@ -479,40 +483,34 @@ export function renderAgentSkills(params: {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
!params.configForm
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the gateway config to set per-agent skills.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
usingAllowlist
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">This agent uses a custom skill allowlist.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
All skills are enabled. Disabling any skill will create a per-agent allowlist.
|
||||
</div>
|
||||
`
|
||||
}
|
||||
${
|
||||
!reportReady && !params.loading
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load skills for this agent to view workspace-specific entries.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
${!params.configForm
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load the gateway config to set per-agent skills.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${usingAllowlist
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
This agent uses a custom skill allowlist.
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
All skills are enabled. Disabling any skill will create a per-agent allowlist.
|
||||
</div>
|
||||
`}
|
||||
${!reportReady && !params.loading
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
Load skills for this agent to view workspace-specific entries.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${params.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.error}</div>`
|
||||
: nothing}
|
||||
|
||||
<div class="filters" style="margin-top: 14px;">
|
||||
<label class="field" style="flex: 1;">
|
||||
|
|
@ -528,25 +526,21 @@ export function renderAgentSkills(params: {
|
|||
<div class="muted">${filtered.length} shown</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
filtered.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">No skills found.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-skills-groups" style="margin-top: 16px;">
|
||||
${groups.map((group) =>
|
||||
renderAgentSkillGroup(group, {
|
||||
agentId: params.agentId,
|
||||
allowSet,
|
||||
usingAllowlist,
|
||||
editable,
|
||||
onToggle: params.onToggle,
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
${filtered.length === 0
|
||||
? html` <div class="muted" style="margin-top: 16px">No skills found.</div> `
|
||||
: html`
|
||||
<div class="agent-skills-groups" style="margin-top: 16px;">
|
||||
${groups.map((group) =>
|
||||
renderAgentSkillGroup(group, {
|
||||
agentId: params.agentId,
|
||||
allowSet,
|
||||
usingAllowlist,
|
||||
editable,
|
||||
onToggle: params.onToggle,
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
|
@ -602,16 +596,12 @@ function renderAgentSkillRow(
|
|||
<div class="list-title">${skill.emoji ? `${skill.emoji} ` : ""}${skill.name}</div>
|
||||
<div class="list-sub">${skill.description}</div>
|
||||
${renderSkillStatusChips({ skill })}
|
||||
${
|
||||
missing.length > 0
|
||||
? html`<div class="muted" style="margin-top: 6px;">Missing: ${missing.join(", ")}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
reasons.length > 0
|
||||
? html`<div class="muted" style="margin-top: 6px;">Reason: ${reasons.join(", ")}</div>`
|
||||
: nothing
|
||||
}
|
||||
${missing.length > 0
|
||||
? html`<div class="muted" style="margin-top: 6px;">Missing: ${missing.join(", ")}</div>`
|
||||
: nothing}
|
||||
${reasons.length > 0
|
||||
? html`<div class="muted" style="margin-top: 6px;">Reason: ${reasons.join(", ")}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<label class="cfg-toggle">
|
||||
|
|
|
|||
|
|
@ -154,195 +154,188 @@ export function renderAgents(props: AgentsProps) {
|
|||
?disabled=${props.loading || agents.length === 0}
|
||||
@change=${(e: Event) => props.onSelectAgent((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
${
|
||||
agents.length === 0
|
||||
? html`
|
||||
<option value="">No agents</option>
|
||||
`
|
||||
: agents.map(
|
||||
(agent) => html`
|
||||
<option value=${agent.id} ?selected=${agent.id === selectedId}>
|
||||
${normalizeAgentLabel(agent)}${agentBadgeText(agent.id, defaultId) ? ` (${agentBadgeText(agent.id, defaultId)})` : ""}
|
||||
</option>
|
||||
`,
|
||||
)
|
||||
}
|
||||
${agents.length === 0
|
||||
? html` <option value="">No agents</option> `
|
||||
: agents.map(
|
||||
(agent) => html`
|
||||
<option value=${agent.id} ?selected=${agent.id === selectedId}>
|
||||
${normalizeAgentLabel(agent)}${agentBadgeText(agent.id, defaultId)
|
||||
? ` (${agentBadgeText(agent.id, defaultId)})`
|
||||
: ""}
|
||||
</option>
|
||||
`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
<div class="agents-toolbar-actions">
|
||||
${
|
||||
selectedAgent
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--sm btn--ghost"
|
||||
@click=${() => void navigator.clipboard.writeText(selectedAgent.id)}
|
||||
title="Copy agent ID to clipboard"
|
||||
>Copy ID</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--sm btn--ghost"
|
||||
?disabled=${Boolean(defaultId && selectedAgent.id === defaultId)}
|
||||
@click=${() => props.onSetDefault(selectedAgent.id)}
|
||||
title=${defaultId && selectedAgent.id === defaultId ? "Already the default agent" : "Set as the default agent"}
|
||||
>${defaultId && selectedAgent.id === defaultId ? "Default" : "Set Default"}</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<button class="btn btn--sm agents-refresh-btn" ?disabled=${props.loading} @click=${props.onRefresh}>
|
||||
${selectedAgent
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--sm btn--ghost"
|
||||
@click=${() => void navigator.clipboard.writeText(selectedAgent.id)}
|
||||
title="Copy agent ID to clipboard"
|
||||
>
|
||||
Copy ID
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn--sm btn--ghost"
|
||||
?disabled=${Boolean(defaultId && selectedAgent.id === defaultId)}
|
||||
@click=${() => props.onSetDefault(selectedAgent.id)}
|
||||
title=${defaultId && selectedAgent.id === defaultId
|
||||
? "Already the default agent"
|
||||
: "Set as the default agent"}
|
||||
>
|
||||
${defaultId && selectedAgent.id === defaultId ? "Default" : "Set Default"}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
<button
|
||||
class="btn btn--sm agents-refresh-btn"
|
||||
?disabled=${props.loading}
|
||||
@click=${props.onRefresh}
|
||||
>
|
||||
${props.loading ? "Loading…" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
props.error
|
||||
? html`<div class="callout danger" style="margin-top: 8px;">${props.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
${props.error
|
||||
? html`<div class="callout danger" style="margin-top: 8px;">${props.error}</div>`
|
||||
: nothing}
|
||||
</section>
|
||||
<section class="agents-main">
|
||||
${
|
||||
!selectedAgent
|
||||
? html`
|
||||
<div class="card">
|
||||
<div class="card-title">Select an agent</div>
|
||||
<div class="card-sub">Pick an agent to inspect its workspace and tools.</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
${renderAgentTabs(props.activePanel, (panel) => props.onSelectPanel(panel), tabCounts)}
|
||||
${
|
||||
props.activePanel === "overview"
|
||||
? renderAgentOverview({
|
||||
agent: selectedAgent,
|
||||
basePath: props.basePath,
|
||||
defaultId,
|
||||
configForm: props.config.form,
|
||||
agentFilesList: props.agentFiles.list,
|
||||
agentIdentity: props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
agentIdentityError: props.agentIdentityError,
|
||||
agentIdentityLoading: props.agentIdentityLoading,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
modelCatalog: props.modelCatalog,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
onModelChange: props.onModelChange,
|
||||
onModelFallbacksChange: props.onModelFallbacksChange,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.activePanel === "files"
|
||||
? renderAgentFiles({
|
||||
agentId: selectedAgent.id,
|
||||
agentFilesList: props.agentFiles.list,
|
||||
agentFilesLoading: props.agentFiles.loading,
|
||||
agentFilesError: props.agentFiles.error,
|
||||
agentFileActive: props.agentFiles.active,
|
||||
agentFileContents: props.agentFiles.contents,
|
||||
agentFileDrafts: props.agentFiles.drafts,
|
||||
agentFileSaving: props.agentFiles.saving,
|
||||
onLoadFiles: props.onLoadFiles,
|
||||
onSelectFile: props.onSelectFile,
|
||||
onFileDraftChange: props.onFileDraftChange,
|
||||
onFileReset: props.onFileReset,
|
||||
onFileSave: props.onFileSave,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.activePanel === "tools"
|
||||
? renderAgentTools({
|
||||
agentId: selectedAgent.id,
|
||||
configForm: props.config.form,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
toolsCatalogLoading: props.toolsCatalog.loading,
|
||||
toolsCatalogError: props.toolsCatalog.error,
|
||||
toolsCatalogResult: props.toolsCatalog.result,
|
||||
toolsEffectiveLoading: props.toolsEffective.loading,
|
||||
toolsEffectiveError: props.toolsEffective.error,
|
||||
toolsEffectiveResult: props.toolsEffective.result,
|
||||
runtimeSessionKey: props.runtimeSessionKey,
|
||||
runtimeSessionMatchesSelectedAgent:
|
||||
props.runtimeSessionMatchesSelectedAgent,
|
||||
onProfileChange: props.onToolsProfileChange,
|
||||
onOverridesChange: props.onToolsOverridesChange,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.activePanel === "skills"
|
||||
? renderAgentSkills({
|
||||
agentId: selectedAgent.id,
|
||||
report: props.agentSkills.report,
|
||||
loading: props.agentSkills.loading,
|
||||
error: props.agentSkills.error,
|
||||
activeAgentId: props.agentSkills.agentId,
|
||||
configForm: props.config.form,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
filter: props.agentSkills.filter,
|
||||
onFilterChange: props.onSkillsFilterChange,
|
||||
onRefresh: props.onSkillsRefresh,
|
||||
onToggle: props.onAgentSkillToggle,
|
||||
onClear: props.onAgentSkillsClear,
|
||||
onDisableAll: props.onAgentSkillsDisableAll,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.activePanel === "channels"
|
||||
? renderAgentChannels({
|
||||
context: buildAgentContext(
|
||||
selectedAgent,
|
||||
props.config.form,
|
||||
props.agentFiles.list,
|
||||
defaultId,
|
||||
props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
),
|
||||
configForm: props.config.form,
|
||||
snapshot: props.channels.snapshot,
|
||||
loading: props.channels.loading,
|
||||
error: props.channels.error,
|
||||
lastSuccess: props.channels.lastSuccess,
|
||||
onRefresh: props.onChannelsRefresh,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.activePanel === "cron"
|
||||
? renderAgentCron({
|
||||
context: buildAgentContext(
|
||||
selectedAgent,
|
||||
props.config.form,
|
||||
props.agentFiles.list,
|
||||
defaultId,
|
||||
props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
),
|
||||
agentId: selectedAgent.id,
|
||||
jobs: props.cron.jobs,
|
||||
status: props.cron.status,
|
||||
loading: props.cron.loading,
|
||||
error: props.cron.error,
|
||||
onRefresh: props.onCronRefresh,
|
||||
onRunNow: props.onCronRunNow,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
}
|
||||
${!selectedAgent
|
||||
? html`
|
||||
<div class="card">
|
||||
<div class="card-title">Select an agent</div>
|
||||
<div class="card-sub">Pick an agent to inspect its workspace and tools.</div>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
${renderAgentTabs(
|
||||
props.activePanel,
|
||||
(panel) => props.onSelectPanel(panel),
|
||||
tabCounts,
|
||||
)}
|
||||
${props.activePanel === "overview"
|
||||
? renderAgentOverview({
|
||||
agent: selectedAgent,
|
||||
basePath: props.basePath,
|
||||
defaultId,
|
||||
configForm: props.config.form,
|
||||
agentFilesList: props.agentFiles.list,
|
||||
agentIdentity: props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
agentIdentityError: props.agentIdentityError,
|
||||
agentIdentityLoading: props.agentIdentityLoading,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
modelCatalog: props.modelCatalog,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
onModelChange: props.onModelChange,
|
||||
onModelFallbacksChange: props.onModelFallbacksChange,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing}
|
||||
${props.activePanel === "files"
|
||||
? renderAgentFiles({
|
||||
agentId: selectedAgent.id,
|
||||
agentFilesList: props.agentFiles.list,
|
||||
agentFilesLoading: props.agentFiles.loading,
|
||||
agentFilesError: props.agentFiles.error,
|
||||
agentFileActive: props.agentFiles.active,
|
||||
agentFileContents: props.agentFiles.contents,
|
||||
agentFileDrafts: props.agentFiles.drafts,
|
||||
agentFileSaving: props.agentFiles.saving,
|
||||
onLoadFiles: props.onLoadFiles,
|
||||
onSelectFile: props.onSelectFile,
|
||||
onFileDraftChange: props.onFileDraftChange,
|
||||
onFileReset: props.onFileReset,
|
||||
onFileSave: props.onFileSave,
|
||||
})
|
||||
: nothing}
|
||||
${props.activePanel === "tools"
|
||||
? renderAgentTools({
|
||||
agentId: selectedAgent.id,
|
||||
configForm: props.config.form,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
toolsCatalogLoading: props.toolsCatalog.loading,
|
||||
toolsCatalogError: props.toolsCatalog.error,
|
||||
toolsCatalogResult: props.toolsCatalog.result,
|
||||
toolsEffectiveLoading: props.toolsEffective.loading,
|
||||
toolsEffectiveError: props.toolsEffective.error,
|
||||
toolsEffectiveResult: props.toolsEffective.result,
|
||||
runtimeSessionKey: props.runtimeSessionKey,
|
||||
runtimeSessionMatchesSelectedAgent: props.runtimeSessionMatchesSelectedAgent,
|
||||
onProfileChange: props.onToolsProfileChange,
|
||||
onOverridesChange: props.onToolsOverridesChange,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
})
|
||||
: nothing}
|
||||
${props.activePanel === "skills"
|
||||
? renderAgentSkills({
|
||||
agentId: selectedAgent.id,
|
||||
report: props.agentSkills.report,
|
||||
loading: props.agentSkills.loading,
|
||||
error: props.agentSkills.error,
|
||||
activeAgentId: props.agentSkills.agentId,
|
||||
configForm: props.config.form,
|
||||
configLoading: props.config.loading,
|
||||
configSaving: props.config.saving,
|
||||
configDirty: props.config.dirty,
|
||||
filter: props.agentSkills.filter,
|
||||
onFilterChange: props.onSkillsFilterChange,
|
||||
onRefresh: props.onSkillsRefresh,
|
||||
onToggle: props.onAgentSkillToggle,
|
||||
onClear: props.onAgentSkillsClear,
|
||||
onDisableAll: props.onAgentSkillsDisableAll,
|
||||
onConfigReload: props.onConfigReload,
|
||||
onConfigSave: props.onConfigSave,
|
||||
})
|
||||
: nothing}
|
||||
${props.activePanel === "channels"
|
||||
? renderAgentChannels({
|
||||
context: buildAgentContext(
|
||||
selectedAgent,
|
||||
props.config.form,
|
||||
props.agentFiles.list,
|
||||
defaultId,
|
||||
props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
),
|
||||
configForm: props.config.form,
|
||||
snapshot: props.channels.snapshot,
|
||||
loading: props.channels.loading,
|
||||
error: props.channels.error,
|
||||
lastSuccess: props.channels.lastSuccess,
|
||||
onRefresh: props.onChannelsRefresh,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing}
|
||||
${props.activePanel === "cron"
|
||||
? renderAgentCron({
|
||||
context: buildAgentContext(
|
||||
selectedAgent,
|
||||
props.config.form,
|
||||
props.agentFiles.list,
|
||||
defaultId,
|
||||
props.agentIdentityById[selectedAgent.id] ?? null,
|
||||
),
|
||||
agentId: selectedAgent.id,
|
||||
jobs: props.cron.jobs,
|
||||
status: props.cron.status,
|
||||
loading: props.cron.loading,
|
||||
error: props.cron.error,
|
||||
onRefresh: props.onCronRefresh,
|
||||
onRunNow: props.onCronRunNow,
|
||||
onSelectPanel: props.onSelectPanel,
|
||||
})
|
||||
: nothing}
|
||||
`}
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -370,7 +363,9 @@ function renderAgentTabs(
|
|||
type="button"
|
||||
@click=${() => onSelect(tab.id)}
|
||||
>
|
||||
${tab.label}${counts[tab.id] != null ? html`<span class="agent-tab-count">${counts[tab.id]}</span>` : nothing}
|
||||
${tab.label}${counts[tab.id] != null
|
||||
? html`<span class="agent-tab-count">${counts[tab.id]}</span>`
|
||||
: nothing}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -86,15 +86,11 @@ export function renderChannelConfigForm(props: ChannelConfigFormProps) {
|
|||
const analysis = analyzeConfigSchema(props.schema);
|
||||
const normalized = analysis.schema;
|
||||
if (!normalized) {
|
||||
return html`
|
||||
<div class="callout danger">Schema unavailable. Use Raw.</div>
|
||||
`;
|
||||
return html` <div class="callout danger">Schema unavailable. Use Raw.</div> `;
|
||||
}
|
||||
const node = resolveSchemaNode(normalized, ["channels", props.channelId]);
|
||||
if (!node) {
|
||||
return html`
|
||||
<div class="callout danger">Channel config schema unavailable.</div>
|
||||
`;
|
||||
return html` <div class="callout danger">Channel config schema unavailable.</div> `;
|
||||
}
|
||||
const configValue = props.configValue ?? {};
|
||||
const value = resolveChannelValue(configValue, props.channelId);
|
||||
|
|
@ -120,20 +116,16 @@ export function renderChannelConfigSection(params: { channelId: string; props: C
|
|||
const disabled = props.configSaving || props.configSchemaLoading;
|
||||
return html`
|
||||
<div style="margin-top: 16px;">
|
||||
${
|
||||
props.configSchemaLoading
|
||||
? html`
|
||||
<div class="muted">Loading config schema…</div>
|
||||
`
|
||||
: renderChannelConfigForm({
|
||||
channelId,
|
||||
configValue: props.configForm,
|
||||
schema: props.configSchema,
|
||||
uiHints: props.configUiHints,
|
||||
disabled,
|
||||
onPatch: props.onConfigPatch,
|
||||
})
|
||||
}
|
||||
${props.configSchemaLoading
|
||||
? html` <div class="muted">Loading config schema…</div> `
|
||||
: renderChannelConfigForm({
|
||||
channelId,
|
||||
configValue: props.configForm,
|
||||
schema: props.configSchema,
|
||||
uiHints: props.configUiHints,
|
||||
disabled,
|
||||
onPatch: props.onConfigPatch,
|
||||
})}
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button
|
||||
class="btn primary"
|
||||
|
|
@ -142,11 +134,7 @@ export function renderChannelConfigSection(params: { channelId: string; props: C
|
|||
>
|
||||
${props.configSaving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${disabled}
|
||||
@click=${() => props.onConfigReload()}
|
||||
>
|
||||
<button class="btn" ?disabled=${disabled} @click=${() => props.onConfigReload()}>
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -36,15 +36,13 @@ export function renderDiscordCard(params: {
|
|||
lastError: discord?.lastError,
|
||||
secondaryCallout: discord?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${discord.probe.ok ? "ok" : "failed"} ·
|
||||
${discord.probe.status ?? ""} ${discord.probe.error ?? ""}
|
||||
Probe ${discord.probe.ok ? "ok" : "failed"} · ${discord.probe.status ?? ""}
|
||||
${discord.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "discord", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>Probe</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,15 +46,13 @@ export function renderGoogleChatCard(params: {
|
|||
lastError: googleChat?.lastError,
|
||||
secondaryCallout: googleChat?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${googleChat.probe.ok ? "ok" : "failed"} ·
|
||||
${googleChat.probe.status ?? ""} ${googleChat.probe.error ?? ""}
|
||||
Probe ${googleChat.probe.ok ? "ok" : "failed"} · ${googleChat.probe.status ?? ""}
|
||||
${googleChat.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "googlechat", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>Probe</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,15 +36,12 @@ export function renderIMessageCard(params: {
|
|||
lastError: imessage?.lastError,
|
||||
secondaryCallout: imessage?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${imessage.probe.ok ? "ok" : "failed"} ·
|
||||
${imessage.probe.error ?? ""}
|
||||
Probe ${imessage.probe.ok ? "ok" : "failed"} · ${imessage.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "imessage", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>Probe</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,8 +108,16 @@ export function renderNostrProfileForm(params: {
|
|||
}}
|
||||
?disabled=${state.saving}
|
||||
></textarea>
|
||||
${help ? html`<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">${help}</div>` : nothing}
|
||||
${error ? html`<div style="font-size: 12px; color: var(--danger-color); margin-top: 2px;">${error}</div>` : nothing}
|
||||
${help
|
||||
? html`<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
||||
${help}
|
||||
</div>`
|
||||
: nothing}
|
||||
${error
|
||||
? html`<div style="font-size: 12px; color: var(--danger-color); margin-top: 2px;">
|
||||
${error}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -132,8 +140,16 @@ export function renderNostrProfileForm(params: {
|
|||
}}
|
||||
?disabled=${state.saving}
|
||||
/>
|
||||
${help ? html`<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">${help}</div>` : nothing}
|
||||
${error ? html`<div style="font-size: 12px; color: var(--danger-color); margin-top: 2px;">${error}</div>` : nothing}
|
||||
${help
|
||||
? html`<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">
|
||||
${help}
|
||||
</div>`
|
||||
: nothing}
|
||||
${error
|
||||
? html`<div style="font-size: 12px; color: var(--danger-color); margin-top: 2px;">
|
||||
${error}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
|
@ -164,82 +180,75 @@ export function renderNostrProfileForm(params: {
|
|||
};
|
||||
|
||||
return html`
|
||||
<div class="nostr-profile-form" style="padding: 16px; background: var(--bg-secondary); border-radius: var(--radius-md); margin-top: 12px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<div
|
||||
class="nostr-profile-form"
|
||||
style="padding: 16px; background: var(--bg-secondary); border-radius: var(--radius-md); margin-top: 12px;"
|
||||
>
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;"
|
||||
>
|
||||
<div style="font-weight: 600; font-size: 16px;">Edit Profile</div>
|
||||
<div style="font-size: 12px; color: var(--text-muted);">Account: ${accountId}</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
state.error
|
||||
? html`<div class="callout danger" style="margin-bottom: 12px;">${state.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
state.success
|
||||
? html`<div class="callout success" style="margin-bottom: 12px;">${state.success}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${state.error
|
||||
? html`<div class="callout danger" style="margin-bottom: 12px;">${state.error}</div>`
|
||||
: nothing}
|
||||
${state.success
|
||||
? html`<div class="callout success" style="margin-bottom: 12px;">${state.success}</div>`
|
||||
: nothing}
|
||||
${renderPicturePreview()}
|
||||
|
||||
${renderField("name", "Username", {
|
||||
placeholder: "satoshi",
|
||||
maxLength: 256,
|
||||
help: "Short username (e.g., satoshi)",
|
||||
})}
|
||||
|
||||
${renderField("displayName", "Display Name", {
|
||||
placeholder: "Satoshi Nakamoto",
|
||||
maxLength: 256,
|
||||
help: "Your full display name",
|
||||
})}
|
||||
|
||||
${renderField("about", "Bio", {
|
||||
type: "textarea",
|
||||
placeholder: "Tell people about yourself...",
|
||||
maxLength: 2000,
|
||||
help: "A brief bio or description",
|
||||
})}
|
||||
|
||||
${renderField("picture", "Avatar URL", {
|
||||
type: "url",
|
||||
placeholder: "https://example.com/avatar.jpg",
|
||||
help: "HTTPS URL to your profile picture",
|
||||
})}
|
||||
|
||||
${
|
||||
state.showAdvanced
|
||||
? html`
|
||||
<div style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;">
|
||||
<div style="font-weight: 500; margin-bottom: 12px; color: var(--text-muted);">Advanced</div>
|
||||
${state.showAdvanced
|
||||
? html`
|
||||
<div
|
||||
style="border-top: 1px solid var(--border-color); padding-top: 12px; margin-top: 12px;"
|
||||
>
|
||||
<div style="font-weight: 500; margin-bottom: 12px; color: var(--text-muted);">
|
||||
Advanced
|
||||
</div>
|
||||
|
||||
${renderField("banner", "Banner URL", {
|
||||
type: "url",
|
||||
placeholder: "https://example.com/banner.jpg",
|
||||
help: "HTTPS URL to a banner image",
|
||||
})}
|
||||
|
||||
${renderField("website", "Website", {
|
||||
type: "url",
|
||||
placeholder: "https://example.com",
|
||||
help: "Your personal website",
|
||||
})}
|
||||
|
||||
${renderField("nip05", "NIP-05 Identifier", {
|
||||
placeholder: "you@example.com",
|
||||
help: "Verifiable identifier (e.g., you@domain.com)",
|
||||
})}
|
||||
|
||||
${renderField("lud16", "Lightning Address", {
|
||||
placeholder: "you@getalby.com",
|
||||
help: "Lightning address for tips (LUD-16)",
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
|
||||
<div style="display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap;">
|
||||
<button
|
||||
|
|
@ -258,31 +267,20 @@ export function renderNostrProfileForm(params: {
|
|||
${state.importing ? "Importing..." : "Import from Relays"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn"
|
||||
@click=${callbacks.onToggleAdvanced}
|
||||
>
|
||||
<button class="btn" @click=${callbacks.onToggleAdvanced}>
|
||||
${state.showAdvanced ? "Hide Advanced" : "Show Advanced"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn"
|
||||
@click=${callbacks.onCancel}
|
||||
?disabled=${state.saving}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn" @click=${callbacks.onCancel} ?disabled=${state.saving}>Cancel</button>
|
||||
</div>
|
||||
|
||||
${
|
||||
isDirty
|
||||
? html`
|
||||
<div style="font-size: 12px; color: var(--warning-color); margin-top: 8px">
|
||||
You have unsaved changes
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${isDirty
|
||||
? html`
|
||||
<div style="font-size: 12px; color: var(--warning-color); margin-top: 8px">
|
||||
You have unsaved changes
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,15 +79,15 @@ export function renderNostrCard(params: {
|
|||
</div>
|
||||
<div>
|
||||
<span class="label">Last inbound</span>
|
||||
<span>${account.lastInboundAt ? formatRelativeTimestamp(account.lastInboundAt) : "n/a"}</span>
|
||||
<span
|
||||
>${account.lastInboundAt
|
||||
? formatRelativeTimestamp(account.lastInboundAt)
|
||||
: "n/a"}</span
|
||||
>
|
||||
</div>
|
||||
${
|
||||
account.lastError
|
||||
? html`
|
||||
<div class="account-card-error">${account.lastError}</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${account.lastError
|
||||
? html` <div class="account-card-error">${account.lastError}</div> `
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -121,12 +121,15 @@ export function renderNostrCard(params: {
|
|||
const hasAnyProfileData = name || displayName || about || picture || nip05;
|
||||
|
||||
return html`
|
||||
<div style="margin-top: 16px; padding: 12px; background: var(--bg-secondary); border-radius: var(--radius-md);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<div
|
||||
style="margin-top: 16px; padding: 12px; background: var(--bg-secondary); border-radius: var(--radius-md);"
|
||||
>
|
||||
<div
|
||||
style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"
|
||||
>
|
||||
<div style="font-weight: 500;">Profile</div>
|
||||
${
|
||||
summaryConfigured
|
||||
? html`
|
||||
${summaryConfigured
|
||||
? html`
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
@click=${onEditProfile}
|
||||
|
|
@ -135,16 +138,13 @@ export function renderNostrCard(params: {
|
|||
Edit Profile
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</div>
|
||||
${
|
||||
hasAnyProfileData
|
||||
? html`
|
||||
${hasAnyProfileData
|
||||
? html`
|
||||
<div class="status-list">
|
||||
${
|
||||
picture
|
||||
? html`
|
||||
${picture
|
||||
? html`
|
||||
<div style="margin-bottom: 8px;">
|
||||
<img
|
||||
src=${picture}
|
||||
|
|
@ -156,28 +156,33 @@ export function renderNostrCard(params: {
|
|||
/>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${name ? html`<div><span class="label">Name</span><span>${name}</span></div>` : nothing}
|
||||
${
|
||||
displayName
|
||||
? html`<div><span class="label">Display Name</span><span>${displayName}</span></div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
about
|
||||
? html`<div><span class="label">About</span><span style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;">${about}</span></div>`
|
||||
: nothing
|
||||
}
|
||||
${nip05 ? html`<div><span class="label">NIP-05</span><span>${nip05}</span></div>` : nothing}
|
||||
: nothing}
|
||||
${name
|
||||
? html`<div><span class="label">Name</span><span>${name}</span></div>`
|
||||
: nothing}
|
||||
${displayName
|
||||
? html`<div>
|
||||
<span class="label">Display Name</span><span>${displayName}</span>
|
||||
</div>`
|
||||
: nothing}
|
||||
${about
|
||||
? html`<div>
|
||||
<span class="label">About</span
|
||||
><span style="max-width: 300px; overflow: hidden; text-overflow: ellipsis;"
|
||||
>${about}</span
|
||||
>
|
||||
</div>`
|
||||
: nothing}
|
||||
${nip05
|
||||
? html`<div><span class="label">NIP-05</span><span>${nip05}</span></div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div style="color: var(--text-muted); font-size: 13px">
|
||||
No profile set. Click "Edit Profile" to add your name, bio, and avatar.
|
||||
</div>
|
||||
`
|
||||
}
|
||||
: html`
|
||||
<div style="color: var(--text-muted); font-size: 13px">
|
||||
No profile set. Click "Edit Profile" to add your name, bio, and avatar.
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
|
@ -187,15 +192,13 @@ export function renderNostrCard(params: {
|
|||
<div class="card-title">Nostr</div>
|
||||
<div class="card-sub">Decentralized DMs via Nostr relays (NIP-04).</div>
|
||||
${accountCountLabel}
|
||||
|
||||
${
|
||||
hasMultipleAccounts
|
||||
? html`
|
||||
${hasMultipleAccounts
|
||||
? html`
|
||||
<div class="account-card-list">
|
||||
${nostrAccounts.map((account) => renderAccountCard(account))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
|
|
@ -213,21 +216,16 @@ export function renderNostrCard(params: {
|
|||
</div>
|
||||
<div>
|
||||
<span class="label">Last start</span>
|
||||
<span>${summaryLastStartAt ? formatRelativeTimestamp(summaryLastStartAt) : "n/a"}</span>
|
||||
<span
|
||||
>${summaryLastStartAt ? formatRelativeTimestamp(summaryLastStartAt) : "n/a"}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
${
|
||||
summaryLastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${summaryLastError}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderProfileSection()}
|
||||
|
||||
${renderChannelConfigSection({ channelId: "nostr", props })}
|
||||
`}
|
||||
${summaryLastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${summaryLastError}</div>`
|
||||
: nothing}
|
||||
${renderProfileSection()} ${renderChannelConfigSection({ channelId: "nostr", props })}
|
||||
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(false)}>Refresh</button>
|
||||
|
|
|
|||
|
|
@ -120,18 +120,11 @@ export function renderSingleAccountChannelCard(params: {
|
|||
)}
|
||||
</div>
|
||||
|
||||
${
|
||||
params.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${params.lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${params.secondaryCallout ?? nothing}
|
||||
${params.extraContent ?? nothing}
|
||||
${params.configSection}
|
||||
${params.footer ?? nothing}
|
||||
${params.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${params.lastError}</div>`
|
||||
: nothing}
|
||||
${params.secondaryCallout ?? nothing} ${params.extraContent ?? nothing}
|
||||
${params.configSection} ${params.footer ?? nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,15 +37,13 @@ export function renderSignalCard(params: {
|
|||
lastError: signal?.lastError,
|
||||
secondaryCallout: signal?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${signal.probe.ok ? "ok" : "failed"} ·
|
||||
${signal.probe.status ?? ""} ${signal.probe.error ?? ""}
|
||||
Probe ${signal.probe.ok ? "ok" : "failed"} · ${signal.probe.status ?? ""}
|
||||
${signal.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "signal", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>Probe</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,15 +36,13 @@ export function renderSlackCard(params: {
|
|||
lastError: slack?.lastError,
|
||||
secondaryCallout: slack?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${slack.probe.ok ? "ok" : "failed"} ·
|
||||
${slack.probe.status ?? ""} ${slack.probe.error ?? ""}
|
||||
Probe ${slack.probe.ok ? "ok" : "failed"} · ${slack.probe.status ?? ""}
|
||||
${slack.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "slack", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>Probe</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,7 @@ export function renderTelegramCard(params: {
|
|||
return html`
|
||||
<div class="account-card">
|
||||
<div class="account-card-header">
|
||||
<div class="account-card-title">
|
||||
${botUsername ? `@${botUsername}` : label}
|
||||
</div>
|
||||
<div class="account-card-title">${botUsername ? `@${botUsername}` : label}</div>
|
||||
<div class="account-card-id">${account.accountId}</div>
|
||||
</div>
|
||||
<div class="status-list account-card-status">
|
||||
|
|
@ -42,17 +40,15 @@ export function renderTelegramCard(params: {
|
|||
</div>
|
||||
<div>
|
||||
<span class="label">Last inbound</span>
|
||||
<span>${account.lastInboundAt ? formatRelativeTimestamp(account.lastInboundAt) : "n/a"}</span>
|
||||
<span
|
||||
>${account.lastInboundAt
|
||||
? formatRelativeTimestamp(account.lastInboundAt)
|
||||
: "n/a"}</span
|
||||
>
|
||||
</div>
|
||||
${
|
||||
account.lastError
|
||||
? html`
|
||||
<div class="account-card-error">
|
||||
${account.lastError}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${account.lastError
|
||||
? html` <div class="account-card-error">${account.lastError}</div> `
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -69,29 +65,19 @@ export function renderTelegramCard(params: {
|
|||
${telegramAccounts.map((account) => renderAccountCard(account))}
|
||||
</div>
|
||||
|
||||
${
|
||||
telegram?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${telegram.lastError}
|
||||
${telegram?.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${telegram.lastError}</div>`
|
||||
: nothing}
|
||||
${telegram?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${telegram.probe.ok ? "ok" : "failed"} · ${telegram.probe.status ?? ""}
|
||||
${telegram.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
telegram?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${telegram.probe.ok ? "ok" : "failed"} ·
|
||||
${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
: nothing}
|
||||
${renderChannelConfigSection({ channelId: "telegram", props })}
|
||||
|
||||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>Probe</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -117,15 +103,13 @@ export function renderTelegramCard(params: {
|
|||
lastError: telegram?.lastError,
|
||||
secondaryCallout: telegram?.probe
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
Probe ${telegram.probe.ok ? "ok" : "failed"} ·
|
||||
${telegram.probe.status ?? ""} ${telegram.probe.error ?? ""}
|
||||
Probe ${telegram.probe.ok ? "ok" : "failed"} · ${telegram.probe.status ?? ""}
|
||||
${telegram.probe.error ?? ""}
|
||||
</div>`
|
||||
: nothing,
|
||||
configSection: renderChannelConfigSection({ channelId: "telegram", props }),
|
||||
footer: html`<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Probe
|
||||
</button>
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>Probe</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,18 +78,17 @@ export function renderChannels(props: ChannelsProps) {
|
|||
<div class="card-title">Channel health</div>
|
||||
<div class="card-sub">Channel status snapshots from the gateway.</div>
|
||||
</div>
|
||||
<div class="muted">${props.lastSuccessAt ? formatRelativeTimestamp(props.lastSuccessAt) : "n/a"}</div>
|
||||
<div class="muted">
|
||||
${props.lastSuccessAt ? formatRelativeTimestamp(props.lastSuccessAt) : "n/a"}
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${props.lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.lastError}</div>`
|
||||
: nothing}
|
||||
<pre class="code-block" style="margin-top: 12px;">
|
||||
${props.snapshot ? JSON.stringify(props.snapshot, null, 2) : "No snapshot yet."}
|
||||
</pre>
|
||||
</pre
|
||||
>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
|
@ -199,15 +198,13 @@ function renderGenericChannelCard(
|
|||
<div class="card-title">${label}</div>
|
||||
<div class="card-sub">Channel status and configuration.</div>
|
||||
${accountCountLabel}
|
||||
|
||||
${
|
||||
accounts.length > 0
|
||||
? html`
|
||||
${accounts.length > 0
|
||||
? html`
|
||||
<div class="account-card-list">
|
||||
${accounts.map((account) => renderGenericAccount(account))}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<div class="status-list" style="margin-top: 16px;">
|
||||
<div>
|
||||
<span class="label">Configured</span>
|
||||
|
|
@ -222,17 +219,10 @@ function renderGenericChannelCard(
|
|||
<span>${formatNullableBoolean(displayState.connected)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
${
|
||||
lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
`}
|
||||
${lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${lastError}</div>`
|
||||
: nothing}
|
||||
${renderChannelConfigSection({ channelId: key, props })}
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -311,17 +301,13 @@ function renderGenericAccount(account: ChannelAccountSnapshot) {
|
|||
</div>
|
||||
<div>
|
||||
<span class="label">Last inbound</span>
|
||||
<span>${account.lastInboundAt ? formatRelativeTimestamp(account.lastInboundAt) : "n/a"}</span>
|
||||
<span
|
||||
>${account.lastInboundAt ? formatRelativeTimestamp(account.lastInboundAt) : "n/a"}</span
|
||||
>
|
||||
</div>
|
||||
${
|
||||
account.lastError
|
||||
? html`
|
||||
<div class="account-card-error">
|
||||
${account.lastError}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${account.lastError
|
||||
? html` <div class="account-card-error">${account.lastError}</div> `
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -43,21 +43,14 @@ export function renderWhatsAppCard(params: {
|
|||
],
|
||||
lastError: whatsapp?.lastError,
|
||||
extraContent: html`
|
||||
${
|
||||
props.whatsappMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${props.whatsappMessage}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
props.whatsappQrDataUrl
|
||||
? html`<div class="qr-wrap">
|
||||
<img src=${props.whatsappQrDataUrl} alt="WhatsApp QR" />
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${props.whatsappMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">${props.whatsappMessage}</div>`
|
||||
: nothing}
|
||||
${props.whatsappQrDataUrl
|
||||
? html`<div class="qr-wrap">
|
||||
<img src=${props.whatsappQrDataUrl} alt="WhatsApp QR" />
|
||||
</div>`
|
||||
: nothing}
|
||||
`,
|
||||
configSection: renderChannelConfigSection({ channelId: "whatsapp", props }),
|
||||
footer: html`<div class="row" style="margin-top: 14px; flex-wrap: wrap;">
|
||||
|
|
@ -75,11 +68,7 @@ export function renderWhatsAppCard(params: {
|
|||
>
|
||||
Relink
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${props.whatsappBusy}
|
||||
@click=${() => props.onWhatsAppWait()}
|
||||
>
|
||||
<button class="btn" ?disabled=${props.whatsappBusy} @click=${() => props.onWhatsAppWait()}>
|
||||
Wait for scan
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -89,9 +78,7 @@ export function renderWhatsAppCard(params: {
|
|||
>
|
||||
Logout
|
||||
</button>
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>
|
||||
Refresh
|
||||
</button>
|
||||
<button class="btn" @click=${() => props.onRefresh(true)}>Refresh</button>
|
||||
</div>`,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,7 +195,11 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
|
|||
}
|
||||
if (status.active) {
|
||||
return html`
|
||||
<div class="compaction-indicator compaction-indicator--active" role="status" aria-live="polite">
|
||||
<div
|
||||
class="compaction-indicator compaction-indicator--active"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
${icons.loader} Compacting context...
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -204,7 +208,11 @@ function renderCompactionIndicator(status: CompactionIndicatorStatus | null | un
|
|||
const elapsed = Date.now() - status.completedAt;
|
||||
if (elapsed < COMPACTION_TOAST_DURATION_MS) {
|
||||
return html`
|
||||
<div class="compaction-indicator compaction-indicator--complete" role="status" aria-live="polite">
|
||||
<div
|
||||
class="compaction-indicator compaction-indicator--complete"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
${icons.check} Context compacted
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -314,9 +322,25 @@ function renderContextNotice(
|
|||
const bg = `rgba(${r}, ${g}, ${b}, ${bgOpacity})`;
|
||||
return html`
|
||||
<div class="context-notice" role="status" style="--ctx-color:${color};--ctx-bg:${bg}">
|
||||
<svg class="context-notice__icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
<svg
|
||||
class="context-notice__icon"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
<span>${pct}% context used</span>
|
||||
<span class="context-notice__detail">${formatTokensCompact(used)} / ${formatTokensCompact(limit)}</span>
|
||||
<span class="context-notice__detail"
|
||||
>${formatTokensCompact(used)} / ${formatTokensCompact(limit)}</span
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -451,7 +475,9 @@ function renderAttachmentPreview(props: ChatProps): TemplateResult | typeof noth
|
|||
const next = (props.attachments ?? []).filter((a) => a.id !== att.id);
|
||||
props.onAttachmentsChange?.(next);
|
||||
}}
|
||||
>×</button>
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
|
|
@ -616,18 +642,20 @@ function renderWelcomeState(props: ChatProps): TemplateResult {
|
|||
return html`
|
||||
<div class="agent-chat__welcome" style="--agent-color: var(--accent)">
|
||||
<div class="agent-chat__welcome-glow"></div>
|
||||
${
|
||||
avatar
|
||||
? html`<img src=${avatar} alt=${name} style="width:56px; height:56px; border-radius:50%; object-fit:cover;" />`
|
||||
: html`<div class="agent-chat__avatar agent-chat__avatar--logo"><img src=${logoUrl} alt="OpenClaw" /></div>`
|
||||
}
|
||||
${avatar
|
||||
? html`<img
|
||||
src=${avatar}
|
||||
alt=${name}
|
||||
style="width:56px; height:56px; border-radius:50%; object-fit:cover;"
|
||||
/>`
|
||||
: html`<div class="agent-chat__avatar agent-chat__avatar--logo">
|
||||
<img src=${logoUrl} alt="OpenClaw" />
|
||||
</div>`}
|
||||
<h2>${name}</h2>
|
||||
<div class="agent-chat__badges">
|
||||
<span class="agent-chat__badge"><img src=${logoUrl} alt="" /> Ready to chat</span>
|
||||
</div>
|
||||
<p class="agent-chat__hint">
|
||||
Type a message below · <kbd>/</kbd> for commands
|
||||
</p>
|
||||
<p class="agent-chat__hint">Type a message below · <kbd>/</kbd> for commands</p>
|
||||
<div class="agent-chat__suggestions">
|
||||
${WELCOME_SUGGESTIONS.map(
|
||||
(text) => html`
|
||||
|
|
@ -638,7 +666,9 @@ function renderWelcomeState(props: ChatProps): TemplateResult {
|
|||
props.onDraftChange(text);
|
||||
props.onSend();
|
||||
}}
|
||||
>${text}</button>
|
||||
>
|
||||
${text}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -663,11 +693,15 @@ function renderSearchBar(requestUpdate: () => void): TemplateResult | typeof not
|
|||
requestUpdate();
|
||||
}}
|
||||
/>
|
||||
<button class="btn btn--ghost" aria-label="Close search" @click=${() => {
|
||||
vs.searchOpen = false;
|
||||
vs.searchQuery = "";
|
||||
requestUpdate();
|
||||
}}>
|
||||
<button
|
||||
class="btn btn--ghost"
|
||||
aria-label="Close search"
|
||||
@click=${() => {
|
||||
vs.searchOpen = false;
|
||||
vs.searchQuery = "";
|
||||
requestUpdate();
|
||||
}}
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -695,36 +729,46 @@ function renderPinnedSection(
|
|||
}
|
||||
return html`
|
||||
<div class="agent-chat__pinned">
|
||||
<button class="agent-chat__pinned-toggle" @click=${() => {
|
||||
vs.pinnedExpanded = !vs.pinnedExpanded;
|
||||
requestUpdate();
|
||||
}}>
|
||||
${icons.bookmark}
|
||||
${entries.length} pinned
|
||||
<span class="collapse-chevron ${vs.pinnedExpanded ? "" : "collapse-chevron--collapsed"}">${icons.chevronDown}</span>
|
||||
<button
|
||||
class="agent-chat__pinned-toggle"
|
||||
@click=${() => {
|
||||
vs.pinnedExpanded = !vs.pinnedExpanded;
|
||||
requestUpdate();
|
||||
}}
|
||||
>
|
||||
${icons.bookmark} ${entries.length} pinned
|
||||
<span class="collapse-chevron ${vs.pinnedExpanded ? "" : "collapse-chevron--collapsed"}"
|
||||
>${icons.chevronDown}</span
|
||||
>
|
||||
</button>
|
||||
${
|
||||
vs.pinnedExpanded
|
||||
? html`
|
||||
${vs.pinnedExpanded
|
||||
? html`
|
||||
<div class="agent-chat__pinned-list">
|
||||
${entries.map(
|
||||
({ index, text, role }) => html`
|
||||
<div class="agent-chat__pinned-item">
|
||||
<span class="agent-chat__pinned-role">${role === "user" ? "You" : "Assistant"}</span>
|
||||
<span class="agent-chat__pinned-text">${text.slice(0, 100)}${text.length > 100 ? "..." : ""}</span>
|
||||
<button class="btn btn--ghost" @click=${() => {
|
||||
pinned.unpin(index);
|
||||
requestUpdate();
|
||||
}} title="Unpin">
|
||||
${icons.x}
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
<div class="agent-chat__pinned-item">
|
||||
<span class="agent-chat__pinned-role"
|
||||
>${role === "user" ? "You" : "Assistant"}</span
|
||||
>
|
||||
<span class="agent-chat__pinned-text"
|
||||
>${text.slice(0, 100)}${text.length > 100 ? "..." : ""}</span
|
||||
>
|
||||
<button
|
||||
class="btn btn--ghost"
|
||||
@click=${() => {
|
||||
pinned.unpin(index);
|
||||
requestUpdate();
|
||||
}}
|
||||
title="Unpin"
|
||||
>
|
||||
${icons.x}
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -742,7 +786,9 @@ function renderSlashMenu(
|
|||
return html`
|
||||
<div class="slash-menu" role="listbox" aria-label="Command arguments">
|
||||
<div class="slash-menu-group">
|
||||
<div class="slash-menu-group__label">/${vs.slashMenuCommand.name} ${vs.slashMenuCommand.description}</div>
|
||||
<div class="slash-menu-group__label">
|
||||
/${vs.slashMenuCommand.name} ${vs.slashMenuCommand.description}
|
||||
</div>
|
||||
${vs.slashMenuArgItems.map(
|
||||
(arg, i) => html`
|
||||
<div
|
||||
|
|
@ -755,7 +801,9 @@ function renderSlashMenu(
|
|||
requestUpdate();
|
||||
}}
|
||||
>
|
||||
${vs.slashMenuCommand?.icon ? html`<span class="slash-menu-icon">${icons[vs.slashMenuCommand.icon]}</span>` : nothing}
|
||||
${vs.slashMenuCommand?.icon
|
||||
? html`<span class="slash-menu-icon">${icons[vs.slashMenuCommand.icon]}</span>`
|
||||
: nothing}
|
||||
<span class="slash-menu-name">${arg}</span>
|
||||
<span class="slash-menu-desc">/${vs.slashMenuCommand?.name} ${arg}</span>
|
||||
</div>
|
||||
|
|
@ -763,10 +811,7 @@ function renderSlashMenu(
|
|||
)}
|
||||
</div>
|
||||
<div class="slash-menu-footer">
|
||||
<kbd>↑↓</kbd> navigate
|
||||
<kbd>Tab</kbd> fill
|
||||
<kbd>Enter</kbd> run
|
||||
<kbd>Esc</kbd> close
|
||||
<kbd>↑↓</kbd> navigate <kbd>Tab</kbd> fill <kbd>Enter</kbd> run <kbd>Esc</kbd> close
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -800,7 +845,9 @@ function renderSlashMenu(
|
|||
${entries.map(
|
||||
({ cmd, globalIdx }) => html`
|
||||
<div
|
||||
class="slash-menu-item ${globalIdx === vs.slashMenuIndex ? "slash-menu-item--active" : ""}"
|
||||
class="slash-menu-item ${globalIdx === vs.slashMenuIndex
|
||||
? "slash-menu-item--active"
|
||||
: ""}"
|
||||
role="option"
|
||||
aria-selected=${globalIdx === vs.slashMenuIndex}
|
||||
@click=${() => selectSlashCommand(cmd, props, requestUpdate)}
|
||||
|
|
@ -813,15 +860,11 @@ function renderSlashMenu(
|
|||
<span class="slash-menu-name">/${cmd.name}</span>
|
||||
${cmd.args ? html`<span class="slash-menu-args">${cmd.args}</span>` : nothing}
|
||||
<span class="slash-menu-desc">${cmd.description}</span>
|
||||
${
|
||||
cmd.argOptions?.length
|
||||
? html`<span class="slash-menu-badge">${cmd.argOptions.length} options</span>`
|
||||
: cmd.executeLocal && !cmd.args
|
||||
? html`
|
||||
<span class="slash-menu-badge">instant</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${cmd.argOptions?.length
|
||||
? html`<span class="slash-menu-badge">${cmd.argOptions.length} options</span>`
|
||||
: cmd.executeLocal && !cmd.args
|
||||
? html` <span class="slash-menu-badge">instant</span> `
|
||||
: nothing}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
|
|
@ -833,10 +876,7 @@ function renderSlashMenu(
|
|||
<div class="slash-menu" role="listbox" aria-label="Slash commands">
|
||||
${sections}
|
||||
<div class="slash-menu-footer">
|
||||
<kbd>↑↓</kbd> navigate
|
||||
<kbd>Tab</kbd> fill
|
||||
<kbd>Enter</kbd> select
|
||||
<kbd>Esc</kbd> close
|
||||
<kbd>↑↓</kbd> navigate <kbd>Tab</kbd> fill <kbd>Enter</kbd> select <kbd>Esc</kbd> close
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -904,15 +944,20 @@ export function renderChat(props: ChatProps) {
|
|||
@click=${handleCodeBlockCopy}
|
||||
>
|
||||
<div class="chat-thread-inner">
|
||||
${
|
||||
props.loading
|
||||
${props.loading
|
||||
? html`
|
||||
<div class="chat-loading-skeleton" aria-label="Loading chat">
|
||||
<div class="chat-line assistant">
|
||||
<div class="chat-msg">
|
||||
<div class="chat-bubble">
|
||||
<div class="skeleton skeleton-line skeleton-line--long" style="margin-bottom: 8px"></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--medium" style="margin-bottom: 8px"></div>
|
||||
<div
|
||||
class="skeleton skeleton-line skeleton-line--long"
|
||||
style="margin-bottom: 8px"
|
||||
></div>
|
||||
<div
|
||||
class="skeleton skeleton-line skeleton-line--medium"
|
||||
style="margin-bottom: 8px"
|
||||
></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--short"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -927,70 +972,68 @@ export function renderChat(props: ChatProps) {
|
|||
<div class="chat-line assistant" style="margin-top: 12px">
|
||||
<div class="chat-msg">
|
||||
<div class="chat-bubble">
|
||||
<div class="skeleton skeleton-line skeleton-line--long" style="margin-bottom: 8px"></div>
|
||||
<div
|
||||
class="skeleton skeleton-line skeleton-line--long"
|
||||
style="margin-bottom: 8px"
|
||||
></div>
|
||||
<div class="skeleton skeleton-line skeleton-line--short"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${isEmpty && !vs.searchOpen ? renderWelcomeState(props) : nothing}
|
||||
${
|
||||
isEmpty && vs.searchOpen
|
||||
? html`
|
||||
<div class="agent-chat__empty">No matching messages</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${repeat(
|
||||
chatItems,
|
||||
(item) => item.key,
|
||||
(item) => {
|
||||
if (item.kind === "divider") {
|
||||
return html`
|
||||
<div class="chat-divider" role="separator" data-ts=${String(item.timestamp)}>
|
||||
<span class="chat-divider__line"></span>
|
||||
<span class="chat-divider__label">${item.label}</span>
|
||||
<span class="chat-divider__line"></span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (item.kind === "reading-indicator") {
|
||||
return renderReadingIndicatorGroup(assistantIdentity, props.basePath);
|
||||
}
|
||||
if (item.kind === "stream") {
|
||||
return renderStreamingGroup(
|
||||
item.text,
|
||||
item.startedAt,
|
||||
props.onOpenSidebar,
|
||||
assistantIdentity,
|
||||
props.basePath,
|
||||
);
|
||||
}
|
||||
if (item.kind === "group") {
|
||||
if (deleted.has(item.key)) {
|
||||
return nothing;
|
||||
: nothing}
|
||||
${isEmpty && !vs.searchOpen ? renderWelcomeState(props) : nothing}
|
||||
${isEmpty && vs.searchOpen
|
||||
? html` <div class="agent-chat__empty">No matching messages</div> `
|
||||
: nothing}
|
||||
${repeat(
|
||||
chatItems,
|
||||
(item) => item.key,
|
||||
(item) => {
|
||||
if (item.kind === "divider") {
|
||||
return html`
|
||||
<div class="chat-divider" role="separator" data-ts=${String(item.timestamp)}>
|
||||
<span class="chat-divider__line"></span>
|
||||
<span class="chat-divider__label">${item.label}</span>
|
||||
<span class="chat-divider__line"></span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return renderMessageGroup(item, {
|
||||
onOpenSidebar: props.onOpenSidebar,
|
||||
showReasoning,
|
||||
showToolCalls: props.showToolCalls,
|
||||
assistantName: props.assistantName,
|
||||
assistantAvatar: assistantIdentity.avatar,
|
||||
basePath: props.basePath,
|
||||
contextWindow:
|
||||
activeSession?.contextTokens ?? props.sessions?.defaults?.contextTokens ?? null,
|
||||
onDelete: () => {
|
||||
deleted.delete(item.key);
|
||||
requestUpdate();
|
||||
},
|
||||
});
|
||||
}
|
||||
return nothing;
|
||||
},
|
||||
)}
|
||||
if (item.kind === "reading-indicator") {
|
||||
return renderReadingIndicatorGroup(assistantIdentity, props.basePath);
|
||||
}
|
||||
if (item.kind === "stream") {
|
||||
return renderStreamingGroup(
|
||||
item.text,
|
||||
item.startedAt,
|
||||
props.onOpenSidebar,
|
||||
assistantIdentity,
|
||||
props.basePath,
|
||||
);
|
||||
}
|
||||
if (item.kind === "group") {
|
||||
if (deleted.has(item.key)) {
|
||||
return nothing;
|
||||
}
|
||||
return renderMessageGroup(item, {
|
||||
onOpenSidebar: props.onOpenSidebar,
|
||||
showReasoning,
|
||||
showToolCalls: props.showToolCalls,
|
||||
assistantName: props.assistantName,
|
||||
assistantAvatar: assistantIdentity.avatar,
|
||||
basePath: props.basePath,
|
||||
contextWindow:
|
||||
activeSession?.contextTokens ?? props.sessions?.defaults?.contextTokens ?? null,
|
||||
onDelete: () => {
|
||||
deleted.delete(item.key);
|
||||
requestUpdate();
|
||||
},
|
||||
});
|
||||
}
|
||||
return nothing;
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1121,10 +1164,8 @@ export function renderChat(props: ChatProps) {
|
|||
>
|
||||
${props.disabledReason ? html`<div class="callout">${props.disabledReason}</div>` : nothing}
|
||||
${props.error ? html`<div class="callout danger">${props.error}</div>` : nothing}
|
||||
|
||||
${
|
||||
props.focusMode
|
||||
? html`
|
||||
${props.focusMode
|
||||
? html`
|
||||
<button
|
||||
class="chat-focus-exit"
|
||||
type="button"
|
||||
|
|
@ -1135,11 +1176,8 @@ export function renderChat(props: ChatProps) {
|
|||
${icons.x}
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${renderSearchBar(requestUpdate)}
|
||||
${renderPinnedSection(props, pinned, requestUpdate)}
|
||||
: nothing}
|
||||
${renderSearchBar(requestUpdate)} ${renderPinnedSection(props, pinned, requestUpdate)}
|
||||
|
||||
<div class="chat-split-container ${sidebarOpen ? "chat-split-container--open" : ""}">
|
||||
<div
|
||||
|
|
@ -1149,9 +1187,8 @@ export function renderChat(props: ChatProps) {
|
|||
${thread}
|
||||
</div>
|
||||
|
||||
${
|
||||
sidebarOpen
|
||||
? html`
|
||||
${sidebarOpen
|
||||
? html`
|
||||
<resizable-divider
|
||||
.splitRatio=${splitRatio}
|
||||
@resize=${(e: CustomEvent) => props.onSplitRatioChange?.(e.detail.splitRatio)}
|
||||
|
|
@ -1170,13 +1207,11 @@ export function renderChat(props: ChatProps) {
|
|||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
${
|
||||
props.queue.length
|
||||
? html`
|
||||
${props.queue.length
|
||||
? html`
|
||||
<div class="chat-queue" role="status" aria-live="polite">
|
||||
<div class="chat-queue__title">Queued (${props.queue.length})</div>
|
||||
<div class="chat-queue__list">
|
||||
|
|
@ -1184,10 +1219,8 @@ export function renderChat(props: ChatProps) {
|
|||
(item) => html`
|
||||
<div class="chat-queue__item">
|
||||
<div class="chat-queue__text">
|
||||
${
|
||||
item.text ||
|
||||
(item.attachments?.length ? `Image (${item.attachments.length})` : "")
|
||||
}
|
||||
${item.text ||
|
||||
(item.attachments?.length ? `Image (${item.attachments.length})` : "")}
|
||||
</div>
|
||||
<button
|
||||
class="btn chat-queue__remove"
|
||||
|
|
@ -1203,31 +1236,21 @@ export function renderChat(props: ChatProps) {
|
|||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
: nothing}
|
||||
${renderFallbackIndicator(props.fallbackStatus)}
|
||||
${renderCompactionIndicator(props.compactionStatus)}
|
||||
${renderContextNotice(activeSession, props.sessions?.defaults?.contextTokens ?? null)}
|
||||
|
||||
${
|
||||
props.showNewMessages
|
||||
? html`
|
||||
<button
|
||||
class="chat-new-messages"
|
||||
type="button"
|
||||
@click=${props.onScrollToBottom}
|
||||
>
|
||||
${props.showNewMessages
|
||||
? html`
|
||||
<button class="chat-new-messages" type="button" @click=${props.onScrollToBottom}>
|
||||
${icons.arrowDown} New messages
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
|
||||
<!-- Input bar -->
|
||||
<div class="agent-chat__input">
|
||||
${renderSlashMenu(requestUpdate, props)}
|
||||
${renderAttachmentPreview(props)}
|
||||
${renderSlashMenu(requestUpdate, props)} ${renderAttachmentPreview(props)}
|
||||
|
||||
<input
|
||||
type="file"
|
||||
|
|
@ -1237,7 +1260,9 @@ export function renderChat(props: ChatProps) {
|
|||
@change=${(e: Event) => handleFileSelect(e, props)}
|
||||
/>
|
||||
|
||||
${vs.sttRecording && vs.sttInterimText ? html`<div class="agent-chat__stt-interim">${vs.sttInterimText}</div>` : nothing}
|
||||
${vs.sttRecording && vs.sttInterimText
|
||||
? html`<div class="agent-chat__stt-interim">${vs.sttInterimText}</div>`
|
||||
: nothing}
|
||||
|
||||
<textarea
|
||||
${ref((el) => el && adjustTextareaHeight(el as HTMLTextAreaElement))}
|
||||
|
|
@ -1265,11 +1290,12 @@ export function renderChat(props: ChatProps) {
|
|||
${icons.paperclip}
|
||||
</button>
|
||||
|
||||
${
|
||||
isSttSupported()
|
||||
? html`
|
||||
${isSttSupported()
|
||||
? html`
|
||||
<button
|
||||
class="agent-chat__input-btn ${vs.sttRecording ? "agent-chat__input-btn--recording" : ""}"
|
||||
class="agent-chat__input-btn ${vs.sttRecording
|
||||
? "agent-chat__input-btn--recording"
|
||||
: ""}"
|
||||
@click=${() => {
|
||||
if (vs.sttRecording) {
|
||||
stopStt();
|
||||
|
|
@ -1316,40 +1342,46 @@ export function renderChat(props: ChatProps) {
|
|||
${vs.sttRecording ? icons.micOff : icons.mic}
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
: nothing}
|
||||
${tokens ? html`<span class="agent-chat__token-count">${tokens}</span>` : nothing}
|
||||
</div>
|
||||
|
||||
<div class="agent-chat__toolbar-right">
|
||||
${nothing /* search hidden for now */}
|
||||
${
|
||||
canAbort
|
||||
? nothing
|
||||
: html`
|
||||
<button
|
||||
class="btn btn--ghost"
|
||||
@click=${props.onNewSession}
|
||||
title="New session"
|
||||
aria-label="New session"
|
||||
>
|
||||
${icons.plus}
|
||||
</button>
|
||||
`
|
||||
}
|
||||
<button class="btn btn--ghost" @click=${() => exportMarkdown(props)} title="Export" aria-label="Export chat" ?disabled=${props.messages.length === 0}>
|
||||
${canAbort
|
||||
? nothing
|
||||
: html`
|
||||
<button
|
||||
class="btn btn--ghost"
|
||||
@click=${props.onNewSession}
|
||||
title="New session"
|
||||
aria-label="New session"
|
||||
>
|
||||
${icons.plus}
|
||||
</button>
|
||||
`}
|
||||
<button
|
||||
class="btn btn--ghost"
|
||||
@click=${() => exportMarkdown(props)}
|
||||
title="Export"
|
||||
aria-label="Export chat"
|
||||
?disabled=${props.messages.length === 0}
|
||||
>
|
||||
${icons.download}
|
||||
</button>
|
||||
|
||||
${
|
||||
canAbort && (isBusy || props.sending)
|
||||
? html`
|
||||
<button class="chat-send-btn chat-send-btn--stop" @click=${props.onAbort} title="Stop" aria-label="Stop generating">
|
||||
${canAbort && (isBusy || props.sending)
|
||||
? html`
|
||||
<button
|
||||
class="chat-send-btn chat-send-btn--stop"
|
||||
@click=${props.onAbort}
|
||||
title="Stop"
|
||||
aria-label="Stop generating"
|
||||
>
|
||||
${icons.stop}
|
||||
</button>
|
||||
`
|
||||
: html`
|
||||
: html`
|
||||
<button
|
||||
class="chat-send-btn"
|
||||
@click=${() => {
|
||||
|
|
@ -1364,8 +1396,7 @@ export function renderChat(props: ChatProps) {
|
|||
>
|
||||
${icons.send}
|
||||
</button>
|
||||
`
|
||||
}
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -197,10 +197,13 @@ export function renderCommandPalette(props: CommandPaletteProps) {
|
|||
const grouped = groupItems(items);
|
||||
|
||||
return html`
|
||||
<div class="cmd-palette-overlay" @click=${() => {
|
||||
props.onToggle();
|
||||
restoreFocus();
|
||||
}}>
|
||||
<div
|
||||
class="cmd-palette-overlay"
|
||||
@click=${() => {
|
||||
props.onToggle();
|
||||
restoreFocus();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="cmd-palette"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
|
|
@ -217,40 +220,42 @@ export function renderCommandPalette(props: CommandPaletteProps) {
|
|||
}}
|
||||
/>
|
||||
<div class="cmd-palette__results">
|
||||
${
|
||||
grouped.length === 0
|
||||
? html`<div class="cmd-palette__empty">
|
||||
<span class="nav-item__icon" style="opacity:0.3;width:20px;height:20px">${icons.search}</span>
|
||||
<span>${t("overview.palette.noResults")}</span>
|
||||
</div>`
|
||||
: grouped.map(
|
||||
([category, groupedItems]) => html`
|
||||
<div class="cmd-palette__group-label">${CATEGORY_LABELS[category] ?? category}</div>
|
||||
${groupedItems.map((item) => {
|
||||
const globalIndex = items.indexOf(item);
|
||||
const isActive = globalIndex === props.activeIndex;
|
||||
return html`
|
||||
<div
|
||||
class="cmd-palette__item ${isActive ? "cmd-palette__item--active" : ""}"
|
||||
@click=${(e: Event) => {
|
||||
e.stopPropagation();
|
||||
selectItem(item, props);
|
||||
}}
|
||||
@mouseenter=${() => props.onActiveIndexChange(globalIndex)}
|
||||
>
|
||||
<span class="nav-item__icon">${icons[item.icon]}</span>
|
||||
<span>${item.label}</span>
|
||||
${
|
||||
item.description
|
||||
? html`<span class="cmd-palette__item-desc muted">${item.description}</span>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
`,
|
||||
)
|
||||
}
|
||||
${grouped.length === 0
|
||||
? html`<div class="cmd-palette__empty">
|
||||
<span class="nav-item__icon" style="opacity:0.3;width:20px;height:20px"
|
||||
>${icons.search}</span
|
||||
>
|
||||
<span>${t("overview.palette.noResults")}</span>
|
||||
</div>`
|
||||
: grouped.map(
|
||||
([category, groupedItems]) => html`
|
||||
<div class="cmd-palette__group-label">
|
||||
${CATEGORY_LABELS[category] ?? category}
|
||||
</div>
|
||||
${groupedItems.map((item) => {
|
||||
const globalIndex = items.indexOf(item);
|
||||
const isActive = globalIndex === props.activeIndex;
|
||||
return html`
|
||||
<div
|
||||
class="cmd-palette__item ${isActive ? "cmd-palette__item--active" : ""}"
|
||||
@click=${(e: Event) => {
|
||||
e.stopPropagation();
|
||||
selectItem(item, props);
|
||||
}}
|
||||
@mouseenter=${() => props.onActiveIndexChange(globalIndex)}
|
||||
>
|
||||
<span class="nav-item__icon">${icons[item.icon]}</span>
|
||||
<span>${item.label}</span>
|
||||
${item.description
|
||||
? html`<span class="cmd-palette__item-desc muted"
|
||||
>${item.description}</span
|
||||
>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
<div class="cmd-palette__footer">
|
||||
<span><kbd>↑↓</kbd> navigate</span>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,9 @@ const icons = {
|
|||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="3 6 5 6 21 6"></polyline>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
||||
<path
|
||||
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
|
||||
></path>
|
||||
</svg>
|
||||
`,
|
||||
edit: html`
|
||||
|
|
@ -151,20 +153,16 @@ function renderSensitiveToggleButton(params: {
|
|||
type="button"
|
||||
class="btn btn--icon ${state.isRevealed ? "active" : ""}"
|
||||
style="width:28px;height:28px;padding:0;"
|
||||
title=${
|
||||
state.canReveal
|
||||
? state.isRevealed
|
||||
? "Hide value"
|
||||
: "Reveal value"
|
||||
: "Disable stream mode to reveal value"
|
||||
}
|
||||
aria-label=${
|
||||
state.canReveal
|
||||
? state.isRevealed
|
||||
? "Hide value"
|
||||
: "Reveal value"
|
||||
: "Disable stream mode to reveal value"
|
||||
}
|
||||
title=${state.canReveal
|
||||
? state.isRevealed
|
||||
? "Hide value"
|
||||
: "Reveal value"
|
||||
: "Disable stream mode to reveal value"}
|
||||
aria-label=${state.canReveal
|
||||
? state.isRevealed
|
||||
? "Hide value"
|
||||
: "Reveal value"
|
||||
: "Disable stream mode to reveal value"}
|
||||
aria-pressed=${state.isRevealed}
|
||||
?disabled=${params.disabled || !state.canReveal}
|
||||
@click=${() => params.onToggleSensitivePath?.(params.path)}
|
||||
|
|
@ -385,9 +383,7 @@ function renderTags(tags: string[]): TemplateResult | typeof nothing {
|
|||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<div class="cfg-tags">
|
||||
${tags.map((tag) => html`<span class="cfg-tag">${tag}</span>`)}
|
||||
</div>
|
||||
<div class="cfg-tags">${tags.map((tag) => html`<span class="cfg-tag">${tag}</span>`)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
@ -456,26 +452,25 @@ export function renderNode(params: {
|
|||
return html`
|
||||
<div class="cfg-field">
|
||||
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
||||
${renderTags(tags)}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing} ${renderTags(tags)}
|
||||
<div class="cfg-segmented">
|
||||
${literals.map(
|
||||
(lit) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-segmented__btn ${
|
||||
// oxlint-disable typescript/no-base-to-string
|
||||
lit === resolvedValue || String(lit) === String(resolvedValue) ? "active" : ""
|
||||
}"
|
||||
?disabled=${disabled}
|
||||
@click=${() => onPatch(path, lit)}
|
||||
>
|
||||
${
|
||||
// oxlint-disable typescript/no-base-to-string
|
||||
String(lit)
|
||||
}
|
||||
</button>
|
||||
`,
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-segmented__btn ${
|
||||
// oxlint-disable typescript/no-base-to-string
|
||||
lit === resolvedValue || String(lit) === String(resolvedValue) ? "active" : ""
|
||||
}"
|
||||
?disabled=${disabled}
|
||||
@click=${() => onPatch(path, lit)}
|
||||
>
|
||||
${
|
||||
// oxlint-disable typescript/no-base-to-string
|
||||
String(lit)
|
||||
}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -536,20 +531,22 @@ export function renderNode(params: {
|
|||
return html`
|
||||
<div class="cfg-field">
|
||||
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
||||
${renderTags(tags)}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing} ${renderTags(tags)}
|
||||
<div class="cfg-segmented">
|
||||
${options.map(
|
||||
(opt) => html`
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-segmented__btn ${opt === resolvedValue || String(opt) === String(resolvedValue) ? "active" : ""}"
|
||||
?disabled=${disabled}
|
||||
@click=${() => onPatch(path, opt)}
|
||||
>
|
||||
${String(opt)}
|
||||
</button>
|
||||
`,
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-segmented__btn ${opt === resolvedValue ||
|
||||
String(opt) === String(resolvedValue)
|
||||
? "active"
|
||||
: ""}"
|
||||
?disabled=${disabled}
|
||||
@click=${() => onPatch(path, opt)}
|
||||
>
|
||||
${String(opt)}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -652,8 +649,7 @@ function renderTextInput(params: {
|
|||
return html`
|
||||
<div class="cfg-field">
|
||||
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
||||
${renderTags(tags)}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing} ${renderTags(tags)}
|
||||
<div class="cfg-input-wrap">
|
||||
<input
|
||||
type=${effectiveInputType}
|
||||
|
|
@ -697,19 +693,19 @@ function renderTextInput(params: {
|
|||
disabled,
|
||||
onToggleSensitivePath: params.onToggleSensitivePath,
|
||||
})}
|
||||
${
|
||||
schema.default !== undefined
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-input__reset"
|
||||
title="Reset to default"
|
||||
?disabled=${disabled || sensitiveState.isRedacted}
|
||||
@click=${() => onPatch(path, schema.default)}
|
||||
>↺</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${schema.default !== undefined
|
||||
? html`
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-input__reset"
|
||||
title="Reset to default"
|
||||
?disabled=${disabled || sensitiveState.isRedacted}
|
||||
@click=${() => onPatch(path, schema.default)}
|
||||
>
|
||||
↺
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -734,15 +730,16 @@ function renderNumberInput(params: {
|
|||
return html`
|
||||
<div class="cfg-field">
|
||||
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
||||
${renderTags(tags)}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing} ${renderTags(tags)}
|
||||
<div class="cfg-number">
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-number__btn"
|
||||
?disabled=${disabled}
|
||||
@click=${() => onPatch(path, numValue - 1)}
|
||||
>−</button>
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
class="cfg-number__input"
|
||||
|
|
@ -759,7 +756,9 @@ function renderNumberInput(params: {
|
|||
class="cfg-number__btn"
|
||||
?disabled=${disabled}
|
||||
@click=${() => onPatch(path, numValue + 1)}
|
||||
>+</button>
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -788,8 +787,7 @@ function renderSelect(params: {
|
|||
return html`
|
||||
<div class="cfg-field">
|
||||
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
||||
${renderTags(tags)}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing} ${renderTags(tags)}
|
||||
<select
|
||||
class="cfg-select"
|
||||
?disabled=${disabled}
|
||||
|
|
@ -800,11 +798,7 @@ function renderSelect(params: {
|
|||
}}
|
||||
>
|
||||
<option value=${unset}>Select...</option>
|
||||
${options.map(
|
||||
(opt, idx) => html`
|
||||
<option value=${String(idx)}>${String(opt)}</option>
|
||||
`,
|
||||
)}
|
||||
${options.map((opt, idx) => html` <option value=${String(idx)}>${String(opt)}</option> `)}
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -838,8 +832,7 @@ function renderJsonTextarea(params: {
|
|||
return html`
|
||||
<div class="cfg-field">
|
||||
${showLabel ? html`<label class="cfg-field__label">${label}</label>` : nothing}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing}
|
||||
${renderTags(tags)}
|
||||
${help ? html`<div class="cfg-field__help">${help}</div>` : nothing} ${renderTags(tags)}
|
||||
<div class="cfg-input-wrap">
|
||||
<textarea
|
||||
class="cfg-textarea${sensitiveState.isRedacted ? " cfg-textarea--redacted" : ""}"
|
||||
|
|
@ -954,41 +947,31 @@ function renderObject(params: {
|
|||
onPatch,
|
||||
}),
|
||||
)}
|
||||
${
|
||||
allowExtra
|
||||
? renderMapField({
|
||||
schema: additional,
|
||||
value: obj,
|
||||
path,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
reservedKeys: reserved,
|
||||
searchCriteria: childSearchCriteria,
|
||||
revealSensitive,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath,
|
||||
onPatch,
|
||||
})
|
||||
: nothing
|
||||
}
|
||||
${allowExtra
|
||||
? renderMapField({
|
||||
schema: additional,
|
||||
value: obj,
|
||||
path,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
reservedKeys: reserved,
|
||||
searchCriteria: childSearchCriteria,
|
||||
revealSensitive,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath,
|
||||
onPatch,
|
||||
})
|
||||
: nothing}
|
||||
`;
|
||||
|
||||
// For top-level, don't wrap in collapsible
|
||||
if (path.length === 1) {
|
||||
return html`
|
||||
<div class="cfg-fields">
|
||||
${fields}
|
||||
</div>
|
||||
`;
|
||||
return html` <div class="cfg-fields">${fields}</div> `;
|
||||
}
|
||||
|
||||
if (!showLabel) {
|
||||
return html`
|
||||
<div class="cfg-fields cfg-fields--inline">
|
||||
${fields}
|
||||
</div>
|
||||
`;
|
||||
return html` <div class="cfg-fields cfg-fields--inline">${fields}</div> `;
|
||||
}
|
||||
|
||||
// Nested objects get collapsible treatment
|
||||
|
|
@ -1002,9 +985,7 @@ function renderObject(params: {
|
|||
<span class="cfg-object__chevron">${icons.chevronDown}</span>
|
||||
</summary>
|
||||
${help ? html`<div class="cfg-object__help">${help}</div>` : nothing}
|
||||
<div class="cfg-object__content">
|
||||
${fields}
|
||||
</div>
|
||||
<div class="cfg-object__content">${fields}</div>
|
||||
</details>
|
||||
`;
|
||||
}
|
||||
|
|
@ -1078,55 +1059,50 @@ function renderArray(params: {
|
|||
</button>
|
||||
</div>
|
||||
${help ? html`<div class="cfg-array__help">${help}</div>` : nothing}
|
||||
|
||||
${
|
||||
arr.length === 0
|
||||
? html`
|
||||
<div class="cfg-array__empty">No items yet. Click "Add" to create one.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="cfg-array__items">
|
||||
${arr.map(
|
||||
(item, idx) => html`
|
||||
<div class="cfg-array__item">
|
||||
<div class="cfg-array__item-header">
|
||||
<span class="cfg-array__item-index">#${idx + 1}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-array__item-remove"
|
||||
title="Remove item"
|
||||
?disabled=${disabled}
|
||||
@click=${() => {
|
||||
const next = [...arr];
|
||||
next.splice(idx, 1);
|
||||
onPatch(path, next);
|
||||
}}
|
||||
>
|
||||
${icons.trash}
|
||||
</button>
|
||||
</div>
|
||||
<div class="cfg-array__item-content">
|
||||
${renderNode({
|
||||
schema: itemsSchema,
|
||||
value: item,
|
||||
path: [...path, idx],
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
searchCriteria: childSearchCriteria,
|
||||
showLabel: false,
|
||||
revealSensitive,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath,
|
||||
onPatch,
|
||||
})}
|
||||
</div>
|
||||
${arr.length === 0
|
||||
? html` <div class="cfg-array__empty">No items yet. Click "Add" to create one.</div> `
|
||||
: html`
|
||||
<div class="cfg-array__items">
|
||||
${arr.map(
|
||||
(item, idx) => html`
|
||||
<div class="cfg-array__item">
|
||||
<div class="cfg-array__item-header">
|
||||
<span class="cfg-array__item-index">#${idx + 1}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-array__item-remove"
|
||||
title="Remove item"
|
||||
?disabled=${disabled}
|
||||
@click=${() => {
|
||||
const next = [...arr];
|
||||
next.splice(idx, 1);
|
||||
onPatch(path, next);
|
||||
}}
|
||||
>
|
||||
${icons.trash}
|
||||
</button>
|
||||
</div>
|
||||
<div class="cfg-array__item-content">
|
||||
${renderNode({
|
||||
schema: itemsSchema,
|
||||
value: item,
|
||||
path: [...path, idx],
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
searchCriteria: childSearchCriteria,
|
||||
showLabel: false,
|
||||
revealSensitive,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath,
|
||||
onPatch,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -1199,128 +1175,124 @@ function renderMapField(params: {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
${
|
||||
visibleEntries.length === 0
|
||||
? html`
|
||||
<div class="cfg-map__empty">No custom entries.</div>
|
||||
`
|
||||
: html`
|
||||
<div class="cfg-map__items">
|
||||
${visibleEntries.map(([key, entryValue]) => {
|
||||
const valuePath = [...path, key];
|
||||
const fallback = jsonValue(entryValue);
|
||||
const sensitiveState = getSensitiveRenderState({
|
||||
path: valuePath,
|
||||
value: entryValue,
|
||||
hints,
|
||||
revealSensitive: revealSensitive ?? false,
|
||||
isSensitivePathRevealed,
|
||||
});
|
||||
return html`
|
||||
<div class="cfg-map__item">
|
||||
<div class="cfg-map__item-header">
|
||||
<div class="cfg-map__item-key">
|
||||
<input
|
||||
type="text"
|
||||
class="cfg-input cfg-input--sm"
|
||||
placeholder="Key"
|
||||
.value=${key}
|
||||
?disabled=${disabled}
|
||||
@change=${(e: Event) => {
|
||||
const nextKey = (e.target as HTMLInputElement).value.trim();
|
||||
if (!nextKey || nextKey === key) {
|
||||
return;
|
||||
}
|
||||
const next = { ...value };
|
||||
if (nextKey in next) {
|
||||
return;
|
||||
}
|
||||
next[nextKey] = next[key];
|
||||
delete next[key];
|
||||
onPatch(path, next);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-map__item-remove"
|
||||
title="Remove entry"
|
||||
?disabled=${disabled}
|
||||
@click=${() => {
|
||||
const next = { ...value };
|
||||
delete next[key];
|
||||
onPatch(path, next);
|
||||
}}
|
||||
>
|
||||
${icons.trash}
|
||||
</button>
|
||||
</div>
|
||||
<div class="cfg-map__item-value">
|
||||
${
|
||||
anySchema
|
||||
? html`
|
||||
<div class="cfg-input-wrap">
|
||||
<textarea
|
||||
class="cfg-textarea cfg-textarea--sm${sensitiveState.isRedacted ? " cfg-textarea--redacted" : ""}"
|
||||
placeholder=${
|
||||
sensitiveState.isRedacted ? REDACTED_PLACEHOLDER : "JSON value"
|
||||
${visibleEntries.length === 0
|
||||
? html` <div class="cfg-map__empty">No custom entries.</div> `
|
||||
: html`
|
||||
<div class="cfg-map__items">
|
||||
${visibleEntries.map(([key, entryValue]) => {
|
||||
const valuePath = [...path, key];
|
||||
const fallback = jsonValue(entryValue);
|
||||
const sensitiveState = getSensitiveRenderState({
|
||||
path: valuePath,
|
||||
value: entryValue,
|
||||
hints,
|
||||
revealSensitive: revealSensitive ?? false,
|
||||
isSensitivePathRevealed,
|
||||
});
|
||||
return html`
|
||||
<div class="cfg-map__item">
|
||||
<div class="cfg-map__item-header">
|
||||
<div class="cfg-map__item-key">
|
||||
<input
|
||||
type="text"
|
||||
class="cfg-input cfg-input--sm"
|
||||
placeholder="Key"
|
||||
.value=${key}
|
||||
?disabled=${disabled}
|
||||
@change=${(e: Event) => {
|
||||
const nextKey = (e.target as HTMLInputElement).value.trim();
|
||||
if (!nextKey || nextKey === key) {
|
||||
return;
|
||||
}
|
||||
rows="2"
|
||||
.value=${sensitiveState.isRedacted ? "" : fallback}
|
||||
?disabled=${disabled}
|
||||
?readonly=${sensitiveState.isRedacted}
|
||||
@click=${() => {
|
||||
if (sensitiveState.isRedacted && onToggleSensitivePath) {
|
||||
onToggleSensitivePath(valuePath);
|
||||
}
|
||||
}}
|
||||
@change=${(e: Event) => {
|
||||
if (sensitiveState.isRedacted) {
|
||||
return;
|
||||
}
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
const raw = target.value.trim();
|
||||
if (!raw) {
|
||||
onPatch(valuePath, undefined);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
onPatch(valuePath, JSON.parse(raw));
|
||||
} catch {
|
||||
target.value = fallback;
|
||||
}
|
||||
}}
|
||||
></textarea>
|
||||
${renderSensitiveToggleButton({
|
||||
const next = { ...value };
|
||||
if (nextKey in next) {
|
||||
return;
|
||||
}
|
||||
next[nextKey] = next[key];
|
||||
delete next[key];
|
||||
onPatch(path, next);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="cfg-map__item-remove"
|
||||
title="Remove entry"
|
||||
?disabled=${disabled}
|
||||
@click=${() => {
|
||||
const next = { ...value };
|
||||
delete next[key];
|
||||
onPatch(path, next);
|
||||
}}
|
||||
>
|
||||
${icons.trash}
|
||||
</button>
|
||||
</div>
|
||||
<div class="cfg-map__item-value">
|
||||
${anySchema
|
||||
? html`
|
||||
<div class="cfg-input-wrap">
|
||||
<textarea
|
||||
class="cfg-textarea cfg-textarea--sm${sensitiveState.isRedacted
|
||||
? " cfg-textarea--redacted"
|
||||
: ""}"
|
||||
placeholder=${sensitiveState.isRedacted
|
||||
? REDACTED_PLACEHOLDER
|
||||
: "JSON value"}
|
||||
rows="2"
|
||||
.value=${sensitiveState.isRedacted ? "" : fallback}
|
||||
?disabled=${disabled}
|
||||
?readonly=${sensitiveState.isRedacted}
|
||||
@click=${() => {
|
||||
if (sensitiveState.isRedacted && onToggleSensitivePath) {
|
||||
onToggleSensitivePath(valuePath);
|
||||
}
|
||||
}}
|
||||
@change=${(e: Event) => {
|
||||
if (sensitiveState.isRedacted) {
|
||||
return;
|
||||
}
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
const raw = target.value.trim();
|
||||
if (!raw) {
|
||||
onPatch(valuePath, undefined);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
onPatch(valuePath, JSON.parse(raw));
|
||||
} catch {
|
||||
target.value = fallback;
|
||||
}
|
||||
}}
|
||||
></textarea>
|
||||
${renderSensitiveToggleButton({
|
||||
path: valuePath,
|
||||
state: sensitiveState,
|
||||
disabled,
|
||||
onToggleSensitivePath,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: renderNode({
|
||||
schema,
|
||||
value: entryValue,
|
||||
path: valuePath,
|
||||
state: sensitiveState,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
searchCriteria,
|
||||
showLabel: false,
|
||||
revealSensitive,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath,
|
||||
onPatch,
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: renderNode({
|
||||
schema,
|
||||
value: entryValue,
|
||||
path: valuePath,
|
||||
hints,
|
||||
unsupported,
|
||||
disabled,
|
||||
searchCriteria,
|
||||
showLabel: false,
|
||||
revealSensitive,
|
||||
isSensitivePathRevealed,
|
||||
onToggleSensitivePath,
|
||||
onPatch,
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -360,16 +360,12 @@ function matchesSearch(params: {
|
|||
|
||||
export function renderConfigForm(props: ConfigFormProps) {
|
||||
if (!props.schema) {
|
||||
return html`
|
||||
<div class="muted">Schema unavailable.</div>
|
||||
`;
|
||||
return html` <div class="muted">Schema unavailable.</div> `;
|
||||
}
|
||||
const schema = props.schema;
|
||||
const value = props.value ?? {};
|
||||
if (schemaType(schema) !== "object" || !schema.properties) {
|
||||
return html`
|
||||
<div class="callout danger">Unsupported schema. Use Raw.</div>
|
||||
`;
|
||||
return html` <div class="callout danger">Unsupported schema. Use Raw.</div> `;
|
||||
}
|
||||
const unsupported = new Set(props.unsupportedPaths ?? []);
|
||||
const properties = schema.properties;
|
||||
|
|
@ -449,11 +445,9 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||
<span class="config-section-card__icon">${getSectionIcon(params.sectionKey)}</span>
|
||||
<div class="config-section-card__titles">
|
||||
<h3 class="config-section-card__title">${params.label}</h3>
|
||||
${
|
||||
params.description
|
||||
? html`<p class="config-section-card__desc">${params.description}</p>`
|
||||
: nothing
|
||||
}
|
||||
${params.description
|
||||
? html`<p class="config-section-card__desc">${params.description}</p>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-section-card__content">
|
||||
|
|
@ -477,45 +471,43 @@ export function renderConfigForm(props: ConfigFormProps) {
|
|||
|
||||
return html`
|
||||
<div class="config-form config-form--modern">
|
||||
${
|
||||
subsectionContext
|
||||
? (() => {
|
||||
const { sectionKey, subsectionKey, schema: node } = subsectionContext;
|
||||
const hint = hintForPath([sectionKey, subsectionKey], props.uiHints);
|
||||
const label = hint?.label ?? node.title ?? humanize(subsectionKey);
|
||||
const description = hint?.help ?? node.description ?? "";
|
||||
const sectionValue = value[sectionKey];
|
||||
const scopedValue =
|
||||
sectionValue && typeof sectionValue === "object"
|
||||
? (sectionValue as Record<string, unknown>)[subsectionKey]
|
||||
: undefined;
|
||||
return renderSectionCard({
|
||||
id: `config-section-${sectionKey}-${subsectionKey}`,
|
||||
sectionKey,
|
||||
label,
|
||||
description,
|
||||
node,
|
||||
nodeValue: scopedValue,
|
||||
path: [sectionKey, subsectionKey],
|
||||
});
|
||||
})()
|
||||
: filteredEntries.map(([key, node]) => {
|
||||
const meta = SECTION_META[key] ?? {
|
||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
description: node.description ?? "",
|
||||
};
|
||||
${subsectionContext
|
||||
? (() => {
|
||||
const { sectionKey, subsectionKey, schema: node } = subsectionContext;
|
||||
const hint = hintForPath([sectionKey, subsectionKey], props.uiHints);
|
||||
const label = hint?.label ?? node.title ?? humanize(subsectionKey);
|
||||
const description = hint?.help ?? node.description ?? "";
|
||||
const sectionValue = value[sectionKey];
|
||||
const scopedValue =
|
||||
sectionValue && typeof sectionValue === "object"
|
||||
? (sectionValue as Record<string, unknown>)[subsectionKey]
|
||||
: undefined;
|
||||
return renderSectionCard({
|
||||
id: `config-section-${sectionKey}-${subsectionKey}`,
|
||||
sectionKey,
|
||||
label,
|
||||
description,
|
||||
node,
|
||||
nodeValue: scopedValue,
|
||||
path: [sectionKey, subsectionKey],
|
||||
});
|
||||
})()
|
||||
: filteredEntries.map(([key, node]) => {
|
||||
const meta = SECTION_META[key] ?? {
|
||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
description: node.description ?? "",
|
||||
};
|
||||
|
||||
return renderSectionCard({
|
||||
id: `config-section-${key}`,
|
||||
sectionKey: key,
|
||||
label: meta.label,
|
||||
description: meta.description,
|
||||
node,
|
||||
nodeValue: value[key],
|
||||
path: [key],
|
||||
});
|
||||
})
|
||||
}
|
||||
return renderSectionCard({
|
||||
id: `config-section-${key}`,
|
||||
sectionKey: key,
|
||||
label: meta.label,
|
||||
description: meta.description,
|
||||
node,
|
||||
nodeValue: value[key],
|
||||
path: [key],
|
||||
});
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -578,7 +578,9 @@ function renderAppearanceSection(props: ConfigProps) {
|
|||
${THEME_OPTIONS.map(
|
||||
(opt) => html`
|
||||
<button
|
||||
class="settings-theme-card ${opt.id === props.theme ? "settings-theme-card--active" : ""}"
|
||||
class="settings-theme-card ${opt.id === props.theme
|
||||
? "settings-theme-card--active"
|
||||
: ""}"
|
||||
title=${opt.description}
|
||||
@click=${(e: Event) => {
|
||||
if (opt.id !== props.theme) {
|
||||
|
|
@ -591,11 +593,11 @@ function renderAppearanceSection(props: ConfigProps) {
|
|||
>
|
||||
<span class="settings-theme-card__icon" aria-hidden="true">${opt.icon}</span>
|
||||
<span class="settings-theme-card__label">${opt.label}</span>
|
||||
${
|
||||
opt.id === props.theme
|
||||
? html`<span class="settings-theme-card__check" aria-hidden="true">${icons.check}</span>`
|
||||
: nothing
|
||||
}
|
||||
${opt.id === props.theme
|
||||
? html`<span class="settings-theme-card__check" aria-hidden="true"
|
||||
>${icons.check}</span
|
||||
>`
|
||||
: nothing}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
|
|
@ -636,20 +638,20 @@ function renderAppearanceSection(props: ConfigProps) {
|
|||
<div class="settings-info-row">
|
||||
<span class="settings-info-row__label">Status</span>
|
||||
<span class="settings-info-row__value">
|
||||
<span class="settings-status-dot ${props.connected ? "settings-status-dot--ok" : ""}"></span>
|
||||
<span
|
||||
class="settings-status-dot ${props.connected ? "settings-status-dot--ok" : ""}"
|
||||
></span>
|
||||
${props.connected ? "Connected" : "Offline"}
|
||||
</span>
|
||||
</div>
|
||||
${
|
||||
props.assistantName
|
||||
? html`
|
||||
${props.assistantName
|
||||
? html`
|
||||
<div class="settings-info-row">
|
||||
<span class="settings-info-row__label">Assistant</span>
|
||||
<span class="settings-info-row__value">${props.assistantName}</span>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -784,132 +786,102 @@ export function renderConfig(props: ConfigProps) {
|
|||
<main class="config-main">
|
||||
<div class="config-actions">
|
||||
<div class="config-actions__left">
|
||||
${
|
||||
showModeToggle
|
||||
? html`
|
||||
<div class="config-mode-toggle">
|
||||
<button
|
||||
class="config-mode-toggle__btn ${formMode === "form" ? "active" : ""}"
|
||||
?disabled=${props.schemaLoading || !props.schema}
|
||||
title=${formUnsafe ? "Form view can't safely edit some fields" : ""}
|
||||
@click=${() => props.onFormModeChange("form")}
|
||||
>
|
||||
Form
|
||||
</button>
|
||||
<button
|
||||
class="config-mode-toggle__btn ${formMode === "raw" ? "active" : ""}"
|
||||
@click=${() => props.onFormModeChange("raw")}
|
||||
>
|
||||
Raw
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
hasChanges
|
||||
? html`
|
||||
<span class="config-changes-badge"
|
||||
>${
|
||||
formMode === "raw"
|
||||
? "Unsaved changes"
|
||||
: `${diff.length} unsaved change${diff.length !== 1 ? "s" : ""}`
|
||||
}</span
|
||||
>
|
||||
`
|
||||
: html`
|
||||
<span class="config-status muted">No changes</span>
|
||||
`
|
||||
}
|
||||
${showModeToggle
|
||||
? html`
|
||||
<div class="config-mode-toggle">
|
||||
<button
|
||||
class="config-mode-toggle__btn ${formMode === "form" ? "active" : ""}"
|
||||
?disabled=${props.schemaLoading || !props.schema}
|
||||
title=${formUnsafe ? "Form view can't safely edit some fields" : ""}
|
||||
@click=${() => props.onFormModeChange("form")}
|
||||
>
|
||||
Form
|
||||
</button>
|
||||
<button
|
||||
class="config-mode-toggle__btn ${formMode === "raw" ? "active" : ""}"
|
||||
@click=${() => props.onFormModeChange("raw")}
|
||||
>
|
||||
Raw
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${hasChanges
|
||||
? html`
|
||||
<span class="config-changes-badge"
|
||||
>${formMode === "raw"
|
||||
? "Unsaved changes"
|
||||
: `${diff.length} unsaved change${diff.length !== 1 ? "s" : ""}`}</span
|
||||
>
|
||||
`
|
||||
: html` <span class="config-status muted">No changes</span> `}
|
||||
</div>
|
||||
<div class="config-actions__right">
|
||||
${
|
||||
props.onOpenFile
|
||||
? html`
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
title=${props.configPath ? `Open ${props.configPath}` : "Open config file"}
|
||||
@click=${props.onOpenFile}
|
||||
>
|
||||
${icons.fileText} Open
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${props.loading}
|
||||
@click=${props.onReload}
|
||||
>
|
||||
${props.onOpenFile
|
||||
? html`
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
title=${props.configPath ? `Open ${props.configPath}` : "Open config file"}
|
||||
@click=${props.onOpenFile}
|
||||
>
|
||||
${icons.fileText} Open
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
<button class="btn btn--sm" ?disabled=${props.loading} @click=${props.onReload}>
|
||||
${props.loading ? "Loading…" : "Reload"}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm primary"
|
||||
?disabled=${!canSave}
|
||||
@click=${props.onSave}
|
||||
>
|
||||
<button class="btn btn--sm primary" ?disabled=${!canSave} @click=${props.onSave}>
|
||||
${props.saving ? "Saving…" : "Save"}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${!canApply}
|
||||
@click=${props.onApply}
|
||||
>
|
||||
<button class="btn btn--sm" ?disabled=${!canApply} @click=${props.onApply}>
|
||||
${props.applying ? "Applying…" : "Apply"}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${!canUpdate}
|
||||
@click=${props.onUpdate}
|
||||
>
|
||||
<button class="btn btn--sm" ?disabled=${!canUpdate} @click=${props.onUpdate}>
|
||||
${props.updating ? "Updating…" : "Update"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-top-tabs">
|
||||
${
|
||||
formMode === "form"
|
||||
? html`
|
||||
<div class="config-search config-search--top">
|
||||
<div class="config-search__input-row">
|
||||
<svg
|
||||
class="config-search__icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="M21 21l-4.35-4.35"></path>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
class="config-search__input"
|
||||
placeholder="Search settings..."
|
||||
aria-label="Search settings"
|
||||
.value=${props.searchQuery}
|
||||
@input=${(e: Event) =>
|
||||
props.onSearchChange((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
${
|
||||
props.searchQuery
|
||||
? html`
|
||||
<button
|
||||
class="config-search__clear"
|
||||
aria-label="Clear search"
|
||||
@click=${() => props.onSearchChange("")}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${formMode === "form"
|
||||
? html`
|
||||
<div class="config-search config-search--top">
|
||||
<div class="config-search__input-row">
|
||||
<svg
|
||||
class="config-search__icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="M21 21l-4.35-4.35"></path>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
class="config-search__input"
|
||||
placeholder="Search settings..."
|
||||
aria-label="Search settings"
|
||||
.value=${props.searchQuery}
|
||||
@input=${(e: Event) =>
|
||||
props.onSearchChange((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
${props.searchQuery
|
||||
? html`
|
||||
<button
|
||||
class="config-search__clear"
|
||||
aria-label="Clear search"
|
||||
@click=${() => props.onSearchChange("")}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="config-top-tabs__scroller" role="tablist" aria-label="Settings sections">
|
||||
${topTabs.map(
|
||||
|
|
@ -926,41 +898,50 @@ export function renderConfig(props: ConfigProps) {
|
|||
`,
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
${
|
||||
validity === "invalid" && !cvs.validityDismissed
|
||||
? html`
|
||||
${validity === "invalid" && !cvs.validityDismissed
|
||||
? html`
|
||||
<div class="config-validity-warning">
|
||||
<svg class="config-validity-warning__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<svg
|
||||
class="config-validity-warning__icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
|
||||
></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>
|
||||
<span class="config-validity-warning__text">Your configuration is invalid. Some settings may not work as expected.</span>
|
||||
<span class="config-validity-warning__text"
|
||||
>Your configuration is invalid. Some settings may not work as expected.</span
|
||||
>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
@click=${() => {
|
||||
cvs.validityDismissed = true;
|
||||
requestUpdate();
|
||||
}}
|
||||
>Don't remind again</button>
|
||||
>
|
||||
Don't remind again
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
|
||||
<!-- Diff panel (form mode only - raw mode doesn't have granular diff) -->
|
||||
${
|
||||
hasChanges && formMode === "form"
|
||||
? html`
|
||||
${hasChanges && formMode === "form"
|
||||
? html`
|
||||
<details class="config-diff">
|
||||
<summary class="config-diff__summary">
|
||||
<span
|
||||
>View ${diff.length} pending
|
||||
change${diff.length !== 1 ? "s" : ""}</span
|
||||
>
|
||||
<span>View ${diff.length} pending change${diff.length !== 1 ? "s" : ""}</span>
|
||||
<svg
|
||||
class="config-diff__chevron"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -991,63 +972,63 @@ export function renderConfig(props: ConfigProps) {
|
|||
</div>
|
||||
</details>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
activeSectionMeta && formMode === "form"
|
||||
? html`
|
||||
<div class="config-section-hero">
|
||||
<div class="config-section-hero__icon">
|
||||
${getSectionIcon(props.activeSection ?? "")}
|
||||
: nothing}
|
||||
${activeSectionMeta && formMode === "form"
|
||||
? html`
|
||||
<div class="config-section-hero">
|
||||
<div class="config-section-hero__icon">
|
||||
${getSectionIcon(props.activeSection ?? "")}
|
||||
</div>
|
||||
<div class="config-section-hero__text">
|
||||
<div class="config-section-hero__title">
|
||||
${activeSectionMeta.label}
|
||||
</div>
|
||||
${
|
||||
activeSectionMeta.description
|
||||
? html`<div class="config-section-hero__desc">
|
||||
<div class="config-section-hero__title">${activeSectionMeta.label}</div>
|
||||
${activeSectionMeta.description
|
||||
? html`<div class="config-section-hero__desc">
|
||||
${activeSectionMeta.description}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</div>
|
||||
${
|
||||
props.activeSection === "env"
|
||||
? html`
|
||||
${props.activeSection === "env"
|
||||
? html`
|
||||
<button
|
||||
class="config-env-peek-btn ${envSensitiveVisible ? "config-env-peek-btn--active" : ""}"
|
||||
class="config-env-peek-btn ${envSensitiveVisible
|
||||
? "config-env-peek-btn--active"
|
||||
: ""}"
|
||||
title=${envSensitiveVisible ? "Hide env values" : "Reveal env values"}
|
||||
@click=${() => {
|
||||
cvs.envRevealed = !cvs.envRevealed;
|
||||
requestUpdate();
|
||||
}}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
Peek
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
<!-- Form content -->
|
||||
<div class="config-content">
|
||||
${
|
||||
props.activeSection === "__appearance__"
|
||||
? includeVirtualSections
|
||||
? renderAppearanceSection(props)
|
||||
: nothing
|
||||
: formMode === "form"
|
||||
? html`
|
||||
${showAppearanceOnRoot ? renderAppearanceSection(props) : nothing}
|
||||
${
|
||||
props.schemaLoading
|
||||
${props.activeSection === "__appearance__"
|
||||
? includeVirtualSections
|
||||
? renderAppearanceSection(props)
|
||||
: nothing
|
||||
: formMode === "form"
|
||||
? html`
|
||||
${showAppearanceOnRoot ? renderAppearanceSection(props) : nothing}
|
||||
${props.schemaLoading
|
||||
? html`
|
||||
<div class="config-loading">
|
||||
<div class="config-loading__spinner"></div>
|
||||
|
|
@ -1071,39 +1052,38 @@ export function renderConfig(props: ConfigProps) {
|
|||
toggleSensitivePathReveal(path);
|
||||
requestUpdate();
|
||||
},
|
||||
})
|
||||
}
|
||||
`
|
||||
: (() => {
|
||||
const sensitiveCount = countSensitiveConfigValues(
|
||||
props.formValue,
|
||||
[],
|
||||
props.uiHints,
|
||||
);
|
||||
const blurred = sensitiveCount > 0 && !cvs.rawRevealed;
|
||||
return html`
|
||||
${
|
||||
formUnsafe
|
||||
? html`
|
||||
<div class="callout info" style="margin-bottom: 12px">
|
||||
Your config contains fields the form editor can't safely represent. Use Raw mode to edit those
|
||||
entries.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
})}
|
||||
`
|
||||
: (() => {
|
||||
const sensitiveCount = countSensitiveConfigValues(
|
||||
props.formValue,
|
||||
[],
|
||||
props.uiHints,
|
||||
);
|
||||
const blurred = sensitiveCount > 0 && !cvs.rawRevealed;
|
||||
return html`
|
||||
${formUnsafe
|
||||
? html`
|
||||
<div class="callout info" style="margin-bottom: 12px">
|
||||
Your config contains fields the form editor can't safely represent. Use
|
||||
Raw mode to edit those entries.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="field config-raw-field">
|
||||
<span style="display:flex;align-items:center;gap:8px;">
|
||||
Raw config (JSON/JSON5)
|
||||
${
|
||||
sensitiveCount > 0
|
||||
? html`
|
||||
<span class="pill pill--sm">${sensitiveCount} secret${sensitiveCount === 1 ? "" : "s"} ${blurred ? "redacted" : "visible"}</span>
|
||||
${sensitiveCount > 0
|
||||
? html`
|
||||
<span class="pill pill--sm"
|
||||
>${sensitiveCount} secret${sensitiveCount === 1 ? "" : "s"}
|
||||
${blurred ? "redacted" : "visible"}</span
|
||||
>
|
||||
<button
|
||||
class="btn btn--icon config-raw-toggle ${blurred ? "" : "active"}"
|
||||
title=${
|
||||
blurred ? "Reveal sensitive values" : "Hide sensitive values"
|
||||
}
|
||||
title=${blurred
|
||||
? "Reveal sensitive values"
|
||||
: "Hide sensitive values"}
|
||||
aria-label="Toggle raw config redaction"
|
||||
aria-pressed=${!blurred}
|
||||
@click=${() => {
|
||||
|
|
@ -1114,42 +1094,34 @@ export function renderConfig(props: ConfigProps) {
|
|||
${blurred ? icons.eyeOff : icons.eye}
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</span>
|
||||
${
|
||||
blurred
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
${sensitiveCount} sensitive value${sensitiveCount === 1 ? "" : "s"} hidden. Use the
|
||||
reveal button above to edit the raw config.
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<textarea
|
||||
placeholder="Raw config (JSON/JSON5)"
|
||||
.value=${props.raw}
|
||||
@input=${(e: Event) => {
|
||||
props.onRawChange((e.target as HTMLTextAreaElement).value);
|
||||
}}
|
||||
></textarea>
|
||||
`
|
||||
}
|
||||
${blurred
|
||||
? html`
|
||||
<div class="callout info" style="margin-top: 12px">
|
||||
${sensitiveCount} sensitive value${sensitiveCount === 1 ? "" : "s"}
|
||||
hidden. Use the reveal button above to edit the raw config.
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<textarea
|
||||
placeholder="Raw config (JSON/JSON5)"
|
||||
.value=${props.raw}
|
||||
@input=${(e: Event) => {
|
||||
props.onRawChange((e.target as HTMLTextAreaElement).value);
|
||||
}}
|
||||
></textarea>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
})()
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
|
||||
${
|
||||
props.issues.length > 0
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
<pre class="code-block">
|
||||
${JSON.stringify(props.issues, null, 2)}</pre
|
||||
>
|
||||
${props.issues.length > 0
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
<pre class="code-block">${JSON.stringify(props.issues, null, 2)}</pre>
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -48,14 +48,12 @@ export function renderDebug(props: DebugProps) {
|
|||
<div class="stack" style="margin-top: 12px;">
|
||||
<div>
|
||||
<div class="muted">Status</div>
|
||||
${
|
||||
securitySummary
|
||||
? html`<div class="callout ${securityTone}" style="margin-top: 8px;">
|
||||
${securitySummary
|
||||
? html`<div class="callout ${securityTone}" style="margin-top: 8px;">
|
||||
Security audit: ${securityLabel}${info > 0 ? ` · ${info} info` : ""}. Run
|
||||
<span class="mono">openclaw security audit --deep</span> for details.
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
<pre class="code-block">${JSON.stringify(props.status ?? {}, null, 2)}</pre>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -77,15 +75,12 @@ export function renderDebug(props: DebugProps) {
|
|||
<span>Method</span>
|
||||
<select
|
||||
.value=${props.callMethod}
|
||||
@change=${(e: Event) => props.onCallMethodChange((e.target as HTMLSelectElement).value)}
|
||||
@change=${(e: Event) =>
|
||||
props.onCallMethodChange((e.target as HTMLSelectElement).value)}
|
||||
>
|
||||
${
|
||||
!props.callMethod
|
||||
? html`
|
||||
<option value="" disabled>Select a method…</option>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${!props.callMethod
|
||||
? html` <option value="" disabled>Select a method…</option> `
|
||||
: nothing}
|
||||
${props.methods.map((m) => html`<option value=${m}>${m}</option>`)}
|
||||
</select>
|
||||
</label>
|
||||
|
|
@ -102,40 +97,29 @@ export function renderDebug(props: DebugProps) {
|
|||
<div class="row" style="margin-top: 12px;">
|
||||
<button class="btn primary" @click=${props.onCall}>Call</button>
|
||||
</div>
|
||||
${
|
||||
props.callError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${props.callError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.callResult
|
||||
? html`<pre class="code-block" style="margin-top: 12px;">${props.callResult}</pre>`
|
||||
: nothing
|
||||
}
|
||||
${props.callError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.callError}</div>`
|
||||
: nothing}
|
||||
${props.callResult
|
||||
? html`<pre class="code-block" style="margin-top: 12px;">${props.callResult}</pre>`
|
||||
: nothing}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" style="margin-top: 18px;">
|
||||
<div class="card-title">Models</div>
|
||||
<div class="card-sub">Catalog from models.list.</div>
|
||||
<pre class="code-block" style="margin-top: 12px;">${JSON.stringify(
|
||||
props.models ?? [],
|
||||
null,
|
||||
2,
|
||||
)}</pre>
|
||||
<pre class="code-block" style="margin-top: 12px;">
|
||||
${JSON.stringify(props.models ?? [], null, 2)}</pre
|
||||
>
|
||||
</section>
|
||||
|
||||
<section class="card" style="margin-top: 18px;">
|
||||
<div class="card-title">Event Log</div>
|
||||
<div class="card-sub">Latest gateway events.</div>
|
||||
${
|
||||
props.eventLog.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px">No events yet.</div>
|
||||
`
|
||||
: html`
|
||||
${props.eventLog.length === 0
|
||||
? html` <div class="muted" style="margin-top: 12px">No events yet.</div> `
|
||||
: html`
|
||||
<div class="list debug-event-log" style="margin-top: 12px;">
|
||||
${props.eventLog.map(
|
||||
(evt) => html`
|
||||
|
|
@ -145,16 +129,15 @@ export function renderDebug(props: DebugProps) {
|
|||
<div class="list-sub">${new Date(evt.ts).toLocaleTimeString()}</div>
|
||||
</div>
|
||||
<div class="list-meta debug-event-log__meta">
|
||||
<pre class="code-block debug-event-log__payload">${formatEventPayload(
|
||||
evt.payload,
|
||||
)}</pre>
|
||||
<pre class="code-block debug-event-log__payload">
|
||||
${formatEventPayload(evt.payload)}</pre
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
`}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,27 +39,20 @@ export function renderExecApprovalPrompt(state: AppViewState) {
|
|||
<div class="exec-approval-title">Exec approval needed</div>
|
||||
<div class="exec-approval-sub">${remaining}</div>
|
||||
</div>
|
||||
${
|
||||
queueCount > 1
|
||||
? html`<div class="exec-approval-queue">${queueCount} pending</div>`
|
||||
: nothing
|
||||
}
|
||||
${queueCount > 1
|
||||
? html`<div class="exec-approval-queue">${queueCount} pending</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="exec-approval-command mono">${request.command}</div>
|
||||
<div class="exec-approval-meta">
|
||||
${renderMetaRow("Host", request.host)}
|
||||
${renderMetaRow("Agent", request.agentId)}
|
||||
${renderMetaRow("Session", request.sessionKey)}
|
||||
${renderMetaRow("CWD", request.cwd)}
|
||||
${renderMetaRow("Host", request.host)} ${renderMetaRow("Agent", request.agentId)}
|
||||
${renderMetaRow("Session", request.sessionKey)} ${renderMetaRow("CWD", request.cwd)}
|
||||
${renderMetaRow("Resolved", request.resolvedPath)}
|
||||
${renderMetaRow("Security", request.security)}
|
||||
${renderMetaRow("Ask", request.ask)}
|
||||
${renderMetaRow("Security", request.security)} ${renderMetaRow("Ask", request.ask)}
|
||||
</div>
|
||||
${
|
||||
state.execApprovalError
|
||||
? html`<div class="exec-approval-error">${state.execApprovalError}</div>`
|
||||
: nothing
|
||||
}
|
||||
${state.execApprovalError
|
||||
? html`<div class="exec-approval-error">${state.execApprovalError}</div>`
|
||||
: nothing}
|
||||
<div class="exec-approval-actions">
|
||||
<button
|
||||
class="btn primary"
|
||||
|
|
|
|||
|
|
@ -21,18 +21,10 @@ export function renderGatewayUrlConfirmation(state: AppViewState) {
|
|||
Only confirm if you trust this URL. Malicious URLs can compromise your system.
|
||||
</div>
|
||||
<div class="exec-approval-actions">
|
||||
<button
|
||||
class="btn primary"
|
||||
@click=${() => state.handleGatewayUrlConfirm()}
|
||||
>
|
||||
<button class="btn primary" @click=${() => state.handleGatewayUrlConfirm()}>
|
||||
Confirm
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click=${() => state.handleGatewayUrlCancel()}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button class="btn" @click=${() => state.handleGatewayUrlCancel()}>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -42,28 +42,16 @@ export function renderInstances(props: InstancesProps) {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">
|
||||
${props.lastError}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.statusMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">
|
||||
${props.statusMessage}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.lastError}</div>`
|
||||
: nothing}
|
||||
${props.statusMessage
|
||||
? html`<div class="callout" style="margin-top: 12px;">${props.statusMessage}</div>`
|
||||
: nothing}
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${
|
||||
props.entries.length === 0
|
||||
? html`
|
||||
<div class="muted">No instances reported yet.</div>
|
||||
`
|
||||
: props.entries.map((entry) => renderEntry(entry, masked))
|
||||
}
|
||||
${props.entries.length === 0
|
||||
? html` <div class="muted">No instances reported yet.</div> `
|
||||
: props.entries.map((entry) => renderEntry(entry, masked))}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
|
@ -89,7 +77,8 @@ function renderEntry(entry: PresenceEntry, masked: boolean) {
|
|||
<span class="${masked ? "redacted" : ""}">${host}</span>
|
||||
</div>
|
||||
<div class="list-sub">
|
||||
${ip ? html`<span class="${masked ? "redacted" : ""}">${ip}</span> ` : nothing}${mode} ${entry.version ?? ""}
|
||||
${ip ? html`<span class="${masked ? "redacted" : ""}">${ip}</span> ` : nothing}${mode}
|
||||
${entry.version ?? ""}
|
||||
</div>
|
||||
<div class="chip-row">
|
||||
<span class="chip">${mode}</span>
|
||||
|
|
@ -97,11 +86,9 @@ function renderEntry(entry: PresenceEntry, masked: boolean) {
|
|||
${scopesLabel ? html`<span class="chip">${scopesLabel}</span>` : nothing}
|
||||
${entry.platform ? html`<span class="chip">${entry.platform}</span>` : nothing}
|
||||
${entry.deviceFamily ? html`<span class="chip">${entry.deviceFamily}</span>` : nothing}
|
||||
${
|
||||
entry.modelIdentifier
|
||||
? html`<span class="chip">${entry.modelIdentifier}</span>`
|
||||
: nothing
|
||||
}
|
||||
${entry.modelIdentifier
|
||||
? html`<span class="chip">${entry.modelIdentifier}</span>`
|
||||
: nothing}
|
||||
${entry.version ? html`<span class="chip">${entry.version}</span>` : nothing}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -95,20 +95,15 @@ export function renderLoginGate(state: AppViewState) {
|
|||
</button>
|
||||
</div>
|
||||
</label>
|
||||
<button
|
||||
class="btn primary login-gate__connect"
|
||||
@click=${() => state.connect()}
|
||||
>
|
||||
<button class="btn primary login-gate__connect" @click=${() => state.connect()}>
|
||||
${t("common.connect")}
|
||||
</button>
|
||||
</div>
|
||||
${
|
||||
state.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 14px;">
|
||||
<div>${state.lastError}</div>
|
||||
</div>`
|
||||
: ""
|
||||
}
|
||||
${state.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 14px;">
|
||||
<div>${state.lastError}</div>
|
||||
</div>`
|
||||
: ""}
|
||||
<div class="login-gate__help">
|
||||
<div class="login-gate__help-title">${t("overview.connection.title")}</div>
|
||||
<ol class="login-gate__steps">
|
||||
|
|
@ -122,7 +117,8 @@ export function renderLoginGate(state: AppViewState) {
|
|||
href="https://docs.openclaw.ai/web/dashboard"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${t("overview.connection.docsLink")}</a>
|
||||
>${t("overview.connection.docsLink")}</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -114,32 +114,25 @@ export function renderLogs(props: LogsProps) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
${
|
||||
props.file
|
||||
? html`<div class="muted" style="margin-top: 10px;">File: ${props.file}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.truncated
|
||||
? html`
|
||||
<div class="callout" style="margin-top: 10px">Log output truncated; showing latest chunk.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.error
|
||||
? html`<div class="callout danger" style="margin-top: 10px;">${props.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
${props.file
|
||||
? html`<div class="muted" style="margin-top: 10px;">File: ${props.file}</div>`
|
||||
: nothing}
|
||||
${props.truncated
|
||||
? html`
|
||||
<div class="callout" style="margin-top: 10px">
|
||||
Log output truncated; showing latest chunk.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${props.error
|
||||
? html`<div class="callout danger" style="margin-top: 10px;">${props.error}</div>`
|
||||
: nothing}
|
||||
|
||||
<div class="log-stream" style="margin-top: 12px;" @scroll=${props.onScroll}>
|
||||
${
|
||||
filtered.length === 0
|
||||
? html`
|
||||
<div class="muted" style="padding: 12px">No log entries.</div>
|
||||
`
|
||||
: filtered.map(
|
||||
(entry) => html`
|
||||
${filtered.length === 0
|
||||
? html` <div class="muted" style="padding: 12px">No log entries.</div> `
|
||||
: filtered.map(
|
||||
(entry) => html`
|
||||
<div class="log-row">
|
||||
<div class="log-time mono">${formatTime(entry.time)}</div>
|
||||
<div class="log-level ${entry.level ?? ""}">${entry.level ?? ""}</div>
|
||||
|
|
@ -147,8 +140,7 @@ export function renderLogs(props: LogsProps) {
|
|||
<div class="log-message mono">${entry.message ?? entry.raw}</div>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -15,25 +15,21 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
|||
<div class="sidebar-panel">
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-title">Tool Output</div>
|
||||
<button @click=${props.onClose} class="btn" title="Close sidebar">
|
||||
${icons.x}
|
||||
</button>
|
||||
<button @click=${props.onClose} class="btn" title="Close sidebar">${icons.x}</button>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
${
|
||||
props.error
|
||||
? html`
|
||||
${props.error
|
||||
? html`
|
||||
<div class="callout danger">${props.error}</div>
|
||||
<button @click=${props.onViewRawText} class="btn" style="margin-top: 12px;">
|
||||
View Raw Text
|
||||
</button>
|
||||
`
|
||||
: props.content
|
||||
? html`<div class="sidebar-markdown">${unsafeHTML(toSanitizedMarkdownHtml(props.content))}</div>`
|
||||
: html`
|
||||
<div class="muted">No content available</div>
|
||||
`
|
||||
}
|
||||
: props.content
|
||||
? html`<div class="sidebar-markdown">
|
||||
${unsafeHTML(toSanitizedMarkdownHtml(props.content))}
|
||||
</div>`
|
||||
: html` <div class="muted">No content available</div> `}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -211,25 +211,19 @@ export function renderExecApprovals(state: ExecApprovalsState) {
|
|||
</div>
|
||||
|
||||
${renderExecApprovalsTarget(state)}
|
||||
|
||||
${
|
||||
!ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
${!ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
<div class="muted">Load exec approvals to edit allowlists.</div>
|
||||
<button class="btn" ?disabled=${state.loading || !targetReady} @click=${state.onLoad}>
|
||||
${state.loading ? "Loading…" : "Load approvals"}
|
||||
</button>
|
||||
</div>`
|
||||
: html`
|
||||
${renderExecApprovalsTabs(state)}
|
||||
${renderExecApprovalsPolicy(state)}
|
||||
${
|
||||
state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE
|
||||
? nothing
|
||||
: renderExecApprovalsAllowlist(state)
|
||||
}
|
||||
`
|
||||
}
|
||||
: html`
|
||||
${renderExecApprovalsTabs(state)} ${renderExecApprovalsPolicy(state)}
|
||||
${state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE
|
||||
? nothing
|
||||
: renderExecApprovalsAllowlist(state)}
|
||||
`}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
|
@ -242,9 +236,7 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
|||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
<div class="list-title">Target</div>
|
||||
<div class="list-sub">
|
||||
Gateway edits local approvals; node edits the selected node.
|
||||
</div>
|
||||
<div class="list-sub">Gateway edits local approvals; node edits the selected node.</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
<label class="field">
|
||||
|
|
@ -266,9 +258,8 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
|||
<option value="node" ?selected=${state.target === "node"}>Node</option>
|
||||
</select>
|
||||
</label>
|
||||
${
|
||||
state.target === "node"
|
||||
? html`
|
||||
${state.target === "node"
|
||||
? html`
|
||||
<label class="field">
|
||||
<span>Node</span>
|
||||
<select
|
||||
|
|
@ -282,27 +273,19 @@ function renderExecApprovalsTarget(state: ExecApprovalsState) {
|
|||
<option value="" ?selected=${nodeValue === ""}>Select node</option>
|
||||
${state.targetNodes.map(
|
||||
(node) =>
|
||||
html`<option
|
||||
value=${node.id}
|
||||
?selected=${nodeValue === node.id}
|
||||
>
|
||||
html`<option value=${node.id} ?selected=${nodeValue === node.id}>
|
||||
${node.label}
|
||||
</option>`,
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
state.target === "node" && !hasNodes
|
||||
? html`
|
||||
<div class="muted">No nodes advertise exec approvals yet.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${state.target === "node" && !hasNodes
|
||||
? html` <div class="muted">No nodes advertise exec approvals yet.</div> `
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -313,7 +296,9 @@ function renderExecApprovalsTabs(state: ExecApprovalsState) {
|
|||
<span class="label">Scope</span>
|
||||
<div class="row" style="gap: 8px; flex-wrap: wrap;">
|
||||
<button
|
||||
class="btn btn--sm ${state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE ? "active" : ""}"
|
||||
class="btn btn--sm ${state.selectedScope === EXEC_APPROVALS_DEFAULT_SCOPE
|
||||
? "active"
|
||||
: ""}"
|
||||
@click=${() => state.onSelectScope(EXEC_APPROVALS_DEFAULT_SCOPE)}
|
||||
>
|
||||
Defaults
|
||||
|
|
@ -374,19 +359,14 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||
}
|
||||
}}
|
||||
>
|
||||
${
|
||||
!isDefaults
|
||||
? html`<option value="__default__" ?selected=${securityValue === "__default__"}>
|
||||
${!isDefaults
|
||||
? html`<option value="__default__" ?selected=${securityValue === "__default__"}>
|
||||
Use default (${defaults.security})
|
||||
</option>`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
${SECURITY_OPTIONS.map(
|
||||
(option) =>
|
||||
html`<option
|
||||
value=${option.value}
|
||||
?selected=${securityValue === option.value}
|
||||
>
|
||||
html`<option value=${option.value} ?selected=${securityValue === option.value}>
|
||||
${option.label}
|
||||
</option>`,
|
||||
)}
|
||||
|
|
@ -417,19 +397,14 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||
}
|
||||
}}
|
||||
>
|
||||
${
|
||||
!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askValue === "__default__"}>
|
||||
${!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askValue === "__default__"}>
|
||||
Use default (${defaults.ask})
|
||||
</option>`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
${ASK_OPTIONS.map(
|
||||
(option) =>
|
||||
html`<option
|
||||
value=${option.value}
|
||||
?selected=${askValue === option.value}
|
||||
>
|
||||
html`<option value=${option.value} ?selected=${askValue === option.value}>
|
||||
${option.label}
|
||||
</option>`,
|
||||
)}
|
||||
|
|
@ -442,11 +417,9 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||
<div class="list-main">
|
||||
<div class="list-title">Ask fallback</div>
|
||||
<div class="list-sub">
|
||||
${
|
||||
isDefaults
|
||||
? "Applied when the UI prompt is unavailable."
|
||||
: `Default: ${defaults.askFallback}.`
|
||||
}
|
||||
${isDefaults
|
||||
? "Applied when the UI prompt is unavailable."
|
||||
: `Default: ${defaults.askFallback}.`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
|
|
@ -464,19 +437,14 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||
}
|
||||
}}
|
||||
>
|
||||
${
|
||||
!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askFallbackValue === "__default__"}>
|
||||
${!isDefaults
|
||||
? html`<option value="__default__" ?selected=${askFallbackValue === "__default__"}>
|
||||
Use default (${defaults.askFallback})
|
||||
</option>`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
${SECURITY_OPTIONS.map(
|
||||
(option) =>
|
||||
html`<option
|
||||
value=${option.value}
|
||||
?selected=${askFallbackValue === option.value}
|
||||
>
|
||||
html`<option value=${option.value} ?selected=${askFallbackValue === option.value}>
|
||||
${option.label}
|
||||
</option>`,
|
||||
)}
|
||||
|
|
@ -489,13 +457,11 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||
<div class="list-main">
|
||||
<div class="list-title">Auto-allow skill CLIs</div>
|
||||
<div class="list-sub">
|
||||
${
|
||||
isDefaults
|
||||
? "Allow skill executables listed by the Gateway."
|
||||
: autoIsDefault
|
||||
? `Using default (${defaults.autoAllowSkills ? "on" : "off"}).`
|
||||
: `Override (${autoEffective ? "on" : "off"}).`
|
||||
}
|
||||
${isDefaults
|
||||
? "Allow skill executables listed by the Gateway."
|
||||
: autoIsDefault
|
||||
? `Using default (${defaults.autoAllowSkills ? "on" : "off"}).`
|
||||
: `Override (${autoEffective ? "on" : "off"}).`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
|
|
@ -511,17 +477,15 @@ function renderExecApprovalsPolicy(state: ExecApprovalsState) {
|
|||
}}
|
||||
/>
|
||||
</label>
|
||||
${
|
||||
!isDefaults && !autoIsDefault
|
||||
? html`<button
|
||||
${!isDefaults && !autoIsDefault
|
||||
? html`<button
|
||||
class="btn btn--sm"
|
||||
?disabled=${state.disabled}
|
||||
@click=${() => state.onRemove([...basePath, "autoAllowSkills"])}
|
||||
>
|
||||
Use default
|
||||
</button>`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -549,13 +513,9 @@ function renderExecApprovalsAllowlist(state: ExecApprovalsState) {
|
|||
</button>
|
||||
</div>
|
||||
<div class="list" style="margin-top: 12px;">
|
||||
${
|
||||
entries.length === 0
|
||||
? html`
|
||||
<div class="muted">No allowlist entries yet.</div>
|
||||
`
|
||||
: entries.map((entry, index) => renderAllowlistEntry(state, entry, index))
|
||||
}
|
||||
${entries.length === 0
|
||||
? html` <div class="muted">No allowlist entries yet.</div> `
|
||||
: entries.map((entry, index) => renderAllowlistEntry(state, entry, index))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,9 +50,7 @@ export function renderNodes(props: NodesProps) {
|
|||
const bindingState = resolveBindingsState(props);
|
||||
const approvalsState = resolveExecApprovalsState(props);
|
||||
return html`
|
||||
${renderExecApprovals(approvalsState)}
|
||||
${renderBindings(bindingState)}
|
||||
${renderDevices(props)}
|
||||
${renderExecApprovals(approvalsState)} ${renderBindings(bindingState)} ${renderDevices(props)}
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content: space-between;">
|
||||
<div>
|
||||
|
|
@ -64,13 +62,9 @@ export function renderNodes(props: NodesProps) {
|
|||
</button>
|
||||
</div>
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${
|
||||
props.nodes.length === 0
|
||||
? html`
|
||||
<div class="muted">No nodes found.</div>
|
||||
`
|
||||
: props.nodes.map((n) => renderNode(n))
|
||||
}
|
||||
${props.nodes.length === 0
|
||||
? html` <div class="muted">No nodes found.</div> `
|
||||
: props.nodes.map((n) => renderNode(n))}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
|
@ -91,35 +85,25 @@ function renderDevices(props: NodesProps) {
|
|||
${props.devicesLoading ? "Loading…" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
${
|
||||
props.devicesError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.devicesError}</div>`
|
||||
: nothing
|
||||
}
|
||||
${props.devicesError
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.devicesError}</div>`
|
||||
: nothing}
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
${
|
||||
pending.length > 0
|
||||
? html`
|
||||
${pending.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-bottom: 8px;">Pending</div>
|
||||
${pending.map((req) => renderPendingDevice(req, props))}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
paired.length > 0
|
||||
? html`
|
||||
: nothing}
|
||||
${paired.length > 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px; margin-bottom: 8px;">Paired</div>
|
||||
${paired.map((device) => renderPairedDevice(device, props))}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
pending.length === 0 && paired.length === 0
|
||||
? html`
|
||||
<div class="muted">No paired devices.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
${pending.length === 0 && paired.length === 0
|
||||
? html` <div class="muted">No paired devices.</div> `
|
||||
: nothing}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
|
@ -167,18 +151,14 @@ function renderPairedDevice(device: PairedDevice, props: NodesProps) {
|
|||
<div class="list-title">${name}</div>
|
||||
<div class="list-sub">${device.deviceId}${ip}</div>
|
||||
<div class="muted" style="margin-top: 6px;">${roles} · ${scopes}</div>
|
||||
${
|
||||
tokens.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 6px">Tokens: none</div>
|
||||
`
|
||||
: html`
|
||||
${tokens.length === 0
|
||||
? html` <div class="muted" style="margin-top: 6px">Tokens: none</div> `
|
||||
: html`
|
||||
<div class="muted" style="margin-top: 10px;">Tokens</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; margin-top: 6px;">
|
||||
${tokens.map((token) => renderTokenRow(device.deviceId, token, props))}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -200,18 +180,16 @@ function renderTokenRow(deviceId: string, token: DeviceTokenSummary, props: Node
|
|||
>
|
||||
Rotate
|
||||
</button>
|
||||
${
|
||||
token.revokedAtMs
|
||||
? nothing
|
||||
: html`
|
||||
${token.revokedAtMs
|
||||
? nothing
|
||||
: html`
|
||||
<button
|
||||
class="btn btn--sm danger"
|
||||
@click=${() => props.onDeviceRevoke(deviceId, token.role)}
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
`
|
||||
}
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -287,25 +265,21 @@ function renderBindings(state: BindingState) {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
${
|
||||
state.formMode === "raw"
|
||||
? html`
|
||||
<div class="callout warn" style="margin-top: 12px">
|
||||
Switch the Config tab to <strong>Form</strong> mode to edit bindings here.
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
!state.ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
${state.formMode === "raw"
|
||||
? html`
|
||||
<div class="callout warn" style="margin-top: 12px">
|
||||
Switch the Config tab to <strong>Form</strong> mode to edit bindings here.
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${!state.ready
|
||||
? html`<div class="row" style="margin-top: 12px; gap: 12px;">
|
||||
<div class="muted">Load config to edit bindings.</div>
|
||||
<button class="btn" ?disabled=${state.configLoading} @click=${state.onLoadConfig}>
|
||||
${state.configLoading ? "Loading…" : "Load config"}
|
||||
</button>
|
||||
</div>`
|
||||
: html`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 16px;">
|
||||
<div class="list-item">
|
||||
<div class="list-main">
|
||||
|
|
@ -326,35 +300,23 @@ function renderBindings(state: BindingState) {
|
|||
<option value="" ?selected=${defaultValue === ""}>Any node</option>
|
||||
${state.nodes.map(
|
||||
(node) =>
|
||||
html`<option
|
||||
value=${node.id}
|
||||
?selected=${defaultValue === node.id}
|
||||
>
|
||||
html`<option value=${node.id} ?selected=${defaultValue === node.id}>
|
||||
${node.label}
|
||||
</option>`,
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
${
|
||||
!supportsBinding
|
||||
? html`
|
||||
<div class="muted">No nodes with system.run available.</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${!supportsBinding
|
||||
? html` <div class="muted">No nodes with system.run available.</div> `
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
state.agents.length === 0
|
||||
? html`
|
||||
<div class="muted">No agents found.</div>
|
||||
`
|
||||
: state.agents.map((agent) => renderAgentBinding(agent, state))
|
||||
}
|
||||
${state.agents.length === 0
|
||||
? html` <div class="muted">No agents found.</div> `
|
||||
: state.agents.map((agent) => renderAgentBinding(agent, state))}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
`}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
|
@ -369,11 +331,9 @@ function renderAgentBinding(agent: BindingAgent, state: BindingState) {
|
|||
<div class="list-title">${label}</div>
|
||||
<div class="list-sub">
|
||||
${agent.isDefault ? "default agent" : "agent"} ·
|
||||
${
|
||||
bindingValue === "__default__"
|
||||
? `uses default (${state.defaultBinding ?? "any"})`
|
||||
: `override: ${agent.binding}`
|
||||
}
|
||||
${bindingValue === "__default__"
|
||||
? `uses default (${state.defaultBinding ?? "any"})`
|
||||
: `override: ${agent.binding}`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-meta">
|
||||
|
|
@ -392,10 +352,7 @@ function renderAgentBinding(agent: BindingAgent, state: BindingState) {
|
|||
</option>
|
||||
${state.nodes.map(
|
||||
(node) =>
|
||||
html`<option
|
||||
value=${node.id}
|
||||
?selected=${bindingValue === node.id}
|
||||
>
|
||||
html`<option value=${node.id} ?selected=${bindingValue === node.id}>
|
||||
${node.label}
|
||||
</option>`,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -42,16 +42,15 @@ export function renderOverviewAttention(props: OverviewAttentionProps) {
|
|||
<div class="ov-attention-title">${item.title}</div>
|
||||
<div class="muted">${item.description}</div>
|
||||
</div>
|
||||
${
|
||||
item.href
|
||||
? html`<a
|
||||
${item.href
|
||||
? html`<a
|
||||
class="ov-attention-link"
|
||||
href=${item.href}
|
||||
target=${item.external ? EXTERNAL_LINK_TARGET : nothing}
|
||||
rel=${item.external ? buildExternalLinkRel() : nothing}
|
||||
>${t("common.docs")}</a>`
|
||||
: nothing
|
||||
}
|
||||
>${t("common.docs")}</a
|
||||
>`
|
||||
: nothing}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -134,29 +134,29 @@ export function renderOverviewCards(props: OverviewCardsProps) {
|
|||
const sessions = props.sessionsResult?.sessions.slice(0, 5) ?? [];
|
||||
|
||||
return html`
|
||||
<section class="ov-cards">
|
||||
${cards.map((c) => renderStatCard(c, props.onNavigate))}
|
||||
</section>
|
||||
<section class="ov-cards">${cards.map((c) => renderStatCard(c, props.onNavigate))}</section>
|
||||
|
||||
${
|
||||
sessions.length > 0
|
||||
? html`
|
||||
<section class="ov-recent">
|
||||
<h3 class="ov-recent__title">${t("overview.cards.recentSessions")}</h3>
|
||||
<ul class="ov-recent__list">
|
||||
${sessions.map(
|
||||
(s) => html`
|
||||
<li class="ov-recent__row">
|
||||
<span class="ov-recent__key">${blurDigits(s.displayName || s.label || s.key)}</span>
|
||||
<span class="ov-recent__model">${s.model ?? ""}</span>
|
||||
<span class="ov-recent__time">${s.updatedAt ? formatRelativeTimestamp(s.updatedAt) : ""}</span>
|
||||
</li>
|
||||
`,
|
||||
)}
|
||||
</ul>
|
||||
</section>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${sessions.length > 0
|
||||
? html`
|
||||
<section class="ov-recent">
|
||||
<h3 class="ov-recent__title">${t("overview.cards.recentSessions")}</h3>
|
||||
<ul class="ov-recent__list">
|
||||
${sessions.map(
|
||||
(s) => html`
|
||||
<li class="ov-recent__row">
|
||||
<span class="ov-recent__key"
|
||||
>${blurDigits(s.displayName || s.label || s.key)}</span
|
||||
>
|
||||
<span class="ov-recent__model">${s.model ?? ""}</span>
|
||||
<span class="ov-recent__time"
|
||||
>${s.updatedAt ? formatRelativeTimestamp(s.updatedAt) : ""}</span
|
||||
>
|
||||
</li>
|
||||
`,
|
||||
)}
|
||||
</ul>
|
||||
</section>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,11 +28,11 @@ export function renderOverviewEventLog(props: OverviewEventLogProps) {
|
|||
<div class="ov-event-log-entry">
|
||||
<span class="ov-event-log-ts">${new Date(entry.ts).toLocaleTimeString()}</span>
|
||||
<span class="ov-event-log-name">${entry.event}</span>
|
||||
${
|
||||
entry.payload
|
||||
? html`<span class="ov-event-log-payload muted">${formatEventPayload(entry.payload).slice(0, 120)}</span>`
|
||||
: nothing
|
||||
}
|
||||
${entry.payload
|
||||
? html`<span class="ov-event-log-payload muted"
|
||||
>${formatEventPayload(entry.payload).slice(0, 120)}</span
|
||||
>`
|
||||
: nothing}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ export function renderOverviewLogTail(props: OverviewLogTailProps) {
|
|||
e.stopPropagation();
|
||||
props.onRefreshLogs();
|
||||
}}
|
||||
>${icons.loader}</span>
|
||||
>${icons.loader}</span
|
||||
>
|
||||
</summary>
|
||||
<pre class="ov-log-tail-content">${displayLines}</pre>
|
||||
</details>
|
||||
|
|
|
|||
|
|
@ -84,9 +84,7 @@ export function renderOverview(props: OverviewProps) {
|
|||
<span class="mono">openclaw devices list</span><br />
|
||||
<span class="mono">openclaw devices approve <requestId></span>
|
||||
</div>
|
||||
<div style="margin-top: 6px; font-size: 12px;">
|
||||
${t("overview.pairing.mobileHint")}
|
||||
</div>
|
||||
<div style="margin-top: 6px; font-size: 12px;">${t("overview.pairing.mobileHint")}</div>
|
||||
<div style="margin-top: 6px">
|
||||
<a
|
||||
class="session-link"
|
||||
|
|
@ -165,7 +163,9 @@ export function renderOverview(props: OverviewProps) {
|
|||
<div class="muted" style="margin-top: 8px">
|
||||
${t("overview.insecure.hint", { url: "http://127.0.0.1:18789" })}
|
||||
<div style="margin-top: 6px">
|
||||
${t("overview.insecure.stayHttp", { config: "gateway.controlUi.allowInsecureAuth: true" })}
|
||||
${t("overview.insecure.stayHttp", {
|
||||
config: "gateway.controlUi.allowInsecureAuth: true",
|
||||
})}
|
||||
</div>
|
||||
<div style="margin-top: 6px">
|
||||
<a
|
||||
|
|
@ -215,10 +215,9 @@ export function renderOverview(props: OverviewProps) {
|
|||
placeholder="ws://100.x.y.z:18789"
|
||||
/>
|
||||
</label>
|
||||
${
|
||||
isTrustedProxy
|
||||
? ""
|
||||
: html`
|
||||
${isTrustedProxy
|
||||
? ""
|
||||
: html`
|
||||
<label class="field">
|
||||
<span>${t("overview.access.token")}</span>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
|
|
@ -273,8 +272,7 @@ export function renderOverview(props: OverviewProps) {
|
|||
</button>
|
||||
</div>
|
||||
</label>
|
||||
`
|
||||
}
|
||||
`}
|
||||
<label class="field">
|
||||
<span>${t("overview.access.sessionKey")}</span>
|
||||
<input
|
||||
|
|
@ -307,34 +305,41 @@ export function renderOverview(props: OverviewProps) {
|
|||
<div class="row" style="margin-top: 14px;">
|
||||
<button class="btn" @click=${() => props.onConnect()}>${t("common.connect")}</button>
|
||||
<button class="btn" @click=${() => props.onRefresh()}>${t("common.refresh")}</button>
|
||||
<span class="muted">${
|
||||
isTrustedProxy ? t("overview.access.trustedProxy") : t("overview.access.connectHint")
|
||||
}</span>
|
||||
<span class="muted"
|
||||
>${isTrustedProxy
|
||||
? t("overview.access.trustedProxy")
|
||||
: t("overview.access.connectHint")}</span
|
||||
>
|
||||
</div>
|
||||
${
|
||||
!props.connected
|
||||
? html`
|
||||
<div class="login-gate__help" style="margin-top: 16px;">
|
||||
<div class="login-gate__help-title">${t("overview.connection.title")}</div>
|
||||
<ol class="login-gate__steps">
|
||||
<li>${t("overview.connection.step1")}<code>openclaw gateway run</code></li>
|
||||
<li>${t("overview.connection.step2")}<code>openclaw dashboard --no-open</code></li>
|
||||
<li>${t("overview.connection.step3")}</li>
|
||||
<li>${t("overview.connection.step4")}<code>openclaw doctor --generate-gateway-token</code></li>
|
||||
</ol>
|
||||
<div class="login-gate__docs">
|
||||
${t("overview.connection.docsHint")}
|
||||
<a
|
||||
class="session-link"
|
||||
href="https://docs.openclaw.ai/web/dashboard"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${t("overview.connection.docsLink")}</a>
|
||||
</div>
|
||||
${!props.connected
|
||||
? html`
|
||||
<div class="login-gate__help" style="margin-top: 16px;">
|
||||
<div class="login-gate__help-title">${t("overview.connection.title")}</div>
|
||||
<ol class="login-gate__steps">
|
||||
<li>${t("overview.connection.step1")}<code>openclaw gateway run</code></li>
|
||||
<li>
|
||||
${t("overview.connection.step2")}<code>openclaw dashboard --no-open</code>
|
||||
</li>
|
||||
<li>${t("overview.connection.step3")}</li>
|
||||
<li>
|
||||
${t("overview.connection.step4")}<code
|
||||
>openclaw doctor --generate-gateway-token</code
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
<div class="login-gate__docs">
|
||||
${t("overview.connection.docsHint")}
|
||||
<a
|
||||
class="session-link"
|
||||
href="https://docs.openclaw.ai/web/dashboard"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${t("overview.connection.docsLink")}</a
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
|
@ -358,24 +363,22 @@ export function renderOverview(props: OverviewProps) {
|
|||
<div class="stat">
|
||||
<div class="stat-label">${t("overview.snapshot.lastChannelsRefresh")}</div>
|
||||
<div class="stat-value">
|
||||
${props.lastChannelsRefresh ? formatRelativeTimestamp(props.lastChannelsRefresh) : t("common.na")}
|
||||
${props.lastChannelsRefresh
|
||||
? formatRelativeTimestamp(props.lastChannelsRefresh)
|
||||
: t("common.na")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 14px;">
|
||||
${props.lastError
|
||||
? html`<div class="callout danger" style="margin-top: 14px;">
|
||||
<div>${props.lastError}</div>
|
||||
${pairingHint ?? ""}
|
||||
${authHint ?? ""}
|
||||
${insecureContextHint ?? ""}
|
||||
${pairingHint ?? ""} ${authHint ?? ""} ${insecureContextHint ?? ""}
|
||||
</div>`
|
||||
: html`
|
||||
<div class="callout" style="margin-top: 14px">
|
||||
${t("overview.snapshot.channelsHint")}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
: html`
|
||||
<div class="callout" style="margin-top: 14px">
|
||||
${t("overview.snapshot.channelsHint")}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -390,7 +393,6 @@ export function renderOverview(props: OverviewProps) {
|
|||
presenceCount: props.presenceCount,
|
||||
onNavigate: props.onNavigate,
|
||||
})}
|
||||
|
||||
${renderOverviewAttention({ items: props.attentionItems })}
|
||||
|
||||
<div class="ov-section-divider"></div>
|
||||
|
|
@ -399,12 +401,10 @@ export function renderOverview(props: OverviewProps) {
|
|||
${renderOverviewEventLog({
|
||||
events: props.eventLog,
|
||||
})}
|
||||
|
||||
${renderOverviewLogTail({
|
||||
lines: props.overviewLogLines,
|
||||
onRefreshLogs: props.onRefreshLogs,
|
||||
})}
|
||||
</div>
|
||||
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,7 +215,11 @@ export function renderSessions(props: SessionsProps) {
|
|||
<div class="row" style="justify-content: space-between; margin-bottom: 12px;">
|
||||
<div>
|
||||
<div class="card-title">Sessions</div>
|
||||
<div class="card-sub">${props.result ? `Store: ${props.result.path}` : "Active session keys and per-session overrides."}</div>
|
||||
<div class="card-sub">
|
||||
${props.result
|
||||
? `Store: ${props.result.path}`
|
||||
: "Active session keys and per-session overrides."}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn" ?disabled=${props.loading} @click=${props.onRefresh}>
|
||||
${props.loading ? "Loading…" : "Refresh"}
|
||||
|
|
@ -282,11 +286,9 @@ export function renderSessions(props: SessionsProps) {
|
|||
</label>
|
||||
</div>
|
||||
|
||||
${
|
||||
props.error
|
||||
? html`<div class="callout danger" style="margin-bottom: 12px;">${props.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
${props.error
|
||||
? html`<div class="callout danger" style="margin-bottom: 12px;">${props.error}</div>`
|
||||
: nothing}
|
||||
|
||||
<div class="data-table-wrapper">
|
||||
<div class="data-table-toolbar">
|
||||
|
|
@ -300,40 +302,34 @@ export function renderSessions(props: SessionsProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
props.selectedKeys.size > 0
|
||||
? html`
|
||||
<div class="data-table-bulk-bar">
|
||||
<span>${props.selectedKeys.size} selected</span>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
@click=${props.onDeselectAll}
|
||||
>
|
||||
Unselect
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm danger"
|
||||
?disabled=${props.loading}
|
||||
@click=${props.onDeleteSelected}
|
||||
>
|
||||
${icons.trash} Delete
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${props.selectedKeys.size > 0
|
||||
? html`
|
||||
<div class="data-table-bulk-bar">
|
||||
<span>${props.selectedKeys.size} selected</span>
|
||||
<button class="btn btn--sm" @click=${props.onDeselectAll}>Unselect</button>
|
||||
<button
|
||||
class="btn btn--sm danger"
|
||||
?disabled=${props.loading}
|
||||
@click=${props.onDeleteSelected}
|
||||
>
|
||||
${icons.trash} Delete
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="data-table-container">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="data-table-checkbox-col">
|
||||
${
|
||||
paginated.length > 0
|
||||
? html`<input
|
||||
${paginated.length > 0
|
||||
? html`<input
|
||||
type="checkbox"
|
||||
.checked=${paginated.length > 0 && paginated.every((r) => props.selectedKeys.has(r.key))}
|
||||
.indeterminate=${paginated.some((r) => props.selectedKeys.has(r.key)) && !paginated.every((r) => props.selectedKeys.has(r.key))}
|
||||
.checked=${paginated.length > 0 &&
|
||||
paginated.every((r) => props.selectedKeys.has(r.key))}
|
||||
.indeterminate=${paginated.some((r) => props.selectedKeys.has(r.key)) &&
|
||||
!paginated.every((r) => props.selectedKeys.has(r.key))}
|
||||
@change=${() => {
|
||||
const allSelected = paginated.every((r) => props.selectedKeys.has(r.key));
|
||||
if (allSelected) {
|
||||
|
|
@ -344,13 +340,11 @@ export function renderSessions(props: SessionsProps) {
|
|||
}}
|
||||
aria-label="Select all on page"
|
||||
/>`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</th>
|
||||
${sortHeader("key", "Key", "data-table-key-col")}
|
||||
<th>Label</th>
|
||||
${sortHeader("kind", "Kind")}
|
||||
${sortHeader("updated", "Updated")}
|
||||
${sortHeader("kind", "Kind")} ${sortHeader("updated", "Updated")}
|
||||
${sortHeader("tokens", "Tokens")}
|
||||
<th>Thinking</th>
|
||||
<th>Fast</th>
|
||||
|
|
@ -359,65 +353,61 @@ export function renderSessions(props: SessionsProps) {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${
|
||||
paginated.length === 0
|
||||
? html`
|
||||
<tr>
|
||||
<td colspan="10" style="text-align: center; padding: 48px 16px; color: var(--muted)">
|
||||
No sessions found.
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: paginated.map((row) =>
|
||||
renderRow(
|
||||
row,
|
||||
props.basePath,
|
||||
props.onPatch,
|
||||
props.selectedKeys.has(row.key),
|
||||
props.onToggleSelect,
|
||||
props.loading,
|
||||
props.onNavigateToChat,
|
||||
),
|
||||
)
|
||||
}
|
||||
${paginated.length === 0
|
||||
? html`
|
||||
<tr>
|
||||
<td
|
||||
colspan="10"
|
||||
style="text-align: center; padding: 48px 16px; color: var(--muted)"
|
||||
>
|
||||
No sessions found.
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
: paginated.map((row) =>
|
||||
renderRow(
|
||||
row,
|
||||
props.basePath,
|
||||
props.onPatch,
|
||||
props.selectedKeys.has(row.key),
|
||||
props.onToggleSelect,
|
||||
props.loading,
|
||||
props.onNavigateToChat,
|
||||
),
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
${
|
||||
totalRows > 0
|
||||
? html`
|
||||
<div class="data-table-pagination">
|
||||
<div class="data-table-pagination__info">
|
||||
${page * props.pageSize + 1}-${Math.min((page + 1) * props.pageSize, totalRows)}
|
||||
of ${totalRows} row${totalRows === 1 ? "" : "s"}
|
||||
</div>
|
||||
<div class="data-table-pagination__controls">
|
||||
<select
|
||||
style="height: 32px; padding: 0 8px; font-size: 13px; border-radius: var(--radius-md); border: 1px solid var(--border); background: var(--card);"
|
||||
.value=${String(props.pageSize)}
|
||||
@change=${(e: Event) =>
|
||||
props.onPageSizeChange(Number((e.target as HTMLSelectElement).value))}
|
||||
>
|
||||
${PAGE_SIZES.map((s) => html`<option value=${s}>${s} per page</option>`)}
|
||||
</select>
|
||||
<button
|
||||
?disabled=${page <= 0}
|
||||
@click=${() => props.onPageChange(page - 1)}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
?disabled=${page >= totalPages - 1}
|
||||
@click=${() => props.onPageChange(page + 1)}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
${totalRows > 0
|
||||
? html`
|
||||
<div class="data-table-pagination">
|
||||
<div class="data-table-pagination__info">
|
||||
${page * props.pageSize + 1}-${Math.min((page + 1) * props.pageSize, totalRows)}
|
||||
of ${totalRows} row${totalRows === 1 ? "" : "s"}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div class="data-table-pagination__controls">
|
||||
<select
|
||||
style="height: 32px; padding: 0 8px; font-size: 13px; border-radius: var(--radius-md); border: 1px solid var(--border); background: var(--card);"
|
||||
.value=${String(props.pageSize)}
|
||||
@change=${(e: Event) =>
|
||||
props.onPageSizeChange(Number((e.target as HTMLSelectElement).value))}
|
||||
>
|
||||
${PAGE_SIZES.map((s) => html`<option value=${s}>${s} per page</option>`)}
|
||||
</select>
|
||||
<button ?disabled=${page <= 0} @click=${() => props.onPageChange(page - 1)}>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
?disabled=${page >= totalPages - 1}
|
||||
@click=${() => props.onPageChange(page + 1)}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
|
@ -477,35 +467,32 @@ function renderRow(
|
|||
</td>
|
||||
<td class="data-table-key-col">
|
||||
<div class="mono session-key-cell">
|
||||
${
|
||||
canLink
|
||||
? html`<a
|
||||
href=${chatUrl}
|
||||
class="session-link"
|
||||
@click=${(e: MouseEvent) => {
|
||||
if (
|
||||
e.defaultPrevented ||
|
||||
e.button !== 0 ||
|
||||
e.metaKey ||
|
||||
e.ctrlKey ||
|
||||
e.shiftKey ||
|
||||
e.altKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (onNavigateToChat) {
|
||||
e.preventDefault();
|
||||
onNavigateToChat(row.key);
|
||||
}
|
||||
}}
|
||||
>${row.key}</a>`
|
||||
: row.key
|
||||
}
|
||||
${
|
||||
showDisplayName
|
||||
? html`<span class="muted session-key-display-name">${displayName}</span>`
|
||||
: nothing
|
||||
}
|
||||
${canLink
|
||||
? html`<a
|
||||
href=${chatUrl}
|
||||
class="session-link"
|
||||
@click=${(e: MouseEvent) => {
|
||||
if (
|
||||
e.defaultPrevented ||
|
||||
e.button !== 0 ||
|
||||
e.metaKey ||
|
||||
e.ctrlKey ||
|
||||
e.shiftKey ||
|
||||
e.altKey
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (onNavigateToChat) {
|
||||
e.preventDefault();
|
||||
onNavigateToChat(row.key);
|
||||
}
|
||||
}}
|
||||
>${row.key}</a
|
||||
>`
|
||||
: row.key}
|
||||
${showDisplayName
|
||||
? html`<span class="muted session-key-display-name">${displayName}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
|
|
|
|||
|
|
@ -30,23 +30,11 @@ export function renderSkillStatusChips(params: {
|
|||
return html`
|
||||
<div class="chip-row" style="margin-top: 6px;">
|
||||
<span class="chip">${skill.source}</span>
|
||||
${
|
||||
showBundledBadge
|
||||
? html`
|
||||
<span class="chip">bundled</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${showBundledBadge ? html` <span class="chip">bundled</span> ` : nothing}
|
||||
<span class="chip ${skill.eligible ? "chip-ok" : "chip-warn"}">
|
||||
${skill.eligible ? "eligible" : "blocked"}
|
||||
</span>
|
||||
${
|
||||
skill.disabled
|
||||
? html`
|
||||
<span class="chip chip-warn">disabled</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${skill.disabled ? html` <span class="chip chip-warn">disabled</span> ` : nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,7 +113,11 @@ export function renderSkills(props: SkillsProps) {
|
|||
<div class="card-title">Skills</div>
|
||||
<div class="card-sub">Installed skills and their status.</div>
|
||||
</div>
|
||||
<button class="btn" ?disabled=${props.loading || !props.connected} @click=${props.onRefresh}>
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${props.loading || !props.connected}
|
||||
@click=${props.onRefresh}
|
||||
>
|
||||
${props.loading ? "Loading\u2026" : "Refresh"}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -131,14 +135,18 @@ export function renderSkills(props: SkillsProps) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div class="filters" style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px;">
|
||||
<div
|
||||
class="filters"
|
||||
style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 12px;"
|
||||
>
|
||||
<a
|
||||
class="btn btn--sm"
|
||||
href="https://clawhub.com"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Browse skills on ClawHub"
|
||||
>Browse Skills Store</a>
|
||||
>Browse Skills Store</a
|
||||
>
|
||||
<label class="field" style="flex: 1; min-width: 180px;">
|
||||
<input
|
||||
.value=${props.filter}
|
||||
|
|
@ -151,24 +159,18 @@ export function renderSkills(props: SkillsProps) {
|
|||
<div class="muted">${filtered.length} shown</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
props.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
filtered.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">
|
||||
${
|
||||
!props.connected && !props.report
|
||||
? "Not connected to gateway."
|
||||
: "No skills found."
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
${props.error
|
||||
? html`<div class="callout danger" style="margin-top: 12px;">${props.error}</div>`
|
||||
: nothing}
|
||||
${filtered.length === 0
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 16px">
|
||||
${!props.connected && !props.report
|
||||
? "Not connected to gateway."
|
||||
: "No skills found."}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="agent-skills-groups" style="margin-top: 16px;">
|
||||
${groups.map((group) => {
|
||||
return html`
|
||||
|
|
@ -184,8 +186,7 @@ export function renderSkills(props: SkillsProps) {
|
|||
`;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
`}
|
||||
</section>
|
||||
|
||||
${detailSkill ? renderSkillDetail(detailSkill, props) : nothing}
|
||||
|
|
@ -197,10 +198,7 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
|||
const dotClass = skillStatusClass(skill);
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="list-item list-item-clickable"
|
||||
@click=${() => props.onDetailOpen(skill.skillKey)}
|
||||
>
|
||||
<div class="list-item list-item-clickable" @click=${() => props.onDetailOpen(skill.skillKey)}>
|
||||
<div class="list-main">
|
||||
<div class="list-title" style="display: flex; align-items: center; gap: 8px;">
|
||||
<span class="statusDot ${dotClass}"></span>
|
||||
|
|
@ -209,11 +207,11 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
|
|||
</div>
|
||||
<div class="list-sub">${clampText(skill.description, 140)}</div>
|
||||
</div>
|
||||
<div class="list-meta" style="display: flex; align-items: center; justify-content: flex-end; gap: 10px;">
|
||||
<label
|
||||
class="skill-toggle-wrap"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
class="list-meta"
|
||||
style="display: flex; align-items: center; justify-content: flex-end; gap: 10px;"
|
||||
>
|
||||
<label class="skill-toggle-wrap" @click=${(e: Event) => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="skill-toggle"
|
||||
|
|
@ -240,14 +238,21 @@ function renderSkillDetail(skill: SkillStatusEntry, props: SkillsProps) {
|
|||
const reasons = computeSkillReasons(skill);
|
||||
|
||||
return html`
|
||||
<dialog class="md-preview-dialog" open @click=${(e: Event) => {
|
||||
if ((e.target as HTMLElement).classList.contains("md-preview-dialog")) {
|
||||
props.onDetailClose();
|
||||
}
|
||||
}}>
|
||||
<dialog
|
||||
class="md-preview-dialog"
|
||||
open
|
||||
@click=${(e: Event) => {
|
||||
if ((e.target as HTMLElement).classList.contains("md-preview-dialog")) {
|
||||
props.onDetailClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="md-preview-dialog__panel">
|
||||
<div class="md-preview-dialog__header">
|
||||
<div class="md-preview-dialog__title" style="display: flex; align-items: center; gap: 8px;">
|
||||
<div
|
||||
class="md-preview-dialog__title"
|
||||
style="display: flex; align-items: center; gap: 8px;"
|
||||
>
|
||||
<span class="statusDot ${skillStatusClass(skill)}"></span>
|
||||
${skill.emoji ? html`<span style="font-size: 18px;">${skill.emoji}</span>` : nothing}
|
||||
<span>${skill.name}</span>
|
||||
|
|
@ -256,30 +261,28 @@ function renderSkillDetail(skill: SkillStatusEntry, props: SkillsProps) {
|
|||
</div>
|
||||
<div class="md-preview-dialog__body" style="display: grid; gap: 16px;">
|
||||
<div>
|
||||
<div style="font-size: 14px; line-height: 1.5; color: var(--text);">${skill.description}</div>
|
||||
<div style="font-size: 14px; line-height: 1.5; color: var(--text);">
|
||||
${skill.description}
|
||||
</div>
|
||||
${renderSkillStatusChips({ skill, showBundledBadge })}
|
||||
</div>
|
||||
|
||||
${
|
||||
missing.length > 0
|
||||
? html`
|
||||
<div class="callout" style="border-color: var(--warn-subtle); background: var(--warn-subtle); color: var(--warn);">
|
||||
${missing.length > 0
|
||||
? html`
|
||||
<div
|
||||
class="callout"
|
||||
style="border-color: var(--warn-subtle); background: var(--warn-subtle); color: var(--warn);"
|
||||
>
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">Missing requirements</div>
|
||||
<div>${missing.join(", ")}</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
reasons.length > 0
|
||||
? html`
|
||||
<div class="muted" style="font-size: 13px;">
|
||||
Reason: ${reasons.join(", ")}
|
||||
</div>
|
||||
: nothing}
|
||||
${reasons.length > 0
|
||||
? html`
|
||||
<div class="muted" style="font-size: 13px;">Reason: ${reasons.join(", ")}</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<label class="skill-toggle-wrap">
|
||||
|
|
@ -294,35 +297,32 @@ function renderSkillDetail(skill: SkillStatusEntry, props: SkillsProps) {
|
|||
<span style="font-size: 13px; font-weight: 500;">
|
||||
${skill.disabled ? "Disabled" : "Enabled"}
|
||||
</span>
|
||||
${
|
||||
canInstall
|
||||
? html`<button
|
||||
${canInstall
|
||||
? html`<button
|
||||
class="btn"
|
||||
?disabled=${busy}
|
||||
@click=${() => props.onInstall(skill.skillKey, skill.name, skill.install[0].id)}
|
||||
>
|
||||
${busy ? "Installing\u2026" : skill.install[0].label}
|
||||
</button>`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
${
|
||||
message
|
||||
? html`<div
|
||||
class="callout ${message.kind === "error" ? "danger" : "success"}"
|
||||
>
|
||||
${message
|
||||
? html`<div class="callout ${message.kind === "error" ? "danger" : "success"}">
|
||||
${message.message}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
|
||||
${
|
||||
skill.primaryEnv
|
||||
? html`
|
||||
: nothing}
|
||||
${skill.primaryEnv
|
||||
? html`
|
||||
<div style="display: grid; gap: 8px;">
|
||||
<div class="field">
|
||||
<span>API key <span class="muted" style="font-weight: normal; font-size: 0.88em;">(${skill.primaryEnv})</span></span>
|
||||
<span
|
||||
>API key
|
||||
<span class="muted" style="font-weight: normal; font-size: 0.88em;"
|
||||
>(${skill.primaryEnv})</span
|
||||
></span
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
.value=${apiKey}
|
||||
|
|
@ -334,8 +334,11 @@ function renderSkillDetail(skill: SkillStatusEntry, props: SkillsProps) {
|
|||
const href = safeExternalHref(skill.homepage);
|
||||
return href
|
||||
? html`<div class="muted" style="font-size: 13px;">
|
||||
Get your key: <a href="${href}" target="_blank" rel="noopener noreferrer">${skill.homepage}</a>
|
||||
</div>`
|
||||
Get your key:
|
||||
<a href="${href}" target="_blank" rel="noopener noreferrer"
|
||||
>${skill.homepage}</a
|
||||
>
|
||||
</div>`
|
||||
: nothing;
|
||||
})()}
|
||||
<button
|
||||
|
|
@ -347,16 +350,21 @@ function renderSkillDetail(skill: SkillStatusEntry, props: SkillsProps) {
|
|||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
|
||||
<div style="border-top: 1px solid var(--border); padding-top: 12px; display: grid; gap: 6px; font-size: 12px; color: var(--muted);">
|
||||
<div
|
||||
style="border-top: 1px solid var(--border); padding-top: 12px; display: grid; gap: 6px; font-size: 12px; color: var(--muted);"
|
||||
>
|
||||
<div><span style="font-weight: 600;">Source:</span> ${skill.source}</div>
|
||||
<div style="font-family: var(--mono); word-break: break-all;">${skill.filePath}</div>
|
||||
${(() => {
|
||||
const safeHref = safeExternalHref(skill.homepage);
|
||||
return safeHref
|
||||
? html`<div><a href="${safeHref}" target="_blank" rel="noopener noreferrer">${skill.homepage}</a></div>`
|
||||
? html`<div>
|
||||
<a href="${safeHref}" target="_blank" rel="noopener noreferrer"
|
||||
>${skill.homepage}</a
|
||||
>
|
||||
</div>`
|
||||
: nothing;
|
||||
})()}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -197,7 +197,9 @@ function renderUsageMosaic(
|
|||
<div class="usage-mosaic-title">${t("usage.mosaic.title")}</div>
|
||||
<div class="usage-mosaic-sub">${t("usage.mosaic.subtitleEmpty")}</div>
|
||||
</div>
|
||||
<div class="usage-mosaic-total">${formatTokens(0)} ${t("usage.metrics.tokens").toLowerCase()}</div>
|
||||
<div class="usage-mosaic-total">
|
||||
${formatTokens(0)} ${t("usage.metrics.tokens").toLowerCase()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="usage-empty-block usage-empty-block--compact">
|
||||
${t("usage.mosaic.noTimelineData")}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,7 @@ function renderSessionSummary(
|
|||
) {
|
||||
const usage = filteredUsage || session.usage;
|
||||
if (!usage) {
|
||||
return html`
|
||||
<div class="usage-empty-block">${t("usage.details.noUsageData")}</div>
|
||||
`;
|
||||
return html` <div class="usage-empty-block">${t("usage.details.noUsageData")}</div> `;
|
||||
}
|
||||
|
||||
const formatTs = (ts?: number): string =>
|
||||
|
|
@ -116,7 +114,11 @@ function renderSessionSummary(
|
|||
})) ?? [];
|
||||
|
||||
return html`
|
||||
${badges.length > 0 ? html`<div class="usage-badges">${badges.map((b) => html`<span class="usage-badge">${b}</span>`)}</div>` : nothing}
|
||||
${badges.length > 0
|
||||
? html`<div class="usage-badges">
|
||||
${badges.map((b) => html`<span class="usage-badge">${b}</span>`)}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="session-summary-grid">
|
||||
<div class="stat session-summary-card">
|
||||
<div class="session-summary-title">${t("usage.overview.messages")}</div>
|
||||
|
|
@ -141,9 +143,12 @@ function renderSessionSummary(
|
|||
<div class="stat session-summary-card">
|
||||
<div class="session-summary-title">${t("usage.details.duration")}</div>
|
||||
<div class="stat-value session-summary-value">
|
||||
${formatDurationCompact(usage.durationMs, { spaced: true }) ?? t("usage.common.emptyValue")}
|
||||
${formatDurationCompact(usage.durationMs, { spaced: true }) ??
|
||||
t("usage.common.emptyValue")}
|
||||
</div>
|
||||
<div class="session-summary-meta">
|
||||
${formatTs(usage.firstActivity)} → ${formatTs(usage.lastActivity)}
|
||||
</div>
|
||||
<div class="session-summary-meta">${formatTs(usage.firstActivity)} → ${formatTs(usage.lastActivity)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="usage-insights-grid usage-insights-grid--tight">
|
||||
|
|
@ -266,22 +271,22 @@ function renderSessionDetailPanel(
|
|||
<div class="session-detail-header-left">
|
||||
<div class="session-detail-title">
|
||||
${displayLabel}
|
||||
${
|
||||
cursorIndicator
|
||||
? html`<span class="session-detail-indicator">${cursorIndicator}</span>`
|
||||
: nothing
|
||||
}
|
||||
${cursorIndicator
|
||||
? html`<span class="session-detail-indicator">${cursorIndicator}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-detail-stats">
|
||||
${
|
||||
usage
|
||||
? html`
|
||||
<span><strong>${formatTokens(headerStats.totalTokens)}</strong> ${t("usage.metrics.tokens").toLowerCase()}${cursorIndicator}</span>
|
||||
<span><strong>${formatCost(headerStats.totalCost)}</strong>${cursorIndicator}</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${usage
|
||||
? html`
|
||||
<span
|
||||
><strong>${formatTokens(headerStats.totalTokens)}</strong> ${t(
|
||||
"usage.metrics.tokens",
|
||||
).toLowerCase()}${cursorIndicator}</span
|
||||
>
|
||||
<span><strong>${formatCost(headerStats.totalCost)}</strong>${cursorIndicator}</span>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<button
|
||||
class="btn btn--sm btn--ghost"
|
||||
|
|
@ -331,7 +336,12 @@ function renderSessionDetailPanel(
|
|||
hasRange ? timeSeriesCursorStart : null,
|
||||
hasRange ? timeSeriesCursorEnd : null,
|
||||
)}
|
||||
${renderContextPanel(session.contextWeight, usage, contextExpanded, onToggleContextExpanded)}
|
||||
${renderContextPanel(
|
||||
session.contextWeight,
|
||||
usage,
|
||||
contextExpanded,
|
||||
onToggleContextExpanded,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -470,17 +480,18 @@ function renderTimeSeriesCompact(
|
|||
<div class="timeseries-header-row">
|
||||
<div class="card-title usage-section-title">${t("usage.details.usageOverTime")}</div>
|
||||
<div class="timeseries-controls">
|
||||
${
|
||||
hasSelection
|
||||
? html`
|
||||
<div class="chart-toggle small">
|
||||
<button class="btn btn--sm toggle-btn active" @click=${() => onCursorRangeChange?.(null, null)}>
|
||||
${t("usage.details.reset")}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${hasSelection
|
||||
? html`
|
||||
<div class="chart-toggle small">
|
||||
<button
|
||||
class="btn btn--sm toggle-btn active"
|
||||
@click=${() => onCursorRangeChange?.(null, null)}
|
||||
>
|
||||
${t("usage.details.reset")}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<div class="chart-toggle small">
|
||||
<button
|
||||
class="btn btn--sm toggle-btn ${!isCumulative ? "active" : ""}"
|
||||
|
|
@ -495,49 +506,68 @@ function renderTimeSeriesCompact(
|
|||
${t("usage.details.cumulative")}
|
||||
</button>
|
||||
</div>
|
||||
${
|
||||
!isCumulative
|
||||
? html`
|
||||
<div class="chart-toggle small">
|
||||
<button
|
||||
class="btn btn--sm toggle-btn ${breakdownMode === "total" ? "active" : ""}"
|
||||
@click=${() => onBreakdownChange("total")}
|
||||
>
|
||||
${t("usage.daily.total")}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm toggle-btn ${breakdownMode === "by-type" ? "active" : ""}"
|
||||
@click=${() => onBreakdownChange("by-type")}
|
||||
>
|
||||
${t("usage.daily.byType")}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${!isCumulative
|
||||
? html`
|
||||
<div class="chart-toggle small">
|
||||
<button
|
||||
class="btn btn--sm toggle-btn ${breakdownMode === "total" ? "active" : ""}"
|
||||
@click=${() => onBreakdownChange("total")}
|
||||
>
|
||||
${t("usage.daily.total")}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm toggle-btn ${breakdownMode === "by-type" ? "active" : ""}"
|
||||
@click=${() => onBreakdownChange("by-type")}
|
||||
>
|
||||
${t("usage.daily.byType")}
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeseries-chart-wrapper">
|
||||
<svg
|
||||
viewBox="0 0 ${width} ${height + 18}"
|
||||
class="timeseries-svg"
|
||||
>
|
||||
<svg viewBox="0 0 ${width} ${height + 18}" class="timeseries-svg">
|
||||
<!-- Y axis -->
|
||||
<line x1="${padding.left}" y1="${padding.top}" x2="${padding.left}" y2="${padding.top + chartHeight}" stroke="var(--border)" />
|
||||
<line
|
||||
x1="${padding.left}"
|
||||
y1="${padding.top}"
|
||||
x2="${padding.left}"
|
||||
y2="${padding.top + chartHeight}"
|
||||
stroke="var(--border)"
|
||||
/>
|
||||
<!-- X axis -->
|
||||
<line x1="${padding.left}" y1="${padding.top + chartHeight}" x2="${width - padding.right}" y2="${padding.top + chartHeight}" stroke="var(--border)" />
|
||||
<line
|
||||
x1="${padding.left}"
|
||||
y1="${padding.top + chartHeight}"
|
||||
x2="${width - padding.right}"
|
||||
y2="${padding.top + chartHeight}"
|
||||
stroke="var(--border)"
|
||||
/>
|
||||
<!-- Y axis labels -->
|
||||
<text x="${padding.left - 4}" y="${padding.top + 5}" text-anchor="end" class="ts-axis-label">${formatTokens(maxValue)}</text>
|
||||
<text x="${padding.left - 4}" y="${padding.top + chartHeight}" text-anchor="end" class="ts-axis-label">0</text>
|
||||
<text
|
||||
x="${padding.left - 4}"
|
||||
y="${padding.top + 5}"
|
||||
text-anchor="end"
|
||||
class="ts-axis-label"
|
||||
>
|
||||
${formatTokens(maxValue)}
|
||||
</text>
|
||||
<text
|
||||
x="${padding.left - 4}"
|
||||
y="${padding.top + chartHeight}"
|
||||
text-anchor="end"
|
||||
class="ts-axis-label"
|
||||
>
|
||||
0
|
||||
</text>
|
||||
<!-- X axis labels (first and last) -->
|
||||
${
|
||||
points.length > 0
|
||||
? svg`
|
||||
${points.length > 0
|
||||
? svg`
|
||||
<text x="${padding.left}" y="${padding.top + chartHeight + 10}" text-anchor="start" class="ts-axis-label">${new Date(points[0].timestamp).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}</text>
|
||||
<text x="${width - padding.right}" y="${padding.top + chartHeight + 10}" text-anchor="end" class="ts-axis-label">${new Date(points[points.length - 1].timestamp).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}</text>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
<!-- Bars -->
|
||||
${points.map((p, i) => {
|
||||
const val = barTotals[i];
|
||||
|
|
@ -677,65 +707,92 @@ function renderTimeSeriesCompact(
|
|||
};
|
||||
|
||||
return html`
|
||||
<div class="chart-handle-zone chart-handle-left"
|
||||
style="left: ${leftHandlePos};"
|
||||
@mousedown=${makeDragHandler("left")}></div>
|
||||
<div class="chart-handle-zone chart-handle-right"
|
||||
style="left: ${rightHandlePos};"
|
||||
@mousedown=${makeDragHandler("right")}></div>
|
||||
<div
|
||||
class="chart-handle-zone chart-handle-left"
|
||||
style="left: ${leftHandlePos};"
|
||||
@mousedown=${makeDragHandler("left")}
|
||||
></div>
|
||||
<div
|
||||
class="chart-handle-zone chart-handle-right"
|
||||
style="left: ${rightHandlePos};"
|
||||
@mousedown=${makeDragHandler("right")}
|
||||
></div>
|
||||
`;
|
||||
})()}
|
||||
</div>
|
||||
<div class="timeseries-summary">
|
||||
${
|
||||
hasSelection
|
||||
? html`
|
||||
${hasSelection
|
||||
? html`
|
||||
<span class="timeseries-summary__range">
|
||||
${t("usage.details.turnRange", {
|
||||
start: String(rangeStartIdx + 1),
|
||||
end: String(rangeEndIdx),
|
||||
total: String(points.length),
|
||||
})}
|
||||
</span> ·
|
||||
${new Date(rangeStartTs).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}–${new Date(rangeEndTs).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })} ·
|
||||
${formatTokens(filteredOutput + filteredInput + filteredCacheRead + filteredCacheWrite)} ·
|
||||
${formatCost(filteredPoints.reduce((s, p) => s + (p.cost || 0), 0))}
|
||||
</span>
|
||||
·
|
||||
${new Date(rangeStartTs).toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}–${new Date(rangeEndTs).toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
·
|
||||
${formatTokens(
|
||||
filteredOutput + filteredInput + filteredCacheRead + filteredCacheWrite,
|
||||
)}
|
||||
· ${formatCost(filteredPoints.reduce((s, p) => s + (p.cost || 0), 0))}
|
||||
`
|
||||
: html`${points.length} ${t("usage.overview.messagesAbbrev")} · ${formatTokens(cumTokens)} · ${formatCost(cumCost)}`
|
||||
}
|
||||
: html`${points.length} ${t("usage.overview.messagesAbbrev")} · ${formatTokens(cumTokens)}
|
||||
· ${formatCost(cumCost)}`}
|
||||
</div>
|
||||
${
|
||||
breakdownByType
|
||||
? html`
|
||||
<div class="timeseries-breakdown">
|
||||
<div class="card-title usage-section-title">${t("usage.breakdown.tokensByType")}</div>
|
||||
<div class="cost-breakdown-bar cost-breakdown-bar--compact">
|
||||
<div class="cost-segment output" style="width: ${pct(filteredOutput, totalTypeTokens).toFixed(1)}%"></div>
|
||||
<div class="cost-segment input" style="width: ${pct(filteredInput, totalTypeTokens).toFixed(1)}%"></div>
|
||||
<div class="cost-segment cache-write" style="width: ${pct(filteredCacheWrite, totalTypeTokens).toFixed(1)}%"></div>
|
||||
<div class="cost-segment cache-read" style="width: ${pct(filteredCacheRead, totalTypeTokens).toFixed(1)}%"></div>
|
||||
${breakdownByType
|
||||
? html`
|
||||
<div class="timeseries-breakdown">
|
||||
<div class="card-title usage-section-title">${t("usage.breakdown.tokensByType")}</div>
|
||||
<div class="cost-breakdown-bar cost-breakdown-bar--compact">
|
||||
<div
|
||||
class="cost-segment output"
|
||||
style="width: ${pct(filteredOutput, totalTypeTokens).toFixed(1)}%"
|
||||
></div>
|
||||
<div
|
||||
class="cost-segment input"
|
||||
style="width: ${pct(filteredInput, totalTypeTokens).toFixed(1)}%"
|
||||
></div>
|
||||
<div
|
||||
class="cost-segment cache-write"
|
||||
style="width: ${pct(filteredCacheWrite, totalTypeTokens).toFixed(1)}%"
|
||||
></div>
|
||||
<div
|
||||
class="cost-segment cache-read"
|
||||
style="width: ${pct(filteredCacheRead, totalTypeTokens).toFixed(1)}%"
|
||||
></div>
|
||||
</div>
|
||||
<div class="cost-breakdown-legend">
|
||||
<div class="legend-item" title=${t("usage.details.assistantOutputTokens")}>
|
||||
<span class="legend-dot output"></span>${t("usage.breakdown.output")}
|
||||
${formatTokens(filteredOutput)}
|
||||
</div>
|
||||
<div class="cost-breakdown-legend">
|
||||
<div class="legend-item" title=${t("usage.details.assistantOutputTokens")}>
|
||||
<span class="legend-dot output"></span>${t("usage.breakdown.output")} ${formatTokens(filteredOutput)}
|
||||
</div>
|
||||
<div class="legend-item" title=${t("usage.details.userToolInputTokens")}>
|
||||
<span class="legend-dot input"></span>${t("usage.breakdown.input")} ${formatTokens(filteredInput)}
|
||||
</div>
|
||||
<div class="legend-item" title=${t("usage.details.tokensWrittenToCache")}>
|
||||
<span class="legend-dot cache-write"></span>${t("usage.breakdown.cacheWrite")} ${formatTokens(filteredCacheWrite)}
|
||||
</div>
|
||||
<div class="legend-item" title=${t("usage.details.tokensReadFromCache")}>
|
||||
<span class="legend-dot cache-read"></span>${t("usage.breakdown.cacheRead")} ${formatTokens(filteredCacheRead)}
|
||||
</div>
|
||||
<div class="legend-item" title=${t("usage.details.userToolInputTokens")}>
|
||||
<span class="legend-dot input"></span>${t("usage.breakdown.input")}
|
||||
${formatTokens(filteredInput)}
|
||||
</div>
|
||||
<div class="cost-breakdown-total">
|
||||
${t("usage.breakdown.total")}: ${formatTokens(totalTypeTokens)}
|
||||
<div class="legend-item" title=${t("usage.details.tokensWrittenToCache")}>
|
||||
<span class="legend-dot cache-write"></span>${t("usage.breakdown.cacheWrite")}
|
||||
${formatTokens(filteredCacheWrite)}
|
||||
</div>
|
||||
<div class="legend-item" title=${t("usage.details.tokensReadFromCache")}>
|
||||
<span class="legend-dot cache-read"></span>${t("usage.breakdown.cacheRead")}
|
||||
${formatTokens(filteredCacheRead)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div class="cost-breakdown-total">
|
||||
${t("usage.breakdown.total")}: ${formatTokens(totalTypeTokens)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -791,131 +848,151 @@ function renderContextPanel(
|
|||
return html`
|
||||
<div class="context-details-panel">
|
||||
<div class="context-breakdown-header">
|
||||
<div class="card-title usage-section-title">${t("usage.details.systemPromptBreakdown")}</div>
|
||||
${
|
||||
hasMore
|
||||
? html`<button class="btn btn--sm" @click=${onToggleExpanded}>
|
||||
${showAll ? t("usage.details.collapse") : t("usage.details.expandAll")}
|
||||
</button>`
|
||||
: nothing
|
||||
}
|
||||
<div class="card-title usage-section-title">
|
||||
${t("usage.details.systemPromptBreakdown")}
|
||||
</div>
|
||||
${hasMore
|
||||
? html`<button class="btn btn--sm" @click=${onToggleExpanded}>
|
||||
${showAll ? t("usage.details.collapse") : t("usage.details.expandAll")}
|
||||
</button>`
|
||||
: nothing}
|
||||
</div>
|
||||
<p class="context-weight-desc">
|
||||
${contextPct || t("usage.details.baseContextPerMessage")}
|
||||
</p>
|
||||
<p class="context-weight-desc">${contextPct || t("usage.details.baseContextPerMessage")}</p>
|
||||
<div class="context-stacked-bar">
|
||||
<div class="context-segment system" style="width: ${pct(systemTokens, totalContextTokens).toFixed(1)}%" title="${t("usage.details.system")}: ~${formatTokens(systemTokens)}"></div>
|
||||
<div class="context-segment skills" style="width: ${pct(skillsTokens, totalContextTokens).toFixed(1)}%" title="${t("usage.details.skills")}: ~${formatTokens(skillsTokens)}"></div>
|
||||
<div class="context-segment tools" style="width: ${pct(toolsTokens, totalContextTokens).toFixed(1)}%" title="${t("usage.details.tools")}: ~${formatTokens(toolsTokens)}"></div>
|
||||
<div class="context-segment files" style="width: ${pct(filesTokens, totalContextTokens).toFixed(1)}%" title="${t("usage.details.files")}: ~${formatTokens(filesTokens)}"></div>
|
||||
<div
|
||||
class="context-segment system"
|
||||
style="width: ${pct(systemTokens, totalContextTokens).toFixed(1)}%"
|
||||
title="${t("usage.details.system")}: ~${formatTokens(systemTokens)}"
|
||||
></div>
|
||||
<div
|
||||
class="context-segment skills"
|
||||
style="width: ${pct(skillsTokens, totalContextTokens).toFixed(1)}%"
|
||||
title="${t("usage.details.skills")}: ~${formatTokens(skillsTokens)}"
|
||||
></div>
|
||||
<div
|
||||
class="context-segment tools"
|
||||
style="width: ${pct(toolsTokens, totalContextTokens).toFixed(1)}%"
|
||||
title="${t("usage.details.tools")}: ~${formatTokens(toolsTokens)}"
|
||||
></div>
|
||||
<div
|
||||
class="context-segment files"
|
||||
style="width: ${pct(filesTokens, totalContextTokens).toFixed(1)}%"
|
||||
title="${t("usage.details.files")}: ~${formatTokens(filesTokens)}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="context-legend">
|
||||
<span class="legend-item"><span class="legend-dot system"></span>${t("usage.details.systemShort")} ~${formatTokens(systemTokens)}</span>
|
||||
<span class="legend-item"><span class="legend-dot skills"></span>${t("usage.details.skills")} ~${formatTokens(skillsTokens)}</span>
|
||||
<span class="legend-item"><span class="legend-dot tools"></span>${t("usage.details.tools")} ~${formatTokens(toolsTokens)}</span>
|
||||
<span class="legend-item"><span class="legend-dot files"></span>${t("usage.details.files")} ~${formatTokens(filesTokens)}</span>
|
||||
<span class="legend-item"
|
||||
><span class="legend-dot system"></span>${t("usage.details.systemShort")}
|
||||
~${formatTokens(systemTokens)}</span
|
||||
>
|
||||
<span class="legend-item"
|
||||
><span class="legend-dot skills"></span>${t("usage.details.skills")}
|
||||
~${formatTokens(skillsTokens)}</span
|
||||
>
|
||||
<span class="legend-item"
|
||||
><span class="legend-dot tools"></span>${t("usage.details.tools")}
|
||||
~${formatTokens(toolsTokens)}</span
|
||||
>
|
||||
<span class="legend-item"
|
||||
><span class="legend-dot files"></span>${t("usage.details.files")}
|
||||
~${formatTokens(filesTokens)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="context-total">
|
||||
${t("usage.breakdown.total")}: ~${formatTokens(totalContextTokens)}
|
||||
</div>
|
||||
<div class="context-total">${t("usage.breakdown.total")}: ~${formatTokens(totalContextTokens)}</div>
|
||||
<div class="context-breakdown-grid">
|
||||
${
|
||||
skillsList.length > 0
|
||||
? (() => {
|
||||
const more = skillsList.length - skillsTop.length;
|
||||
return html`
|
||||
<div class="context-breakdown-card">
|
||||
<div class="context-breakdown-title">
|
||||
${t("usage.details.skills")} (${skillsList.length})
|
||||
</div>
|
||||
<div class="context-breakdown-list">
|
||||
${skillsTop.map(
|
||||
(s) => html`
|
||||
<div class="context-breakdown-item">
|
||||
<span class="mono">${s.name}</span>
|
||||
<span class="muted">~${formatTokens(charsToTokens(s.blockChars))}</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
${
|
||||
more > 0
|
||||
? html`
|
||||
<div class="context-breakdown-more">
|
||||
${t("usage.sessions.more", { count: String(more) })}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${skillsList.length > 0
|
||||
? (() => {
|
||||
const more = skillsList.length - skillsTop.length;
|
||||
return html`
|
||||
<div class="context-breakdown-card">
|
||||
<div class="context-breakdown-title">
|
||||
${t("usage.details.skills")} (${skillsList.length})
|
||||
</div>
|
||||
`;
|
||||
})()
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
toolsList.length > 0
|
||||
? (() => {
|
||||
const more = toolsList.length - toolsTop.length;
|
||||
return html`
|
||||
<div class="context-breakdown-card">
|
||||
<div class="context-breakdown-title">
|
||||
${t("usage.details.tools")} (${toolsList.length})
|
||||
</div>
|
||||
<div class="context-breakdown-list">
|
||||
${toolsTop.map(
|
||||
(t) => html`
|
||||
<div class="context-breakdown-item">
|
||||
<span class="mono">${t.name}</span>
|
||||
<span class="muted">~${formatTokens(charsToTokens(t.summaryChars + t.schemaChars))}</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
${
|
||||
more > 0
|
||||
? html`
|
||||
<div class="context-breakdown-more">
|
||||
${t("usage.sessions.more", { count: String(more) })}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<div class="context-breakdown-list">
|
||||
${skillsTop.map(
|
||||
(s) => html`
|
||||
<div class="context-breakdown-item">
|
||||
<span class="mono">${s.name}</span>
|
||||
<span class="muted">~${formatTokens(charsToTokens(s.blockChars))}</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
})()
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
filesList.length > 0
|
||||
? (() => {
|
||||
const more = filesList.length - filesTop.length;
|
||||
return html`
|
||||
<div class="context-breakdown-card">
|
||||
<div class="context-breakdown-title">
|
||||
${t("usage.details.files")} (${filesList.length})
|
||||
</div>
|
||||
<div class="context-breakdown-list">
|
||||
${filesTop.map(
|
||||
(f) => html`
|
||||
<div class="context-breakdown-item">
|
||||
<span class="mono">${f.name}</span>
|
||||
<span class="muted">~${formatTokens(charsToTokens(f.injectedChars))}</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
${
|
||||
more > 0
|
||||
? html`
|
||||
<div class="context-breakdown-more">
|
||||
${t("usage.sessions.more", { count: String(more) })}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${more > 0
|
||||
? html`
|
||||
<div class="context-breakdown-more">
|
||||
${t("usage.sessions.more", { count: String(more) })}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
})()
|
||||
: nothing}
|
||||
${toolsList.length > 0
|
||||
? (() => {
|
||||
const more = toolsList.length - toolsTop.length;
|
||||
return html`
|
||||
<div class="context-breakdown-card">
|
||||
<div class="context-breakdown-title">
|
||||
${t("usage.details.tools")} (${toolsList.length})
|
||||
</div>
|
||||
`;
|
||||
})()
|
||||
: nothing
|
||||
}
|
||||
<div class="context-breakdown-list">
|
||||
${toolsTop.map(
|
||||
(t) => html`
|
||||
<div class="context-breakdown-item">
|
||||
<span class="mono">${t.name}</span>
|
||||
<span class="muted"
|
||||
>~${formatTokens(charsToTokens(t.summaryChars + t.schemaChars))}</span
|
||||
>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
${more > 0
|
||||
? html`
|
||||
<div class="context-breakdown-more">
|
||||
${t("usage.sessions.more", { count: String(more) })}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
})()
|
||||
: nothing}
|
||||
${filesList.length > 0
|
||||
? (() => {
|
||||
const more = filesList.length - filesTop.length;
|
||||
return html`
|
||||
<div class="context-breakdown-card">
|
||||
<div class="context-breakdown-title">
|
||||
${t("usage.details.files")} (${filesList.length})
|
||||
</div>
|
||||
<div class="context-breakdown-list">
|
||||
${filesTop.map(
|
||||
(f) => html`
|
||||
<div class="context-breakdown-item">
|
||||
<span class="mono">${f.name}</span>
|
||||
<span class="muted"
|
||||
>~${formatTokens(charsToTokens(f.injectedChars))}</span
|
||||
>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
${more > 0
|
||||
? html`
|
||||
<div class="context-breakdown-more">
|
||||
${t("usage.sessions.more", { count: String(more) })}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
})()
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1035,10 +1112,18 @@ function renderSessionLogsCompact(
|
|||
),
|
||||
)}
|
||||
>
|
||||
<option value="user" ?selected=${roleSelected.has("user")}>${t("usage.overview.user")}</option>
|
||||
<option value="assistant" ?selected=${roleSelected.has("assistant")}>${t("usage.overview.assistant")}</option>
|
||||
<option value="tool" ?selected=${roleSelected.has("tool")}>${t("usage.details.tool")}</option>
|
||||
<option value="toolResult" ?selected=${roleSelected.has("toolResult")}>${t("usage.details.toolResult")}</option>
|
||||
<option value="user" ?selected=${roleSelected.has("user")}>
|
||||
${t("usage.overview.user")}
|
||||
</option>
|
||||
<option value="assistant" ?selected=${roleSelected.has("assistant")}>
|
||||
${t("usage.overview.assistant")}
|
||||
</option>
|
||||
<option value="tool" ?selected=${roleSelected.has("tool")}>
|
||||
${t("usage.details.tool")}
|
||||
</option>
|
||||
<option value="toolResult" ?selected=${roleSelected.has("toolResult")}>
|
||||
${t("usage.details.toolResult")}
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
multiple
|
||||
|
|
@ -1072,9 +1157,7 @@ function renderSessionLogsCompact(
|
|||
.value=${filters.query}
|
||||
@input=${(event: Event) => onFilterQueryChange((event.target as HTMLInputElement).value)}
|
||||
/>
|
||||
<button class="btn btn--sm" @click=${onFilterClear}>
|
||||
${t("usage.filters.clear")}
|
||||
</button>
|
||||
<button class="btn btn--sm" @click=${onFilterClear}>${t("usage.filters.clear")}</button>
|
||||
</div>
|
||||
<div class="session-logs-list">
|
||||
${filteredEntries.map((entry) => {
|
||||
|
|
@ -1087,15 +1170,14 @@ function renderSessionLogsCompact(
|
|||
? t("usage.overview.assistant")
|
||||
: t("usage.details.tool");
|
||||
return html`
|
||||
<div class="session-log-entry ${roleClass}">
|
||||
<div class="session-log-meta">
|
||||
<span class="session-log-role">${roleLabel}</span>
|
||||
<span>${new Date(log.timestamp).toLocaleString()}</span>
|
||||
${log.tokens ? html`<span>${formatTokens(log.tokens)}</span>` : nothing}
|
||||
</div>
|
||||
<div class="session-log-content">${cleanContent}</div>
|
||||
${
|
||||
toolInfo.tools.length > 0
|
||||
<div class="session-log-entry ${roleClass}">
|
||||
<div class="session-log-meta">
|
||||
<span class="session-log-role">${roleLabel}</span>
|
||||
<span>${new Date(log.timestamp).toLocaleString()}</span>
|
||||
${log.tokens ? html`<span>${formatTokens(log.tokens)}</span>` : nothing}
|
||||
</div>
|
||||
<div class="session-log-content">${cleanContent}</div>
|
||||
${toolInfo.tools.length > 0
|
||||
? html`
|
||||
<details class="session-log-tools" ?open=${expandedAll}>
|
||||
<summary>${toolInfo.summary}</summary>
|
||||
|
|
@ -1108,20 +1190,17 @@ function renderSessionLogsCompact(
|
|||
</div>
|
||||
</details>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
${
|
||||
filteredEntries.length === 0
|
||||
? html`
|
||||
<div class="usage-empty-block usage-empty-block--compact">
|
||||
${t("usage.details.noMessagesMatch")}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${filteredEntries.length === 0
|
||||
? html`
|
||||
<div class="usage-empty-block usage-empty-block--compact">
|
||||
${t("usage.details.noMessagesMatch")}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -93,9 +93,8 @@ function renderFilterChips(
|
|||
|
||||
return html`
|
||||
<div class="active-filters">
|
||||
${
|
||||
selectedDays.length > 0
|
||||
? html`
|
||||
${selectedDays.length > 0
|
||||
? html`
|
||||
<div class="filter-chip">
|
||||
<span class="filter-chip-label">${t("usage.filters.days")}: ${daysLabel}</span>
|
||||
<button
|
||||
|
|
@ -108,11 +107,9 @@ function renderFilterChips(
|
|||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
selectedHours.length > 0
|
||||
? html`
|
||||
: nothing}
|
||||
${selectedHours.length > 0
|
||||
? html`
|
||||
<div class="filter-chip">
|
||||
<span class="filter-chip-label">${t("usage.filters.hours")}: ${hoursLabel}</span>
|
||||
<button
|
||||
|
|
@ -125,11 +122,9 @@ function renderFilterChips(
|
|||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
selectedSessions.length > 0
|
||||
? html`
|
||||
: nothing}
|
||||
${selectedSessions.length > 0
|
||||
? html`
|
||||
<div class="filter-chip" title="${sessionsFullName}">
|
||||
<span class="filter-chip-label">${t("usage.filters.session")}: ${sessionsLabel}</span>
|
||||
<button
|
||||
|
|
@ -142,17 +137,14 @@ function renderFilterChips(
|
|||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
(selectedDays.length > 0 || selectedHours.length > 0) && selectedSessions.length > 0
|
||||
? html`
|
||||
: nothing}
|
||||
${(selectedDays.length > 0 || selectedHours.length > 0) && selectedSessions.length > 0
|
||||
? html`
|
||||
<button class="btn btn--sm" @click=${onClearFilters}>
|
||||
${t("usage.filters.clearAll")}
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -267,41 +259,35 @@ function renderDailyChartCompact(
|
|||
class="daily-bar-wrapper ${isSelected ? "selected" : ""}"
|
||||
@click=${(e: MouseEvent) => onSelectDay(d.date, e.shiftKey)}
|
||||
>
|
||||
${
|
||||
dailyChartMode === "by-type"
|
||||
? html`
|
||||
<div
|
||||
class="daily-bar daily-bar--stacked"
|
||||
style="height: ${heightPx.toFixed(0)}px;"
|
||||
>
|
||||
${(() => {
|
||||
const total = segments.reduce((sum, seg) => sum + seg.value, 0) || 1;
|
||||
return segments.map(
|
||||
(seg) => html`
|
||||
<div
|
||||
class="cost-segment ${seg.class}"
|
||||
style="height: ${(seg.value / total) * 100}%"
|
||||
></div>
|
||||
`,
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="daily-bar" style="height: ${heightPx.toFixed(0)}px"></div>
|
||||
`
|
||||
}
|
||||
${dailyChartMode === "by-type"
|
||||
? html`
|
||||
<div
|
||||
class="daily-bar daily-bar--stacked"
|
||||
style="height: ${heightPx.toFixed(0)}px;"
|
||||
>
|
||||
${(() => {
|
||||
const total = segments.reduce((sum, seg) => sum + seg.value, 0) || 1;
|
||||
return segments.map(
|
||||
(seg) => html`
|
||||
<div
|
||||
class="cost-segment ${seg.class}"
|
||||
style="height: ${(seg.value / total) * 100}%"
|
||||
></div>
|
||||
`,
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
`
|
||||
: html` <div class="daily-bar" style="height: ${heightPx.toFixed(0)}px"></div> `}
|
||||
${showTotals ? html`<div class="daily-bar-total">${totalLabel}</div>` : nothing}
|
||||
<div class="${labelClass}">${shortLabel}</div>
|
||||
<div class="daily-bar-tooltip">
|
||||
<strong>${formatFullDate(d.date)}</strong><br />
|
||||
${formatTokens(d.totalTokens)} ${t("usage.metrics.tokens").toLowerCase()}<br />
|
||||
${formatCost(d.totalCost)}
|
||||
${
|
||||
breakdownLines.length
|
||||
? html`${breakdownLines.map((line) => html`<div>${line}</div>`)}`
|
||||
: nothing
|
||||
}
|
||||
${breakdownLines.length
|
||||
? html`${breakdownLines.map((line) => html`<div>${line}</div>`)}`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -329,23 +315,64 @@ function renderCostBreakdownCompact(totals: UsageTotals, mode: "tokens" | "cost"
|
|||
${isTokenMode ? t("usage.breakdown.tokensByType") : t("usage.breakdown.costByType")}
|
||||
</div>
|
||||
<div class="cost-breakdown-bar">
|
||||
<div class="cost-segment output" style="width: ${(isTokenMode ? tokenPcts.output : breakdown.output.pct).toFixed(1)}%"
|
||||
title="${t("usage.breakdown.output")}: ${isTokenMode ? formatTokens(totals.output) : formatCost(breakdown.output.cost)}"></div>
|
||||
<div class="cost-segment input" style="width: ${(isTokenMode ? tokenPcts.input : breakdown.input.pct).toFixed(1)}%"
|
||||
title="${t("usage.breakdown.input")}: ${isTokenMode ? formatTokens(totals.input) : formatCost(breakdown.input.cost)}"></div>
|
||||
<div class="cost-segment cache-write" style="width: ${(isTokenMode ? tokenPcts.cacheWrite : breakdown.cacheWrite.pct).toFixed(1)}%"
|
||||
title="${t("usage.breakdown.cacheWrite")}: ${isTokenMode ? formatTokens(totals.cacheWrite) : formatCost(breakdown.cacheWrite.cost)}"></div>
|
||||
<div class="cost-segment cache-read" style="width: ${(isTokenMode ? tokenPcts.cacheRead : breakdown.cacheRead.pct).toFixed(1)}%"
|
||||
title="${t("usage.breakdown.cacheRead")}: ${isTokenMode ? formatTokens(totals.cacheRead) : formatCost(breakdown.cacheRead.cost)}"></div>
|
||||
<div
|
||||
class="cost-segment output"
|
||||
style="width: ${(isTokenMode ? tokenPcts.output : breakdown.output.pct).toFixed(1)}%"
|
||||
title="${t("usage.breakdown.output")}: ${isTokenMode
|
||||
? formatTokens(totals.output)
|
||||
: formatCost(breakdown.output.cost)}"
|
||||
></div>
|
||||
<div
|
||||
class="cost-segment input"
|
||||
style="width: ${(isTokenMode ? tokenPcts.input : breakdown.input.pct).toFixed(1)}%"
|
||||
title="${t("usage.breakdown.input")}: ${isTokenMode
|
||||
? formatTokens(totals.input)
|
||||
: formatCost(breakdown.input.cost)}"
|
||||
></div>
|
||||
<div
|
||||
class="cost-segment cache-write"
|
||||
style="width: ${(isTokenMode ? tokenPcts.cacheWrite : breakdown.cacheWrite.pct).toFixed(
|
||||
1,
|
||||
)}%"
|
||||
title="${t("usage.breakdown.cacheWrite")}: ${isTokenMode
|
||||
? formatTokens(totals.cacheWrite)
|
||||
: formatCost(breakdown.cacheWrite.cost)}"
|
||||
></div>
|
||||
<div
|
||||
class="cost-segment cache-read"
|
||||
style="width: ${(isTokenMode ? tokenPcts.cacheRead : breakdown.cacheRead.pct).toFixed(
|
||||
1,
|
||||
)}%"
|
||||
title="${t("usage.breakdown.cacheRead")}: ${isTokenMode
|
||||
? formatTokens(totals.cacheRead)
|
||||
: formatCost(breakdown.cacheRead.cost)}"
|
||||
></div>
|
||||
</div>
|
||||
<div class="cost-breakdown-legend">
|
||||
<span class="legend-item"><span class="legend-dot output"></span>${t("usage.breakdown.output")} ${isTokenMode ? formatTokens(totals.output) : formatCost(breakdown.output.cost)}</span>
|
||||
<span class="legend-item"><span class="legend-dot input"></span>${t("usage.breakdown.input")} ${isTokenMode ? formatTokens(totals.input) : formatCost(breakdown.input.cost)}</span>
|
||||
<span class="legend-item"><span class="legend-dot cache-write"></span>${t("usage.breakdown.cacheWrite")} ${isTokenMode ? formatTokens(totals.cacheWrite) : formatCost(breakdown.cacheWrite.cost)}</span>
|
||||
<span class="legend-item"><span class="legend-dot cache-read"></span>${t("usage.breakdown.cacheRead")} ${isTokenMode ? formatTokens(totals.cacheRead) : formatCost(breakdown.cacheRead.cost)}</span>
|
||||
<span class="legend-item"
|
||||
><span class="legend-dot output"></span>${t("usage.breakdown.output")}
|
||||
${isTokenMode ? formatTokens(totals.output) : formatCost(breakdown.output.cost)}</span
|
||||
>
|
||||
<span class="legend-item"
|
||||
><span class="legend-dot input"></span>${t("usage.breakdown.input")}
|
||||
${isTokenMode ? formatTokens(totals.input) : formatCost(breakdown.input.cost)}</span
|
||||
>
|
||||
<span class="legend-item"
|
||||
><span class="legend-dot cache-write"></span>${t("usage.breakdown.cacheWrite")}
|
||||
${isTokenMode
|
||||
? formatTokens(totals.cacheWrite)
|
||||
: formatCost(breakdown.cacheWrite.cost)}</span
|
||||
>
|
||||
<span class="legend-item"
|
||||
><span class="legend-dot cache-read"></span>${t("usage.breakdown.cacheRead")}
|
||||
${isTokenMode
|
||||
? formatTokens(totals.cacheRead)
|
||||
: formatCost(breakdown.cacheRead.cost)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="cost-breakdown-total">
|
||||
${t("usage.breakdown.total")}: ${isTokenMode ? formatTokens(totals.totalTokens) : formatCost(totals.totalCost)}
|
||||
${t("usage.breakdown.total")}:
|
||||
${isTokenMode ? formatTokens(totals.totalTokens) : formatCost(totals.totalCost)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -359,25 +386,23 @@ function renderInsightList(
|
|||
return html`
|
||||
<div class="usage-insight-card">
|
||||
<div class="usage-insight-title">${title}</div>
|
||||
${
|
||||
items.length === 0
|
||||
? html`<div class="muted">${emptyLabel}</div>`
|
||||
: html`
|
||||
<div class="usage-list">
|
||||
${items.map(
|
||||
(item) => html`
|
||||
<div class="usage-list-item">
|
||||
<span>${item.label}</span>
|
||||
<span class="usage-list-value">
|
||||
<span>${item.value}</span>
|
||||
${item.sub ? html`<span class="usage-list-sub">${item.sub}</span>` : nothing}
|
||||
</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
${items.length === 0
|
||||
? html`<div class="muted">${emptyLabel}</div>`
|
||||
: html`
|
||||
<div class="usage-list">
|
||||
${items.map(
|
||||
(item) => html`
|
||||
<div class="usage-list-item">
|
||||
<span>${item.label}</span>
|
||||
<span class="usage-list-value">
|
||||
<span>${item.value}</span>
|
||||
${item.sub ? html`<span class="usage-list-sub">${item.sub}</span>` : nothing}
|
||||
</span>
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -396,23 +421,21 @@ function renderPeakErrorList(
|
|||
return html`
|
||||
<div class=${cardClass}>
|
||||
<div class="usage-insight-title">${title}</div>
|
||||
${
|
||||
items.length === 0
|
||||
? html`<div class="muted">${emptyLabel}</div>`
|
||||
: html`
|
||||
<div class=${listClass}>
|
||||
${items.map(
|
||||
(item) => html`
|
||||
<div class="usage-error-row">
|
||||
<div class="usage-error-date">${item.label}</div>
|
||||
<div class="usage-error-rate">${item.value}</div>
|
||||
${item.sub ? html`<div class="usage-error-sub">${item.sub}</div>` : nothing}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
${items.length === 0
|
||||
? html`<div class="muted">${emptyLabel}</div>`
|
||||
: html`
|
||||
<div class=${listClass}>
|
||||
${items.map(
|
||||
(item) => html`
|
||||
<div class="usage-error-row">
|
||||
<div class="usage-error-date">${item.label}</div>
|
||||
<div class="usage-error-rate">${item.value}</div>
|
||||
${item.sub ? html`<div class="usage-error-sub">${item.sub}</div>` : nothing}
|
||||
</div>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -777,7 +800,9 @@ function renderSessionsCard(
|
|||
>
|
||||
<div class="session-bar-label">
|
||||
<div class="session-bar-title">${displayLabel}</div>
|
||||
${meta.length > 0 ? html`<div class="session-bar-meta">${meta.join(" · ")}</div>` : nothing}
|
||||
${meta.length > 0
|
||||
? html`<div class="session-bar-meta">${meta.join(" · ")}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="session-bar-actions">
|
||||
<button
|
||||
|
|
@ -790,7 +815,9 @@ function renderSessionsCard(
|
|||
>
|
||||
${t("usage.sessions.copy")}
|
||||
</button>
|
||||
<div class="session-bar-value">${isTokenMode ? formatTokens(value) : formatCost(value)}</div>
|
||||
<div class="session-bar-value">
|
||||
${isTokenMode ? formatTokens(value) : formatCost(value)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -810,17 +837,16 @@ function renderSessionsCard(
|
|||
<div class="card-title">${t("usage.sessions.title")}</div>
|
||||
<div class="sessions-card-count">
|
||||
${t("usage.sessions.shown", { count: String(sessions.length) })}
|
||||
${
|
||||
totalSessions !== sessions.length
|
||||
? ` · ${t("usage.sessions.total", { count: String(totalSessions) })}`
|
||||
: ""
|
||||
}
|
||||
${totalSessions !== sessions.length
|
||||
? ` · ${t("usage.sessions.total", { count: String(totalSessions) })}`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sessions-card-meta">
|
||||
<div class="sessions-card-stats">
|
||||
<span>
|
||||
${isTokenMode ? formatTokens(avgValue) : formatCost(avgValue)} ${t("usage.sessions.avg")}
|
||||
${isTokenMode ? formatTokens(avgValue) : formatCost(avgValue)}
|
||||
${t("usage.sessions.avg")}
|
||||
</span>
|
||||
<span>${totalErrors} ${t("usage.overview.errors").toLowerCase()}</span>
|
||||
</div>
|
||||
|
|
@ -841,82 +867,79 @@ function renderSessionsCard(
|
|||
<label class="sessions-sort">
|
||||
<span>${t("usage.sessions.sort")}</span>
|
||||
<select
|
||||
@change=${(e: Event) => onSessionSortChange((e.target as HTMLSelectElement).value as typeof sessionSort)}
|
||||
@change=${(e: Event) =>
|
||||
onSessionSortChange((e.target as HTMLSelectElement).value as typeof sessionSort)}
|
||||
>
|
||||
<option value="cost" ?selected=${sessionSort === "cost"}>${t("usage.metrics.cost")}</option>
|
||||
<option value="errors" ?selected=${sessionSort === "errors"}>${t("usage.overview.errors")}</option>
|
||||
<option value="messages" ?selected=${sessionSort === "messages"}>${t("usage.overview.messages")}</option>
|
||||
<option value="recent" ?selected=${sessionSort === "recent"}>${t("usage.sessions.recentShort")}</option>
|
||||
<option value="tokens" ?selected=${sessionSort === "tokens"}>${t("usage.metrics.tokens")}</option>
|
||||
<option value="cost" ?selected=${sessionSort === "cost"}>
|
||||
${t("usage.metrics.cost")}
|
||||
</option>
|
||||
<option value="errors" ?selected=${sessionSort === "errors"}>
|
||||
${t("usage.overview.errors")}
|
||||
</option>
|
||||
<option value="messages" ?selected=${sessionSort === "messages"}>
|
||||
${t("usage.overview.messages")}
|
||||
</option>
|
||||
<option value="recent" ?selected=${sessionSort === "recent"}>
|
||||
${t("usage.sessions.recentShort")}
|
||||
</option>
|
||||
<option value="tokens" ?selected=${sessionSort === "tokens"}>
|
||||
${t("usage.metrics.tokens")}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
@click=${() => onSessionSortDirChange(sessionSortDir === "desc" ? "asc" : "desc")}
|
||||
title=${
|
||||
sessionSortDir === "desc"
|
||||
? t("usage.sessions.descending")
|
||||
: t("usage.sessions.ascending")
|
||||
}
|
||||
title=${sessionSortDir === "desc"
|
||||
? t("usage.sessions.descending")
|
||||
: t("usage.sessions.ascending")}
|
||||
>
|
||||
${sessionSortDir === "desc" ? "↓" : "↑"}
|
||||
</button>
|
||||
${
|
||||
selectedCount > 0
|
||||
? html`
|
||||
<button class="btn btn--sm" @click=${onClearSessions}>
|
||||
${t("usage.sessions.clearSelection")}
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${
|
||||
sessionsTab === "recent"
|
||||
? recentEntries.length === 0
|
||||
? html`
|
||||
<div class="usage-empty-block">${t("usage.sessions.noRecent")}</div>
|
||||
`
|
||||
: html`
|
||||
<div class="session-bars session-bars--recent">
|
||||
${recentEntries.map((s) => renderSessionBarRow(s, selectedSet.has(s.key)))}
|
||||
</div>
|
||||
`
|
||||
: sessions.length === 0
|
||||
? html`
|
||||
<div class="usage-empty-block">${t("usage.sessions.noneInRange")}</div>
|
||||
`
|
||||
: html`
|
||||
<div class="session-bars">
|
||||
${sortedWithDir
|
||||
.slice(0, 50)
|
||||
.map((s) => renderSessionBarRow(s, selectedSet.has(s.key)))}
|
||||
${
|
||||
sessions.length > 50
|
||||
? html`
|
||||
<div class="usage-more-sessions">
|
||||
${t("usage.sessions.more", { count: String(sessions.length - 50) })}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
${
|
||||
selectedCount > 1
|
||||
${selectedCount > 0
|
||||
? html`
|
||||
<div class="sessions-selected-group">
|
||||
<div class="sessions-card-count">
|
||||
${t("usage.sessions.selected", { count: String(selectedCount) })}
|
||||
</div>
|
||||
<div class="session-bars session-bars--selected">
|
||||
${selectedEntries.map((s) => renderSessionBarRow(s, true))}
|
||||
</div>
|
||||
<button class="btn btn--sm" @click=${onClearSessions}>
|
||||
${t("usage.sessions.clearSelection")}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${sessionsTab === "recent"
|
||||
? recentEntries.length === 0
|
||||
? html` <div class="usage-empty-block">${t("usage.sessions.noRecent")}</div> `
|
||||
: html`
|
||||
<div class="session-bars session-bars--recent">
|
||||
${recentEntries.map((s) => renderSessionBarRow(s, selectedSet.has(s.key)))}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
: sessions.length === 0
|
||||
? html` <div class="usage-empty-block">${t("usage.sessions.noneInRange")}</div> `
|
||||
: html`
|
||||
<div class="session-bars">
|
||||
${sortedWithDir
|
||||
.slice(0, 50)
|
||||
.map((s) => renderSessionBarRow(s, selectedSet.has(s.key)))}
|
||||
${sessions.length > 50
|
||||
? html`
|
||||
<div class="usage-more-sessions">
|
||||
${t("usage.sessions.more", { count: String(sessions.length - 50) })}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`}
|
||||
${selectedCount > 1
|
||||
? html`
|
||||
<div class="sessions-selected-group">
|
||||
<div class="sessions-card-count">
|
||||
${t("usage.sessions.selected", { count: String(selectedCount) })}
|
||||
</div>
|
||||
<div class="session-bars session-bars--selected">
|
||||
${selectedEntries.map((s) => renderSessionBarRow(s, true))}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,9 +128,7 @@ function renderUsageEmptyState(onRefresh: () => void) {
|
|||
<span class="usage-empty-state__feature">${t("usage.empty.featureTimeline")}</span>
|
||||
</div>
|
||||
<div class="usage-empty-state__actions">
|
||||
<button class="btn primary" @click=${onRefresh}>
|
||||
${t("common.refresh")}
|
||||
</button>
|
||||
<button class="btn primary" @click=${onRefresh}>${t("common.refresh")}</button>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
|
@ -378,13 +376,9 @@ export function renderUsage(props: UsageProps) {
|
|||
>
|
||||
<summary>
|
||||
<span>${label}</span>
|
||||
${
|
||||
selectedCount > 0
|
||||
? html`<span class="usage-filter-badge">${selectedCount}</span>`
|
||||
: html`
|
||||
<span class="usage-filter-badge">${t("usage.filters.all")}</span>
|
||||
`
|
||||
}
|
||||
${selectedCount > 0
|
||||
? html`<span class="usage-filter-badge">${selectedCount}</span>`
|
||||
: html` <span class="usage-filter-badge">${t("usage.filters.all")}</span> `}
|
||||
</summary>
|
||||
<div class="usage-filter-popover">
|
||||
<div class="usage-filter-actions">
|
||||
|
|
@ -453,32 +447,32 @@ export function renderUsage(props: UsageProps) {
|
|||
<div class="usage-header-row">
|
||||
<div class="usage-header-title">
|
||||
<div class="card-title usage-section-title">${t("usage.filters.title")}</div>
|
||||
${data.loading ? html`<span class="usage-refresh-indicator">${t("usage.loading.badge")}</span>` : nothing}
|
||||
${isEmpty ? html`<span class="usage-query-hint">${t("usage.empty.hint")}</span>` : nothing}
|
||||
${data.loading
|
||||
? html`<span class="usage-refresh-indicator">${t("usage.loading.badge")}</span>`
|
||||
: nothing}
|
||||
${isEmpty
|
||||
? html`<span class="usage-query-hint">${t("usage.empty.hint")}</span>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="usage-header-metrics">
|
||||
${
|
||||
displayTotals
|
||||
? html`
|
||||
<span class="usage-metric-badge">
|
||||
<strong>${formatTokens(displayTotals.totalTokens)}</strong>
|
||||
${t("usage.metrics.tokens")}
|
||||
</span>
|
||||
<span class="usage-metric-badge">
|
||||
<strong>${formatCost(displayTotals.totalCost)}</strong>
|
||||
${t("usage.metrics.cost")}
|
||||
</span>
|
||||
<span class="usage-metric-badge">
|
||||
<strong>${displaySessionCount}</strong>
|
||||
${
|
||||
displaySessionCount === 1
|
||||
? t("usage.metrics.session")
|
||||
: t("usage.metrics.sessions")
|
||||
}
|
||||
</span>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${displayTotals
|
||||
? html`
|
||||
<span class="usage-metric-badge">
|
||||
<strong>${formatTokens(displayTotals.totalTokens)}</strong>
|
||||
${t("usage.metrics.tokens")}
|
||||
</span>
|
||||
<span class="usage-metric-badge">
|
||||
<strong>${formatCost(displayTotals.totalCost)}</strong>
|
||||
${t("usage.metrics.cost")}
|
||||
</span>
|
||||
<span class="usage-metric-badge">
|
||||
<strong>${displaySessionCount}</strong>
|
||||
${displaySessionCount === 1
|
||||
? t("usage.metrics.session")
|
||||
: t("usage.metrics.sessions")}
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
<button
|
||||
class="btn btn--sm usage-pin-btn ${display.headerPinned ? "active" : ""}"
|
||||
title=${display.headerPinned ? t("usage.filters.unpin") : t("usage.filters.pin")}
|
||||
|
|
@ -660,27 +654,20 @@ export function renderUsage(props: UsageProps) {
|
|||
>
|
||||
${t("usage.query.apply")}
|
||||
</button>
|
||||
${
|
||||
hasDraftQuery || hasQuery
|
||||
? html`
|
||||
<button
|
||||
class="btn btn--sm"
|
||||
@click=${filterActions.onClearQuery}
|
||||
>
|
||||
${t("usage.filters.clear")}
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${hasDraftQuery || hasQuery
|
||||
? html`
|
||||
<button class="btn btn--sm" @click=${filterActions.onClearQuery}>
|
||||
${t("usage.filters.clear")}
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
<span class="usage-query-hint">
|
||||
${
|
||||
hasQuery
|
||||
? t("usage.query.matching", {
|
||||
shown: String(filteredSessions.length),
|
||||
total: String(totalSessions),
|
||||
})
|
||||
: t("usage.query.inRange", { total: String(totalSessions) })
|
||||
}
|
||||
${hasQuery
|
||||
? t("usage.query.matching", {
|
||||
shown: String(filteredSessions.length),
|
||||
total: String(totalSessions),
|
||||
})
|
||||
: t("usage.query.inRange", { total: String(totalSessions) })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -692,169 +679,153 @@ export function renderUsage(props: UsageProps) {
|
|||
${renderFilterSelect("tool", t("usage.filters.tool"), toolOptions)}
|
||||
<span class="usage-query-hint">${t("usage.query.tip")}</span>
|
||||
</div>
|
||||
${
|
||||
queryTerms.length > 0
|
||||
? html`
|
||||
<div class="usage-query-chips">
|
||||
${queryTerms.map((term) => {
|
||||
const label = term.raw;
|
||||
return html`
|
||||
<span class="usage-query-chip">
|
||||
${label}
|
||||
<button
|
||||
title=${t("usage.filters.remove")}
|
||||
@click=${() =>
|
||||
filterActions.onQueryDraftChange(
|
||||
removeQueryToken(filters.queryDraft, label),
|
||||
)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</span>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
querySuggestions.length > 0
|
||||
? html`
|
||||
<div class="usage-query-suggestions">
|
||||
${querySuggestions.map(
|
||||
(suggestion) => html`
|
||||
${queryTerms.length > 0
|
||||
? html`
|
||||
<div class="usage-query-chips">
|
||||
${queryTerms.map((term) => {
|
||||
const label = term.raw;
|
||||
return html`
|
||||
<span class="usage-query-chip">
|
||||
${label}
|
||||
<button
|
||||
class="usage-query-suggestion"
|
||||
title=${t("usage.filters.remove")}
|
||||
@click=${() =>
|
||||
filterActions.onQueryDraftChange(
|
||||
applySuggestionToQuery(filters.queryDraft, suggestion.value),
|
||||
removeQueryToken(filters.queryDraft, label),
|
||||
)}
|
||||
>
|
||||
${suggestion.label}
|
||||
×
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
queryWarnings.length > 0
|
||||
? html`
|
||||
<div class="callout warning usage-callout usage-callout--tight">
|
||||
${queryWarnings.join(" · ")}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
|
||||
${data.error ? html`<div class="callout danger usage-callout">${data.error}</div>` : nothing}
|
||||
|
||||
${
|
||||
data.sessionsLimitReached
|
||||
? html`
|
||||
<div class="callout warning usage-callout">
|
||||
${t("usage.sessions.limitReached")}
|
||||
</span>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</section>
|
||||
|
||||
${
|
||||
isEmpty
|
||||
? renderUsageEmptyState(filterActions.onRefresh)
|
||||
: html`
|
||||
${renderUsageInsights(
|
||||
displayTotals,
|
||||
activeAggregates,
|
||||
insightStats,
|
||||
hasMissingCost,
|
||||
buildPeakErrorHours(aggregateSessions, filters.timeZone),
|
||||
displaySessionCount,
|
||||
totalSessions,
|
||||
)}
|
||||
|
||||
${renderUsageMosaic(
|
||||
aggregateSessions,
|
||||
filters.timeZone,
|
||||
filters.selectedHours,
|
||||
filterActions.onSelectHour,
|
||||
)}
|
||||
|
||||
<div class="usage-grid">
|
||||
<div class="usage-grid-column">
|
||||
<div class="card usage-left-card">
|
||||
${renderDailyChartCompact(
|
||||
filteredDaily,
|
||||
filters.selectedDays,
|
||||
display.chartMode,
|
||||
display.dailyChartMode,
|
||||
displayActions.onDailyChartModeChange,
|
||||
filterActions.onSelectDay,
|
||||
)}
|
||||
${
|
||||
displayTotals
|
||||
? renderCostBreakdownCompact(displayTotals, display.chartMode)
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${renderSessionsCard(
|
||||
filteredSessions,
|
||||
filters.selectedSessions,
|
||||
filters.selectedDays,
|
||||
isTokenMode,
|
||||
display.sessionSort,
|
||||
display.sessionSortDir,
|
||||
display.recentSessions,
|
||||
display.sessionsTab,
|
||||
detailActions.onSelectSession,
|
||||
displayActions.onSessionSortChange,
|
||||
displayActions.onSessionSortDirChange,
|
||||
displayActions.onSessionsTabChange,
|
||||
display.visibleColumns,
|
||||
totalSessions,
|
||||
filterActions.onClearSessions,
|
||||
: nothing}
|
||||
${querySuggestions.length > 0
|
||||
? html`
|
||||
<div class="usage-query-suggestions">
|
||||
${querySuggestions.map(
|
||||
(suggestion) => html`
|
||||
<button
|
||||
class="usage-query-suggestion"
|
||||
@click=${() =>
|
||||
filterActions.onQueryDraftChange(
|
||||
applySuggestionToQuery(filters.queryDraft, suggestion.value),
|
||||
)}
|
||||
>
|
||||
${suggestion.label}
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
${
|
||||
primarySelectedEntry
|
||||
? html`<div class="usage-grid-column">
|
||||
${renderSessionDetailPanel(
|
||||
primarySelectedEntry,
|
||||
detail.timeSeries,
|
||||
detail.timeSeriesLoading,
|
||||
detail.timeSeriesMode,
|
||||
detailActions.onTimeSeriesModeChange,
|
||||
detail.timeSeriesBreakdownMode,
|
||||
detailActions.onTimeSeriesBreakdownChange,
|
||||
detail.timeSeriesCursorStart,
|
||||
detail.timeSeriesCursorEnd,
|
||||
detailActions.onTimeSeriesCursorRangeChange,
|
||||
filters.startDate,
|
||||
filters.endDate,
|
||||
filters.selectedDays,
|
||||
detail.sessionLogs,
|
||||
detail.sessionLogsLoading,
|
||||
detail.sessionLogsExpanded,
|
||||
detailActions.onToggleSessionLogsExpanded,
|
||||
detail.logFilters,
|
||||
detailActions.onLogFilterRolesChange,
|
||||
detailActions.onLogFilterToolsChange,
|
||||
detailActions.onLogFilterHasToolsChange,
|
||||
detailActions.onLogFilterQueryChange,
|
||||
detailActions.onLogFilterClear,
|
||||
display.contextExpanded,
|
||||
detailActions.onToggleContextExpanded,
|
||||
filterActions.onClearSessions,
|
||||
)}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${queryWarnings.length > 0
|
||||
? html`
|
||||
<div class="callout warning usage-callout usage-callout--tight">
|
||||
${queryWarnings.join(" · ")}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
${data.error
|
||||
? html`<div class="callout danger usage-callout">${data.error}</div>`
|
||||
: nothing}
|
||||
${data.sessionsLimitReached
|
||||
? html`
|
||||
<div class="callout warning usage-callout">${t("usage.sessions.limitReached")}</div>
|
||||
`
|
||||
}
|
||||
: nothing}
|
||||
</section>
|
||||
|
||||
${isEmpty
|
||||
? renderUsageEmptyState(filterActions.onRefresh)
|
||||
: html`
|
||||
${renderUsageInsights(
|
||||
displayTotals,
|
||||
activeAggregates,
|
||||
insightStats,
|
||||
hasMissingCost,
|
||||
buildPeakErrorHours(aggregateSessions, filters.timeZone),
|
||||
displaySessionCount,
|
||||
totalSessions,
|
||||
)}
|
||||
${renderUsageMosaic(
|
||||
aggregateSessions,
|
||||
filters.timeZone,
|
||||
filters.selectedHours,
|
||||
filterActions.onSelectHour,
|
||||
)}
|
||||
|
||||
<div class="usage-grid">
|
||||
<div class="usage-grid-column">
|
||||
<div class="card usage-left-card">
|
||||
${renderDailyChartCompact(
|
||||
filteredDaily,
|
||||
filters.selectedDays,
|
||||
display.chartMode,
|
||||
display.dailyChartMode,
|
||||
displayActions.onDailyChartModeChange,
|
||||
filterActions.onSelectDay,
|
||||
)}
|
||||
${displayTotals
|
||||
? renderCostBreakdownCompact(displayTotals, display.chartMode)
|
||||
: nothing}
|
||||
</div>
|
||||
${renderSessionsCard(
|
||||
filteredSessions,
|
||||
filters.selectedSessions,
|
||||
filters.selectedDays,
|
||||
isTokenMode,
|
||||
display.sessionSort,
|
||||
display.sessionSortDir,
|
||||
display.recentSessions,
|
||||
display.sessionsTab,
|
||||
detailActions.onSelectSession,
|
||||
displayActions.onSessionSortChange,
|
||||
displayActions.onSessionSortDirChange,
|
||||
displayActions.onSessionsTabChange,
|
||||
display.visibleColumns,
|
||||
totalSessions,
|
||||
filterActions.onClearSessions,
|
||||
)}
|
||||
</div>
|
||||
${primarySelectedEntry
|
||||
? html`<div class="usage-grid-column">
|
||||
${renderSessionDetailPanel(
|
||||
primarySelectedEntry,
|
||||
detail.timeSeries,
|
||||
detail.timeSeriesLoading,
|
||||
detail.timeSeriesMode,
|
||||
detailActions.onTimeSeriesModeChange,
|
||||
detail.timeSeriesBreakdownMode,
|
||||
detailActions.onTimeSeriesBreakdownChange,
|
||||
detail.timeSeriesCursorStart,
|
||||
detail.timeSeriesCursorEnd,
|
||||
detailActions.onTimeSeriesCursorRangeChange,
|
||||
filters.startDate,
|
||||
filters.endDate,
|
||||
filters.selectedDays,
|
||||
detail.sessionLogs,
|
||||
detail.sessionLogsLoading,
|
||||
detail.sessionLogsExpanded,
|
||||
detailActions.onToggleSessionLogsExpanded,
|
||||
detail.logFilters,
|
||||
detailActions.onLogFilterRolesChange,
|
||||
detailActions.onLogFilterToolsChange,
|
||||
detailActions.onLogFilterHasToolsChange,
|
||||
detailActions.onLogFilterQueryChange,
|
||||
detailActions.onLogFilterClear,
|
||||
display.contextExpanded,
|
||||
detailActions.onToggleContextExpanded,
|
||||
filterActions.onClearSessions,
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"types": ["node"],
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const isWindowsEnv = (env: EnvMap, platform: NodeJS.Platform): boolean => {
|
|||
type VitestExperimentalConfig = {
|
||||
experimental?: {
|
||||
fsModuleCache?: true;
|
||||
fsModuleCachePath?: string;
|
||||
importDurations?: { print: true };
|
||||
printImportBreakdown?: true;
|
||||
};
|
||||
|
|
@ -32,6 +33,7 @@ export function loadVitestExperimentalConfig(
|
|||
): VitestExperimentalConfig {
|
||||
const experimental: {
|
||||
fsModuleCache?: true;
|
||||
fsModuleCachePath?: string;
|
||||
importDurations?: { print: true };
|
||||
printImportBreakdown?: true;
|
||||
} = {};
|
||||
|
|
@ -43,6 +45,9 @@ export function loadVitestExperimentalConfig(
|
|||
if (windowsEnv && isEnabled(env.OPENCLAW_VITEST_FS_MODULE_CACHE)) {
|
||||
experimental.fsModuleCache = true;
|
||||
}
|
||||
if (experimental.fsModuleCache && env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH?.trim()) {
|
||||
experimental.fsModuleCachePath = env.OPENCLAW_VITEST_FS_MODULE_CACHE_PATH.trim();
|
||||
}
|
||||
if (isEnabled(env.OPENCLAW_VITEST_IMPORT_DURATIONS)) {
|
||||
experimental.importDurations = { print: true };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue