mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 09:58:38 +00:00
[codex] Polish sidebar status, agent skills, and chat rendering (#45451)
* style: update chat layout and spacing for improved UI consistency - Adjusted margin and padding for .chat-thread and .content--chat to enhance layout. - Consolidated CSS selectors for better readability and maintainability. - Introduced new test for log parsing functionality to ensure accurate message extraction. * UI: polish agent skills, chat images, and sidebar status * test: stabilize vitest helper export types * UI: address review feedback on agents refresh and chat styles * test: update outbound gateway client fixture values * test: narrow shared ip fixtures to IPv4
This commit is contained in:
@@ -46,6 +46,7 @@ describe("runServiceRestart token drift", () => {
|
|||||||
});
|
});
|
||||||
resetLifecycleServiceMocks();
|
resetLifecycleServiceMocks();
|
||||||
service.readCommand.mockResolvedValue({
|
service.readCommand.mockResolvedValue({
|
||||||
|
programArguments: [],
|
||||||
environment: { OPENCLAW_GATEWAY_TOKEN: "service-token" },
|
environment: { OPENCLAW_GATEWAY_TOKEN: "service-token" },
|
||||||
});
|
});
|
||||||
stubEmptyGatewayEnv();
|
stubEmptyGatewayEnv();
|
||||||
@@ -77,6 +78,7 @@ describe("runServiceRestart token drift", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
service.readCommand.mockResolvedValue({
|
service.readCommand.mockResolvedValue({
|
||||||
|
programArguments: [],
|
||||||
environment: { OPENCLAW_GATEWAY_TOKEN: "env-token" },
|
environment: { OPENCLAW_GATEWAY_TOKEN: "env-token" },
|
||||||
});
|
});
|
||||||
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "env-token");
|
vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "env-token");
|
||||||
|
|||||||
@@ -1,16 +1,36 @@
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
import type { GatewayService } from "../../../daemon/service.js";
|
||||||
|
import type { RuntimeEnv } from "../../../runtime.js";
|
||||||
|
import type { MockFn } from "../../../test-utils/vitest-mock-fn.js";
|
||||||
|
|
||||||
export const runtimeLogs: string[] = [];
|
export const runtimeLogs: string[] = [];
|
||||||
|
|
||||||
export const defaultRuntime = {
|
type LifecycleRuntimeHarness = RuntimeEnv & {
|
||||||
log: (message: string) => runtimeLogs.push(message),
|
error: MockFn<RuntimeEnv["error"]>;
|
||||||
error: vi.fn(),
|
exit: MockFn<RuntimeEnv["exit"]>;
|
||||||
exit: (code: number) => {
|
|
||||||
throw new Error(`__exit__:${code}`);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const service = {
|
type LifecycleServiceHarness = GatewayService & {
|
||||||
|
install: MockFn<GatewayService["install"]>;
|
||||||
|
uninstall: MockFn<GatewayService["uninstall"]>;
|
||||||
|
stop: MockFn<GatewayService["stop"]>;
|
||||||
|
isLoaded: MockFn<GatewayService["isLoaded"]>;
|
||||||
|
readCommand: MockFn<GatewayService["readCommand"]>;
|
||||||
|
readRuntime: MockFn<GatewayService["readRuntime"]>;
|
||||||
|
restart: MockFn<GatewayService["restart"]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultRuntime: LifecycleRuntimeHarness = {
|
||||||
|
log: (...args: unknown[]) => {
|
||||||
|
runtimeLogs.push(args.map((arg) => String(arg)).join(" "));
|
||||||
|
},
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: vi.fn((code: number) => {
|
||||||
|
throw new Error(`__exit__:${code}`);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const service: LifecycleServiceHarness = {
|
||||||
label: "TestService",
|
label: "TestService",
|
||||||
loadedText: "loaded",
|
loadedText: "loaded",
|
||||||
notLoadedText: "not loaded",
|
notLoadedText: "not loaded",
|
||||||
@@ -32,7 +52,7 @@ export function resetLifecycleServiceMocks() {
|
|||||||
service.readCommand.mockClear();
|
service.readCommand.mockClear();
|
||||||
service.restart.mockClear();
|
service.restart.mockClear();
|
||||||
service.isLoaded.mockResolvedValue(true);
|
service.isLoaded.mockResolvedValue(true);
|
||||||
service.readCommand.mockResolvedValue({ environment: {} });
|
service.readCommand.mockResolvedValue({ programArguments: [], environment: {} });
|
||||||
service.restart.mockResolvedValue({ outcome: "completed" });
|
service.restart.mockResolvedValue({ outcome: "completed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ vi.mock("../schtasks-exec.js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../infra/ports.js", () => ({
|
vi.mock("../../infra/ports.js", () => ({
|
||||||
inspectPortUsage: (...args: unknown[]) => inspectPortUsage(...args),
|
inspectPortUsage: (port: number) => inspectPortUsage(port),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../../process/kill-tree.js", () => ({
|
vi.mock("../../process/kill-tree.js", () => ({
|
||||||
killProcessTree: (...args: unknown[]) => killProcessTree(...args),
|
killProcessTree: (pid: number, opts?: { graceMs?: number }) => killProcessTree(pid, opts),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ import fs from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
import type { PortUsage } from "../../infra/ports-types.js";
|
||||||
|
import type { killProcessTree as killProcessTreeImpl } from "../../process/kill-tree.js";
|
||||||
|
import type { MockFn } from "../../test-utils/vitest-mock-fn.js";
|
||||||
|
|
||||||
export const schtasksResponses: Array<{ code: number; stdout: string; stderr: string }> = [];
|
export const schtasksResponses: Array<{ code: number; stdout: string; stderr: string }> = [];
|
||||||
export const schtasksCalls: string[][] = [];
|
export const schtasksCalls: string[][] = [];
|
||||||
export const inspectPortUsage = vi.fn();
|
|
||||||
export const killProcessTree = vi.fn();
|
export const inspectPortUsage: MockFn<(port: number) => Promise<PortUsage>> = vi.fn();
|
||||||
|
export const killProcessTree: MockFn<typeof killProcessTreeImpl> = vi.fn();
|
||||||
|
|
||||||
export async function withWindowsEnv(
|
export async function withWindowsEnv(
|
||||||
prefix: string,
|
prefix: string,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
getDefaultMediaLocalRoots: vi.fn(() => []),
|
getDefaultMediaLocalRoots: vi.fn(() => []),
|
||||||
@@ -204,8 +205,8 @@ describe("executeSendAction", () => {
|
|||||||
url: "http://127.0.0.1:18789",
|
url: "http://127.0.0.1:18789",
|
||||||
token: "tok",
|
token: "tok",
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
clientName: "gateway",
|
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
||||||
mode: "gateway",
|
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
to: "channel:123",
|
to: "channel:123",
|
||||||
@@ -296,8 +297,8 @@ describe("executeSendAction", () => {
|
|||||||
url: "http://127.0.0.1:18789",
|
url: "http://127.0.0.1:18789",
|
||||||
token: "tok",
|
token: "tok",
|
||||||
timeoutMs: 5000,
|
timeoutMs: 5000,
|
||||||
clientName: "gateway",
|
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
||||||
mode: "gateway",
|
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
to: "channel:123",
|
to: "channel:123",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
isCanonicalDottedDecimalIPv4,
|
isCanonicalDottedDecimalIPv4,
|
||||||
isCarrierGradeNatIpv4Address,
|
isCarrierGradeNatIpv4Address,
|
||||||
isIpInCidr,
|
isIpInCidr,
|
||||||
|
isIpv4Address,
|
||||||
isIpv6Address,
|
isIpv6Address,
|
||||||
isLegacyIpv4Literal,
|
isLegacyIpv4Literal,
|
||||||
isLoopbackIpAddress,
|
isLoopbackIpAddress,
|
||||||
@@ -88,7 +89,7 @@ describe("shared ip helpers", () => {
|
|||||||
|
|
||||||
expect(loopback?.kind()).toBe("ipv4");
|
expect(loopback?.kind()).toBe("ipv4");
|
||||||
expect(benchmark?.kind()).toBe("ipv4");
|
expect(benchmark?.kind()).toBe("ipv4");
|
||||||
if (!loopback || loopback.kind() !== "ipv4" || !benchmark || benchmark.kind() !== "ipv4") {
|
if (!loopback || !isIpv4Address(loopback) || !benchmark || !isIpv4Address(benchmark)) {
|
||||||
throw new Error("expected ipv4 fixtures");
|
throw new Error("expected ipv4 fixtures");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,28 @@ import type { OpenClawConfig } from "../config/config.js";
|
|||||||
import type { ChannelGroupPolicy } from "../config/group-policy.js";
|
import type { ChannelGroupPolicy } from "../config/group-policy.js";
|
||||||
import type { TelegramAccountConfig } from "../config/types.js";
|
import type { TelegramAccountConfig } from "../config/types.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import type { MockFn } from "../test-utils/vitest-mock-fn.js";
|
||||||
import { registerTelegramNativeCommands } from "./bot-native-commands.js";
|
import { registerTelegramNativeCommands } from "./bot-native-commands.js";
|
||||||
|
|
||||||
type RegisterTelegramNativeCommandsParams = Parameters<typeof registerTelegramNativeCommands>[0];
|
type RegisterTelegramNativeCommandsParams = Parameters<typeof registerTelegramNativeCommands>[0];
|
||||||
type GetPluginCommandSpecsFn = typeof import("../plugins/commands.js").getPluginCommandSpecs;
|
type GetPluginCommandSpecsFn = typeof import("../plugins/commands.js").getPluginCommandSpecs;
|
||||||
type MatchPluginCommandFn = typeof import("../plugins/commands.js").matchPluginCommand;
|
type MatchPluginCommandFn = typeof import("../plugins/commands.js").matchPluginCommand;
|
||||||
type ExecutePluginCommandFn = typeof import("../plugins/commands.js").executePluginCommand;
|
type ExecutePluginCommandFn = typeof import("../plugins/commands.js").executePluginCommand;
|
||||||
|
type AnyMock = MockFn<(...args: unknown[]) => unknown>;
|
||||||
|
type AnyAsyncMock = MockFn<(...args: unknown[]) => Promise<unknown>>;
|
||||||
|
type NativeCommandHarness = {
|
||||||
|
handlers: Record<string, (ctx: unknown) => Promise<void>>;
|
||||||
|
sendMessage: AnyAsyncMock;
|
||||||
|
setMyCommands: AnyAsyncMock;
|
||||||
|
log: AnyMock;
|
||||||
|
bot: {
|
||||||
|
api: {
|
||||||
|
setMyCommands: AnyAsyncMock;
|
||||||
|
sendMessage: AnyAsyncMock;
|
||||||
|
};
|
||||||
|
command: (name: string, handler: (ctx: unknown) => Promise<void>) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const pluginCommandMocks = vi.hoisted(() => ({
|
const pluginCommandMocks = vi.hoisted(() => ({
|
||||||
getPluginCommandSpecs: vi.fn<GetPluginCommandSpecsFn>(() => []),
|
getPluginCommandSpecs: vi.fn<GetPluginCommandSpecsFn>(() => []),
|
||||||
@@ -86,12 +102,12 @@ export function createNativeCommandsHarness(params?: {
|
|||||||
nativeEnabled?: boolean;
|
nativeEnabled?: boolean;
|
||||||
groupConfig?: Record<string, unknown>;
|
groupConfig?: Record<string, unknown>;
|
||||||
resolveGroupPolicy?: () => ChannelGroupPolicy;
|
resolveGroupPolicy?: () => ChannelGroupPolicy;
|
||||||
}) {
|
}): NativeCommandHarness {
|
||||||
const handlers: Record<string, (ctx: unknown) => Promise<void>> = {};
|
const handlers: Record<string, (ctx: unknown) => Promise<void>> = {};
|
||||||
const sendMessage = vi.fn().mockResolvedValue(undefined);
|
const sendMessage: AnyAsyncMock = vi.fn(async () => undefined);
|
||||||
const setMyCommands = vi.fn().mockResolvedValue(undefined);
|
const setMyCommands: AnyAsyncMock = vi.fn(async () => undefined);
|
||||||
const log = vi.fn();
|
const log: AnyMock = vi.fn();
|
||||||
const bot = {
|
const bot: NativeCommandHarness["bot"] = {
|
||||||
api: {
|
api: {
|
||||||
setMyCommands,
|
setMyCommands,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
@@ -153,7 +169,7 @@ export function createTelegramGroupCommandContext(params?: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findNotAuthorizedCalls(sendMessage: ReturnType<typeof vi.fn>) {
|
export function findNotAuthorizedCalls(sendMessage: AnyAsyncMock) {
|
||||||
return sendMessage.mock.calls.filter(
|
return sendMessage.mock.calls.filter(
|
||||||
(call) => typeof call[1] === "string" && call[1].includes("not authorized"),
|
(call) => typeof call[1] === "string" && call[1].includes("not authorized"),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const de: TranslationMap = {
|
|||||||
version: "Version",
|
version: "Version",
|
||||||
health: "Status",
|
health: "Status",
|
||||||
ok: "OK",
|
ok: "OK",
|
||||||
|
online: "Online",
|
||||||
offline: "Offline",
|
offline: "Offline",
|
||||||
connect: "Verbinden",
|
connect: "Verbinden",
|
||||||
refresh: "Aktualisieren",
|
refresh: "Aktualisieren",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const en: TranslationMap = {
|
|||||||
common: {
|
common: {
|
||||||
health: "Health",
|
health: "Health",
|
||||||
ok: "OK",
|
ok: "OK",
|
||||||
|
online: "Online",
|
||||||
offline: "Offline",
|
offline: "Offline",
|
||||||
connect: "Connect",
|
connect: "Connect",
|
||||||
refresh: "Refresh",
|
refresh: "Refresh",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const es: TranslationMap = {
|
|||||||
version: "Versión",
|
version: "Versión",
|
||||||
health: "Estado",
|
health: "Estado",
|
||||||
ok: "Correcto",
|
ok: "Correcto",
|
||||||
|
online: "En línea",
|
||||||
offline: "Desconectado",
|
offline: "Desconectado",
|
||||||
connect: "Conectar",
|
connect: "Conectar",
|
||||||
refresh: "Actualizar",
|
refresh: "Actualizar",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const pt_BR: TranslationMap = {
|
|||||||
common: {
|
common: {
|
||||||
health: "Saúde",
|
health: "Saúde",
|
||||||
ok: "OK",
|
ok: "OK",
|
||||||
|
online: "Online",
|
||||||
offline: "Offline",
|
offline: "Offline",
|
||||||
connect: "Conectar",
|
connect: "Conectar",
|
||||||
refresh: "Atualizar",
|
refresh: "Atualizar",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const zh_CN: TranslationMap = {
|
|||||||
common: {
|
common: {
|
||||||
health: "健康状况",
|
health: "健康状况",
|
||||||
ok: "正常",
|
ok: "正常",
|
||||||
|
online: "在线",
|
||||||
offline: "离线",
|
offline: "离线",
|
||||||
connect: "连接",
|
connect: "连接",
|
||||||
refresh: "刷新",
|
refresh: "刷新",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const zh_TW: TranslationMap = {
|
|||||||
common: {
|
common: {
|
||||||
health: "健康狀況",
|
health: "健康狀況",
|
||||||
ok: "正常",
|
ok: "正常",
|
||||||
|
online: "在線",
|
||||||
offline: "離線",
|
offline: "離線",
|
||||||
connect: "連接",
|
connect: "連接",
|
||||||
refresh: "刷新",
|
refresh: "刷新",
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0; /* Allow flex shrinking */
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
/* Allow flex shrinking */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
@@ -24,8 +26,8 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 0;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,16 +51,22 @@
|
|||||||
|
|
||||||
/* Chat thread - scrollable middle section, transparent */
|
/* Chat thread - scrollable middle section, transparent */
|
||||||
.chat-thread {
|
.chat-thread {
|
||||||
flex: 1 1 0; /* Grow, shrink, and use 0 base for proper scrolling */
|
flex: 1 1 0;
|
||||||
|
/* Grow, shrink, and use 0 base for proper scrolling */
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding: 12px 4px;
|
padding: 0 6px 6px;
|
||||||
margin: 0 -4px;
|
margin: 0 0 0 0;
|
||||||
min-height: 0; /* Allow shrinking for flex scroll behavior */
|
min-height: 0;
|
||||||
|
/* Allow shrinking for flex scroll behavior */
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-thread-inner > :first-child {
|
||||||
|
margin-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Focus mode exit button */
|
/* Focus mode exit button */
|
||||||
.chat-focus-exit {
|
.chat-focus-exit {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -146,7 +154,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: auto; /* Push to bottom of flex container */
|
margin-top: auto;
|
||||||
|
/* Push to bottom of flex container */
|
||||||
padding: 12px 4px 4px;
|
padding: 12px 4px 4px;
|
||||||
background: linear-gradient(to bottom, transparent, var(--bg) 20%);
|
background: linear-gradient(to bottom, transparent, var(--bg) 20%);
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
@@ -163,7 +172,8 @@
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
align-self: flex-start; /* Don't stretch in flex column parent */
|
align-self: flex-start;
|
||||||
|
/* Don't stretch in flex column parent */
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-attachment {
|
.chat-attachment {
|
||||||
|
|||||||
@@ -82,6 +82,18 @@
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-markdown .markdown-inline-image {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 420px;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: color-mix(in srgb, var(--secondary) 70%, transparent);
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-markdown pre {
|
.sidebar-markdown pre {
|
||||||
background: rgba(0, 0, 0, 0.12);
|
background: rgba(0, 0, 0, 0.12);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -56,6 +56,19 @@
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat-text :where(.markdown-inline-image) {
|
||||||
|
display: block;
|
||||||
|
max-width: min(100%, 420px);
|
||||||
|
max-height: 320px;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
margin-top: 0.75em;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: color-mix(in srgb, var(--secondary) 70%, transparent);
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-text :where(:not(pre) > code) {
|
.chat-text :where(:not(pre) > code) {
|
||||||
background: rgba(0, 0, 0, 0.15);
|
background: rgba(0, 0, 0, 0.15);
|
||||||
padding: 0.15em 0.4em;
|
padding: 0.15em 0.4em;
|
||||||
|
|||||||
@@ -2157,7 +2157,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-thread {
|
.chat-thread {
|
||||||
margin-top: 16px;
|
margin-top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -2165,7 +2165,7 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
padding: 16px 12px;
|
padding: 0 12px 16px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
.shell {
|
.shell {
|
||||||
--shell-pad: 16px;
|
--shell-pad: 16px;
|
||||||
--shell-gap: 16px;
|
--shell-gap: 16px;
|
||||||
--shell-nav-width: 288px;
|
--shell-nav-width: 258px;
|
||||||
--shell-nav-rail-width: 78px;
|
--shell-nav-rail-width: 78px;
|
||||||
--shell-topbar-height: 52px;
|
--shell-topbar-height: 52px;
|
||||||
--shell-focus-duration: 200ms;
|
--shell-focus-duration: 200ms;
|
||||||
@@ -340,7 +340,7 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 14px 14px 12px;
|
padding: 14px 10px 12px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -503,7 +503,7 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 12px;
|
padding: 0 10px;
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -522,9 +522,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-section__label-text {
|
.nav-section__label-text {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.06em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,9 +555,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
min-height: 38px;
|
min-height: 40px;
|
||||||
padding: 0 12px;
|
padding: 0 9px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -595,8 +595,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.nav-item__text {
|
.nav-item__text {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-weight: 550;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -763,6 +763,24 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-version__status {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-version__status.sidebar-connection-status--online {
|
||||||
|
background: var(--ok);
|
||||||
|
box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 14%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-version__status.sidebar-connection-status--offline {
|
||||||
|
background: var(--danger);
|
||||||
|
box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 14%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar--collapsed .sidebar-shell__footer {
|
.sidebar--collapsed .sidebar-shell__footer {
|
||||||
padding: 8px 0 2px;
|
padding: 8px 0 2px;
|
||||||
}
|
}
|
||||||
@@ -780,6 +798,10 @@
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar--collapsed .sidebar-version__status {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.shell--nav-collapsed .shell-nav {
|
.shell--nav-collapsed .shell-nav {
|
||||||
width: var(--shell-nav-rail-width);
|
width: var(--shell-nav-rail-width);
|
||||||
min-width: var(--shell-nav-rail-width);
|
min-width: var(--shell-nav-rail-width);
|
||||||
@@ -844,7 +866,7 @@
|
|||||||
.content--chat {
|
.content--chat {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 2px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
@@ -905,6 +927,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content--chat .content-header > div:first-child {
|
.content--chat .content-header > div:first-child {
|
||||||
|
|||||||
@@ -323,6 +323,10 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content--chat {
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.content--chat .content-header > div:first-child,
|
.content--chat .content-header > div:first-child,
|
||||||
.content--chat .page-meta,
|
.content--chat .page-meta,
|
||||||
.content--chat .chat-controls {
|
.content--chat .chat-controls {
|
||||||
@@ -417,8 +421,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-thread {
|
.chat-thread {
|
||||||
margin-top: 8px;
|
margin-top: 0;
|
||||||
padding: 12px 8px;
|
padding: 0 8px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-msg {
|
.chat-msg {
|
||||||
|
|||||||
@@ -743,6 +743,23 @@ export function renderTopbarThemeModeToggle(state: AppViewState) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function renderSidebarConnectionStatus(state: AppViewState) {
|
||||||
|
const label = state.connected ? t("common.online") : t("common.offline");
|
||||||
|
const toneClass = state.connected
|
||||||
|
? "sidebar-connection-status--online"
|
||||||
|
: "sidebar-connection-status--offline";
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<span
|
||||||
|
class="sidebar-version__status ${toneClass}"
|
||||||
|
role="img"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-label="Gateway status: ${label}"
|
||||||
|
title="Gateway status: ${label}"
|
||||||
|
></span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
export function renderThemeToggle(state: AppViewState) {
|
export function renderThemeToggle(state: AppViewState) {
|
||||||
const setOpen = (orb: HTMLElement, nextOpen: boolean) => {
|
const setOpen = (orb: HTMLElement, nextOpen: boolean) => {
|
||||||
orb.classList.toggle("theme-orb--open", nextOpen);
|
orb.classList.toggle("theme-orb--open", nextOpen);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
renderChatControls,
|
renderChatControls,
|
||||||
renderChatSessionSelect,
|
renderChatSessionSelect,
|
||||||
renderTab,
|
renderTab,
|
||||||
|
renderSidebarConnectionStatus,
|
||||||
renderTopbarThemeModeToggle,
|
renderTopbarThemeModeToggle,
|
||||||
} from "./app-render.helpers.ts";
|
} from "./app-render.helpers.ts";
|
||||||
import type { AppViewState } from "./app-view-state.ts";
|
import type { AppViewState } from "./app-view-state.ts";
|
||||||
@@ -437,9 +438,7 @@ export function renderApp(state: AppViewState) {
|
|||||||
<span class="topbar-search__label">${t("common.search")}</span>
|
<span class="topbar-search__label">${t("common.search")}</span>
|
||||||
<kbd class="topbar-search__kbd">⌘K</kbd>
|
<kbd class="topbar-search__kbd">⌘K</kbd>
|
||||||
</button>
|
</button>
|
||||||
<div class="topbar-status">
|
<div class="topbar-status">${renderTopbarThemeModeToggle(state)}</div>
|
||||||
${renderTopbarThemeModeToggle(state)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -543,9 +542,10 @@ export function renderApp(state: AppViewState) {
|
|||||||
? html`
|
? html`
|
||||||
<span class="sidebar-version__label">${t("common.version")}</span>
|
<span class="sidebar-version__label">${t("common.version")}</span>
|
||||||
<span class="sidebar-version__text">v${version}</span>
|
<span class="sidebar-version__text">v${version}</span>
|
||||||
|
${renderSidebarConnectionStatus(state)}
|
||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<span class="sidebar-version__dot"></span>
|
${renderSidebarConnectionStatus(state)}
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -924,9 +924,21 @@ export function renderApp(state: AppViewState) {
|
|||||||
state.agentsList?.defaultId ??
|
state.agentsList?.defaultId ??
|
||||||
state.agentsList?.agents?.[0]?.id ??
|
state.agentsList?.agents?.[0]?.id ??
|
||||||
null;
|
null;
|
||||||
|
if (state.agentsPanel === "files" && refreshedAgentId) {
|
||||||
|
void loadAgentFiles(state, refreshedAgentId);
|
||||||
|
}
|
||||||
|
if (state.agentsPanel === "skills" && refreshedAgentId) {
|
||||||
|
void loadAgentSkills(state, refreshedAgentId);
|
||||||
|
}
|
||||||
if (state.agentsPanel === "tools" && refreshedAgentId) {
|
if (state.agentsPanel === "tools" && refreshedAgentId) {
|
||||||
void loadToolsCatalog(state, refreshedAgentId);
|
void loadToolsCatalog(state, refreshedAgentId);
|
||||||
}
|
}
|
||||||
|
if (state.agentsPanel === "channels") {
|
||||||
|
void loadChannels(state, false);
|
||||||
|
}
|
||||||
|
if (state.agentsPanel === "cron") {
|
||||||
|
void state.loadCron();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onSelectAgent: (agentId) => {
|
onSelectAgent: (agentId) => {
|
||||||
if (state.agentsSelectedId === agentId) {
|
if (state.agentsSelectedId === agentId) {
|
||||||
|
|||||||
28
ui/src/ui/controllers/logs.test.ts
Normal file
28
ui/src/ui/controllers/logs.test.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { parseLogLine } from "./logs.ts";
|
||||||
|
|
||||||
|
describe("parseLogLine", () => {
|
||||||
|
it("prefers the human-readable message field when structured data is stored in slot 1", () => {
|
||||||
|
const line = JSON.stringify({
|
||||||
|
0: '{"subsystem":"gateway/ws"}',
|
||||||
|
1: {
|
||||||
|
cause: "unauthorized",
|
||||||
|
authReason: "password_missing",
|
||||||
|
},
|
||||||
|
2: "closed before connect conn=abc code=4008 reason=connect failed",
|
||||||
|
_meta: {
|
||||||
|
date: "2026-03-13T19:07:12.128Z",
|
||||||
|
logLevelName: "WARN",
|
||||||
|
},
|
||||||
|
time: "2026-03-13T14:07:12.138-05:00",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(parseLogLine(line)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
level: "warn",
|
||||||
|
subsystem: "gateway/ws",
|
||||||
|
message: "closed before connect conn=abc code=4008 reason=connect failed",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -77,6 +77,8 @@ export function parseLogLine(line: string): LogEntry {
|
|||||||
let message: string | null = null;
|
let message: string | null = null;
|
||||||
if (typeof obj["1"] === "string") {
|
if (typeof obj["1"] === "string") {
|
||||||
message = obj["1"];
|
message = obj["1"];
|
||||||
|
} else if (typeof obj["2"] === "string") {
|
||||||
|
message = obj["2"];
|
||||||
} else if (!contextObj && typeof obj["0"] === "string") {
|
} else if (!contextObj && typeof obj["0"] === "string") {
|
||||||
message = obj["0"];
|
message = obj["0"];
|
||||||
} else if (typeof obj.message === "string") {
|
} else if (typeof obj.message === "string") {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ describe("toSanitizedMarkdownHtml", () => {
|
|||||||
it("preserves base64 data URI images (#15437)", () => {
|
it("preserves base64 data URI images (#15437)", () => {
|
||||||
const html = toSanitizedMarkdownHtml("");
|
const html = toSanitizedMarkdownHtml("");
|
||||||
expect(html).toContain("<img");
|
expect(html).toContain("<img");
|
||||||
|
expect(html).toContain('class="markdown-inline-image"');
|
||||||
expect(html).toContain("data:image/png;base64,");
|
expect(html).toContain("data:image/png;base64,");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ htmlEscapeRenderer.image = (token: { href?: string | null; text?: string | null
|
|||||||
if (!INLINE_DATA_IMAGE_RE.test(href)) {
|
if (!INLINE_DATA_IMAGE_RE.test(href)) {
|
||||||
return escapeHtml(label);
|
return escapeHtml(label);
|
||||||
}
|
}
|
||||||
return `<img src="${escapeHtml(href)}" alt="${escapeHtml(label)}">`;
|
return `<img class="markdown-inline-image" src="${escapeHtml(href)}" alt="${escapeHtml(label)}">`;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeMarkdownImageLabel(text?: string | null): string {
|
function normalizeMarkdownImageLabel(text?: string | null): string {
|
||||||
|
|||||||
24
ui/src/ui/sidebar-status.browser.test.ts
Normal file
24
ui/src/ui/sidebar-status.browser.test.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { mountApp, registerAppMountHooks } from "./test-helpers/app-mount.ts";
|
||||||
|
|
||||||
|
registerAppMountHooks();
|
||||||
|
|
||||||
|
describe("sidebar connection status", () => {
|
||||||
|
it("shows a single online status dot next to the version", async () => {
|
||||||
|
const app = mountApp("/chat");
|
||||||
|
await app.updateComplete;
|
||||||
|
|
||||||
|
app.hello = {
|
||||||
|
ok: true,
|
||||||
|
server: { version: "1.2.3" },
|
||||||
|
} as never;
|
||||||
|
app.requestUpdate();
|
||||||
|
await app.updateComplete;
|
||||||
|
|
||||||
|
const version = app.querySelector<HTMLElement>(".sidebar-version");
|
||||||
|
const statusDot = app.querySelector<HTMLElement>(".sidebar-version__status");
|
||||||
|
expect(version).not.toBeNull();
|
||||||
|
expect(statusDot).not.toBeNull();
|
||||||
|
expect(statusDot?.getAttribute("aria-label")).toContain("Online");
|
||||||
|
});
|
||||||
|
});
|
||||||
174
ui/src/ui/views/agents.test.ts
Normal file
174
ui/src/ui/views/agents.test.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { render } from "lit";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { renderAgents, type AgentsProps } from "./agents.ts";
|
||||||
|
|
||||||
|
function createSkill() {
|
||||||
|
return {
|
||||||
|
name: "Repo Skill",
|
||||||
|
description: "Skill description",
|
||||||
|
source: "workspace",
|
||||||
|
filePath: "/tmp/skill",
|
||||||
|
baseDir: "/tmp",
|
||||||
|
skillKey: "repo-skill",
|
||||||
|
always: false,
|
||||||
|
disabled: false,
|
||||||
|
blockedByAllowlist: false,
|
||||||
|
eligible: true,
|
||||||
|
requirements: {
|
||||||
|
bins: [],
|
||||||
|
env: [],
|
||||||
|
config: [],
|
||||||
|
os: [],
|
||||||
|
},
|
||||||
|
missing: {
|
||||||
|
bins: [],
|
||||||
|
env: [],
|
||||||
|
config: [],
|
||||||
|
os: [],
|
||||||
|
},
|
||||||
|
configChecks: [],
|
||||||
|
install: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProps(overrides: Partial<AgentsProps> = {}): AgentsProps {
|
||||||
|
return {
|
||||||
|
basePath: "",
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
agentsList: {
|
||||||
|
defaultId: "alpha",
|
||||||
|
mainKey: "main",
|
||||||
|
scope: "workspace",
|
||||||
|
agents: [{ id: "alpha", name: "Alpha" } as never, { id: "beta", name: "Beta" } as never],
|
||||||
|
},
|
||||||
|
selectedAgentId: "beta",
|
||||||
|
activePanel: "overview",
|
||||||
|
config: {
|
||||||
|
form: null,
|
||||||
|
loading: false,
|
||||||
|
saving: false,
|
||||||
|
dirty: false,
|
||||||
|
},
|
||||||
|
channels: {
|
||||||
|
snapshot: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
lastSuccess: null,
|
||||||
|
},
|
||||||
|
cron: {
|
||||||
|
status: null,
|
||||||
|
jobs: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
agentFiles: {
|
||||||
|
list: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
active: null,
|
||||||
|
contents: {},
|
||||||
|
drafts: {},
|
||||||
|
saving: false,
|
||||||
|
},
|
||||||
|
agentIdentityLoading: false,
|
||||||
|
agentIdentityError: null,
|
||||||
|
agentIdentityById: {},
|
||||||
|
agentSkills: {
|
||||||
|
report: null,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
agentId: null,
|
||||||
|
filter: "",
|
||||||
|
},
|
||||||
|
toolsCatalog: {
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
result: null,
|
||||||
|
},
|
||||||
|
onRefresh: () => undefined,
|
||||||
|
onSelectAgent: () => undefined,
|
||||||
|
onSelectPanel: () => undefined,
|
||||||
|
onLoadFiles: () => undefined,
|
||||||
|
onSelectFile: () => undefined,
|
||||||
|
onFileDraftChange: () => undefined,
|
||||||
|
onFileReset: () => undefined,
|
||||||
|
onFileSave: () => undefined,
|
||||||
|
onToolsProfileChange: () => undefined,
|
||||||
|
onToolsOverridesChange: () => undefined,
|
||||||
|
onConfigReload: () => undefined,
|
||||||
|
onConfigSave: () => undefined,
|
||||||
|
onModelChange: () => undefined,
|
||||||
|
onModelFallbacksChange: () => undefined,
|
||||||
|
onChannelsRefresh: () => undefined,
|
||||||
|
onCronRefresh: () => undefined,
|
||||||
|
onCronRunNow: () => undefined,
|
||||||
|
onSkillsFilterChange: () => undefined,
|
||||||
|
onSkillsRefresh: () => undefined,
|
||||||
|
onAgentSkillToggle: () => undefined,
|
||||||
|
onAgentSkillsClear: () => undefined,
|
||||||
|
onAgentSkillsDisableAll: () => undefined,
|
||||||
|
onSetDefault: () => undefined,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("renderAgents", () => {
|
||||||
|
it("shows the skills count only for the selected agent's report", async () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(
|
||||||
|
renderAgents(
|
||||||
|
createProps({
|
||||||
|
agentSkills: {
|
||||||
|
report: {
|
||||||
|
workspaceDir: "/tmp/workspace",
|
||||||
|
managedSkillsDir: "/tmp/skills",
|
||||||
|
skills: [createSkill()],
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
agentId: "alpha",
|
||||||
|
filter: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
const skillsTab = Array.from(container.querySelectorAll<HTMLButtonElement>(".agent-tab")).find(
|
||||||
|
(button) => button.textContent?.includes("Skills"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(skillsTab?.textContent?.trim()).toBe("Skills");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the selected agent's skills count when the report matches", async () => {
|
||||||
|
const container = document.createElement("div");
|
||||||
|
render(
|
||||||
|
renderAgents(
|
||||||
|
createProps({
|
||||||
|
agentSkills: {
|
||||||
|
report: {
|
||||||
|
workspaceDir: "/tmp/workspace",
|
||||||
|
managedSkillsDir: "/tmp/skills",
|
||||||
|
skills: [createSkill()],
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
agentId: "beta",
|
||||||
|
filter: "",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
const skillsTab = Array.from(container.querySelectorAll<HTMLButtonElement>(".agent-tab")).find(
|
||||||
|
(button) => button.textContent?.includes("Skills"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(skillsTab?.textContent?.trim()).toContain("1");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -113,6 +113,10 @@ export function renderAgents(props: AgentsProps) {
|
|||||||
const selectedAgent = selectedId
|
const selectedAgent = selectedId
|
||||||
? (agents.find((agent) => agent.id === selectedId) ?? null)
|
? (agents.find((agent) => agent.id === selectedId) ?? null)
|
||||||
: null;
|
: null;
|
||||||
|
const selectedSkillCount =
|
||||||
|
selectedId && props.agentSkills.agentId === selectedId
|
||||||
|
? (props.agentSkills.report?.skills?.length ?? null)
|
||||||
|
: null;
|
||||||
|
|
||||||
const channelEntryCount = props.channels.snapshot
|
const channelEntryCount = props.channels.snapshot
|
||||||
? Object.keys(props.channels.snapshot.channelAccounts ?? {}).length
|
? Object.keys(props.channels.snapshot.channelAccounts ?? {}).length
|
||||||
@@ -122,7 +126,7 @@ export function renderAgents(props: AgentsProps) {
|
|||||||
: null;
|
: null;
|
||||||
const tabCounts: Record<string, number | null> = {
|
const tabCounts: Record<string, number | null> = {
|
||||||
files: props.agentFiles.list?.files?.length ?? null,
|
files: props.agentFiles.list?.files?.length ?? null,
|
||||||
skills: props.agentSkills.report?.skills?.length ?? null,
|
skills: selectedSkillCount,
|
||||||
channels: channelEntryCount,
|
channels: channelEntryCount,
|
||||||
cron: cronJobCount || null,
|
cron: cronJobCount || null,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user