diff --git a/extensions/bluebubbles/src/actions.ts b/extensions/bluebubbles/src/actions.ts index c3c2832a21..a3074d4e54 100644 --- a/extensions/bluebubbles/src/actions.ts +++ b/extensions/bluebubbles/src/actions.ts @@ -86,7 +86,7 @@ export const bluebubblesMessageActions: ChannelMessageActionAdapter = { if (!spec?.gate) { continue; } - if (spec.unsupportedOnMacOS26 && macOS26) { + if ("unsupportedOnMacOS26" in spec && spec.unsupportedOnMacOS26 && macOS26) { continue; } if (gate(spec.gate)) { diff --git a/extensions/bluebubbles/src/monitor.ts b/extensions/bluebubbles/src/monitor.ts index 173337bfe9..e33b43c69c 100644 --- a/extensions/bluebubbles/src/monitor.ts +++ b/extensions/bluebubbles/src/monitor.ts @@ -361,14 +361,16 @@ function combineDebounceEntries(entries: BlueBubblesDebounceEntry[]): Normalized const webhookTargets = new Map(); +type BlueBubblesDebouncer = { + enqueue: (item: BlueBubblesDebounceEntry) => Promise; + flushKey: (key: string) => Promise; +}; + /** * Maps webhook targets to their inbound debouncers. * Each target gets its own debouncer keyed by a unique identifier. */ -const targetDebouncers = new Map< - WebhookTarget, - ReturnType ->(); +const targetDebouncers = new Map(); function resolveBlueBubblesDebounceMs( config: OpenClawConfig, @@ -1917,7 +1919,7 @@ async function processMessage( maxBytes, }); const saved = await core.channel.media.saveMediaBuffer( - downloaded.buffer, + Buffer.from(downloaded.buffer), downloaded.contentType, "inbound", maxBytes, @@ -2349,7 +2351,7 @@ async function processMessage( }, }); } - if (shouldStopTyping) { + if (shouldStopTyping && chatGuidForActions) { // Stop typing after streaming completes to avoid a stuck indicator. sendBlueBubblesTyping(chatGuidForActions, false, { cfg: config, diff --git a/extensions/bluebubbles/src/types.ts b/extensions/bluebubbles/src/types.ts index f08539f3ff..24c82109cd 100644 --- a/extensions/bluebubbles/src/types.ts +++ b/extensions/bluebubbles/src/types.ts @@ -1,4 +1,5 @@ -export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk"; +import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk"; +export type { DmPolicy, GroupPolicy }; export type BlueBubblesGroupConfig = { /** If true, only respond in this group when mentioned. */ diff --git a/extensions/copilot-proxy/index.ts b/extensions/copilot-proxy/index.ts index e56693b076..b14684ab55 100644 --- a/extensions/copilot-proxy/index.ts +++ b/extensions/copilot-proxy/index.ts @@ -1,4 +1,9 @@ -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; +import { + emptyPluginConfigSchema, + type OpenClawPluginApi, + type ProviderAuthContext, + type ProviderAuthResult, +} from "openclaw/plugin-sdk"; const DEFAULT_BASE_URL = "http://localhost:3000/v1"; const DEFAULT_API_KEY = "n/a"; @@ -57,9 +62,9 @@ function buildModelDefinition(modelId: string) { return { id: modelId, name: modelId, - api: "openai-completions", + api: "openai-completions" as const, reasoning: false, - input: ["text", "image"], + input: ["text", "image"] as Array<"text" | "image">, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: DEFAULT_CONTEXT_WINDOW, maxTokens: DEFAULT_MAX_TOKENS, @@ -71,7 +76,7 @@ const copilotProxyPlugin = { name: "Copilot Proxy", description: "Local Copilot Proxy (VS Code LM) provider plugin", configSchema: emptyPluginConfigSchema(), - register(api) { + register(api: OpenClawPluginApi) { api.registerProvider({ id: "copilot-proxy", label: "Copilot Proxy", @@ -82,7 +87,7 @@ const copilotProxyPlugin = { label: "Local proxy", hint: "Configure base URL + models for the Copilot Proxy server", kind: "custom", - run: async (ctx) => { + run: async (ctx: ProviderAuthContext): Promise => { const baseUrlInput = await ctx.prompter.text({ message: "Copilot Proxy base URL", initialValue: DEFAULT_BASE_URL, @@ -92,7 +97,7 @@ const copilotProxyPlugin = { const modelInput = await ctx.prompter.text({ message: "Model IDs (comma-separated)", initialValue: DEFAULT_MODEL_IDS.join(", "), - validate: (value) => + validate: (value: string) => parseModelIds(value).length > 0 ? undefined : "Enter at least one model id", }); diff --git a/extensions/device-pair/index.ts b/extensions/device-pair/index.ts index 0360205c73..3f9049fdc4 100644 --- a/extensions/device-pair/index.ts +++ b/extensions/device-pair/index.ts @@ -128,7 +128,8 @@ function pickLanIPv4(): string | null { } for (const entry of entries) { const family = entry?.family; - const isIpv4 = family === "IPv4" || family === 4; + // Check for IPv4 (string "IPv4" on Node 18+, number 4 on older) + const isIpv4 = family === "IPv4" || String(family) === "4"; if (!entry || entry.internal || !isIpv4) { continue; } @@ -152,7 +153,8 @@ function pickTailnetIPv4(): string | null { } for (const entry of entries) { const family = entry?.family; - const isIpv4 = family === "IPv4" || family === 4; + // Check for IPv4 (string "IPv4" on Node 18+, number 4 on older) + const isIpv4 = family === "IPv4" || String(family) === "4"; if (!entry || entry.internal || !isIpv4) { continue; } diff --git a/extensions/diagnostics-otel/src/service.ts b/extensions/diagnostics-otel/src/service.ts index fe05fe4bd4..5b747f13cd 100644 --- a/extensions/diagnostics-otel/src/service.ts +++ b/extensions/diagnostics-otel/src/service.ts @@ -4,7 +4,7 @@ import { metrics, trace, SpanStatusCode } from "@opentelemetry/api"; import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http"; import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"; -import { Resource } from "@opentelemetry/resources"; +import { resourceFromAttributes } from "@opentelemetry/resources"; import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs"; import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics"; import { NodeSDK } from "@opentelemetry/sdk-node"; @@ -73,7 +73,7 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { return; } - const resource = new Resource({ + const resource = resourceFromAttributes({ [SemanticResourceAttributes.SERVICE_NAME]: serviceName, }); @@ -210,15 +210,13 @@ export function createDiagnosticsOtelService(): OpenClawPluginService { ...(logUrl ? { url: logUrl } : {}), ...(headers ? { headers } : {}), }); - logProvider = new LoggerProvider({ resource }); - logProvider.addLogRecordProcessor( - new BatchLogRecordProcessor( - logExporter, - typeof otel.flushIntervalMs === "number" - ? { scheduledDelayMillis: Math.max(1000, otel.flushIntervalMs) } - : {}, - ), + const processor = new BatchLogRecordProcessor( + logExporter, + typeof otel.flushIntervalMs === "number" + ? { scheduledDelayMillis: Math.max(1000, otel.flushIntervalMs) } + : {}, ); + logProvider = new LoggerProvider({ resource, processors: [processor] }); const otelLogger = logProvider.getLogger("openclaw"); stopLogTransport = registerLogTransport((logObj) => { diff --git a/extensions/discord/src/channel.ts b/extensions/discord/src/channel.ts index e989795dc9..5d9e101f57 100644 --- a/extensions/discord/src/channel.ts +++ b/extensions/discord/src/channel.ts @@ -31,10 +31,17 @@ import { getDiscordRuntime } from "./runtime.js"; const meta = getChatChannelMeta("discord"); const discordMessageActions: ChannelMessageActionAdapter = { - listActions: (ctx) => getDiscordRuntime().channel.discord.messageActions.listActions(ctx), - extractToolSend: (ctx) => getDiscordRuntime().channel.discord.messageActions.extractToolSend(ctx), - handleAction: async (ctx) => - await getDiscordRuntime().channel.discord.messageActions.handleAction(ctx), + listActions: (ctx) => + getDiscordRuntime().channel.discord.messageActions?.listActions?.(ctx) ?? [], + extractToolSend: (ctx) => + getDiscordRuntime().channel.discord.messageActions?.extractToolSend?.(ctx) ?? null, + handleAction: async (ctx) => { + const ma = getDiscordRuntime().channel.discord.messageActions; + if (!ma?.handleAction) { + throw new Error("Discord message actions not available"); + } + return ma.handleAction(ctx); + }, }; export const discordPlugin: ChannelPlugin = { diff --git a/extensions/feishu/src/bitable.ts b/extensions/feishu/src/bitable.ts index 413e916e46..3ea22fbf4a 100644 --- a/extensions/feishu/src/bitable.ts +++ b/extensions/feishu/src/bitable.ts @@ -212,7 +212,8 @@ async function createRecord( ) { const res = await client.bitable.appTableRecord.create({ path: { app_token: appToken, table_id: tableId }, - data: { fields }, + // oxlint-disable-next-line typescript/no-explicit-any + data: { fields: fields as any }, }); if (res.code !== 0) { throw new Error(res.msg); @@ -232,7 +233,8 @@ async function updateRecord( ) { const res = await client.bitable.appTableRecord.update({ path: { app_token: appToken, table_id: tableId, record_id: recordId }, - data: { fields }, + // oxlint-disable-next-line typescript/no-explicit-any + data: { fields: fields as any }, }); if (res.code !== 0) { throw new Error(res.msg); diff --git a/extensions/feishu/src/channel.ts b/extensions/feishu/src/channel.ts index 40b76722a7..ad5974b99a 100644 --- a/extensions/feishu/src/channel.ts +++ b/extensions/feishu/src/channel.ts @@ -1,4 +1,4 @@ -import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk"; +import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk"; import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js"; import { @@ -19,7 +19,7 @@ import { probeFeishu } from "./probe.js"; import { sendMessageFeishu } from "./send.js"; import { normalizeFeishuTarget, looksLikeFeishuId } from "./targets.js"; -const meta = { +const meta: ChannelMeta = { id: "feishu", label: "Feishu", selectionLabel: "Feishu/Lark (飞书)", @@ -28,7 +28,7 @@ const meta = { blurb: "飞书/Lark enterprise messaging.", aliases: ["lark"], order: 70, -} as const; +}; export const feishuPlugin: ChannelPlugin = { id: "feishu", @@ -38,12 +38,11 @@ export const feishuPlugin: ChannelPlugin = { pairing: { idLabel: "feishuUserId", normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""), - notifyApproval: async ({ cfg, id, accountId }) => { + notifyApproval: async ({ cfg, id }) => { await sendMessageFeishu({ cfg, to: id, text: PAIRING_APPROVED_MESSAGE, - accountId, }); }, }, @@ -202,7 +201,7 @@ export const feishuPlugin: ChannelPlugin = { }), resolveAllowFrom: ({ cfg, accountId }) => { const account = resolveFeishuAccount({ cfg, accountId }); - return account.config?.allowFrom ?? []; + return (account.config?.allowFrom ?? []).map((entry) => String(entry)); }, formatAllowFrom: ({ allowFrom }) => allowFrom @@ -265,7 +264,7 @@ export const feishuPlugin: ChannelPlugin = { }, onboarding: feishuOnboardingAdapter, messaging: { - normalizeTarget: normalizeFeishuTarget, + normalizeTarget: (raw) => normalizeFeishuTarget(raw) ?? undefined, targetResolver: { looksLikeId: looksLikeFeishuId, hint: "", @@ -274,13 +273,33 @@ export const feishuPlugin: ChannelPlugin = { directory: { self: async () => null, listPeers: async ({ cfg, query, limit, accountId }) => - listFeishuDirectoryPeers({ cfg, query, limit, accountId }), + listFeishuDirectoryPeers({ + cfg, + query: query ?? undefined, + limit: limit ?? undefined, + accountId: accountId ?? undefined, + }), listGroups: async ({ cfg, query, limit, accountId }) => - listFeishuDirectoryGroups({ cfg, query, limit, accountId }), + listFeishuDirectoryGroups({ + cfg, + query: query ?? undefined, + limit: limit ?? undefined, + accountId: accountId ?? undefined, + }), listPeersLive: async ({ cfg, query, limit, accountId }) => - listFeishuDirectoryPeersLive({ cfg, query, limit, accountId }), + listFeishuDirectoryPeersLive({ + cfg, + query: query ?? undefined, + limit: limit ?? undefined, + accountId: accountId ?? undefined, + }), listGroupsLive: async ({ cfg, query, limit, accountId }) => - listFeishuDirectoryGroupsLive({ cfg, query, limit, accountId }), + listFeishuDirectoryGroupsLive({ + cfg, + query: query ?? undefined, + limit: limit ?? undefined, + accountId: accountId ?? undefined, + }), }, outbound: feishuOutbound, status: { @@ -302,8 +321,7 @@ export const feishuPlugin: ChannelPlugin = { probe: snapshot.probe, lastProbeAt: snapshot.lastProbeAt ?? null, }), - probeAccount: async ({ cfg, accountId }) => { - const account = resolveFeishuAccount({ cfg, accountId }); + probeAccount: async ({ account }) => { return await probeFeishu(account); }, buildAccountSnapshot: ({ account, runtime, probe }) => ({ diff --git a/extensions/feishu/src/onboarding.ts b/extensions/feishu/src/onboarding.ts index 38b619387c..3b56071074 100644 --- a/extensions/feishu/src/onboarding.ts +++ b/extensions/feishu/src/onboarding.ts @@ -80,7 +80,10 @@ async function promptFeishuAllowFrom(params: { } const unique = [ - ...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...parts]), + ...new Set([ + ...existing.map((v: string | number) => String(v).trim()).filter(Boolean), + ...parts, + ]), ]; return setFeishuAllowFrom(params.cfg, unique); } diff --git a/extensions/feishu/src/outbound.ts b/extensions/feishu/src/outbound.ts index 31885d8e09..50f385525a 100644 --- a/extensions/feishu/src/outbound.ts +++ b/extensions/feishu/src/outbound.ts @@ -9,32 +9,47 @@ export const feishuOutbound: ChannelOutboundAdapter = { chunkerMode: "markdown", textChunkLimit: 4000, sendText: async ({ cfg, to, text, accountId }) => { - const result = await sendMessageFeishu({ cfg, to, text, accountId }); + const result = await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined }); return { channel: "feishu", ...result }; }, sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => { // Send text first if provided if (text?.trim()) { - await sendMessageFeishu({ cfg, to, text, accountId }); + await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined }); } // Upload and send media if URL provided if (mediaUrl) { try { - const result = await sendMediaFeishu({ cfg, to, mediaUrl, accountId }); + const result = await sendMediaFeishu({ + cfg, + to, + mediaUrl, + accountId: accountId ?? undefined, + }); return { channel: "feishu", ...result }; } catch (err) { // Log the error for debugging console.error(`[feishu] sendMediaFeishu failed:`, err); // Fallback to URL link if upload fails const fallbackText = `📎 ${mediaUrl}`; - const result = await sendMessageFeishu({ cfg, to, text: fallbackText, accountId }); + const result = await sendMessageFeishu({ + cfg, + to, + text: fallbackText, + accountId: accountId ?? undefined, + }); return { channel: "feishu", ...result }; } } // No media URL, just return text result - const result = await sendMessageFeishu({ cfg, to, text: text ?? "", accountId }); + const result = await sendMessageFeishu({ + cfg, + to, + text: text ?? "", + accountId: accountId ?? undefined, + }); return { channel: "feishu", ...result }; }, }; diff --git a/extensions/feishu/src/reply-dispatcher.ts b/extensions/feishu/src/reply-dispatcher.ts index f25ae45bf7..9d50042c1d 100644 --- a/extensions/feishu/src/reply-dispatcher.ts +++ b/extensions/feishu/src/reply-dispatcher.ts @@ -90,16 +90,11 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP }, }); - const textChunkLimit = core.channel.text.resolveTextChunkLimit({ - cfg, - channel: "feishu", - defaultLimit: 4000, + const textChunkLimit = core.channel.text.resolveTextChunkLimit(cfg, "feishu", accountId, { + fallbackLimit: 4000, }); const chunkMode = core.channel.text.resolveChunkMode(cfg, "feishu"); - const tableMode = core.channel.text.resolveMarkdownTableMode({ - cfg, - channel: "feishu", - }); + const tableMode = core.channel.text.resolveMarkdownTableMode({ cfg, channel: "feishu" }); const { dispatcher, replyOptions, markDispatchIdle } = core.channel.reply.createReplyDispatcherWithTyping({ diff --git a/extensions/google-antigravity-auth/index.ts b/extensions/google-antigravity-auth/index.ts index 19435dfcac..38c686ac42 100644 --- a/extensions/google-antigravity-auth/index.ts +++ b/extensions/google-antigravity-auth/index.ts @@ -1,7 +1,11 @@ import { createHash, randomBytes } from "node:crypto"; import { readFileSync } from "node:fs"; import { createServer } from "node:http"; -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; +import { + emptyPluginConfigSchema, + type OpenClawPluginApi, + type ProviderAuthContext, +} from "openclaw/plugin-sdk"; // OAuth constants - decoded from pi-ai's base64 encoded values to stay in sync const decode = (s: string) => Buffer.from(s, "base64").toString(); @@ -392,7 +396,7 @@ const antigravityPlugin = { name: "Google Antigravity Auth", description: "OAuth flow for Google Antigravity (Cloud Code Assist)", configSchema: emptyPluginConfigSchema(), - register(api) { + register(api: OpenClawPluginApi) { api.registerProvider({ id: "google-antigravity", label: "Google Antigravity", @@ -404,7 +408,7 @@ const antigravityPlugin = { label: "Google OAuth", hint: "PKCE + localhost callback", kind: "oauth", - run: async (ctx) => { + run: async (ctx: ProviderAuthContext) => { const spin = ctx.prompter.progress("Starting Antigravity OAuth…"); try { const result = await loginAntigravity({ diff --git a/extensions/google-gemini-cli-auth/index.ts b/extensions/google-gemini-cli-auth/index.ts index e66071ccab..ba7913e2d8 100644 --- a/extensions/google-gemini-cli-auth/index.ts +++ b/extensions/google-gemini-cli-auth/index.ts @@ -1,4 +1,8 @@ -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; +import { + emptyPluginConfigSchema, + type OpenClawPluginApi, + type ProviderAuthContext, +} from "openclaw/plugin-sdk"; import { loginGeminiCliOAuth } from "./oauth.js"; const PROVIDER_ID = "google-gemini-cli"; @@ -16,7 +20,7 @@ const geminiCliPlugin = { name: "Google Gemini CLI Auth", description: "OAuth flow for Gemini CLI (Google Code Assist)", configSchema: emptyPluginConfigSchema(), - register(api) { + register(api: OpenClawPluginApi) { api.registerProvider({ id: PROVIDER_ID, label: PROVIDER_LABEL, @@ -29,7 +33,7 @@ const geminiCliPlugin = { label: "Google OAuth", hint: "PKCE + localhost callback", kind: "oauth", - run: async (ctx) => { + run: async (ctx: ProviderAuthContext) => { const spin = ctx.prompter.progress("Starting Gemini CLI OAuth…"); try { const result = await loginGeminiCliOAuth({ diff --git a/extensions/googlechat/src/actions.ts b/extensions/googlechat/src/actions.ts index 011eaa2918..8382cf6a5f 100644 --- a/extensions/googlechat/src/actions.ts +++ b/extensions/googlechat/src/actions.ts @@ -97,11 +97,11 @@ export const googlechatMessageActions: ChannelMessageActionAdapter = { if (mediaUrl) { const core = getGoogleChatRuntime(); const maxBytes = (account.config.mediaMaxMb ?? 20) * 1024 * 1024; - const loaded = await core.channel.media.fetchRemoteMedia(mediaUrl, { maxBytes }); + const loaded = await core.channel.media.fetchRemoteMedia({ url: mediaUrl, maxBytes }); const upload = await uploadGoogleChatAttachment({ account, space, - filename: loaded.filename ?? "attachment", + filename: loaded.fileName ?? "attachment", buffer: loaded.buffer, contentType: loaded.contentType, }); @@ -114,7 +114,7 @@ export const googlechatMessageActions: ChannelMessageActionAdapter = { ? [ { attachmentUploadToken: upload.attachmentUploadToken, - contentName: loaded.filename, + contentName: loaded.fileName, }, ] : undefined, diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index cc1cdf2256..50c8046400 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -15,6 +15,7 @@ import { type ChannelDock, type ChannelMessageActionAdapter, type ChannelPlugin, + type ChannelStatusIssue, type OpenClawConfig, } from "openclaw/plugin-sdk"; import { GoogleChatConfigSchema } from "openclaw/plugin-sdk"; @@ -451,13 +452,14 @@ export const googlechatPlugin: ChannelPlugin = { (cfg.channels?.["googlechat"] as { mediaMaxMb?: number } | undefined)?.mediaMaxMb, accountId, }); - const loaded = await runtime.channel.media.fetchRemoteMedia(mediaUrl, { + const loaded = await runtime.channel.media.fetchRemoteMedia({ + url: mediaUrl, maxBytes: maxBytes ?? (account.config.mediaMaxMb ?? 20) * 1024 * 1024, }); const upload = await uploadGoogleChatAttachment({ account, space, - filename: loaded.filename ?? "attachment", + filename: loaded.fileName ?? "attachment", buffer: loaded.buffer, contentType: loaded.contentType, }); @@ -467,7 +469,7 @@ export const googlechatPlugin: ChannelPlugin = { text, thread, attachments: upload.attachmentUploadToken - ? [{ attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.filename }] + ? [{ attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.fileName }] : undefined, }); return { @@ -485,7 +487,7 @@ export const googlechatPlugin: ChannelPlugin = { lastStopAt: null, lastError: null, }, - collectStatusIssues: (accounts) => + collectStatusIssues: (accounts): ChannelStatusIssue[] => accounts.flatMap((entry) => { const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID); const enabled = entry.enabled !== false; @@ -493,7 +495,7 @@ export const googlechatPlugin: ChannelPlugin = { if (!enabled || !configured) { return []; } - const issues = []; + const issues: ChannelStatusIssue[] = []; if (!entry.audience) { issues.push({ channel: "googlechat", diff --git a/extensions/googlechat/src/monitor.ts b/extensions/googlechat/src/monitor.ts index 431de0a3a3..f0bd347de4 100644 --- a/extensions/googlechat/src/monitor.ts +++ b/extensions/googlechat/src/monitor.ts @@ -835,7 +835,8 @@ async function deliverGoogleChatReply(params: { const caption = first && !suppressCaption ? payload.text : undefined; first = false; try { - const loaded = await core.channel.media.fetchRemoteMedia(mediaUrl, { + const loaded = await core.channel.media.fetchRemoteMedia({ + url: mediaUrl, maxBytes: (account.config.mediaMaxMb ?? 20) * 1024 * 1024, }); const upload = await uploadAttachmentForReply({ @@ -843,7 +844,7 @@ async function deliverGoogleChatReply(params: { spaceId, buffer: loaded.buffer, contentType: loaded.contentType, - filename: loaded.filename ?? "attachment", + filename: loaded.fileName ?? "attachment", }); if (!upload.attachmentUploadToken) { throw new Error("missing attachment upload token"); @@ -854,7 +855,7 @@ async function deliverGoogleChatReply(params: { text: caption, thread: payload.replyToId, attachments: [ - { attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.filename }, + { attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.fileName }, ], }); statusSink?.({ lastOutboundAt: Date.now() }); diff --git a/extensions/line/src/channel.ts b/extensions/line/src/channel.ts index 5b56f42b9d..96c0a51d79 100644 --- a/extensions/line/src/channel.ts +++ b/extensions/line/src/channel.ts @@ -60,7 +60,7 @@ export const linePlugin: ChannelPlugin = { config: { listAccountIds: (cfg) => getLineRuntime().channel.line.listLineAccountIds(cfg), resolveAccount: (cfg, accountId) => - getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId }), + getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined }), defaultAccountId: (cfg) => getLineRuntime().channel.line.resolveDefaultLineAccountId(cfg), setAccountEnabled: ({ cfg, accountId, enabled }) => { const lineConfig = (cfg.channels?.line ?? {}) as LineConfig; @@ -125,11 +125,12 @@ export const linePlugin: ChannelPlugin = { name: account.name, enabled: account.enabled, configured: Boolean(account.channelAccessToken?.trim()), - tokenSource: account.tokenSource, + tokenSource: account.tokenSource ?? undefined, }), resolveAllowFrom: ({ cfg, accountId }) => ( - getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId }).config.allowFrom ?? [] + getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId: accountId ?? undefined }) + .config.allowFrom ?? [] ).map((entry) => String(entry)), formatAllowFrom: ({ allowFrom }) => allowFrom @@ -172,9 +173,12 @@ export const linePlugin: ChannelPlugin = { }, groups: { resolveRequireMention: ({ cfg, accountId, groupId }) => { - const account = getLineRuntime().channel.line.resolveLineAccount({ cfg, accountId }); + const account = getLineRuntime().channel.line.resolveLineAccount({ + cfg, + accountId: accountId ?? undefined, + }); const groups = account.config.groups; - if (!groups) { + if (!groups || !groupId) { return false; } const groupConfig = groups[groupId] ?? groups["*"]; @@ -185,7 +189,7 @@ export const linePlugin: ChannelPlugin = { normalizeTarget: (target) => { const trimmed = target.trim(); if (!trimmed) { - return null; + return undefined; } return trimmed.replace(/^line:(group|room|user):/i, "").replace(/^line:/i, ""); }, @@ -351,12 +355,15 @@ export const linePlugin: ChannelPlugin = { const hasQuickReplies = quickReplies.length > 0; const quickReply = hasQuickReplies ? createQuickReplyItems(quickReplies) : undefined; + // oxlint-disable-next-line typescript/no-explicit-any const sendMessageBatch = async (messages: Array>) => { if (messages.length === 0) { return; } for (let i = 0; i < messages.length; i += 5) { - const result = await sendBatch(to, messages.slice(i, i + 5), { + // LINE SDK expects Message[] but we build dynamically + const batch = messages.slice(i, i + 5) as unknown as Parameters[1]; + const result = await sendBatch(to, batch, { verbose: false, accountId: accountId ?? undefined, }); @@ -381,15 +388,12 @@ export const linePlugin: ChannelPlugin = { if (!shouldSendQuickRepliesInline) { if (lineData.flexMessage) { - lastResult = await sendFlex( - to, - lineData.flexMessage.altText, - lineData.flexMessage.contents, - { - verbose: false, - accountId: accountId ?? undefined, - }, - ); + // LINE SDK expects FlexContainer but we receive contents as unknown + const flexContents = lineData.flexMessage.contents as Parameters[2]; + lastResult = await sendFlex(to, lineData.flexMessage.altText, flexContents, { + verbose: false, + accountId: accountId ?? undefined, + }); } if (lineData.templateMessage) { @@ -410,7 +414,9 @@ export const linePlugin: ChannelPlugin = { } for (const flexMsg of processed.flexMessages) { - lastResult = await sendFlex(to, flexMsg.altText, flexMsg.contents, { + // LINE SDK expects FlexContainer but we receive contents as unknown + const flexContents = flexMsg.contents as Parameters[2]; + lastResult = await sendFlex(to, flexMsg.altText, flexContents, { verbose: false, accountId: accountId ?? undefined, }); @@ -532,7 +538,9 @@ export const linePlugin: ChannelPlugin = { // Send flex messages for tables/code blocks for (const flexMsg of processed.flexMessages) { - await sendFlex(to, flexMsg.altText, flexMsg.contents, { + // LINE SDK expects FlexContainer but we receive contents as unknown + const flexContents = flexMsg.contents as Parameters[2]; + await sendFlex(to, flexMsg.altText, flexContents, { verbose: false, accountId: accountId ?? undefined, }); diff --git a/extensions/llm-task/index.ts b/extensions/llm-task/index.ts index e42634dad0..27bc98dcb7 100644 --- a/extensions/llm-task/index.ts +++ b/extensions/llm-task/index.ts @@ -1,6 +1,6 @@ -import type { OpenClawPluginApi } from "../../src/plugins/types.js"; +import type { AnyAgentTool, OpenClawPluginApi } from "../../src/plugins/types.js"; import { createLlmTaskTool } from "./src/llm-task-tool.js"; export default function register(api: OpenClawPluginApi) { - api.registerTool(createLlmTaskTool(api), { optional: true }); + api.registerTool(createLlmTaskTool(api) as unknown as AnyAgentTool, { optional: true }); } diff --git a/extensions/llm-task/src/llm-task-tool.ts b/extensions/llm-task/src/llm-task-tool.ts index 615c06d1d2..9bec5fdad2 100644 --- a/extensions/llm-task/src/llm-task-tool.ts +++ b/extensions/llm-task/src/llm-task-tool.ts @@ -25,11 +25,11 @@ async function loadRunEmbeddedPiAgent(): Promise { } // Bundled install (built) - const mod = await import("../../../agents/pi-embedded-runner.js"); + const mod = await import("../../../src/agents/pi-embedded-runner.js"); if (typeof mod.runEmbeddedPiAgent !== "function") { throw new Error("Internal error: runEmbeddedPiAgent not available"); } - return mod.runEmbeddedPiAgent; + return mod.runEmbeddedPiAgent as RunEmbeddedPiAgentFn; } function stripCodeFences(s: string): string { @@ -69,6 +69,7 @@ type PluginCfg = { export function createLlmTaskTool(api: OpenClawPluginApi) { return { name: "llm-task", + label: "LLM Task", description: "Run a generic JSON-only LLM task and return schema-validated JSON. Designed for orchestration from Lobster workflows via openclaw.invoke.", parameters: Type.Object({ @@ -214,14 +215,17 @@ export function createLlmTaskTool(api: OpenClawPluginApi) { // oxlint-disable-next-line typescript/no-explicit-any const schema = (params as any).schema as unknown; if (schema && typeof schema === "object" && !Array.isArray(schema)) { - const ajv = new Ajv({ allErrors: true, strict: false }); + const ajv = new Ajv.default({ allErrors: true, strict: false }); // oxlint-disable-next-line typescript/no-explicit-any const validate = ajv.compile(schema as any); const ok = validate(parsed); if (!ok) { const msg = validate.errors - ?.map((e) => `${e.instancePath || ""} ${e.message || "invalid"}`) + ?.map( + (e: { instancePath?: string; message?: string }) => + `${e.instancePath || ""} ${e.message || "invalid"}`, + ) .join("; ") ?? "invalid"; throw new Error(`LLM JSON did not match schema: ${msg}`); } diff --git a/extensions/lobster/index.ts b/extensions/lobster/index.ts index 3b01680165..b0e8f3a00d 100644 --- a/extensions/lobster/index.ts +++ b/extensions/lobster/index.ts @@ -1,14 +1,18 @@ -import type { OpenClawPluginApi } from "../../src/plugins/types.js"; +import type { + AnyAgentTool, + OpenClawPluginApi, + OpenClawPluginToolFactory, +} from "../../src/plugins/types.js"; import { createLobsterTool } from "./src/lobster-tool.js"; export default function register(api: OpenClawPluginApi) { api.registerTool( - (ctx) => { + ((ctx) => { if (ctx.sandboxed) { return null; } - return createLobsterTool(api); - }, + return createLobsterTool(api) as AnyAgentTool; + }) as OpenClawPluginToolFactory, { optional: true }, ); } diff --git a/extensions/lobster/src/lobster-tool.ts b/extensions/lobster/src/lobster-tool.ts index b24670eef4..aa2fbccbed 100644 --- a/extensions/lobster/src/lobster-tool.ts +++ b/extensions/lobster/src/lobster-tool.ts @@ -232,6 +232,7 @@ function parseEnvelope(stdout: string): LobsterEnvelope { export function createLobsterTool(api: OpenClawPluginApi) { return { name: "lobster", + label: "Lobster Workflow", description: "Run Lobster pipelines as a local-first workflow runtime (typed JSON envelope + resumable approvals).", parameters: Type.Object({ diff --git a/extensions/matrix/src/actions.ts b/extensions/matrix/src/actions.ts index a7c219536f..5cbf8eff88 100644 --- a/extensions/matrix/src/actions.ts +++ b/extensions/matrix/src/actions.ts @@ -78,7 +78,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { replyToId: replyTo ?? undefined, threadId: threadId ?? undefined, }, - cfg, + cfg as CoreConfig, ); } @@ -94,7 +94,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { emoji, remove, }, - cfg, + cfg as CoreConfig, ); } @@ -108,7 +108,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { messageId, limit, }, - cfg, + cfg as CoreConfig, ); } @@ -122,7 +122,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { before: readStringParam(params, "before"), after: readStringParam(params, "after"), }, - cfg, + cfg as CoreConfig, ); } @@ -136,7 +136,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { messageId, content, }, - cfg, + cfg as CoreConfig, ); } @@ -148,7 +148,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { roomId: resolveRoomId(), messageId, }, - cfg, + cfg as CoreConfig, ); } @@ -164,7 +164,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { roomId: resolveRoomId(), messageId, }, - cfg, + cfg as CoreConfig, ); } @@ -176,7 +176,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { userId, roomId: readStringParam(params, "roomId") ?? readStringParam(params, "channelId"), }, - cfg, + cfg as CoreConfig, ); } @@ -186,7 +186,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { action: "channelInfo", roomId: resolveRoomId(), }, - cfg, + cfg as CoreConfig, ); } diff --git a/extensions/matrix/src/matrix/actions/client.ts b/extensions/matrix/src/matrix/actions/client.ts index d9fe477db8..d990b13f56 100644 --- a/extensions/matrix/src/matrix/actions/client.ts +++ b/extensions/matrix/src/matrix/actions/client.ts @@ -1,4 +1,4 @@ -import type { CoreConfig } from "../types.js"; +import type { CoreConfig } from "../../types.js"; import type { MatrixActionClient, MatrixActionClientOpts } from "./types.js"; import { getMatrixRuntime } from "../../runtime.js"; import { getActiveMatrixClient } from "../active-client.js"; @@ -47,7 +47,9 @@ export async function resolveActionClient( if (auth.encryption && client.crypto) { try { const joinedRooms = await client.getJoinedRooms(); - await client.crypto.prepare(joinedRooms); + await (client.crypto as { prepare: (rooms?: string[]) => Promise }).prepare( + joinedRooms, + ); } catch { // Ignore crypto prep failures for one-off actions. } diff --git a/extensions/matrix/src/matrix/actions/summary.ts b/extensions/matrix/src/matrix/actions/summary.ts index d200e99273..061829b0de 100644 --- a/extensions/matrix/src/matrix/actions/summary.ts +++ b/extensions/matrix/src/matrix/actions/summary.ts @@ -63,7 +63,7 @@ export async function fetchEventSummary( eventId: string, ): Promise { try { - const raw = (await client.getEvent(roomId, eventId)) as MatrixRawEvent; + const raw = (await client.getEvent(roomId, eventId)) as unknown as MatrixRawEvent; if (raw.unsigned?.redacted_because) { return null; } diff --git a/extensions/matrix/src/matrix/client/config.ts b/extensions/matrix/src/matrix/client/config.ts index 3c6c0da66b..7eba0d59a5 100644 --- a/extensions/matrix/src/matrix/client/config.ts +++ b/extensions/matrix/src/matrix/client/config.ts @@ -1,5 +1,5 @@ import { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { CoreConfig } from "../types.js"; +import type { CoreConfig } from "../../types.js"; import type { MatrixAuth, MatrixResolvedConfig } from "./types.js"; import { getMatrixRuntime } from "../../runtime.js"; import { ensureMatrixSdkLoggingConfigured } from "./logging.js"; diff --git a/extensions/matrix/src/matrix/client/shared.ts b/extensions/matrix/src/matrix/client/shared.ts index 201eb5bbdb..e43de205ee 100644 --- a/extensions/matrix/src/matrix/client/shared.ts +++ b/extensions/matrix/src/matrix/client/shared.ts @@ -1,6 +1,6 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; import { LogService } from "@vector-im/matrix-bot-sdk"; -import type { CoreConfig } from "../types.js"; +import type { CoreConfig } from "../../types.js"; import type { MatrixAuth } from "./types.js"; import { resolveMatrixAuth } from "./config.js"; import { createMatrixClient } from "./create-client.js"; @@ -69,7 +69,9 @@ async function ensureSharedClientStarted(params: { try { const joinedRooms = await client.getJoinedRooms(); if (client.crypto) { - await client.crypto.prepare(joinedRooms); + await (client.crypto as { prepare: (rooms?: string[]) => Promise }).prepare( + joinedRooms, + ); params.state.cryptoReady = true; } } catch (err) { diff --git a/extensions/matrix/src/matrix/monitor/events.ts b/extensions/matrix/src/matrix/monitor/events.ts index 1faeffc819..60bbe574ad 100644 --- a/extensions/matrix/src/matrix/monitor/events.ts +++ b/extensions/matrix/src/matrix/monitor/events.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { PluginRuntime } from "openclaw/plugin-sdk"; +import type { PluginRuntime, RuntimeLogger } from "openclaw/plugin-sdk"; import type { MatrixAuth } from "../client.js"; import type { MatrixRawEvent } from "./types.js"; import { EventType } from "./types.js"; @@ -10,7 +10,7 @@ export function registerMatrixMonitorEvents(params: { logVerboseMessage: (message: string) => void; warnedEncryptedRooms: Set; warnedCryptoMissingRooms: Set; - logger: { warn: (meta: Record, message: string) => void }; + logger: RuntimeLogger; formatNativeDependencyHint: PluginRuntime["system"]["formatNativeDependencyHint"]; onRoomMessage: (roomId: string, event: MatrixRawEvent) => void | Promise; }): void { @@ -42,10 +42,11 @@ export function registerMatrixMonitorEvents(params: { client.on( "room.failed_decryption", async (roomId: string, event: MatrixRawEvent, error: Error) => { - logger.warn( - { roomId, eventId: event.event_id, error: error.message }, - "Failed to decrypt message", - ); + logger.warn("Failed to decrypt message", { + roomId, + eventId: event.event_id, + error: error.message, + }); logVerboseMessage( `matrix: failed decrypt room=${roomId} id=${event.event_id ?? "unknown"} error=${error.message}`, ); @@ -76,7 +77,7 @@ export function registerMatrixMonitorEvents(params: { warnedEncryptedRooms.add(roomId); const warning = "matrix: encrypted event received without encryption enabled; set channels.matrix.encryption=true and verify the device to decrypt"; - logger.warn({ roomId }, warning); + logger.warn(warning, { roomId }); } if (auth.encryption === true && !client.crypto && !warnedCryptoMissingRooms.has(roomId)) { warnedCryptoMissingRooms.add(roomId); @@ -86,7 +87,7 @@ export function registerMatrixMonitorEvents(params: { downloadCommand: "node node_modules/@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js", }); const warning = `matrix: encryption enabled but crypto is unavailable; ${hint}`; - logger.warn({ roomId }, warning); + logger.warn(warning, { roomId }); } return; } diff --git a/extensions/matrix/src/matrix/monitor/handler.ts b/extensions/matrix/src/matrix/monitor/handler.ts index 367f60a195..08f255b5ac 100644 --- a/extensions/matrix/src/matrix/monitor/handler.ts +++ b/extensions/matrix/src/matrix/monitor/handler.ts @@ -6,9 +6,11 @@ import { logInboundDrop, logTypingFailure, resolveControlCommandGate, + type PluginRuntime, type RuntimeEnv, + type RuntimeLogger, } from "openclaw/plugin-sdk"; -import type { CoreConfig, ReplyToMode } from "../../types.js"; +import type { CoreConfig, MatrixRoomConfig, ReplyToMode } from "../../types.js"; import type { MatrixRawEvent, RoomMessageEventContent } from "./types.js"; import { formatPollAsText, @@ -37,34 +39,14 @@ import { EventType, RelationType } from "./types.js"; export type MatrixMonitorHandlerParams = { client: MatrixClient; - core: { - logging: { - shouldLogVerbose: () => boolean; - }; - channel: (typeof import("openclaw/plugin-sdk"))["channel"]; - system: { - enqueueSystemEvent: ( - text: string, - meta: { sessionKey?: string | null; contextKey?: string | null }, - ) => void; - }; - }; + core: PluginRuntime; cfg: CoreConfig; runtime: RuntimeEnv; - logger: { - info: (message: string | Record, ...meta: unknown[]) => void; - warn: (meta: Record, message: string) => void; - }; + logger: RuntimeLogger; logVerboseMessage: (message: string) => void; allowFrom: string[]; - roomsConfig: CoreConfig["channels"] extends { matrix?: infer MatrixConfig } - ? MatrixConfig extends { groups?: infer Groups } - ? Groups - : Record | undefined - : Record | undefined; - mentionRegexes: ReturnType< - (typeof import("openclaw/plugin-sdk"))["channel"]["mentions"]["buildMentionRegexes"] - >; + roomsConfig: Record | undefined; + mentionRegexes: ReturnType; groupPolicy: "open" | "allowlist" | "disabled"; replyToMode: ReplyToMode; threadReplies: "off" | "inbound" | "always"; @@ -121,7 +103,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam } const isPollEvent = isPollStartType(eventType); - const locationContent = event.content as LocationMessageEventContent; + const locationContent = event.content as unknown as LocationMessageEventContent; const isLocationEvent = eventType === EventType.Location || (eventType === EventType.RoomMessage && locationContent.msgtype === EventType.Location); @@ -159,9 +141,9 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam const roomName = roomInfo.name; const roomAliases = [roomInfo.canonicalAlias ?? "", ...roomInfo.altAliases].filter(Boolean); - let content = event.content as RoomMessageEventContent; + let content = event.content as unknown as RoomMessageEventContent; if (isPollEvent) { - const pollStartContent = event.content as PollStartContent; + const pollStartContent = event.content as unknown as PollStartContent; const pollSummary = parsePollStartContent(pollStartContent); if (pollSummary) { pollSummary.eventId = event.event_id ?? ""; @@ -435,7 +417,7 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam hasControlCommandInMessage; const canDetectMention = mentionRegexes.length > 0 || hasExplicitMention; if (isRoom && shouldRequireMention && !wasMentioned && !shouldBypassMention) { - logger.info({ roomId, reason: "no-mention" }, "skipping room message"); + logger.info("skipping room message", { roomId, reason: "no-mention" }); return; } @@ -523,14 +505,11 @@ export function createMatrixRoomMessageHandler(params: MatrixMonitorHandlerParam } : undefined, onRecordError: (err) => { - logger.warn( - { - error: String(err), - storePath, - sessionKey: ctxPayload.SessionKey ?? route.sessionKey, - }, - "failed updating session meta", - ); + logger.warn("failed updating session meta", { + error: String(err), + storePath, + sessionKey: ctxPayload.SessionKey ?? route.sessionKey, + }); }, }); diff --git a/extensions/matrix/src/matrix/monitor/index.ts b/extensions/matrix/src/matrix/monitor/index.ts index aae5f00d58..eae70509a5 100644 --- a/extensions/matrix/src/matrix/monitor/index.ts +++ b/extensions/matrix/src/matrix/monitor/index.ts @@ -55,7 +55,7 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi if (!core.logging.shouldLogVerbose()) { return; } - logger.debug(message); + logger.debug?.(message); }; const normalizeUserEntry = (raw: string) => @@ -75,13 +75,13 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi ): Promise => { let allowList = list ?? []; if (allowList.length === 0) { - return allowList; + return allowList.map(String); } const entries = allowList .map((entry) => normalizeUserEntry(String(entry))) .filter((entry) => entry && entry !== "*"); if (entries.length === 0) { - return allowList; + return allowList.map(String); } const mapping: string[] = []; const unresolved: string[] = []; @@ -118,12 +118,12 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi `${label} entries must be full Matrix IDs (example: @user:server). Unresolved entries are ignored.`, ); } - return allowList; + return allowList.map(String); }; const allowlistOnly = cfg.channels?.matrix?.allowlistOnly === true; - let allowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? []; - let groupAllowFrom = cfg.channels?.matrix?.groupAllowFrom ?? []; + let allowFrom: string[] = (cfg.channels?.matrix?.dm?.allowFrom ?? []).map(String); + let groupAllowFrom: string[] = (cfg.channels?.matrix?.groupAllowFrom ?? []).map(String); let roomsConfig = cfg.channels?.matrix?.groups ?? cfg.channels?.matrix?.rooms; allowFrom = await resolveUserAllowlist("matrix dm allowlist", allowFrom); @@ -307,15 +307,16 @@ export async function monitorMatrixProvider(opts: MonitorMatrixOpts = {}): Promi if (auth.encryption && client.crypto) { try { // Request verification from other sessions - const verificationRequest = await client.crypto.requestOwnUserVerification(); + const verificationRequest = await ( + client.crypto as { requestOwnUserVerification?: () => Promise } + ).requestOwnUserVerification?.(); if (verificationRequest) { logger.info("matrix: device verification requested - please verify in another client"); } } catch (err) { - logger.debug( - { error: String(err) }, - "Device verification request failed (may already be verified)", - ); + logger.debug?.("Device verification request failed (may already be verified)", { + error: String(err), + }); } } diff --git a/extensions/matrix/src/matrix/monitor/media.ts b/extensions/matrix/src/matrix/monitor/media.ts index c88bfc0613..b7ce8e2152 100644 --- a/extensions/matrix/src/matrix/monitor/media.ts +++ b/extensions/matrix/src/matrix/monitor/media.ts @@ -29,7 +29,8 @@ async function fetchMatrixMediaBuffer(params: { // Use the client's download method which handles auth try { - const buffer = await params.client.downloadContent(params.mxcUrl); + const result = await params.client.downloadContent(params.mxcUrl); + const buffer = result.data; if (buffer.byteLength > params.maxBytes) { throw new Error("Matrix media exceeds configured size limit"); } @@ -53,7 +54,9 @@ async function fetchEncryptedMediaBuffer(params: { } // decryptMedia handles downloading and decrypting the encrypted content internally - const decrypted = await params.client.crypto.decryptMedia(params.file); + const decrypted = await params.client.crypto.decryptMedia( + params.file as Parameters[0], + ); if (decrypted.byteLength > params.maxBytes) { throw new Error("Matrix media exceeds configured size limit"); diff --git a/extensions/matrix/src/matrix/poll-types.ts b/extensions/matrix/src/matrix/poll-types.ts index 29897d895c..aa55a83d68 100644 --- a/extensions/matrix/src/matrix/poll-types.ts +++ b/extensions/matrix/src/matrix/poll-types.ts @@ -73,7 +73,7 @@ export type PollSummary = { }; export function isPollStartType(eventType: string): boolean { - return POLL_START_TYPES.includes(eventType); + return (POLL_START_TYPES as readonly string[]).includes(eventType); } export function getTextContent(text?: TextContent): string { @@ -147,7 +147,8 @@ export function buildPollStartContent(poll: PollInput): PollStartContent { ...buildTextContent(option), })); - const maxSelections = poll.multiple ? Math.max(1, answers.length) : 1; + const isMultiple = (poll.maxSelections ?? 1) > 1; + const maxSelections = isMultiple ? Math.max(1, answers.length) : 1; const fallbackText = buildPollFallbackText( question, answers.map((answer) => getTextContent(answer)), @@ -156,7 +157,7 @@ export function buildPollStartContent(poll: PollInput): PollStartContent { return { [M_POLL_START]: { question: buildTextContent(question), - kind: poll.multiple ? "m.poll.undisclosed" : "m.poll.disclosed", + kind: isMultiple ? "m.poll.undisclosed" : "m.poll.disclosed", max_selections: maxSelections, answers, }, diff --git a/extensions/matrix/src/matrix/send/client.ts b/extensions/matrix/src/matrix/send/client.ts index aa0f3badb7..485b9c1cd0 100644 --- a/extensions/matrix/src/matrix/send/client.ts +++ b/extensions/matrix/src/matrix/send/client.ts @@ -1,5 +1,5 @@ import type { MatrixClient } from "@vector-im/matrix-bot-sdk"; -import type { CoreConfig } from "../types.js"; +import type { CoreConfig } from "../../types.js"; import { getMatrixRuntime } from "../../runtime.js"; import { getActiveMatrixClient } from "../active-client.js"; import { @@ -55,7 +55,9 @@ export async function resolveMatrixClient(opts: { if (auth.encryption && client.crypto) { try { const joinedRooms = await client.getJoinedRooms(); - await client.crypto.prepare(joinedRooms); + await (client.crypto as { prepare: (rooms?: string[]) => Promise }).prepare( + joinedRooms, + ); } catch { // Ignore crypto prep failures for one-off sends; normal sync will retry. } diff --git a/extensions/matrix/src/matrix/send/targets.ts b/extensions/matrix/src/matrix/send/targets.ts index 460559798f..d4d4e2b6e0 100644 --- a/extensions/matrix/src/matrix/send/targets.ts +++ b/extensions/matrix/src/matrix/send/targets.ts @@ -70,9 +70,12 @@ async function resolveDirectRoomId(client: MatrixClient, userId: string): Promis // 1) Fast path: use account data (m.direct) for *this* logged-in user (the bot). try { - const directContent = await client.getAccountData(EventType.Direct); + const directContent = (await client.getAccountData(EventType.Direct)) as Record< + string, + string[] | undefined + >; const list = Array.isArray(directContent?.[trimmed]) ? directContent[trimmed] : []; - if (list.length > 0) { + if (list && list.length > 0) { setDirectRoomCached(trimmed, list[0]); return list[0]; } diff --git a/extensions/matrix/src/types.ts b/extensions/matrix/src/types.ts index fc636cc70d..e372744c11 100644 --- a/extensions/matrix/src/types.ts +++ b/extensions/matrix/src/types.ts @@ -1,4 +1,5 @@ -export type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk"; +import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk"; +export type { DmPolicy, GroupPolicy }; export type ReplyToMode = "off" | "first" | "all"; @@ -92,6 +93,19 @@ export type MatrixConfig = { export type CoreConfig = { channels?: { matrix?: MatrixConfig; + defaults?: { + groupPolicy?: "open" | "allowlist" | "disabled"; + }; + }; + commands?: { + useAccessGroups?: boolean; + }; + session?: { + store?: string; + }; + messages?: { + ackReaction?: string; + ackReactionScope?: "group-mentions" | "group-all" | "direct" | "all"; }; [key: string]: unknown; }; diff --git a/extensions/minimax-portal-auth/index.ts b/extensions/minimax-portal-auth/index.ts index b2fd23522e..827d01a476 100644 --- a/extensions/minimax-portal-auth/index.ts +++ b/extensions/minimax-portal-auth/index.ts @@ -1,4 +1,9 @@ -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; +import { + emptyPluginConfigSchema, + type OpenClawPluginApi, + type ProviderAuthContext, + type ProviderAuthResult, +} from "openclaw/plugin-sdk"; import { loginMiniMaxPortalOAuth, type MiniMaxRegion } from "./oauth.js"; const PROVIDER_ID = "minimax-portal"; @@ -38,8 +43,7 @@ function createOAuthHandler(region: MiniMaxRegion) { const defaultBaseUrl = getDefaultBaseUrl(region); const regionLabel = region === "cn" ? "CN" : "Global"; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return async (ctx: any) => { + return async (ctx: ProviderAuthContext): Promise => { const progress = ctx.prompter.progress(`Starting MiniMax OAuth (${regionLabel})…`); try { const result = await loginMiniMaxPortalOAuth({ @@ -126,7 +130,7 @@ const minimaxPortalPlugin = { name: "MiniMax OAuth", description: "OAuth flow for MiniMax models", configSchema: emptyPluginConfigSchema(), - register(api) { + register(api: OpenClawPluginApi) { api.registerProvider({ id: PROVIDER_ID, label: PROVIDER_LABEL, diff --git a/extensions/msteams/src/channel.ts b/extensions/msteams/src/channel.ts index 5bd16bc3ab..d6fd75abf6 100644 --- a/extensions/msteams/src/channel.ts +++ b/extensions/msteams/src/channel.ts @@ -42,6 +42,7 @@ export const msteamsPlugin: ChannelPlugin = { id: "msteams", meta: { ...meta, + aliases: [...meta.aliases], }, onboarding: msteamsOnboardingAdapter, pairing: { @@ -384,7 +385,8 @@ export const msteamsPlugin: ChannelPlugin = { if (!to) { return { isError: true, - content: [{ type: "text", text: "Card send requires a target (to)." }], + content: [{ type: "text" as const, text: "Card send requires a target (to)." }], + details: { error: "Card send requires a target (to)." }, }; } const result = await sendAdaptiveCardMSTeams({ @@ -395,7 +397,7 @@ export const msteamsPlugin: ChannelPlugin = { return { content: [ { - type: "text", + type: "text" as const, text: JSON.stringify({ ok: true, channel: "msteams", @@ -404,6 +406,7 @@ export const msteamsPlugin: ChannelPlugin = { }), }, ], + details: { ok: true, channel: "msteams", messageId: result.messageId }, }; } // Return null to fall through to default handler diff --git a/extensions/msteams/src/directory-live.ts b/extensions/msteams/src/directory-live.ts index e885cdcbc6..949ad1a3af 100644 --- a/extensions/msteams/src/directory-live.ts +++ b/extensions/msteams/src/directory-live.ts @@ -1,4 +1,4 @@ -import type { ChannelDirectoryEntry } from "openclaw/plugin-sdk"; +import type { ChannelDirectoryEntry, MSTeamsConfig } from "openclaw/plugin-sdk"; import { GRAPH_ROOT } from "./attachments/shared.js"; import { loadMSTeamsSdkWithAuth } from "./sdk.js"; import { resolveMSTeamsCredentials } from "./token.js"; @@ -62,7 +62,7 @@ async function fetchGraphJson(params: { async function resolveGraphToken(cfg: unknown): Promise { const creds = resolveMSTeamsCredentials( - (cfg as { channels?: { msteams?: unknown } })?.channels?.msteams, + (cfg as { channels?: { msteams?: unknown } })?.channels?.msteams as MSTeamsConfig | undefined, ); if (!creds) { throw new Error("MS Teams credentials missing"); diff --git a/extensions/msteams/src/monitor-handler.ts b/extensions/msteams/src/monitor-handler.ts index 4186d55719..9f34019a17 100644 --- a/extensions/msteams/src/monitor-handler.ts +++ b/extensions/msteams/src/monitor-handler.ts @@ -49,7 +49,7 @@ async function handleFileConsentInvoke( const consentResponse = parseFileConsentInvoke(activity); if (!consentResponse) { - log.debug("invalid file consent invoke", { value: activity.value }); + log.debug?.("invalid file consent invoke", { value: activity.value }); return false; } @@ -61,7 +61,7 @@ async function handleFileConsentInvoke( if (consentResponse.action === "accept" && consentResponse.uploadInfo) { const pendingFile = getPendingUpload(uploadId); if (pendingFile) { - log.debug("user accepted file consent, uploading", { + log.debug?.("user accepted file consent, uploading", { uploadId, filename: pendingFile.filename, size: pendingFile.buffer.length, @@ -94,20 +94,20 @@ async function handleFileConsentInvoke( uniqueId: consentResponse.uploadInfo.uniqueId, }); } catch (err) { - log.debug("file upload failed", { uploadId, error: String(err) }); + log.debug?.("file upload failed", { uploadId, error: String(err) }); await context.sendActivity(`File upload failed: ${String(err)}`); } finally { removePendingUpload(uploadId); } } else { - log.debug("pending file not found for consent", { uploadId }); + log.debug?.("pending file not found for consent", { uploadId }); await context.sendActivity( "The file upload request has expired. Please try sending the file again.", ); } } else { // User declined - log.debug("user declined file consent", { uploadId }); + log.debug?.("user declined file consent", { uploadId }); removePendingUpload(uploadId); } @@ -151,7 +151,7 @@ export function registerMSTeamsHandlers( const membersAdded = (context as MSTeamsTurnContext).activity?.membersAdded ?? []; for (const member of membersAdded) { if (member.id !== (context as MSTeamsTurnContext).activity?.recipient?.id) { - deps.log.debug("member added", { member: member.id }); + deps.log.debug?.("member added", { member: member.id }); // Don't send welcome message - let the user initiate conversation. } } diff --git a/extensions/msteams/src/monitor-handler/inbound-media.ts b/extensions/msteams/src/monitor-handler/inbound-media.ts index 3b303a25df..f34659652b 100644 --- a/extensions/msteams/src/monitor-handler/inbound-media.ts +++ b/extensions/msteams/src/monitor-handler/inbound-media.ts @@ -10,7 +10,7 @@ import { } from "../attachments.js"; type MSTeamsLogger = { - debug: (message: string, meta?: Record) => void; + debug?: (message: string, meta?: Record) => void; }; export async function resolveMSTeamsInboundMedia(params: { @@ -66,7 +66,7 @@ export async function resolveMSTeamsInboundMedia(params: { channelData: activity.channelData, }); if (messageUrls.length === 0) { - log.debug("graph message url unavailable", { + log.debug?.("graph message url unavailable", { conversationType, hasChannelData: Boolean(activity.channelData), messageId: activity.id ?? undefined, @@ -107,16 +107,16 @@ export async function resolveMSTeamsInboundMedia(params: { } } if (mediaList.length === 0) { - log.debug("graph media fetch empty", { attempts }); + log.debug?.("graph media fetch empty", { attempts }); } } } } if (mediaList.length > 0) { - log.debug("downloaded attachments", { count: mediaList.length }); + log.debug?.("downloaded attachments", { count: mediaList.length }); } else if (htmlSummary?.imgTags) { - log.debug("inline images detected but none downloaded", { + log.debug?.("inline images detected but none downloaded", { imgTags: htmlSummary.imgTags, srcHosts: htmlSummary.srcHosts, dataImages: htmlSummary.dataImages, diff --git a/extensions/msteams/src/monitor-handler/message-handler.ts b/extensions/msteams/src/monitor-handler/message-handler.ts index a24cc05617..d03796ea3f 100644 --- a/extensions/msteams/src/monitor-handler/message-handler.ts +++ b/extensions/msteams/src/monitor-handler/message-handler.ts @@ -54,7 +54,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { const core = getMSTeamsRuntime(); const logVerboseMessage = (message: string) => { if (core.logging.shouldLogVerbose()) { - log.debug(message); + log.debug?.(message); } }; const msteamsCfg = cfg.channels?.msteams; @@ -105,11 +105,11 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { conversation: conversation?.id, }); if (htmlSummary) { - log.debug("html attachment summary", htmlSummary); + log.debug?.("html attachment summary", htmlSummary); } if (!from?.id) { - log.debug("skipping message without from.id"); + log.debug?.("skipping message without from.id"); return; } @@ -137,7 +137,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { const allowFrom = dmAllowFrom; if (dmPolicy === "disabled") { - log.debug("dropping dm (dms disabled)"); + log.debug?.("dropping dm (dms disabled)"); return; } @@ -163,7 +163,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { }); } } - log.debug("dropping dm (not allowlisted)", { + log.debug?.("dropping dm (not allowlisted)", { sender: senderId, label: senderName, allowlistMatch: formatAllowlistMatchMeta(allowMatch), @@ -200,7 +200,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { if (!isDirectMessage && msteamsCfg) { if (groupPolicy === "disabled") { - log.debug("dropping group message (groupPolicy: disabled)", { + log.debug?.("dropping group message (groupPolicy: disabled)", { conversationId, }); return; @@ -208,7 +208,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { if (groupPolicy === "allowlist") { if (channelGate.allowlistConfigured && !channelGate.allowed) { - log.debug("dropping group message (not in team/channel allowlist)", { + log.debug?.("dropping group message (not in team/channel allowlist)", { conversationId, teamKey: channelGate.teamKey ?? "none", channelKey: channelGate.channelKey ?? "none", @@ -218,20 +218,19 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { return; } if (effectiveGroupAllowFrom.length === 0 && !channelGate.allowlistConfigured) { - log.debug("dropping group message (groupPolicy: allowlist, no allowlist)", { + log.debug?.("dropping group message (groupPolicy: allowlist, no allowlist)", { conversationId, }); return; } if (effectiveGroupAllowFrom.length > 0) { const allowMatch = resolveMSTeamsAllowlistMatch({ - groupPolicy, allowFrom: effectiveGroupAllowFrom, senderId, senderName, }); if (!allowMatch.allowed) { - log.debug("dropping group message (not in groupAllowFrom)", { + log.debug?.("dropping group message (not in groupAllowFrom)", { sender: senderId, label: senderName, allowlistMatch: formatAllowlistMatchMeta(allowMatch), @@ -293,7 +292,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { locale: activity.locale, }; conversationStore.upsert(conversationId, conversationRef).catch((err) => { - log.debug("failed to save conversation reference", { + log.debug?.("failed to save conversation reference", { error: formatUnknownError(err), }); }); @@ -307,7 +306,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { selections: pollVote.selections, }); if (!poll) { - log.debug("poll vote ignored (poll not found)", { + log.debug?.("poll vote ignored (poll not found)", { pollId: pollVote.pollId, }); } else { @@ -327,7 +326,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { } if (!rawBody) { - log.debug("skipping empty message after stripping mentions"); + log.debug?.("skipping empty message after stripping mentions"); return; } @@ -377,7 +376,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { }); const mentioned = mentionGate.effectiveWasMentioned; if (requireMention && mentionGate.shouldSkip) { - log.debug("skipping message (mention required)", { + log.debug?.("skipping message (mention required)", { teamId, channelId, requireMention, @@ -413,7 +412,8 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) { channelData: activity.channelData, }, log, - preserveFilenames: cfg.media?.preserveFilenames, + preserveFilenames: (cfg as { media?: { preserveFilenames?: boolean } }).media + ?.preserveFilenames, }); const mediaPayload = buildMSTeamsMediaPayload(mediaList); diff --git a/extensions/msteams/src/monitor-types.ts b/extensions/msteams/src/monitor-types.ts index 014081ffd2..7035838a81 100644 --- a/extensions/msteams/src/monitor-types.ts +++ b/extensions/msteams/src/monitor-types.ts @@ -1,5 +1,5 @@ export type MSTeamsMonitorLogger = { - debug: (message: string, meta?: Record) => void; + debug?: (message: string, meta?: Record) => void; info: (message: string, meta?: Record) => void; error: (message: string, meta?: Record) => void; }; diff --git a/extensions/msteams/src/monitor.ts b/extensions/msteams/src/monitor.ts index df93c081d3..6c97d3c25b 100644 --- a/extensions/msteams/src/monitor.ts +++ b/extensions/msteams/src/monitor.ts @@ -9,7 +9,7 @@ import type { MSTeamsConversationStore } from "./conversation-store.js"; import type { MSTeamsAdapter } from "./messenger.js"; import { createMSTeamsConversationStoreFs } from "./conversation-store-fs.js"; import { formatUnknownError } from "./errors.js"; -import { registerMSTeamsHandlers } from "./monitor-handler.js"; +import { registerMSTeamsHandlers, type MSTeamsActivityHandler } from "./monitor-handler.js"; import { createMSTeamsPollStoreFs, type MSTeamsPollStore } from "./polls.js"; import { resolveMSTeamsChannelAllowlist, @@ -40,7 +40,7 @@ export async function monitorMSTeamsProvider( let cfg = opts.cfg; let msteamsCfg = cfg.channels?.msteams; if (!msteamsCfg?.enabled) { - log.debug("msteams provider disabled"); + log.debug?.("msteams provider disabled"); return { app: null, shutdown: async () => {} }; } @@ -224,7 +224,7 @@ export async function monitorMSTeamsProvider( const tokenProvider = new MsalTokenProvider(authConfig); const adapter = createMSTeamsAdapter(authConfig, sdk); - const handler = registerMSTeamsHandlers(new ActivityHandler(), { + const handler = registerMSTeamsHandlers(new ActivityHandler() as MSTeamsActivityHandler, { cfg, runtime, appId, @@ -246,7 +246,7 @@ export async function monitorMSTeamsProvider( const configuredPath = msteamsCfg.webhook?.path ?? "/api/messages"; const messageHandler = (req: Request, res: Response) => { void adapter - .process(req, res, (context: unknown) => handler.run(context)) + .process(req, res, (context: unknown) => handler.run!(context)) .catch((err: unknown) => { log.error("msteams webhook failed", { error: formatUnknownError(err) }); }); @@ -258,7 +258,7 @@ export async function monitorMSTeamsProvider( expressApp.post("/api/messages", messageHandler); } - log.debug("listening on paths", { + log.debug?.("listening on paths", { primary: configuredPath, fallback: "/api/messages", }); @@ -277,7 +277,7 @@ export async function monitorMSTeamsProvider( return new Promise((resolve) => { httpServer.close((err) => { if (err) { - log.debug("msteams server close error", { error: String(err) }); + log.debug?.("msteams server close error", { error: String(err) }); } resolve(); }); diff --git a/extensions/msteams/src/onboarding.ts b/extensions/msteams/src/onboarding.ts index d1f055dcfe..d950bd2db0 100644 --- a/extensions/msteams/src/onboarding.ts +++ b/extensions/msteams/src/onboarding.ts @@ -4,6 +4,7 @@ import type { OpenClawConfig, DmPolicy, WizardPrompter, + MSTeamsTeamConfig, } from "openclaw/plugin-sdk"; import { addWildcardAllowFrom, @@ -184,7 +185,7 @@ function setMSTeamsTeamsAllowlist( msteams: { ...cfg.channels?.msteams, enabled: true, - teams, + teams: teams as Record, }, }, }; diff --git a/extensions/msteams/src/reply-dispatcher.ts b/extensions/msteams/src/reply-dispatcher.ts index fef1cf4809..aa58c15f2a 100644 --- a/extensions/msteams/src/reply-dispatcher.ts +++ b/extensions/msteams/src/reply-dispatcher.ts @@ -49,7 +49,7 @@ export function createMSTeamsReplyDispatcher(params: { start: sendTypingIndicator, onStartError: (err) => { logTypingFailure({ - log: (message) => params.log.debug(message), + log: (message) => params.log.debug?.(message), channel: "msteams", action: "start", error: err, @@ -94,7 +94,7 @@ export function createMSTeamsReplyDispatcher(params: { // Enable default retry/backoff for throttling/transient failures. retry: {}, onRetry: (event) => { - params.log.debug("retrying send", { + params.log.debug?.("retrying send", { replyStyle: params.replyStyle, ...event, }); diff --git a/extensions/msteams/src/resolve-allowlist.ts b/extensions/msteams/src/resolve-allowlist.ts index 371b615f38..d6317f1c7c 100644 --- a/extensions/msteams/src/resolve-allowlist.ts +++ b/extensions/msteams/src/resolve-allowlist.ts @@ -1,3 +1,4 @@ +import type { MSTeamsConfig } from "openclaw/plugin-sdk"; import { GRAPH_ROOT } from "./attachments/shared.js"; import { loadMSTeamsSdkWithAuth } from "./sdk.js"; import { resolveMSTeamsCredentials } from "./token.js"; @@ -155,7 +156,7 @@ async function fetchGraphJson(params: { async function resolveGraphToken(cfg: unknown): Promise { const creds = resolveMSTeamsCredentials( - (cfg as { channels?: { msteams?: unknown } })?.channels?.msteams, + (cfg as { channels?: { msteams?: unknown } })?.channels?.msteams as MSTeamsConfig | undefined, ); if (!creds) { throw new Error("MS Teams credentials missing"); diff --git a/extensions/msteams/src/send.ts b/extensions/msteams/src/send.ts index 43725ee15d..fa5c87ae2c 100644 --- a/extensions/msteams/src/send.ts +++ b/extensions/msteams/src/send.ts @@ -111,7 +111,7 @@ export async function sendMessageMSTeams( sharePointSiteId, } = ctx; - log.debug("sending proactive message", { + log.debug?.("sending proactive message", { conversationId, conversationType, textLength: messageText.length, @@ -131,7 +131,7 @@ export async function sendMessageMSTeams( const fallbackFileName = await extractFilename(mediaUrl); const fileName = media.fileName ?? fallbackFileName; - log.debug("processing media", { + log.debug?.("processing media", { fileName, contentType: media.contentType, size: media.buffer.length, @@ -155,7 +155,7 @@ export async function sendMessageMSTeams( description: messageText || undefined, }); - log.debug("sending file consent card", { uploadId, fileName, size: media.buffer.length }); + log.debug?.("sending file consent card", { uploadId, fileName, size: media.buffer.length }); const baseRef = buildConversationReference(ref); const proactiveRef = { ...baseRef, activityId: undefined }; @@ -205,7 +205,7 @@ export async function sendMessageMSTeams( try { if (sharePointSiteId) { // Use SharePoint upload + Graph API for native file card - log.debug("uploading to SharePoint for native file card", { + log.debug?.("uploading to SharePoint for native file card", { fileName, conversationType, siteId: sharePointSiteId, @@ -221,7 +221,7 @@ export async function sendMessageMSTeams( usePerUserSharing: conversationType === "groupChat", }); - log.debug("SharePoint upload complete", { + log.debug?.("SharePoint upload complete", { itemId: uploaded.itemId, shareUrl: uploaded.shareUrl, }); @@ -233,7 +233,7 @@ export async function sendMessageMSTeams( tokenProvider, }); - log.debug("driveItem properties retrieved", { + log.debug?.("driveItem properties retrieved", { eTag: driveItem.eTag, webDavUrl: driveItem.webDavUrl, }); @@ -265,7 +265,7 @@ export async function sendMessageMSTeams( } // Fallback: no SharePoint site configured, use OneDrive with markdown link - log.debug("uploading to OneDrive (no SharePoint site configured)", { + log.debug?.("uploading to OneDrive (no SharePoint site configured)", { fileName, conversationType, }); @@ -277,7 +277,7 @@ export async function sendMessageMSTeams( tokenProvider, }); - log.debug("OneDrive upload complete", { + log.debug?.("OneDrive upload complete", { itemId: uploaded.itemId, shareUrl: uploaded.shareUrl, }); @@ -349,7 +349,7 @@ async function sendTextWithMedia( messages: [{ text: text || undefined, mediaUrl }], retry: {}, onRetry: (event) => { - log.debug("retrying send", { conversationId, ...event }); + log.debug?.("retrying send", { conversationId, ...event }); }, tokenProvider, sharePointSiteId, @@ -392,7 +392,7 @@ export async function sendPollMSTeams( maxSelections, }); - log.debug("sending poll", { + log.debug?.("sending poll", { conversationId, pollId: pollCard.pollId, optionCount: pollCard.options.length, @@ -452,7 +452,7 @@ export async function sendAdaptiveCardMSTeams( to, }); - log.debug("sending adaptive card", { + log.debug?.("sending adaptive card", { conversationId, cardType: card.type, cardVersion: card.version, diff --git a/extensions/nextcloud-talk/src/inbound.ts b/extensions/nextcloud-talk/src/inbound.ts index 983ad3fb9b..e6e863a9fd 100644 --- a/extensions/nextcloud-talk/src/inbound.ts +++ b/extensions/nextcloud-talk/src/inbound.ts @@ -6,7 +6,7 @@ import { type RuntimeEnv, } from "openclaw/plugin-sdk"; import type { ResolvedNextcloudTalkAccount } from "./accounts.js"; -import type { CoreConfig, NextcloudTalkInboundMessage } from "./types.js"; +import type { CoreConfig, GroupPolicy, NextcloudTalkInboundMessage } from "./types.js"; import { normalizeNextcloudTalkAllowlist, resolveNextcloudTalkAllowlistMatch, @@ -84,8 +84,12 @@ export async function handleNextcloudTalkInbound(params: { statusSink?.({ lastInboundAt: message.timestamp }); const dmPolicy = account.config.dmPolicy ?? "pairing"; - const defaultGroupPolicy = config.channels?.defaults?.groupPolicy; - const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "allowlist"; + const defaultGroupPolicy = (config.channels as Record | undefined)?.defaults as + | { groupPolicy?: string } + | undefined; + const groupPolicy = (account.config.groupPolicy ?? + defaultGroupPolicy?.groupPolicy ?? + "allowlist") as GroupPolicy; const configAllowFrom = normalizeNextcloudTalkAllowlist(account.config.allowFrom); const configGroupAllowFrom = normalizeNextcloudTalkAllowlist(account.config.groupAllowFrom); @@ -118,7 +122,8 @@ export async function handleNextcloudTalkInbound(params: { cfg: config as OpenClawConfig, surface: CHANNEL_ID, }); - const useAccessGroups = config.commands?.useAccessGroups !== false; + const useAccessGroups = + (config.commands as Record | undefined)?.useAccessGroups !== false; const senderAllowedForCommands = resolveNextcloudTalkAllowlistMatch({ allowFrom: isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom, senderId, @@ -234,9 +239,12 @@ export async function handleNextcloudTalkInbound(params: { }); const fromLabel = isGroup ? `room:${roomName || roomToken}` : senderName || `user:${senderId}`; - const storePath = core.channel.session.resolveStorePath(config.session?.store, { - agentId: route.agentId, - }); + const storePath = core.channel.session.resolveStorePath( + (config.session as Record | undefined)?.store as string | undefined, + { + agentId: route.agentId, + }, + ); const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config as OpenClawConfig); const previousTimestamp = core.channel.session.readSessionUpdatedAt({ storePath, diff --git a/extensions/nextcloud-talk/src/onboarding.ts b/extensions/nextcloud-talk/src/onboarding.ts index ecfebaa7dd..c1f8d70ae3 100644 --- a/extensions/nextcloud-talk/src/onboarding.ts +++ b/extensions/nextcloud-talk/src/onboarding.ts @@ -6,6 +6,7 @@ import { normalizeAccountId, type ChannelOnboardingAdapter, type ChannelOnboardingDmPolicy, + type OpenClawConfig, type WizardPrompter, } from "openclaw/plugin-sdk"; import type { CoreConfig, DmPolicy } from "./types.js"; @@ -159,7 +160,11 @@ const dmPolicy: ChannelOnboardingDmPolicy = { allowFromKey: "channels.nextcloud-talk.allowFrom", getCurrent: (cfg) => cfg.channels?.["nextcloud-talk"]?.dmPolicy ?? "pairing", setPolicy: (cfg, policy) => setNextcloudTalkDmPolicy(cfg as CoreConfig, policy as DmPolicy), - promptAllowFrom: promptNextcloudTalkAllowFromForAccount, + promptAllowFrom: promptNextcloudTalkAllowFromForAccount as (params: { + cfg: OpenClawConfig; + prompter: WizardPrompter; + accountId?: string | undefined; + }) => Promise, }; export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = { @@ -196,7 +201,7 @@ export const nextcloudTalkOnboardingAdapter: ChannelOnboardingAdapter = { prompter, label: "Nextcloud Talk", currentId: accountId, - listAccountIds: listNextcloudTalkAccountIds, + listAccountIds: listNextcloudTalkAccountIds as (cfg: OpenClawConfig) => string[], defaultAccountId, }); } diff --git a/extensions/nextcloud-talk/src/types.ts b/extensions/nextcloud-talk/src/types.ts index 59ce8c0973..9d851b39bc 100644 --- a/extensions/nextcloud-talk/src/types.ts +++ b/extensions/nextcloud-talk/src/types.ts @@ -5,6 +5,8 @@ import type { GroupPolicy, } from "openclaw/plugin-sdk"; +export type { DmPolicy, GroupPolicy }; + export type NextcloudTalkRoomConfig = { requireMention?: boolean; /** Optional tool policy overrides for this room. */ diff --git a/extensions/nostr/src/channel.ts b/extensions/nostr/src/channel.ts index c8c71c99dd..8fa8d58b61 100644 --- a/extensions/nostr/src/channel.ts +++ b/extensions/nostr/src/channel.ts @@ -148,7 +148,11 @@ export const nostrPlugin: ChannelPlugin = { const message = core.channel.text.convertMarkdownTables(text ?? "", tableMode); const normalizedTo = normalizePubkey(to); await bus.sendDm(normalizedTo, message); - return { channel: "nostr", to: normalizedTo }; + return { + channel: "nostr" as const, + to: normalizedTo, + messageId: `nostr-${Date.now()}`, + }; }, }, @@ -224,10 +228,15 @@ export const nostrPlugin: ChannelPlugin = { privateKey: account.privateKey, relays: account.relays, onMessage: async (senderPubkey, text, reply) => { - ctx.log?.debug(`[${account.accountId}] DM from ${senderPubkey}: ${text.slice(0, 50)}...`); + ctx.log?.debug?.( + `[${account.accountId}] DM from ${senderPubkey}: ${text.slice(0, 50)}...`, + ); // Forward to OpenClaw's message pipeline - await runtime.channel.reply.handleInboundMessage({ + // TODO: Replace with proper dispatchReplyWithBufferedBlockDispatcher call + await ( + runtime.channel.reply as { handleInboundMessage?: (params: unknown) => Promise } + ).handleInboundMessage?.({ channel: "nostr", accountId: account.accountId, senderId: senderPubkey, @@ -240,31 +249,33 @@ export const nostrPlugin: ChannelPlugin = { }); }, onError: (error, context) => { - ctx.log?.error(`[${account.accountId}] Nostr error (${context}): ${error.message}`); + ctx.log?.error?.(`[${account.accountId}] Nostr error (${context}): ${error.message}`); }, onConnect: (relay) => { - ctx.log?.debug(`[${account.accountId}] Connected to relay: ${relay}`); + ctx.log?.debug?.(`[${account.accountId}] Connected to relay: ${relay}`); }, onDisconnect: (relay) => { - ctx.log?.debug(`[${account.accountId}] Disconnected from relay: ${relay}`); + ctx.log?.debug?.(`[${account.accountId}] Disconnected from relay: ${relay}`); }, onEose: (relays) => { - ctx.log?.debug(`[${account.accountId}] EOSE received from relays: ${relays}`); + ctx.log?.debug?.(`[${account.accountId}] EOSE received from relays: ${relays}`); }, onMetric: (event: MetricEvent) => { // Log significant metrics at appropriate levels if (event.name.startsWith("event.rejected.")) { - ctx.log?.debug(`[${account.accountId}] Metric: ${event.name}`, event.labels); + ctx.log?.debug?.( + `[${account.accountId}] Metric: ${event.name} ${JSON.stringify(event.labels)}`, + ); } else if (event.name === "relay.circuit_breaker.open") { - ctx.log?.warn( + ctx.log?.warn?.( `[${account.accountId}] Circuit breaker opened for relay: ${event.labels?.relay}`, ); } else if (event.name === "relay.circuit_breaker.close") { - ctx.log?.info( + ctx.log?.info?.( `[${account.accountId}] Circuit breaker closed for relay: ${event.labels?.relay}`, ); } else if (event.name === "relay.error") { - ctx.log?.debug(`[${account.accountId}] Relay error: ${event.labels?.relay}`); + ctx.log?.debug?.(`[${account.accountId}] Relay error: ${event.labels?.relay}`); } // Update cached metrics snapshot if (busHandle) { diff --git a/extensions/nostr/src/nostr-bus.ts b/extensions/nostr/src/nostr-bus.ts index bc19348fa8..0b015dad29 100644 --- a/extensions/nostr/src/nostr-bus.ts +++ b/extensions/nostr/src/nostr-bus.ts @@ -488,24 +488,28 @@ export async function startNostrBus(options: NostrBusOptions): Promise { - // EOSE handler - called when all stored events have been received - for (const relay of relays) { - metrics.emit("relay.message.eose", 1, { relay }); - } - onEose?.(relays.join(", ")); + const sub = pool.subscribeMany( + relays, + [{ kinds: [4], "#p": [pk], since }] as unknown as Parameters[1], + { + onevent: handleEvent, + oneose: () => { + // EOSE handler - called when all stored events have been received + for (const relay of relays) { + metrics.emit("relay.message.eose", 1, { relay }); + } + onEose?.(relays.join(", ")); + }, + onclose: (reason) => { + // Handle subscription close + for (const relay of relays) { + metrics.emit("relay.message.closed", 1, { relay }); + options.onDisconnect?.(relay); + } + onError?.(new Error(`Subscription closed: ${reason.join(", ")}`), "subscription"); + }, }, - onclose: (reason) => { - // Handle subscription close - for (const relay of relays) { - metrics.emit("relay.message.closed", 1, { relay }); - options.onDisconnect?.(relay); - } - onError?.(new Error(`Subscription closed: ${reason.join(", ")}`), "subscription"); - }, - }); + ); // Public sendDm function const sendDm = async (toPubkey: string, text: string): Promise => { @@ -693,7 +697,7 @@ export function normalizePubkey(input: string): string { throw new Error("Invalid npub key"); } // Convert Uint8Array to hex string - return Array.from(decoded.data) + return Array.from(decoded.data as unknown as Uint8Array) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } diff --git a/extensions/nostr/src/nostr-profile-import.ts b/extensions/nostr/src/nostr-profile-import.ts index e5a107c18c..a2ea80019d 100644 --- a/extensions/nostr/src/nostr-profile-import.ts +++ b/extensions/nostr/src/nostr-profile-import.ts @@ -130,7 +130,7 @@ export async function importProfileFromRelays( authors: [pubkey], limit: 1, }, - ], + ] as unknown as Parameters[1], { onevent(event) { events.push({ event, relay }); diff --git a/extensions/phone-control/index.ts b/extensions/phone-control/index.ts index 627a2317fa..d2c418efe3 100644 --- a/extensions/phone-control/index.ts +++ b/extensions/phone-control/index.ts @@ -92,7 +92,8 @@ function resolveStatePath(stateDir: string): string { async function readArmState(statePath: string): Promise { try { const raw = await fs.readFile(statePath, "utf8"); - const parsed = JSON.parse(raw) as Partial; + // Type as unknown record first to allow property access during validation + const parsed = JSON.parse(raw) as Record; if (parsed.version !== 1 && parsed.version !== 2) { return null; } @@ -106,11 +107,11 @@ async function readArmState(statePath: string): Promise { if (parsed.version === 1) { if ( !Array.isArray(parsed.removedFromDeny) || - !parsed.removedFromDeny.every((v) => typeof v === "string") + !parsed.removedFromDeny.every((v: unknown) => typeof v === "string") ) { return null; } - return parsed as ArmStateFile; + return parsed as unknown as ArmStateFile; } const group = typeof parsed.group === "string" ? parsed.group : ""; @@ -119,23 +120,23 @@ async function readArmState(statePath: string): Promise { } if ( !Array.isArray(parsed.armedCommands) || - !parsed.armedCommands.every((v) => typeof v === "string") + !parsed.armedCommands.every((v: unknown) => typeof v === "string") ) { return null; } if ( !Array.isArray(parsed.addedToAllow) || - !parsed.addedToAllow.every((v) => typeof v === "string") + !parsed.addedToAllow.every((v: unknown) => typeof v === "string") ) { return null; } if ( !Array.isArray(parsed.removedFromDeny) || - !parsed.removedFromDeny.every((v) => typeof v === "string") + !parsed.removedFromDeny.every((v: unknown) => typeof v === "string") ) { return null; } - return parsed as ArmStateFile; + return parsed as unknown as ArmStateFile; } catch { return null; } diff --git a/extensions/qwen-portal-auth/index.ts b/extensions/qwen-portal-auth/index.ts index 37994fa4bd..541dd750e1 100644 --- a/extensions/qwen-portal-auth/index.ts +++ b/extensions/qwen-portal-auth/index.ts @@ -1,4 +1,8 @@ -import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; +import { + emptyPluginConfigSchema, + type OpenClawPluginApi, + type ProviderAuthContext, +} from "openclaw/plugin-sdk"; import { loginQwenPortalOAuth } from "./oauth.js"; const PROVIDER_ID = "qwen-portal"; @@ -36,7 +40,7 @@ const qwenPortalPlugin = { name: "Qwen OAuth", description: "OAuth flow for Qwen (free-tier) models", configSchema: emptyPluginConfigSchema(), - register(api) { + register(api: OpenClawPluginApi) { api.registerProvider({ id: PROVIDER_ID, label: PROVIDER_LABEL, @@ -48,7 +52,7 @@ const qwenPortalPlugin = { label: "Qwen OAuth", hint: "Device code login", kind: "device_code", - run: async (ctx) => { + run: async (ctx: ProviderAuthContext) => { const progress = ctx.prompter.progress("Starting Qwen OAuth…"); try { const result = await loginQwenPortalOAuth({ diff --git a/extensions/signal/src/channel.ts b/extensions/signal/src/channel.ts index 3fba7bc6f2..1b270e8946 100644 --- a/extensions/signal/src/channel.ts +++ b/extensions/signal/src/channel.ts @@ -25,10 +25,16 @@ import { import { getSignalRuntime } from "./runtime.js"; const signalMessageActions: ChannelMessageActionAdapter = { - listActions: (ctx) => getSignalRuntime().channel.signal.messageActions.listActions(ctx), - supportsAction: (ctx) => getSignalRuntime().channel.signal.messageActions.supportsAction?.(ctx), - handleAction: async (ctx) => - await getSignalRuntime().channel.signal.messageActions.handleAction(ctx), + listActions: (ctx) => getSignalRuntime().channel.signal.messageActions?.listActions?.(ctx) ?? [], + supportsAction: (ctx) => + getSignalRuntime().channel.signal.messageActions?.supportsAction?.(ctx) ?? false, + handleAction: async (ctx) => { + const ma = getSignalRuntime().channel.signal.messageActions; + if (!ma?.handleAction) { + throw new Error("Signal message actions not available"); + } + return ma.handleAction(ctx); + }, }; const meta = getChatChannelMeta("signal"); diff --git a/extensions/telegram/index.ts b/extensions/telegram/index.ts index e96fe1585f..a2492fca87 100644 --- a/extensions/telegram/index.ts +++ b/extensions/telegram/index.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk"; import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; import { telegramPlugin } from "./src/channel.js"; import { setTelegramRuntime } from "./src/runtime.js"; @@ -10,7 +10,7 @@ const plugin = { configSchema: emptyPluginConfigSchema(), register(api: OpenClawPluginApi) { setTelegramRuntime(api.runtime); - api.registerChannel({ plugin: telegramPlugin }); + api.registerChannel({ plugin: telegramPlugin as ChannelPlugin }); }, }; diff --git a/extensions/telegram/src/channel.ts b/extensions/telegram/src/channel.ts index 8dbf4d0bd7..0b9800be65 100644 --- a/extensions/telegram/src/channel.ts +++ b/extensions/telegram/src/channel.ts @@ -32,11 +32,17 @@ import { getTelegramRuntime } from "./runtime.js"; const meta = getChatChannelMeta("telegram"); const telegramMessageActions: ChannelMessageActionAdapter = { - listActions: (ctx) => getTelegramRuntime().channel.telegram.messageActions.listActions(ctx), + listActions: (ctx) => + getTelegramRuntime().channel.telegram.messageActions?.listActions?.(ctx) ?? [], extractToolSend: (ctx) => - getTelegramRuntime().channel.telegram.messageActions.extractToolSend(ctx), - handleAction: async (ctx) => - await getTelegramRuntime().channel.telegram.messageActions.handleAction(ctx), + getTelegramRuntime().channel.telegram.messageActions?.extractToolSend?.(ctx) ?? null, + handleAction: async (ctx) => { + const ma = getTelegramRuntime().channel.telegram.messageActions; + if (!ma?.handleAction) { + throw new Error("Telegram message actions not available"); + } + return ma.handleAction(ctx); + }, }; function parseReplyToMessageId(replyToId?: string | null) { diff --git a/extensions/tlon/src/channel.ts b/extensions/tlon/src/channel.ts index c3e25f49e6..f00b0d74bf 100644 --- a/extensions/tlon/src/channel.ts +++ b/extensions/tlon/src/channel.ts @@ -1,4 +1,5 @@ import type { + ChannelAccountSnapshot, ChannelOutboundAdapter, ChannelPlugin, ChannelSetupInput, @@ -154,7 +155,7 @@ const tlonOutbound: ChannelOutboundAdapter = { }, sendMedia: async ({ cfg, to, text, mediaUrl, accountId, replyToId, threadId }) => { const mergedText = buildMediaText(text, mediaUrl); - return await tlonOutbound.sendText({ + return await tlonOutbound.sendText!({ cfg, to, text: mergedText, @@ -224,9 +225,11 @@ export const tlonPlugin: ChannelPlugin = { deleteAccount: ({ cfg, accountId }) => { const useDefault = !accountId || accountId === "default"; if (useDefault) { - // @ts-expect-error // oxlint-disable-next-line no-unused-vars - const { ship, code, url, name, ...rest } = cfg.channels?.tlon ?? {}; + const { ship, code, url, name, ...rest } = (cfg.channels?.tlon ?? {}) as Record< + string, + unknown + >; return { ...cfg, channels: { @@ -235,9 +238,9 @@ export const tlonPlugin: ChannelPlugin = { }, } as OpenClawConfig; } - // @ts-expect-error // oxlint-disable-next-line no-unused-vars - const { [accountId]: removed, ...remainingAccounts } = cfg.channels?.tlon?.accounts ?? {}; + const { [accountId]: removed, ...remainingAccounts } = (cfg.channels?.tlon?.accounts ?? + {}) as Record; return { ...cfg, channels: { @@ -334,8 +337,8 @@ export const tlonPlugin: ChannelPlugin = { }, buildChannelSummary: ({ snapshot }) => ({ configured: snapshot.configured ?? false, - ship: snapshot.ship ?? null, - url: snapshot.url ?? null, + ship: (snapshot as { ship?: string | null }).ship ?? null, + url: (snapshot as { url?: string | null }).url ?? null, }), probeAccount: async ({ account }) => { if (!account.configured || !account.ship || !account.url || !account.code) { @@ -356,7 +359,7 @@ export const tlonPlugin: ChannelPlugin = { await api.delete(); } } catch (error) { - return { ok: false, error: error?.message ?? String(error) }; + return { ok: false, error: (error as { message?: string })?.message ?? String(error) }; } }, buildAccountSnapshot: ({ account, runtime, probe }) => ({ @@ -380,7 +383,7 @@ export const tlonPlugin: ChannelPlugin = { accountId: account.accountId, ship: account.ship, url: account.url, - }); + } as ChannelAccountSnapshot); ctx.log?.info(`[${account.accountId}] starting Tlon provider for ${account.ship ?? "tlon"}`); return monitorTlonProvider({ runtime: ctx.runtime, diff --git a/extensions/tlon/src/monitor/discovery.ts b/extensions/tlon/src/monitor/discovery.ts index 93c54a7ba1..cc7f5d6b21 100644 --- a/extensions/tlon/src/monitor/discovery.ts +++ b/extensions/tlon/src/monitor/discovery.ts @@ -17,7 +17,7 @@ export async function fetchGroupChanges( return null; } catch (error) { runtime.log?.( - `[tlon] Failed to fetch changes (falling back to full init): ${error?.message ?? String(error)}`, + `[tlon] Failed to fetch changes (falling back to full init): ${(error as { message?: string })?.message ?? String(error)}`, ); return null; } @@ -66,7 +66,9 @@ export async function fetchAllChannels( return channels; } catch (error) { - runtime.log?.(`[tlon] Auto-discovery failed: ${error?.message ?? String(error)}`); + runtime.log?.( + `[tlon] Auto-discovery failed: ${(error as { message?: string })?.message ?? String(error)}`, + ); runtime.log?.( "[tlon] To monitor group channels, add them to config: channels.tlon.groupChannels", ); diff --git a/extensions/tlon/src/monitor/history.ts b/extensions/tlon/src/monitor/history.ts index 8f20c96b6d..03360a12a6 100644 --- a/extensions/tlon/src/monitor/history.ts +++ b/extensions/tlon/src/monitor/history.ts @@ -68,7 +68,9 @@ export async function fetchChannelHistory( runtime?.log?.(`[tlon] Extracted ${messages.length} messages from history`); return messages; } catch (error) { - runtime?.log?.(`[tlon] Error fetching channel history: ${error?.message ?? String(error)}`); + runtime?.log?.( + `[tlon] Error fetching channel history: ${(error as { message?: string })?.message ?? String(error)}`, + ); return []; } } diff --git a/extensions/tlon/src/monitor/index.ts b/extensions/tlon/src/monitor/index.ts index f4e13ad7ac..9d28fd0ef3 100644 --- a/extensions/tlon/src/monitor/index.ts +++ b/extensions/tlon/src/monitor/index.ts @@ -18,6 +18,11 @@ import { isSummarizationRequest, } from "./utils.js"; +function formatError(err: unknown): string { + if (err instanceof Error) return err.message; + return String(err); +} + export type MonitorTlonOpts = { runtime?: RuntimeEnv; abortSignal?: AbortSignal; @@ -35,6 +40,11 @@ type UrbitMemo = { sent?: number; }; +type UrbitSeal = { + "parent-id"?: string; + parent?: string; +}; + type UrbitUpdate = { id?: string | number; response?: { @@ -42,10 +52,10 @@ type UrbitUpdate = { post?: { id?: string | number; "r-post"?: { - set?: { essay?: UrbitMemo }; + set?: { essay?: UrbitMemo; seal?: UrbitSeal }; reply?: { id?: string | number; - "r-reply"?: { set?: { memo?: UrbitMemo } }; + "r-reply"?: { set?: { memo?: UrbitMemo; seal?: UrbitSeal } }; }; }; }; @@ -113,7 +123,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { + handleIncomingGroupMessage(channelNest)(data as UrbitUpdate); + }, err: (error) => { runtime.error?.(`[tlon] Group subscription error for ${channelNest}: ${String(error)}`); }, @@ -467,9 +487,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { + handleIncomingDM(data as UrbitUpdate); + }, err: (error) => { runtime.error?.(`[tlon] DM subscription error for ${dmShip}: ${String(error)}`); }, @@ -493,9 +513,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { if (!opts.abortSignal?.aborted) { refreshChannelSubscriptions().catch((error) => { - runtime.error?.(`[tlon] Channel refresh error: ${error?.message ?? String(error)}`); + runtime.error?.(`[tlon] Channel refresh error: ${formatError(error)}`); }); } }, @@ -557,8 +575,9 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise { - opts.abortSignal.addEventListener( + signal.addEventListener( "abort", () => { clearInterval(pollInterval); @@ -574,7 +593,7 @@ export async function monitorTlonProvider(opts: MonitorTlonOpts = {}): Promise } | null> => { + handleAction: async (ctx: ChannelMessageActionContext) => { if (ctx.action !== "send") { - return null; + return { + content: [{ type: "text" as const, text: "Unsupported action" }], + details: { ok: false, error: "Unsupported action" }, + }; } const message = readStringParam(ctx.params, "message", { required: true }); @@ -159,7 +160,7 @@ export const twitchMessageActions: ChannelMessageActionAdapter = { return { content: [ { - type: "text", + type: "text" as const, text: JSON.stringify(result), }, ], diff --git a/extensions/twitch/src/outbound.ts b/extensions/twitch/src/outbound.ts index 50afe682c0..8a1c75f5dd 100644 --- a/extensions/twitch/src/outbound.ts +++ b/extensions/twitch/src/outbound.ts @@ -104,7 +104,8 @@ export const twitchOutbound: ChannelOutboundAdapter = { * }); */ sendText: async (params: ChannelOutboundContext): Promise => { - const { cfg, to, text, accountId, signal } = params; + const { cfg, to, text, accountId } = params; + const signal = (params as { signal?: AbortSignal }).signal; if (signal?.aborted) { throw new Error("Outbound delivery aborted"); @@ -142,7 +143,6 @@ export const twitchOutbound: ChannelOutboundAdapter = { channel: "twitch", messageId: result.messageId, timestamp: Date.now(), - to: normalizeTwitchChannel(channel), }; }, @@ -165,7 +165,8 @@ export const twitchOutbound: ChannelOutboundAdapter = { * }); */ sendMedia: async (params: ChannelOutboundContext): Promise => { - const { text, mediaUrl, signal } = params; + const { text, mediaUrl } = params; + const signal = (params as { signal?: AbortSignal }).signal; if (signal?.aborted) { throw new Error("Outbound delivery aborted"); diff --git a/extensions/twitch/src/probe.ts b/extensions/twitch/src/probe.ts index 6e84d49337..56ea99146d 100644 --- a/extensions/twitch/src/probe.ts +++ b/extensions/twitch/src/probe.ts @@ -27,16 +27,16 @@ export async function probeTwitch( ): Promise { const started = Date.now(); - if (!account.token || !account.username) { + if (!account.accessToken || !account.username) { return { ok: false, - error: "missing credentials (token, username)", + error: "missing credentials (accessToken, username)", username: account.username, elapsedMs: Date.now() - started, }; } - const rawToken = normalizeToken(account.token.trim()); + const rawToken = normalizeToken(account.accessToken.trim()); let client: ChatClient | undefined; diff --git a/extensions/twitch/src/resolver.ts b/extensions/twitch/src/resolver.ts index acc578f4b7..b59bc8c9e4 100644 --- a/extensions/twitch/src/resolver.ts +++ b/extensions/twitch/src/resolver.ts @@ -51,8 +51,8 @@ export async function resolveTwitchTargets( ): Promise { const log = createLogger(logger); - if (!account.clientId || !account.token) { - log.error("Missing Twitch client ID or token"); + if (!account.clientId || !account.accessToken) { + log.error("Missing Twitch client ID or accessToken"); return inputs.map((input) => ({ input, resolved: false, @@ -60,7 +60,7 @@ export async function resolveTwitchTargets( })); } - const normalizedToken = normalizeToken(account.token); + const normalizedToken = normalizeToken(account.accessToken); const authProvider = new StaticAuthProvider(account.clientId, normalizedToken); const apiClient = new ApiClient({ authProvider }); diff --git a/extensions/twitch/src/status.ts b/extensions/twitch/src/status.ts index fdc560950d..2cb9ae0dbc 100644 --- a/extensions/twitch/src/status.ts +++ b/extensions/twitch/src/status.ts @@ -4,7 +4,8 @@ * Detects and reports configuration issues for Twitch accounts. */ -import type { ChannelAccountSnapshot, ChannelStatusIssue } from "./types.js"; +import type { ChannelStatusIssue } from "openclaw/plugin-sdk"; +import type { ChannelAccountSnapshot } from "./types.js"; import { getAccountConfig } from "./config.js"; import { resolveTwitchToken } from "./token.js"; import { isAccountConfigured } from "./utils/twitch.js"; diff --git a/extensions/voice-call/index.ts b/extensions/voice-call/index.ts index e21ca6f873..7eb8daa8ff 100644 --- a/extensions/voice-call/index.ts +++ b/extensions/voice-call/index.ts @@ -1,3 +1,4 @@ +import type { GatewayRequestHandlerOptions, OpenClawPluginApi } from "openclaw/plugin-sdk"; import { Type } from "@sinclair/typebox"; import type { CoreConfig } from "./src/core-bridge.js"; import { registerVoiceCallCli } from "./src/cli.js"; @@ -144,7 +145,7 @@ const voiceCallPlugin = { name: "Voice Call", description: "Voice-call plugin with Telnyx/Twilio/Plivo providers", configSchema: voiceCallConfigSchema, - register(api) { + register(api: OpenClawPluginApi) { const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig)); const validation = validateProviderConfig(config); @@ -188,142 +189,160 @@ const voiceCallPlugin = { respond(false, { error: err instanceof Error ? err.message : String(err) }); }; - api.registerGatewayMethod("voicecall.initiate", async ({ params, respond }) => { - try { - const message = typeof params?.message === "string" ? params.message.trim() : ""; - if (!message) { - respond(false, { error: "message required" }); - return; + api.registerGatewayMethod( + "voicecall.initiate", + async ({ params, respond }: GatewayRequestHandlerOptions) => { + try { + const message = typeof params?.message === "string" ? params.message.trim() : ""; + if (!message) { + respond(false, { error: "message required" }); + return; + } + const rt = await ensureRuntime(); + const to = + typeof params?.to === "string" && params.to.trim() + ? params.to.trim() + : rt.config.toNumber; + if (!to) { + respond(false, { error: "to required" }); + return; + } + const mode = + params?.mode === "notify" || params?.mode === "conversation" ? params.mode : undefined; + const result = await rt.manager.initiateCall(to, undefined, { + message, + mode, + }); + if (!result.success) { + respond(false, { error: result.error || "initiate failed" }); + return; + } + respond(true, { callId: result.callId, initiated: true }); + } catch (err) { + sendError(respond, err); } - const rt = await ensureRuntime(); - const to = - typeof params?.to === "string" && params.to.trim() - ? params.to.trim() - : rt.config.toNumber; - if (!to) { - respond(false, { error: "to required" }); - return; - } - const mode = - params?.mode === "notify" || params?.mode === "conversation" ? params.mode : undefined; - const result = await rt.manager.initiateCall(to, undefined, { - message, - mode, - }); - if (!result.success) { - respond(false, { error: result.error || "initiate failed" }); - return; - } - respond(true, { callId: result.callId, initiated: true }); - } catch (err) { - sendError(respond, err); - } - }); + }, + ); - api.registerGatewayMethod("voicecall.continue", async ({ params, respond }) => { - try { - const callId = typeof params?.callId === "string" ? params.callId.trim() : ""; - const message = typeof params?.message === "string" ? params.message.trim() : ""; - if (!callId || !message) { - respond(false, { error: "callId and message required" }); - return; + api.registerGatewayMethod( + "voicecall.continue", + async ({ params, respond }: GatewayRequestHandlerOptions) => { + try { + const callId = typeof params?.callId === "string" ? params.callId.trim() : ""; + const message = typeof params?.message === "string" ? params.message.trim() : ""; + if (!callId || !message) { + respond(false, { error: "callId and message required" }); + return; + } + const rt = await ensureRuntime(); + const result = await rt.manager.continueCall(callId, message); + if (!result.success) { + respond(false, { error: result.error || "continue failed" }); + return; + } + respond(true, { success: true, transcript: result.transcript }); + } catch (err) { + sendError(respond, err); } - const rt = await ensureRuntime(); - const result = await rt.manager.continueCall(callId, message); - if (!result.success) { - respond(false, { error: result.error || "continue failed" }); - return; - } - respond(true, { success: true, transcript: result.transcript }); - } catch (err) { - sendError(respond, err); - } - }); + }, + ); - api.registerGatewayMethod("voicecall.speak", async ({ params, respond }) => { - try { - const callId = typeof params?.callId === "string" ? params.callId.trim() : ""; - const message = typeof params?.message === "string" ? params.message.trim() : ""; - if (!callId || !message) { - respond(false, { error: "callId and message required" }); - return; + api.registerGatewayMethod( + "voicecall.speak", + async ({ params, respond }: GatewayRequestHandlerOptions) => { + try { + const callId = typeof params?.callId === "string" ? params.callId.trim() : ""; + const message = typeof params?.message === "string" ? params.message.trim() : ""; + if (!callId || !message) { + respond(false, { error: "callId and message required" }); + return; + } + const rt = await ensureRuntime(); + const result = await rt.manager.speak(callId, message); + if (!result.success) { + respond(false, { error: result.error || "speak failed" }); + return; + } + respond(true, { success: true }); + } catch (err) { + sendError(respond, err); } - const rt = await ensureRuntime(); - const result = await rt.manager.speak(callId, message); - if (!result.success) { - respond(false, { error: result.error || "speak failed" }); - return; - } - respond(true, { success: true }); - } catch (err) { - sendError(respond, err); - } - }); + }, + ); - api.registerGatewayMethod("voicecall.end", async ({ params, respond }) => { - try { - const callId = typeof params?.callId === "string" ? params.callId.trim() : ""; - if (!callId) { - respond(false, { error: "callId required" }); - return; + api.registerGatewayMethod( + "voicecall.end", + async ({ params, respond }: GatewayRequestHandlerOptions) => { + try { + const callId = typeof params?.callId === "string" ? params.callId.trim() : ""; + if (!callId) { + respond(false, { error: "callId required" }); + return; + } + const rt = await ensureRuntime(); + const result = await rt.manager.endCall(callId); + if (!result.success) { + respond(false, { error: result.error || "end failed" }); + return; + } + respond(true, { success: true }); + } catch (err) { + sendError(respond, err); } - const rt = await ensureRuntime(); - const result = await rt.manager.endCall(callId); - if (!result.success) { - respond(false, { error: result.error || "end failed" }); - return; - } - respond(true, { success: true }); - } catch (err) { - sendError(respond, err); - } - }); + }, + ); - api.registerGatewayMethod("voicecall.status", async ({ params, respond }) => { - try { - const raw = - typeof params?.callId === "string" - ? params.callId.trim() - : typeof params?.sid === "string" - ? params.sid.trim() - : ""; - if (!raw) { - respond(false, { error: "callId required" }); - return; + api.registerGatewayMethod( + "voicecall.status", + async ({ params, respond }: GatewayRequestHandlerOptions) => { + try { + const raw = + typeof params?.callId === "string" + ? params.callId.trim() + : typeof params?.sid === "string" + ? params.sid.trim() + : ""; + if (!raw) { + respond(false, { error: "callId required" }); + return; + } + const rt = await ensureRuntime(); + const call = rt.manager.getCall(raw) || rt.manager.getCallByProviderCallId(raw); + if (!call) { + respond(true, { found: false }); + return; + } + respond(true, { found: true, call }); + } catch (err) { + sendError(respond, err); } - const rt = await ensureRuntime(); - const call = rt.manager.getCall(raw) || rt.manager.getCallByProviderCallId(raw); - if (!call) { - respond(true, { found: false }); - return; - } - respond(true, { found: true, call }); - } catch (err) { - sendError(respond, err); - } - }); + }, + ); - api.registerGatewayMethod("voicecall.start", async ({ params, respond }) => { - try { - const to = typeof params?.to === "string" ? params.to.trim() : ""; - const message = typeof params?.message === "string" ? params.message.trim() : ""; - if (!to) { - respond(false, { error: "to required" }); - return; + api.registerGatewayMethod( + "voicecall.start", + async ({ params, respond }: GatewayRequestHandlerOptions) => { + try { + const to = typeof params?.to === "string" ? params.to.trim() : ""; + const message = typeof params?.message === "string" ? params.message.trim() : ""; + if (!to) { + respond(false, { error: "to required" }); + return; + } + const rt = await ensureRuntime(); + const result = await rt.manager.initiateCall(to, undefined, { + message: message || undefined, + }); + if (!result.success) { + respond(false, { error: result.error || "initiate failed" }); + return; + } + respond(true, { callId: result.callId, initiated: true }); + } catch (err) { + sendError(respond, err); } - const rt = await ensureRuntime(); - const result = await rt.manager.initiateCall(to, undefined, { - message: message || undefined, - }); - if (!result.success) { - respond(false, { error: result.error || "initiate failed" }); - return; - } - respond(true, { callId: result.callId, initiated: true }); - } catch (err) { - sendError(respond, err); - } - }); + }, + ); api.registerTool({ name: "voice_call", @@ -332,7 +351,7 @@ const voiceCallPlugin = { parameters: VoiceCallToolSchema, async execute(_toolCallId, params) { const json = (payload: unknown) => ({ - content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], + content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }], details: payload, }); diff --git a/extensions/voice-call/src/response-generator.ts b/extensions/voice-call/src/response-generator.ts index a13ebc3723..abb02cb7b1 100644 --- a/extensions/voice-call/src/response-generator.ts +++ b/extensions/voice-call/src/response-generator.ts @@ -146,7 +146,7 @@ export async function generateVoiceResponse( const text = texts.join(" ") || null; - if (!text && result.meta.aborted) { + if (!text && result.meta?.aborted) { return { text: null, error: "Response generation was aborted" }; } diff --git a/extensions/voice-call/src/runtime.ts b/extensions/voice-call/src/runtime.ts index 6d37d8ac25..bf25a4c277 100644 --- a/extensions/voice-call/src/runtime.ts +++ b/extensions/voice-call/src/runtime.ts @@ -30,7 +30,7 @@ type Logger = { info: (message: string) => void; warn: (message: string) => void; error: (message: string) => void; - debug: (message: string) => void; + debug?: (message: string) => void; }; function isLoopbackBind(bind: string | undefined): boolean { diff --git a/extensions/zalo/src/accounts.ts b/extensions/zalo/src/accounts.ts index 01e6fa7474..32039e0e51 100644 --- a/extensions/zalo/src/accounts.ts +++ b/extensions/zalo/src/accounts.ts @@ -3,6 +3,8 @@ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk"; import type { ResolvedZaloAccount, ZaloAccountConfig, ZaloConfig } from "./types.js"; import { resolveZaloToken } from "./token.js"; +export type { ResolvedZaloAccount }; + function listConfiguredAccountIds(cfg: OpenClawConfig): string[] { const accounts = (cfg.channels?.zalo as ZaloConfig | undefined)?.accounts; if (!accounts || typeof accounts !== "object") { diff --git a/extensions/zalo/src/proxy.ts b/extensions/zalo/src/proxy.ts index 4c59f16aa1..be348e65f1 100644 --- a/extensions/zalo/src/proxy.ts +++ b/extensions/zalo/src/proxy.ts @@ -1,4 +1,4 @@ -import type { Dispatcher } from "undici"; +import type { Dispatcher, RequestInit as UndiciRequestInit } from "undici"; import { ProxyAgent, fetch as undiciFetch } from "undici"; import type { ZaloFetch } from "./api.js"; @@ -15,7 +15,10 @@ export function resolveZaloProxyFetch(proxyUrl?: string | null): ZaloFetch | und } const agent = new ProxyAgent(trimmed); const fetcher: ZaloFetch = (input, init) => - undiciFetch(input, { ...init, dispatcher: agent as Dispatcher }); + undiciFetch(input, { + ...init, + dispatcher: agent, + } as UndiciRequestInit) as unknown as Promise; proxyCache.set(trimmed, fetcher); return fetcher; } diff --git a/extensions/zalouser/index.ts b/extensions/zalouser/index.ts index fd27aba276..fa80152db3 100644 --- a/extensions/zalouser/index.ts +++ b/extensions/zalouser/index.ts @@ -1,4 +1,4 @@ -import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import type { AnyAgentTool, OpenClawPluginApi } from "openclaw/plugin-sdk"; import { emptyPluginConfigSchema } from "openclaw/plugin-sdk"; import { zalouserDock, zalouserPlugin } from "./src/channel.js"; import { setZalouserRuntime } from "./src/runtime.js"; @@ -24,7 +24,7 @@ const plugin = { "friends (list/search friends), groups (list groups), me (profile info), status (auth check).", parameters: ZalouserToolSchema, execute: executeZalouserTool, - }); + } as AnyAgentTool); }, }; diff --git a/extensions/zalouser/src/channel.ts b/extensions/zalouser/src/channel.ts index e0fd6f8d5f..41cec8c561 100644 --- a/extensions/zalouser/src/channel.ts +++ b/extensions/zalouser/src/channel.ts @@ -625,7 +625,7 @@ export const zalouserPlugin: ChannelPlugin = { } ctx.setStatus({ accountId: account.accountId, - user: userInfo, + profile: userInfo, }); } catch { // ignore probe errors diff --git a/extensions/zalouser/src/tool.ts b/extensions/zalouser/src/tool.ts index 2f4d7be4cb..20d7d1bd6e 100644 --- a/extensions/zalouser/src/tool.ts +++ b/extensions/zalouser/src/tool.ts @@ -3,6 +3,11 @@ import { runZca, parseJsonOutput } from "./zca.js"; const ACTIONS = ["send", "image", "link", "friends", "groups", "me", "status"] as const; +type AgentToolResult = { + content: Array<{ type: string; text: string }>; + details?: unknown; +}; + function stringEnum( values: T, options: { description?: string } = {}, @@ -38,12 +43,7 @@ type ToolParams = { url?: string; }; -type ToolResult = { - content: Array<{ type: string; text: string }>; - details: unknown; -}; - -function json(payload: unknown): ToolResult { +function json(payload: unknown): AgentToolResult { return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], details: payload, @@ -53,7 +53,9 @@ function json(payload: unknown): ToolResult { export async function executeZalouserTool( _toolCallId: string, params: ToolParams, -): Promise { + _signal?: AbortSignal, + _onUpdate?: unknown, +): Promise { try { switch (params.action) { case "send": { diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index b46150cbf0..bd82e98453 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -125,6 +125,7 @@ export type ChannelAccountSnapshot = { botTokenSource?: string; appTokenSource?: string; credentialSource?: string; + secretSource?: string; audienceType?: string; audience?: string; webhookPath?: string; @@ -139,6 +140,10 @@ export type ChannelAccountSnapshot = { audit?: unknown; application?: unknown; bot?: unknown; + publicKey?: string | null; + profile?: unknown; + channelAccessToken?: string; + channelSecret?: string; }; export type ChannelLogSink = { @@ -328,4 +333,5 @@ export type ChannelPollContext = { to: string; poll: PollInput; accountId?: string | null; + threadId?: string | null; }; diff --git a/src/config/types.channels.ts b/src/config/types.channels.ts index b6319f3a53..ce75029778 100644 --- a/src/config/types.channels.ts +++ b/src/config/types.channels.ts @@ -23,6 +23,19 @@ export type ChannelDefaultsConfig = { heartbeat?: ChannelHeartbeatVisibilityConfig; }; +/** + * Base type for extension channel config sections. + * Extensions can use this as a starting point for their channel config. + */ +export type ExtensionChannelConfig = { + enabled?: boolean; + allowFrom?: string | string[]; + dmPolicy?: string; + groupPolicy?: GroupPolicy; + accounts?: Record; + [key: string]: unknown; +}; + export type ChannelsConfig = { defaults?: ChannelDefaultsConfig; whatsapp?: WhatsAppConfig; @@ -33,5 +46,7 @@ export type ChannelsConfig = { signal?: SignalConfig; imessage?: IMessageConfig; msteams?: MSTeamsConfig; - [key: string]: unknown; + // Extension channels use dynamic keys - use ExtensionChannelConfig in extensions + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; }; diff --git a/src/plugin-sdk/index.ts b/src/plugin-sdk/index.ts index 7fd2a04b4d..99dd63ba3b 100644 --- a/src/plugin-sdk/index.ts +++ b/src/plugin-sdk/index.ts @@ -59,20 +59,25 @@ export type { } from "../channels/plugins/types.js"; export type { ChannelConfigSchema, ChannelPlugin } from "../channels/plugins/types.plugin.js"; export type { + AnyAgentTool, OpenClawPluginApi, OpenClawPluginService, OpenClawPluginServiceContext, + ProviderAuthContext, + ProviderAuthResult, } from "../plugins/types.js"; export type { GatewayRequestHandler, GatewayRequestHandlerOptions, RespondFn, } from "../gateway/server-methods/types.js"; -export type { PluginRuntime } from "../plugins/runtime/types.js"; +export type { PluginRuntime, RuntimeLogger } from "../plugins/runtime/types.js"; export { normalizePluginHttpPath } from "../plugins/http-path.js"; export { registerPluginHttpRoute } from "../plugins/http-registry.js"; export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; export type { OpenClawConfig } from "../config/config.js"; +/** @deprecated Use OpenClawConfig instead */ +export type { OpenClawConfig as ClawdbotConfig } from "../config/config.js"; export type { ChannelDock } from "../channels/dock.js"; export { getChatChannelMeta } from "../channels/registry.js"; export type { @@ -130,6 +135,7 @@ export { listDevicePairing, rejectDevicePairing, } from "../infra/device-pairing.js"; +export { formatErrorMessage } from "../infra/errors.js"; export { resolveToolsBySender } from "../config/group-policy.js"; export { buildPendingHistoryContextFromMap, diff --git a/src/plugins/runtime/types.ts b/src/plugins/runtime/types.ts index b7aecaf1a3..3f6af3b318 100644 --- a/src/plugins/runtime/types.ts +++ b/src/plugins/runtime/types.ts @@ -169,10 +169,10 @@ type BuildTemplateMessageFromPayload = type MonitorLineProvider = typeof import("../../line/monitor.js").monitorLineProvider; export type RuntimeLogger = { - debug?: (message: string) => void; - info: (message: string) => void; - warn: (message: string) => void; - error: (message: string) => void; + debug?: (message: string, meta?: Record) => void; + info: (message: string, meta?: Record) => void; + warn: (message: string, meta?: Record) => void; + error: (message: string, meta?: Record) => void; }; export type PluginRuntime = { diff --git a/src/plugins/types.ts b/src/plugins/types.ts index 6ddcb9eef9..27c6fff242 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -17,6 +17,7 @@ import type { WizardPrompter } from "../wizard/prompts.js"; import type { PluginRuntime } from "./runtime/types.js"; export type { PluginRuntime } from "./runtime/types.js"; +export type { AnyAgentTool } from "../agents/tools/common.js"; export type PluginLogger = { debug?: (message: string) => void; diff --git a/test/setup.ts b/test/setup.ts index 725554b7f3..53e7fe8d15 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -13,7 +13,7 @@ import type { OutboundSendDeps } from "../src/infra/outbound/deliver.js"; import { installProcessWarningFilter } from "../src/infra/warning-filter.js"; import { setActivePluginRegistry } from "../src/plugins/runtime.js"; import { createTestRegistry } from "../src/test-utils/channel-plugins.js"; -import { withIsolatedTestHome } from "./test-env"; +import { withIsolatedTestHome } from "./test-env.js"; installProcessWarningFilter(); @@ -46,7 +46,8 @@ const createStubOutbound = ( sendText: async ({ deps, to, text }) => { const send = pickSendFn(id, deps); if (send) { - const result = await send(to, text, {}); + // oxlint-disable-next-line typescript/no-explicit-any + const result = await send(to, text, { verbose: false } as any); return { channel: id, ...result }; } return { channel: id, messageId: "test" }; @@ -54,7 +55,8 @@ const createStubOutbound = ( sendMedia: async ({ deps, to, text, mediaUrl }) => { const send = pickSendFn(id, deps); if (send) { - const result = await send(to, text, { mediaUrl }); + // oxlint-disable-next-line typescript/no-explicit-any + const result = await send(to, text, { verbose: false, mediaUrl } as any); return { channel: id, ...result }; } return { channel: id, messageId: "test" }; @@ -90,14 +92,14 @@ const createStubPlugin = (params: { const ids = accounts ? Object.keys(accounts).filter(Boolean) : []; return ids.length > 0 ? ids : ["default"]; }, - resolveAccount: (cfg: OpenClawConfig, accountId: string) => { + resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => { const channels = cfg.channels as Record | undefined; const entry = channels?.[params.id]; if (!entry || typeof entry !== "object") { return {}; } const accounts = (entry as { accounts?: Record }).accounts; - const match = accounts?.[accountId]; + const match = accountId ? accounts?.[accountId] : undefined; return (match && typeof match === "object") || typeof match === "string" ? match : entry; }, isConfigured: async (_account, cfg: OpenClawConfig) => { diff --git a/tsconfig.json b/tsconfig.json index 060982ee20..31e28edad2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,8 +16,12 @@ "skipLibCheck": true, "strict": true, "target": "es2023", - "useDefineForClassFields": false + "useDefineForClassFields": false, + "paths": { + "*": ["./*"], + "openclaw/plugin-sdk": ["./src/plugin-sdk/index.ts"] + } }, - "include": ["src/**/*", "ui/**/*"], - "exclude": ["node_modules", "dist", "src/**/*.test.ts"] + "include": ["src/**/*", "ui/**/*", "extensions/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "extensions/**/*.test.ts"] } diff --git a/ui/src/ui/views/usage.ts b/ui/src/ui/views/usage.ts index f241a27dc4..b6a0ec60f2 100644 --- a/ui/src/ui/views/usage.ts +++ b/ui/src/ui/views/usage.ts @@ -1,2205 +1,20 @@ import { html, svg, nothing } from "lit"; import { formatDurationCompact } from "../../../../src/infra/format-time/format-duration.ts"; import { extractQueryTerms, filterSessionsByQuery, parseToolSummary } from "../usage-helpers.ts"; +import { usageStylesString } from "./usageStyles.ts"; +import { + UsageSessionEntry, + UsageTotals, + UsageAggregates, + CostDailyEntry, + UsageColumnId, + TimeSeriesPoint, + SessionLogEntry, + SessionLogRole, + UsageProps, +} from "./usageTypes.ts"; -// Inline styles for usage view (app uses light DOM, so static styles don't work) -const usageStylesString = ` - .usage-page-header { - margin: 4px 0 12px; - } - .usage-page-title { - font-size: 28px; - font-weight: 700; - letter-spacing: -0.02em; - margin-bottom: 4px; - } - .usage-page-subtitle { - font-size: 13px; - color: var(--text-muted); - margin: 0 0 12px; - } - /* ===== FILTERS & HEADER ===== */ - .usage-filters-inline { - display: flex; - gap: 8px; - align-items: center; - flex-wrap: wrap; - } - .usage-filters-inline select { - padding: 6px 10px; - border: 1px solid var(--border); - border-radius: 6px; - background: var(--bg); - color: var(--text); - font-size: 13px; - } - .usage-filters-inline input[type="date"] { - padding: 6px 10px; - border: 1px solid var(--border); - border-radius: 6px; - background: var(--bg); - color: var(--text); - font-size: 13px; - } - .usage-filters-inline input[type="text"] { - padding: 6px 10px; - border: 1px solid var(--border); - border-radius: 6px; - background: var(--bg); - color: var(--text); - font-size: 13px; - min-width: 180px; - } - .usage-filters-inline .btn-sm { - padding: 6px 12px; - font-size: 14px; - } - .usage-refresh-indicator { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 10px; - background: rgba(255, 77, 77, 0.1); - border-radius: 4px; - font-size: 12px; - color: #ff4d4d; - } - .usage-refresh-indicator::before { - content: ""; - width: 10px; - height: 10px; - border: 2px solid #ff4d4d; - border-top-color: transparent; - border-radius: 50%; - animation: usage-spin 0.6s linear infinite; - } - @keyframes usage-spin { - to { transform: rotate(360deg); } - } - .active-filters { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - } - .filter-chip { - display: flex; - align-items: center; - gap: 6px; - padding: 4px 8px 4px 12px; - background: var(--accent-subtle); - border: 1px solid var(--accent); - border-radius: 16px; - font-size: 12px; - } - .filter-chip-label { - color: var(--accent); - font-weight: 500; - } - .filter-chip-remove { - background: none; - border: none; - color: var(--accent); - cursor: pointer; - padding: 2px 4px; - font-size: 14px; - line-height: 1; - opacity: 0.7; - transition: opacity 0.15s; - } - .filter-chip-remove:hover { - opacity: 1; - } - .filter-clear-btn { - padding: 4px 10px !important; - font-size: 12px !important; - line-height: 1 !important; - margin-left: 8px; - } - .usage-query-bar { - display: grid; - grid-template-columns: minmax(220px, 1fr) auto; - gap: 10px; - align-items: center; - /* Keep the dropdown filter row from visually touching the query row. */ - margin-bottom: 10px; - } - .usage-query-actions { - display: flex; - align-items: center; - gap: 6px; - flex-wrap: nowrap; - justify-self: end; - } - .usage-query-actions .btn { - height: 34px; - padding: 0 14px; - border-radius: 999px; - font-weight: 600; - font-size: 13px; - line-height: 1; - border: 1px solid var(--border); - background: var(--bg-secondary); - color: var(--text); - box-shadow: none; - transition: background 0.15s, border-color 0.15s, color 0.15s; - } - .usage-query-actions .btn:hover { - background: var(--bg); - border-color: var(--border-strong); - } - .usage-action-btn { - height: 34px; - padding: 0 14px; - border-radius: 999px; - font-weight: 600; - font-size: 13px; - line-height: 1; - border: 1px solid var(--border); - background: var(--bg-secondary); - color: var(--text); - box-shadow: none; - transition: background 0.15s, border-color 0.15s, color 0.15s; - } - .usage-action-btn:hover { - background: var(--bg); - border-color: var(--border-strong); - } - .usage-primary-btn { - background: #ff4d4d; - color: #fff; - border-color: #ff4d4d; - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.12); - } - .btn.usage-primary-btn { - background: #ff4d4d !important; - border-color: #ff4d4d !important; - color: #fff !important; - } - .usage-primary-btn:hover { - background: #e64545; - border-color: #e64545; - } - .btn.usage-primary-btn:hover { - background: #e64545 !important; - border-color: #e64545 !important; - } - .usage-primary-btn:disabled { - background: rgba(255, 77, 77, 0.18); - border-color: rgba(255, 77, 77, 0.3); - color: #ff4d4d; - box-shadow: none; - cursor: default; - opacity: 1; - } - .usage-primary-btn[disabled] { - background: rgba(255, 77, 77, 0.18) !important; - border-color: rgba(255, 77, 77, 0.3) !important; - color: #ff4d4d !important; - opacity: 1 !important; - } - .usage-secondary-btn { - background: var(--bg-secondary); - color: var(--text); - border-color: var(--border); - } - .usage-query-input { - width: 100%; - min-width: 220px; - padding: 6px 10px; - border: 1px solid var(--border); - border-radius: 6px; - background: var(--bg); - color: var(--text); - font-size: 13px; - } - .usage-query-suggestions { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-top: 6px; - } - .usage-query-suggestion { - padding: 4px 8px; - border-radius: 999px; - border: 1px solid var(--border); - background: var(--bg-secondary); - font-size: 11px; - color: var(--text); - cursor: pointer; - transition: background 0.15s; - } - .usage-query-suggestion:hover { - background: var(--bg-hover); - } - .usage-filter-row { - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; - margin-top: 14px; - } - details.usage-filter-select { - position: relative; - border: 1px solid var(--border); - border-radius: 10px; - padding: 6px 10px; - background: var(--bg); - font-size: 12px; - min-width: 140px; - } - details.usage-filter-select summary { - cursor: pointer; - list-style: none; - display: flex; - align-items: center; - justify-content: space-between; - gap: 6px; - font-weight: 500; - } - details.usage-filter-select summary::-webkit-details-marker { - display: none; - } - .usage-filter-badge { - font-size: 11px; - color: var(--text-muted); - } - .usage-filter-popover { - position: absolute; - left: 0; - top: calc(100% + 6px); - background: var(--bg); - border: 1px solid var(--border); - border-radius: 10px; - padding: 10px; - box-shadow: 0 10px 30px rgba(0,0,0,0.08); - min-width: 220px; - z-index: 20; - } - .usage-filter-actions { - display: flex; - gap: 6px; - margin-bottom: 8px; - } - .usage-filter-actions button { - border-radius: 999px; - padding: 4px 10px; - font-size: 11px; - } - .usage-filter-options { - display: flex; - flex-direction: column; - gap: 6px; - max-height: 200px; - overflow: auto; - } - .usage-filter-option { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - } - .usage-query-hint { - font-size: 11px; - color: var(--text-muted); - } - .usage-query-chips { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-top: 6px; - } - .usage-query-chip { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - border-radius: 999px; - border: 1px solid var(--border); - background: var(--bg-secondary); - font-size: 11px; - } - .usage-query-chip button { - background: none; - border: none; - color: var(--text-muted); - cursor: pointer; - padding: 0; - line-height: 1; - } - .usage-header { - display: flex; - flex-direction: column; - gap: 10px; - background: var(--bg); - } - .usage-header.pinned { - position: sticky; - top: 12px; - z-index: 6; - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06); - } - .usage-pin-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - border-radius: 999px; - border: 1px solid var(--border); - background: var(--bg-secondary); - font-size: 11px; - color: var(--text); - cursor: pointer; - } - .usage-pin-btn.active { - background: var(--accent-subtle); - border-color: var(--accent); - color: var(--accent); - } - .usage-header-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - flex-wrap: wrap; - } - .usage-header-title { - display: flex; - align-items: center; - gap: 10px; - } - .usage-header-metrics { - display: flex; - align-items: center; - gap: 12px; - flex-wrap: wrap; - } - .usage-metric-badge { - display: inline-flex; - align-items: baseline; - gap: 6px; - padding: 2px 8px; - border-radius: 999px; - border: 1px solid var(--border); - background: transparent; - font-size: 11px; - color: var(--text-muted); - } - .usage-metric-badge strong { - font-size: 12px; - color: var(--text); - } - .usage-controls { - display: flex; - align-items: center; - gap: 10px; - flex-wrap: wrap; - } - .usage-controls .active-filters { - flex: 1 1 100%; - } - .usage-controls input[type="date"] { - min-width: 140px; - } - .usage-presets { - display: inline-flex; - gap: 6px; - flex-wrap: wrap; - } - .usage-presets .btn { - padding: 4px 8px; - font-size: 11px; - } - .usage-quick-filters { - display: flex; - gap: 8px; - align-items: center; - flex-wrap: wrap; - } - .usage-select { - min-width: 120px; - padding: 6px 10px; - border: 1px solid var(--border); - border-radius: 6px; - background: var(--bg); - color: var(--text); - font-size: 12px; - } - .usage-export-menu summary { - cursor: pointer; - font-weight: 500; - color: var(--text); - list-style: none; - display: inline-flex; - align-items: center; - gap: 6px; - } - .usage-export-menu summary::-webkit-details-marker { - display: none; - } - .usage-export-menu { - position: relative; - } - .usage-export-button { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 10px; - border-radius: 8px; - border: 1px solid var(--border); - background: var(--bg); - font-size: 12px; - } - .usage-export-popover { - position: absolute; - right: 0; - top: calc(100% + 6px); - background: var(--bg); - border: 1px solid var(--border); - border-radius: 10px; - padding: 8px; - box-shadow: 0 10px 30px rgba(0,0,0,0.08); - min-width: 160px; - z-index: 10; - } - .usage-export-list { - display: flex; - flex-direction: column; - gap: 6px; - } - .usage-export-item { - text-align: left; - padding: 6px 10px; - border-radius: 8px; - border: 1px solid var(--border); - background: var(--bg-secondary); - font-size: 12px; - } - .usage-summary-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 12px; - margin-top: 12px; - } - .usage-summary-card { - padding: 12px; - border-radius: 8px; - background: var(--bg-secondary); - border: 1px solid var(--border); - } - .usage-mosaic { - margin-top: 16px; - padding: 16px; - } - .usage-mosaic-header { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 12px; - margin-bottom: 12px; - } - .usage-mosaic-title { - font-weight: 600; - } - .usage-mosaic-sub { - font-size: 12px; - color: var(--text-muted); - } - .usage-mosaic-grid { - display: grid; - grid-template-columns: minmax(200px, 1fr) minmax(260px, 2fr); - gap: 16px; - align-items: start; - } - .usage-mosaic-section { - background: var(--bg-subtle); - border: 1px solid var(--border); - border-radius: 10px; - padding: 12px; - } - .usage-mosaic-section-title { - font-size: 12px; - font-weight: 600; - margin-bottom: 10px; - display: flex; - align-items: center; - justify-content: space-between; - } - .usage-mosaic-total { - font-size: 20px; - font-weight: 700; - } - .usage-daypart-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); - gap: 8px; - } - .usage-daypart-cell { - border-radius: 8px; - padding: 10px; - color: var(--text); - background: rgba(255, 77, 77, 0.08); - border: 1px solid rgba(255, 77, 77, 0.2); - display: flex; - flex-direction: column; - gap: 4px; - } - .usage-daypart-label { - font-size: 12px; - font-weight: 600; - } - .usage-daypart-value { - font-size: 14px; - } - .usage-hour-grid { - display: grid; - grid-template-columns: repeat(24, minmax(6px, 1fr)); - gap: 4px; - } - .usage-hour-cell { - height: 28px; - border-radius: 6px; - background: rgba(255, 77, 77, 0.1); - border: 1px solid rgba(255, 77, 77, 0.2); - cursor: pointer; - transition: border-color 0.15s, box-shadow 0.15s; - } - .usage-hour-cell.selected { - border-color: rgba(255, 77, 77, 0.8); - box-shadow: 0 0 0 2px rgba(255, 77, 77, 0.2); - } - .usage-hour-labels { - display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); - gap: 6px; - margin-top: 8px; - font-size: 11px; - color: var(--text-muted); - } - .usage-hour-legend { - display: flex; - gap: 8px; - align-items: center; - margin-top: 10px; - font-size: 11px; - color: var(--text-muted); - } - .usage-hour-legend span { - display: inline-block; - width: 14px; - height: 10px; - border-radius: 4px; - background: rgba(255, 77, 77, 0.15); - border: 1px solid rgba(255, 77, 77, 0.2); - } - .usage-calendar-labels { - display: grid; - grid-template-columns: repeat(7, minmax(10px, 1fr)); - gap: 6px; - font-size: 10px; - color: var(--text-muted); - margin-bottom: 6px; - } - .usage-calendar { - display: grid; - grid-template-columns: repeat(7, minmax(10px, 1fr)); - gap: 6px; - } - .usage-calendar-cell { - height: 18px; - border-radius: 4px; - border: 1px solid rgba(255, 77, 77, 0.2); - background: rgba(255, 77, 77, 0.08); - } - .usage-calendar-cell.empty { - background: transparent; - border-color: transparent; - } - .usage-summary-title { - font-size: 11px; - color: var(--text-muted); - margin-bottom: 6px; - display: inline-flex; - align-items: center; - gap: 6px; - } - .usage-info { - display: inline-flex; - align-items: center; - justify-content: center; - width: 16px; - height: 16px; - margin-left: 6px; - border-radius: 999px; - border: 1px solid var(--border); - background: var(--bg); - font-size: 10px; - color: var(--text-muted); - cursor: help; - } - .usage-summary-value { - font-size: 16px; - font-weight: 600; - color: var(--text-strong); - } - .usage-summary-value.good { - color: #1f8f4e; - } - .usage-summary-value.warn { - color: #c57a00; - } - .usage-summary-value.bad { - color: #c9372c; - } - .usage-summary-hint { - font-size: 10px; - color: var(--text-muted); - cursor: help; - border: 1px solid var(--border); - border-radius: 999px; - padding: 0 6px; - line-height: 16px; - height: 16px; - display: inline-flex; - align-items: center; - justify-content: center; - } - .usage-summary-sub { - font-size: 11px; - color: var(--text-muted); - margin-top: 4px; - } - .usage-list { - display: flex; - flex-direction: column; - gap: 8px; - } - .usage-list-item { - display: flex; - justify-content: space-between; - gap: 12px; - font-size: 12px; - color: var(--text); - align-items: flex-start; - } - .usage-list-value { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 2px; - text-align: right; - } - .usage-list-sub { - font-size: 11px; - color: var(--text-muted); - } - .usage-list-item.button { - border: none; - background: transparent; - padding: 0; - text-align: left; - cursor: pointer; - } - .usage-list-item.button:hover { - color: var(--text-strong); - } - .usage-list-item .muted { - font-size: 11px; - } - .usage-error-list { - display: flex; - flex-direction: column; - gap: 10px; - } - .usage-error-row { - display: grid; - grid-template-columns: 1fr auto; - gap: 8px; - align-items: center; - font-size: 12px; - } - .usage-error-date { - font-weight: 600; - } - .usage-error-rate { - font-variant-numeric: tabular-nums; - } - .usage-error-sub { - grid-column: 1 / -1; - font-size: 11px; - color: var(--text-muted); - } - .usage-badges { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 8px; - } - .usage-badge { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 2px 8px; - border: 1px solid var(--border); - border-radius: 999px; - font-size: 11px; - background: var(--bg); - color: var(--text); - } - .usage-meta-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); - gap: 12px; - } - .usage-meta-item { - display: flex; - flex-direction: column; - gap: 4px; - font-size: 12px; - } - .usage-meta-item span { - color: var(--text-muted); - font-size: 11px; - } - .usage-insights-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 16px; - margin-top: 12px; - } - .usage-insight-card { - padding: 14px; - border-radius: 10px; - border: 1px solid var(--border); - background: var(--bg-secondary); - } - .usage-insight-title { - font-size: 12px; - font-weight: 600; - margin-bottom: 10px; - } - .usage-insight-subtitle { - font-size: 11px; - color: var(--text-muted); - margin-top: 6px; - } - /* ===== CHART TOGGLE ===== */ - .chart-toggle { - display: flex; - background: var(--bg); - border-radius: 6px; - overflow: hidden; - border: 1px solid var(--border); - } - .chart-toggle .toggle-btn { - padding: 6px 14px; - font-size: 13px; - background: transparent; - border: none; - color: var(--text-muted); - cursor: pointer; - transition: all 0.15s; - } - .chart-toggle .toggle-btn:hover { - color: var(--text); - } - .chart-toggle .toggle-btn.active { - background: #ff4d4d; - color: white; - } - .chart-toggle.small .toggle-btn { - padding: 4px 8px; - font-size: 11px; - } - .sessions-toggle { - border-radius: 4px; - } - .sessions-toggle .toggle-btn { - border-radius: 4px; - } - .daily-chart-header { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 8px; - margin-bottom: 6px; - } - - /* ===== DAILY BAR CHART ===== */ - .daily-chart { - margin-top: 12px; - } - .daily-chart-bars { - display: flex; - align-items: flex-end; - height: 200px; - gap: 4px; - padding: 8px 4px 36px; - } - .daily-bar-wrapper { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - height: 100%; - justify-content: flex-end; - cursor: pointer; - position: relative; - border-radius: 4px 4px 0 0; - transition: background 0.15s; - min-width: 0; - } - .daily-bar-wrapper:hover { - background: var(--bg-hover); - } - .daily-bar-wrapper.selected { - background: var(--accent-subtle); - } - .daily-bar-wrapper.selected .daily-bar { - background: var(--accent); - } - .daily-bar { - width: 100%; - max-width: var(--bar-max-width, 32px); - background: #ff4d4d; - border-radius: 3px 3px 0 0; - min-height: 2px; - transition: all 0.15s; - overflow: hidden; - } - .daily-bar-wrapper:hover .daily-bar { - background: #cc3d3d; - } - .daily-bar-label { - position: absolute; - bottom: -28px; - font-size: 10px; - color: var(--text-muted); - white-space: nowrap; - text-align: center; - transform: rotate(-35deg); - transform-origin: top center; - } - .daily-bar-total { - position: absolute; - top: -16px; - left: 50%; - transform: translateX(-50%); - font-size: 10px; - color: var(--text-muted); - white-space: nowrap; - } - .daily-bar-tooltip { - position: absolute; - bottom: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - background: var(--bg); - border: 1px solid var(--border); - border-radius: 6px; - padding: 8px 12px; - font-size: 12px; - white-space: nowrap; - z-index: 100; - box-shadow: 0 4px 12px rgba(0,0,0,0.15); - pointer-events: none; - opacity: 0; - transition: opacity 0.15s; - } - .daily-bar-wrapper:hover .daily-bar-tooltip { - opacity: 1; - } - - /* ===== COST/TOKEN BREAKDOWN BAR ===== */ - .cost-breakdown { - margin-top: 18px; - padding: 16px; - background: var(--bg-secondary); - border-radius: 8px; - } - .cost-breakdown-header { - font-weight: 600; - font-size: 15px; - letter-spacing: -0.02em; - margin-bottom: 12px; - color: var(--text-strong); - } - .cost-breakdown-bar { - height: 28px; - background: var(--bg); - border-radius: 6px; - overflow: hidden; - display: flex; - } - .cost-segment { - height: 100%; - transition: width 0.3s ease; - position: relative; - } - .cost-segment.output { - background: #ef4444; - } - .cost-segment.input { - background: #f59e0b; - } - .cost-segment.cache-write { - background: #10b981; - } - .cost-segment.cache-read { - background: #06b6d4; - } - .cost-breakdown-legend { - display: flex; - flex-wrap: wrap; - gap: 16px; - margin-top: 12px; - } - .cost-breakdown-total { - margin-top: 10px; - font-size: 12px; - color: var(--text-muted); - } - .legend-item { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: var(--text); - cursor: help; - } - .legend-dot { - width: 10px; - height: 10px; - border-radius: 2px; - flex-shrink: 0; - } - .legend-dot.output { - background: #ef4444; - } - .legend-dot.input { - background: #f59e0b; - } - .legend-dot.cache-write { - background: #10b981; - } - .legend-dot.cache-read { - background: #06b6d4; - } - .legend-dot.system { - background: #ff4d4d; - } - .legend-dot.skills { - background: #8b5cf6; - } - .legend-dot.tools { - background: #ec4899; - } - .legend-dot.files { - background: #f59e0b; - } - .cost-breakdown-note { - margin-top: 10px; - font-size: 11px; - color: var(--text-muted); - line-height: 1.4; - } - - /* ===== SESSION BARS (scrollable list) ===== */ - .session-bars { - margin-top: 16px; - max-height: 400px; - overflow-y: auto; - border: 1px solid var(--border); - border-radius: 8px; - background: var(--bg); - } - .session-bar-row { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 14px; - border-bottom: 1px solid var(--border); - cursor: pointer; - transition: background 0.15s; - } - .session-bar-row:last-child { - border-bottom: none; - } - .session-bar-row:hover { - background: var(--bg-hover); - } - .session-bar-row.selected { - background: var(--accent-subtle); - } - .session-bar-label { - flex: 1 1 auto; - min-width: 0; - font-size: 13px; - color: var(--text); - display: flex; - flex-direction: column; - gap: 2px; - } - .session-bar-title { - /* Prefer showing the full name; wrap instead of truncating. */ - white-space: normal; - overflow-wrap: anywhere; - word-break: break-word; - } - .session-bar-meta { - font-size: 10px; - color: var(--text-muted); - font-weight: 400; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .session-bar-track { - flex: 0 0 90px; - height: 6px; - background: var(--bg-secondary); - border-radius: 4px; - overflow: hidden; - opacity: 0.6; - } - .session-bar-fill { - height: 100%; - background: rgba(255, 77, 77, 0.7); - border-radius: 4px; - transition: width 0.3s ease; - } - .session-bar-value { - flex: 0 0 70px; - text-align: right; - font-size: 12px; - font-family: var(--font-mono); - color: var(--text-muted); - } - .session-bar-actions { - display: inline-flex; - align-items: center; - gap: 8px; - flex: 0 0 auto; - } - .session-copy-btn { - height: 26px; - padding: 0 10px; - border-radius: 999px; - border: 1px solid var(--border); - background: var(--bg-secondary); - font-size: 11px; - font-weight: 600; - color: var(--text-muted); - cursor: pointer; - transition: background 0.15s, border-color 0.15s, color 0.15s; - } - .session-copy-btn:hover { - background: var(--bg); - border-color: var(--border-strong); - color: var(--text); - } - - /* ===== TIME SERIES CHART ===== */ - .session-timeseries { - margin-top: 24px; - padding: 16px; - background: var(--bg-secondary); - border-radius: 8px; - } - .timeseries-header-row { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; - } - .timeseries-controls { - display: flex; - gap: 6px; - align-items: center; - } - .timeseries-header { - font-weight: 600; - color: var(--text); - } - .timeseries-chart { - width: 100%; - overflow: hidden; - } - .timeseries-svg { - width: 100%; - height: auto; - display: block; - } - .timeseries-svg .axis-label { - font-size: 10px; - fill: var(--text-muted); - } - .timeseries-svg .ts-area { - fill: #ff4d4d; - fill-opacity: 0.1; - } - .timeseries-svg .ts-line { - fill: none; - stroke: #ff4d4d; - stroke-width: 2; - } - .timeseries-svg .ts-dot { - fill: #ff4d4d; - transition: r 0.15s, fill 0.15s; - } - .timeseries-svg .ts-dot:hover { - r: 5; - } - .timeseries-svg .ts-bar { - fill: #ff4d4d; - transition: fill 0.15s; - } - .timeseries-svg .ts-bar:hover { - fill: #cc3d3d; - } - .timeseries-svg .ts-bar.output { fill: #ef4444; } - .timeseries-svg .ts-bar.input { fill: #f59e0b; } - .timeseries-svg .ts-bar.cache-write { fill: #10b981; } - .timeseries-svg .ts-bar.cache-read { fill: #06b6d4; } - .timeseries-summary { - margin-top: 12px; - font-size: 13px; - color: var(--text-muted); - display: flex; - flex-wrap: wrap; - gap: 8px; - } - .timeseries-loading { - padding: 24px; - text-align: center; - color: var(--text-muted); - } - - /* ===== SESSION LOGS ===== */ - .session-logs { - margin-top: 24px; - background: var(--bg-secondary); - border-radius: 8px; - overflow: hidden; - } - .session-logs-header { - padding: 10px 14px; - font-weight: 600; - border-bottom: 1px solid var(--border); - display: flex; - justify-content: space-between; - align-items: center; - font-size: 13px; - background: var(--bg-secondary); - } - .session-logs-loading { - padding: 24px; - text-align: center; - color: var(--text-muted); - } - .session-logs-list { - max-height: 400px; - overflow-y: auto; - } - .session-log-entry { - padding: 10px 14px; - border-bottom: 1px solid var(--border); - display: flex; - flex-direction: column; - gap: 6px; - background: var(--bg); - } - .session-log-entry:last-child { - border-bottom: none; - } - .session-log-entry.user { - border-left: 3px solid var(--accent); - } - .session-log-entry.assistant { - border-left: 3px solid var(--border-strong); - } - .session-log-meta { - display: flex; - gap: 8px; - align-items: center; - font-size: 11px; - color: var(--text-muted); - flex-wrap: wrap; - } - .session-log-role { - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.04em; - font-size: 10px; - padding: 2px 6px; - border-radius: 999px; - background: var(--bg-secondary); - border: 1px solid var(--border); - } - .session-log-entry.user .session-log-role { - color: var(--accent); - } - .session-log-entry.assistant .session-log-role { - color: var(--text-muted); - } - .session-log-content { - font-size: 13px; - line-height: 1.5; - color: var(--text); - white-space: pre-wrap; - word-break: break-word; - background: var(--bg-secondary); - border-radius: 8px; - padding: 8px 10px; - border: 1px solid var(--border); - max-height: 220px; - overflow-y: auto; - } - - /* ===== CONTEXT WEIGHT BREAKDOWN ===== */ - .context-weight-breakdown { - margin-top: 24px; - padding: 16px; - background: var(--bg-secondary); - border-radius: 8px; - } - .context-weight-breakdown .context-weight-header { - font-weight: 600; - font-size: 13px; - margin-bottom: 4px; - color: var(--text); - } - .context-weight-desc { - font-size: 12px; - color: var(--text-muted); - margin: 0 0 12px 0; - } - .context-stacked-bar { - height: 24px; - background: var(--bg); - border-radius: 6px; - overflow: hidden; - display: flex; - } - .context-segment { - height: 100%; - transition: width 0.3s ease; - } - .context-segment.system { - background: #ff4d4d; - } - .context-segment.skills { - background: #8b5cf6; - } - .context-segment.tools { - background: #ec4899; - } - .context-segment.files { - background: #f59e0b; - } - .context-legend { - display: flex; - flex-wrap: wrap; - gap: 16px; - margin-top: 12px; - } - .context-total { - margin-top: 10px; - font-size: 12px; - font-weight: 600; - color: var(--text-muted); - } - .context-details { - margin-top: 12px; - border: 1px solid var(--border); - border-radius: 6px; - overflow: hidden; - } - .context-details summary { - padding: 10px 14px; - font-size: 13px; - font-weight: 500; - cursor: pointer; - background: var(--bg); - border-bottom: 1px solid var(--border); - } - .context-details[open] summary { - border-bottom: 1px solid var(--border); - } - .context-list { - max-height: 200px; - overflow-y: auto; - } - .context-list-header { - display: flex; - justify-content: space-between; - padding: 8px 14px; - font-size: 11px; - text-transform: uppercase; - color: var(--text-muted); - background: var(--bg-secondary); - border-bottom: 1px solid var(--border); - } - .context-list-item { - display: flex; - justify-content: space-between; - padding: 8px 14px; - font-size: 12px; - border-bottom: 1px solid var(--border); - } - .context-list-item:last-child { - border-bottom: none; - } - .context-list-item .mono { - font-family: var(--font-mono); - color: var(--text); - } - .context-list-item .muted { - color: var(--text-muted); - font-family: var(--font-mono); - } - - /* ===== NO CONTEXT NOTE ===== */ - .no-context-note { - margin-top: 24px; - padding: 16px; - background: var(--bg-secondary); - border-radius: 8px; - font-size: 13px; - color: var(--text-muted); - line-height: 1.5; - } - - /* ===== TWO COLUMN LAYOUT ===== */ - .usage-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 18px; - margin-top: 18px; - align-items: stretch; - } - .usage-grid-left { - display: flex; - flex-direction: column; - } - .usage-grid-right { - display: flex; - flex-direction: column; - } - - /* ===== LEFT CARD (Daily + Breakdown) ===== */ - .usage-left-card { - /* inherits background, border, shadow from .card */ - flex: 1; - display: flex; - flex-direction: column; - } - .usage-left-card .daily-chart-bars { - flex: 1; - min-height: 200px; - } - .usage-left-card .sessions-panel-title { - font-weight: 600; - font-size: 14px; - margin-bottom: 12px; - } - - /* ===== COMPACT DAILY CHART ===== */ - .daily-chart-compact { - margin-bottom: 16px; - } - .daily-chart-compact .sessions-panel-title { - margin-bottom: 8px; - } - .daily-chart-compact .daily-chart-bars { - height: 100px; - padding-bottom: 20px; - } - - /* ===== COMPACT COST BREAKDOWN ===== */ - .cost-breakdown-compact { - padding: 0; - margin: 0; - background: transparent; - border-top: 1px solid var(--border); - padding-top: 12px; - } - .cost-breakdown-compact .cost-breakdown-header { - margin-bottom: 8px; - } - .cost-breakdown-compact .cost-breakdown-legend { - gap: 12px; - } - .cost-breakdown-compact .cost-breakdown-note { - display: none; - } - - /* ===== SESSIONS CARD ===== */ - .sessions-card { - /* inherits background, border, shadow from .card */ - flex: 1; - display: flex; - flex-direction: column; - } - .sessions-card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - } - .sessions-card-title { - font-weight: 600; - font-size: 14px; - } - .sessions-card-count { - font-size: 12px; - color: var(--text-muted); - } - .sessions-card-meta { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin: 8px 0 10px; - font-size: 12px; - color: var(--text-muted); - } - .sessions-card-stats { - display: inline-flex; - gap: 12px; - } - .sessions-sort { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: var(--text-muted); - } - .sessions-sort select { - padding: 4px 8px; - border-radius: 6px; - border: 1px solid var(--border); - background: var(--bg); - color: var(--text); - font-size: 12px; - } - .sessions-action-btn { - height: 28px; - padding: 0 10px; - border-radius: 8px; - font-size: 12px; - line-height: 1; - } - .sessions-action-btn.icon { - width: 32px; - padding: 0; - display: inline-flex; - align-items: center; - justify-content: center; - } - .sessions-card-hint { - font-size: 11px; - color: var(--text-muted); - margin-bottom: 8px; - } - .sessions-card .session-bars { - max-height: 280px; - background: var(--bg); - border-radius: 6px; - border: 1px solid var(--border); - margin: 0; - overflow-y: auto; - padding: 8px; - } - .sessions-card .session-bar-row { - padding: 6px 8px; - border-radius: 6px; - margin-bottom: 3px; - border: 1px solid transparent; - transition: all 0.15s; - } - .sessions-card .session-bar-row:hover { - border-color: var(--border); - background: var(--bg-hover); - } - .sessions-card .session-bar-row.selected { - border-color: var(--accent); - background: var(--accent-subtle); - box-shadow: inset 0 0 0 1px rgba(255, 77, 77, 0.15); - } - .sessions-card .session-bar-label { - flex: 1 1 auto; - min-width: 140px; - font-size: 12px; - } - .sessions-card .session-bar-value { - flex: 0 0 60px; - font-size: 11px; - font-weight: 600; - } - .sessions-card .session-bar-track { - flex: 0 0 70px; - height: 5px; - opacity: 0.5; - } - .sessions-card .session-bar-fill { - background: rgba(255, 77, 77, 0.55); - } - .sessions-clear-btn { - margin-left: auto; - } - - /* ===== EMPTY DETAIL STATE ===== */ - .session-detail-empty { - margin-top: 18px; - background: var(--bg-secondary); - border-radius: 8px; - border: 2px dashed var(--border); - padding: 32px; - text-align: center; - } - .session-detail-empty-title { - font-size: 15px; - font-weight: 600; - color: var(--text); - margin-bottom: 8px; - } - .session-detail-empty-desc { - font-size: 13px; - color: var(--text-muted); - margin-bottom: 16px; - line-height: 1.5; - } - .session-detail-empty-features { - display: flex; - justify-content: center; - gap: 24px; - flex-wrap: wrap; - } - .session-detail-empty-feature { - display: flex; - align-items: center; - gap: 6px; - font-size: 12px; - color: var(--text-muted); - } - .session-detail-empty-feature .icon { - font-size: 16px; - } - - /* ===== SESSION DETAIL PANEL ===== */ - .session-detail-panel { - margin-top: 12px; - /* inherits background, border-radius, shadow from .card */ - border: 2px solid var(--accent) !important; - } - .session-detail-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 12px; - border-bottom: 1px solid var(--border); - cursor: pointer; - } - .session-detail-header:hover { - background: var(--bg-hover); - } - .session-detail-title { - font-weight: 600; - font-size: 14px; - display: flex; - align-items: center; - gap: 8px; - } - .session-detail-header-left { - display: flex; - align-items: center; - gap: 8px; - } - .session-close-btn { - background: var(--bg); - border: 1px solid var(--border); - color: var(--text); - cursor: pointer; - padding: 2px 8px; - font-size: 16px; - line-height: 1; - border-radius: 4px; - transition: background 0.15s, color 0.15s; - } - .session-close-btn:hover { - background: var(--bg-hover); - color: var(--text); - border-color: var(--accent); - } - .session-detail-stats { - display: flex; - gap: 10px; - font-size: 12px; - color: var(--text-muted); - } - .session-detail-stats strong { - color: var(--text); - font-family: var(--font-mono); - } - .session-detail-content { - padding: 12px; - } - .session-summary-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 8px; - margin-bottom: 12px; - } - .session-summary-card { - border: 1px solid var(--border); - border-radius: 8px; - padding: 8px; - background: var(--bg-secondary); - } - .session-summary-title { - font-size: 11px; - color: var(--text-muted); - margin-bottom: 4px; - } - .session-summary-value { - font-size: 14px; - font-weight: 600; - } - .session-summary-meta { - font-size: 11px; - color: var(--text-muted); - margin-top: 4px; - } - .session-detail-row { - display: grid; - grid-template-columns: 1fr; - gap: 10px; - /* Separate "Usage Over Time" from the summary + Top Tools/Model Mix cards above. */ - margin-top: 12px; - margin-bottom: 10px; - } - .session-detail-bottom { - display: grid; - grid-template-columns: minmax(0, 1.8fr) minmax(0, 1fr); - gap: 10px; - align-items: stretch; - } - .session-detail-bottom .session-logs-compact { - margin: 0; - display: flex; - flex-direction: column; - } - .session-detail-bottom .session-logs-compact .session-logs-list { - flex: 1 1 auto; - max-height: none; - } - .context-details-panel { - display: flex; - flex-direction: column; - gap: 8px; - background: var(--bg); - border-radius: 6px; - border: 1px solid var(--border); - padding: 12px; - } - .context-breakdown-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); - gap: 10px; - margin-top: 8px; - } - .context-breakdown-card { - border: 1px solid var(--border); - border-radius: 8px; - padding: 8px; - background: var(--bg-secondary); - } - .context-breakdown-title { - font-size: 11px; - font-weight: 600; - margin-bottom: 6px; - } - .context-breakdown-list { - display: flex; - flex-direction: column; - gap: 6px; - font-size: 11px; - } - .context-breakdown-item { - display: flex; - justify-content: space-between; - gap: 8px; - } - .context-breakdown-more { - font-size: 10px; - color: var(--text-muted); - margin-top: 4px; - } - .context-breakdown-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - } - .context-expand-btn { - border: 1px solid var(--border); - background: var(--bg-secondary); - color: var(--text-muted); - font-size: 11px; - padding: 4px 8px; - border-radius: 999px; - cursor: pointer; - transition: all 0.15s; - } - .context-expand-btn:hover { - color: var(--text); - border-color: var(--border-strong); - background: var(--bg); - } - - /* ===== COMPACT TIMESERIES ===== */ - .session-timeseries-compact { - background: var(--bg); - border-radius: 6px; - border: 1px solid var(--border); - padding: 12px; - margin: 0; - } - .session-timeseries-compact .timeseries-header-row { - margin-bottom: 8px; - } - .session-timeseries-compact .timeseries-header { - font-size: 12px; - } - .session-timeseries-compact .timeseries-summary { - font-size: 11px; - margin-top: 8px; - } - - /* ===== COMPACT CONTEXT ===== */ - .context-weight-compact { - background: var(--bg); - border-radius: 6px; - border: 1px solid var(--border); - padding: 12px; - margin: 0; - } - .context-weight-compact .context-weight-header { - font-size: 12px; - margin-bottom: 4px; - } - .context-weight-compact .context-weight-desc { - font-size: 11px; - margin-bottom: 8px; - } - .context-weight-compact .context-stacked-bar { - height: 16px; - } - .context-weight-compact .context-legend { - font-size: 11px; - gap: 10px; - margin-top: 8px; - } - .context-weight-compact .context-total { - font-size: 11px; - margin-top: 6px; - } - .context-weight-compact .context-details { - margin-top: 8px; - } - .context-weight-compact .context-details summary { - font-size: 12px; - padding: 6px 10px; - } - - /* ===== COMPACT LOGS ===== */ - .session-logs-compact { - background: var(--bg); - border-radius: 10px; - border: 1px solid var(--border); - overflow: hidden; - margin: 0; - display: flex; - flex-direction: column; - } - .session-logs-compact .session-logs-header { - padding: 10px 12px; - font-size: 12px; - } - .session-logs-compact .session-logs-list { - max-height: none; - flex: 1 1 auto; - overflow: auto; - } - .session-logs-compact .session-log-entry { - padding: 8px 12px; - } - .session-logs-compact .session-log-content { - font-size: 12px; - max-height: 160px; - } - .session-log-tools { - margin-top: 6px; - border: 1px solid var(--border); - border-radius: 8px; - background: var(--bg-secondary); - padding: 6px 8px; - font-size: 11px; - color: var(--text); - } - .session-log-tools summary { - cursor: pointer; - list-style: none; - display: flex; - align-items: center; - gap: 6px; - font-weight: 600; - } - .session-log-tools summary::-webkit-details-marker { - display: none; - } - .session-log-tools-list { - margin-top: 6px; - display: flex; - flex-wrap: wrap; - gap: 6px; - } - .session-log-tools-pill { - border: 1px solid var(--border); - border-radius: 999px; - padding: 2px 8px; - font-size: 10px; - background: var(--bg); - color: var(--text); - } - - /* ===== RESPONSIVE ===== */ - @media (max-width: 900px) { - .usage-grid { - grid-template-columns: 1fr; - } - .session-detail-row { - grid-template-columns: 1fr; - } - } - @media (max-width: 600px) { - .session-bar-label { - flex: 0 0 100px; - } - .cost-breakdown-legend { - gap: 10px; - } - .legend-item { - font-size: 11px; - } - .daily-chart-bars { - height: 170px; - gap: 6px; - padding-bottom: 40px; - } - .daily-bar-label { - font-size: 8px; - bottom: -30px; - transform: rotate(-45deg); - } - .usage-mosaic-grid { - grid-template-columns: 1fr; - } - .usage-hour-grid { - grid-template-columns: repeat(12, minmax(10px, 1fr)); - } - .usage-hour-cell { - height: 22px; - } - } -`; - -export type UsageSessionEntry = { - key: string; - label?: string; - sessionId?: string; - updatedAt?: number; - agentId?: string; - channel?: string; - chatType?: string; - origin?: { - label?: string; - provider?: string; - surface?: string; - chatType?: string; - from?: string; - to?: string; - accountId?: string; - threadId?: string | number; - }; - modelOverride?: string; - providerOverride?: string; - modelProvider?: string; - model?: string; - usage: { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - totalTokens: number; - totalCost: number; - inputCost?: number; - outputCost?: number; - cacheReadCost?: number; - cacheWriteCost?: number; - missingCostEntries: number; - firstActivity?: number; - lastActivity?: number; - durationMs?: number; - activityDates?: string[]; // YYYY-MM-DD dates when session had activity - dailyBreakdown?: Array<{ date: string; tokens: number; cost: number }>; // Per-day breakdown - dailyMessageCounts?: Array<{ - date: string; - total: number; - user: number; - assistant: number; - toolCalls: number; - toolResults: number; - errors: number; - }>; - dailyLatency?: Array<{ - date: string; - count: number; - avgMs: number; - p95Ms: number; - minMs: number; - maxMs: number; - }>; - dailyModelUsage?: Array<{ - date: string; - provider?: string; - model?: string; - tokens: number; - cost: number; - count: number; - }>; - messageCounts?: { - total: number; - user: number; - assistant: number; - toolCalls: number; - toolResults: number; - errors: number; - }; - toolUsage?: { - totalCalls: number; - uniqueTools: number; - tools: Array<{ name: string; count: number }>; - }; - modelUsage?: Array<{ - provider?: string; - model?: string; - count: number; - totals: UsageTotals; - }>; - latency?: { - count: number; - avgMs: number; - p95Ms: number; - minMs: number; - maxMs: number; - }; - } | null; - contextWeight?: { - systemPrompt: { chars: number; projectContextChars: number; nonProjectContextChars: number }; - skills: { promptChars: number; entries: Array<{ name: string; blockChars: number }> }; - tools: { - listChars: number; - schemaChars: number; - entries: Array<{ name: string; summaryChars: number; schemaChars: number }>; - }; - injectedWorkspaceFiles: Array<{ - name: string; - path: string; - rawChars: number; - injectedChars: number; - truncated: boolean; - }>; - } | null; -}; - -export type UsageTotals = { - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - totalTokens: number; - totalCost: number; - inputCost: number; - outputCost: number; - cacheReadCost: number; - cacheWriteCost: number; - missingCostEntries: number; -}; - -export type CostDailyEntry = UsageTotals & { date: string }; - -export type UsageAggregates = { - messages: { - total: number; - user: number; - assistant: number; - toolCalls: number; - toolResults: number; - errors: number; - }; - tools: { - totalCalls: number; - uniqueTools: number; - tools: Array<{ name: string; count: number }>; - }; - byModel: Array<{ - provider?: string; - model?: string; - count: number; - totals: UsageTotals; - }>; - byProvider: Array<{ - provider?: string; - model?: string; - count: number; - totals: UsageTotals; - }>; - byAgent: Array<{ agentId: string; totals: UsageTotals }>; - byChannel: Array<{ channel: string; totals: UsageTotals }>; - latency?: { - count: number; - avgMs: number; - p95Ms: number; - minMs: number; - maxMs: number; - }; - dailyLatency?: Array<{ - date: string; - count: number; - avgMs: number; - p95Ms: number; - minMs: number; - maxMs: number; - }>; - modelDaily?: Array<{ - date: string; - provider?: string; - model?: string; - tokens: number; - cost: number; - count: number; - }>; - daily: Array<{ - date: string; - tokens: number; - cost: number; - messages: number; - toolCalls: number; - errors: number; - }>; -}; - -export type UsageColumnId = - | "channel" - | "agent" - | "provider" - | "model" - | "messages" - | "tools" - | "errors" - | "duration"; - -export type TimeSeriesPoint = { - timestamp: number; - input: number; - output: number; - cacheRead: number; - cacheWrite: number; - totalTokens: number; - cost: number; - cumulativeTokens: number; - cumulativeCost: number; -}; - -export type UsageProps = { - loading: boolean; - error: string | null; - startDate: string; - endDate: string; - sessions: UsageSessionEntry[]; - sessionsLimitReached: boolean; // True if 1000 session cap was hit - totals: UsageTotals | null; - aggregates: UsageAggregates | null; - costDaily: CostDailyEntry[]; - selectedSessions: string[]; // Support multiple session selection - selectedDays: string[]; // Support multiple day selection - selectedHours: number[]; // Support multiple hour selection - chartMode: "tokens" | "cost"; - dailyChartMode: "total" | "by-type"; - timeSeriesMode: "cumulative" | "per-turn"; - timeSeriesBreakdownMode: "total" | "by-type"; - timeSeries: { points: TimeSeriesPoint[] } | null; - timeSeriesLoading: boolean; - sessionLogs: SessionLogEntry[] | null; - sessionLogsLoading: boolean; - sessionLogsExpanded: boolean; - logFilterRoles: SessionLogRole[]; - logFilterTools: string[]; - logFilterHasTools: boolean; - logFilterQuery: string; - query: string; - queryDraft: string; - sessionSort: "tokens" | "cost" | "recent" | "messages" | "errors"; - sessionSortDir: "asc" | "desc"; - recentSessions: string[]; - sessionsTab: "all" | "recent"; - visibleColumns: UsageColumnId[]; - timeZone: "local" | "utc"; - contextExpanded: boolean; - headerPinned: boolean; - onStartDateChange: (date: string) => void; - onEndDateChange: (date: string) => void; - onRefresh: () => void; - onTimeZoneChange: (zone: "local" | "utc") => void; - onToggleContextExpanded: () => void; - onToggleHeaderPinned: () => void; - onToggleSessionLogsExpanded: () => void; - onLogFilterRolesChange: (next: SessionLogRole[]) => void; - onLogFilterToolsChange: (next: string[]) => void; - onLogFilterHasToolsChange: (next: boolean) => void; - onLogFilterQueryChange: (next: string) => void; - onLogFilterClear: () => void; - onSelectSession: (key: string, shiftKey: boolean) => void; - onChartModeChange: (mode: "tokens" | "cost") => void; - onDailyChartModeChange: (mode: "total" | "by-type") => void; - onTimeSeriesModeChange: (mode: "cumulative" | "per-turn") => void; - onTimeSeriesBreakdownChange: (mode: "total" | "by-type") => void; - onSelectDay: (day: string, shiftKey: boolean) => void; // Support shift-click - onSelectHour: (hour: number, shiftKey: boolean) => void; - onClearDays: () => void; - onClearHours: () => void; - onClearSessions: () => void; - onClearFilters: () => void; - onQueryDraftChange: (query: string) => void; - onApplyQuery: () => void; - onClearQuery: () => void; - onSessionSortChange: (sort: "tokens" | "cost" | "recent" | "messages" | "errors") => void; - onSessionSortDirChange: (dir: "asc" | "desc") => void; - onSessionsTabChange: (tab: "all" | "recent") => void; - onToggleColumn: (column: UsageColumnId) => void; -}; - -export type SessionLogEntry = { - timestamp: number; - role: "user" | "assistant" | "tool" | "toolResult"; - content: string; - tokens?: number; - cost?: number; -}; - -export type SessionLogRole = SessionLogEntry["role"]; +export type { UsageColumnId, SessionLogEntry, SessionLogRole }; // ~4 chars per token is a rough approximation const CHARS_PER_TOKEN = 4; diff --git a/ui/src/ui/views/usageStyles.ts b/ui/src/ui/views/usageStyles.ts new file mode 100644 index 0000000000..dd8302a4d0 --- /dev/null +++ b/ui/src/ui/views/usageStyles.ts @@ -0,0 +1,1911 @@ +export const usageStylesString = ` + .usage-page-header { + margin: 4px 0 12px; + } + .usage-page-title { + font-size: 28px; + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 4px; + } + .usage-page-subtitle { + font-size: 13px; + color: var(--text-muted); + margin: 0 0 12px; + } + /* ===== FILTERS & HEADER ===== */ + .usage-filters-inline { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + } + .usage-filters-inline select { + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + font-size: 13px; + } + .usage-filters-inline input[type="date"] { + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + font-size: 13px; + } + .usage-filters-inline input[type="text"] { + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + font-size: 13px; + min-width: 180px; + } + .usage-filters-inline .btn-sm { + padding: 6px 12px; + font-size: 14px; + } + .usage-refresh-indicator { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + background: rgba(255, 77, 77, 0.1); + border-radius: 4px; + font-size: 12px; + color: #ff4d4d; + } + .usage-refresh-indicator::before { + content: ""; + width: 10px; + height: 10px; + border: 2px solid #ff4d4d; + border-top-color: transparent; + border-radius: 50%; + animation: usage-spin 0.6s linear infinite; + } + @keyframes usage-spin { + to { transform: rotate(360deg); } + } + .active-filters { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + } + .filter-chip { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px 4px 12px; + background: var(--accent-subtle); + border: 1px solid var(--accent); + border-radius: 16px; + font-size: 12px; + } + .filter-chip-label { + color: var(--accent); + font-weight: 500; + } + .filter-chip-remove { + background: none; + border: none; + color: var(--accent); + cursor: pointer; + padding: 2px 4px; + font-size: 14px; + line-height: 1; + opacity: 0.7; + transition: opacity 0.15s; + } + .filter-chip-remove:hover { + opacity: 1; + } + .filter-clear-btn { + padding: 4px 10px !important; + font-size: 12px !important; + line-height: 1 !important; + margin-left: 8px; + } + .usage-query-bar { + display: grid; + grid-template-columns: minmax(220px, 1fr) auto; + gap: 10px; + align-items: center; + /* Keep the dropdown filter row from visually touching the query row. */ + margin-bottom: 10px; + } + .usage-query-actions { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: nowrap; + justify-self: end; + } + .usage-query-actions .btn { + height: 34px; + padding: 0 14px; + border-radius: 999px; + font-weight: 600; + font-size: 13px; + line-height: 1; + border: 1px solid var(--border); + background: var(--bg-secondary); + color: var(--text); + box-shadow: none; + transition: background 0.15s, border-color 0.15s, color 0.15s; + } + .usage-query-actions .btn:hover { + background: var(--bg); + border-color: var(--border-strong); + } + .usage-action-btn { + height: 34px; + padding: 0 14px; + border-radius: 999px; + font-weight: 600; + font-size: 13px; + line-height: 1; + border: 1px solid var(--border); + background: var(--bg-secondary); + color: var(--text); + box-shadow: none; + transition: background 0.15s, border-color 0.15s, color 0.15s; + } + .usage-action-btn:hover { + background: var(--bg); + border-color: var(--border-strong); + } + .usage-primary-btn { + background: #ff4d4d; + color: #fff; + border-color: #ff4d4d; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.12); + } + .btn.usage-primary-btn { + background: #ff4d4d !important; + border-color: #ff4d4d !important; + color: #fff !important; + } + .usage-primary-btn:hover { + background: #e64545; + border-color: #e64545; + } + .btn.usage-primary-btn:hover { + background: #e64545 !important; + border-color: #e64545 !important; + } + .usage-primary-btn:disabled { + background: rgba(255, 77, 77, 0.18); + border-color: rgba(255, 77, 77, 0.3); + color: #ff4d4d; + box-shadow: none; + cursor: default; + opacity: 1; + } + .usage-primary-btn[disabled] { + background: rgba(255, 77, 77, 0.18) !important; + border-color: rgba(255, 77, 77, 0.3) !important; + color: #ff4d4d !important; + opacity: 1 !important; + } + .usage-secondary-btn { + background: var(--bg-secondary); + color: var(--text); + border-color: var(--border); + } + .usage-query-input { + width: 100%; + min-width: 220px; + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + font-size: 13px; + } + .usage-query-suggestions { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 6px; + } + .usage-query-suggestion { + padding: 4px 8px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--bg-secondary); + font-size: 11px; + color: var(--text); + cursor: pointer; + transition: background 0.15s; + } + .usage-query-suggestion:hover { + background: var(--bg-hover); + } + .usage-filter-row { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; + margin-top: 14px; + } + details.usage-filter-select { + position: relative; + border: 1px solid var(--border); + border-radius: 10px; + padding: 6px 10px; + background: var(--bg); + font-size: 12px; + min-width: 140px; + } + details.usage-filter-select summary { + cursor: pointer; + list-style: none; + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + font-weight: 500; + } + details.usage-filter-select summary::-webkit-details-marker { + display: none; + } + .usage-filter-badge { + font-size: 11px; + color: var(--text-muted); + } + .usage-filter-popover { + position: absolute; + left: 0; + top: calc(100% + 6px); + background: var(--bg); + border: 1px solid var(--border); + border-radius: 10px; + padding: 10px; + box-shadow: 0 10px 30px rgba(0,0,0,0.08); + min-width: 220px; + z-index: 20; + } + .usage-filter-actions { + display: flex; + gap: 6px; + margin-bottom: 8px; + } + .usage-filter-actions button { + border-radius: 999px; + padding: 4px 10px; + font-size: 11px; + } + .usage-filter-options { + display: flex; + flex-direction: column; + gap: 6px; + max-height: 200px; + overflow: auto; + } + .usage-filter-option { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + } + .usage-query-hint { + font-size: 11px; + color: var(--text-muted); + } + .usage-query-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-top: 6px; + } + .usage-query-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--bg-secondary); + font-size: 11px; + } + .usage-query-chip button { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + padding: 0; + line-height: 1; + } + .usage-header { + display: flex; + flex-direction: column; + gap: 10px; + background: var(--bg); + } + .usage-header.pinned { + position: sticky; + top: 12px; + z-index: 6; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06); + } + .usage-pin-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--bg-secondary); + font-size: 11px; + color: var(--text); + cursor: pointer; + } + .usage-pin-btn.active { + background: var(--accent-subtle); + border-color: var(--accent); + color: var(--accent); + } + .usage-header-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + } + .usage-header-title { + display: flex; + align-items: center; + gap: 10px; + } + .usage-header-metrics { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + } + .usage-metric-badge { + display: inline-flex; + align-items: baseline; + gap: 6px; + padding: 2px 8px; + border-radius: 999px; + border: 1px solid var(--border); + background: transparent; + font-size: 11px; + color: var(--text-muted); + } + .usage-metric-badge strong { + font-size: 12px; + color: var(--text); + } + .usage-controls { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + } + .usage-controls .active-filters { + flex: 1 1 100%; + } + .usage-controls input[type="date"] { + min-width: 140px; + } + .usage-presets { + display: inline-flex; + gap: 6px; + flex-wrap: wrap; + } + .usage-presets .btn { + padding: 4px 8px; + font-size: 11px; + } + .usage-quick-filters { + display: flex; + gap: 8px; + align-items: center; + flex-wrap: wrap; + } + .usage-select { + min-width: 120px; + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + color: var(--text); + font-size: 12px; + } + .usage-export-menu summary { + cursor: pointer; + font-weight: 500; + color: var(--text); + list-style: none; + display: inline-flex; + align-items: center; + gap: 6px; + } + .usage-export-menu summary::-webkit-details-marker { + display: none; + } + .usage-export-menu { + position: relative; + } + .usage-export-button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--bg); + font-size: 12px; + } + .usage-export-popover { + position: absolute; + right: 0; + top: calc(100% + 6px); + background: var(--bg); + border: 1px solid var(--border); + border-radius: 10px; + padding: 8px; + box-shadow: 0 10px 30px rgba(0,0,0,0.08); + min-width: 160px; + z-index: 10; + } + .usage-export-list { + display: flex; + flex-direction: column; + gap: 6px; + } + .usage-export-item { + text-align: left; + padding: 6px 10px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--bg-secondary); + font-size: 12px; + } + .usage-summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 12px; + margin-top: 12px; + } + .usage-summary-card { + padding: 12px; + border-radius: 8px; + background: var(--bg-secondary); + border: 1px solid var(--border); + } + .usage-mosaic { + margin-top: 16px; + padding: 16px; + } + .usage-mosaic-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; + } + .usage-mosaic-title { + font-weight: 600; + } + .usage-mosaic-sub { + font-size: 12px; + color: var(--text-muted); + } + .usage-mosaic-grid { + display: grid; + grid-template-columns: minmax(200px, 1fr) minmax(260px, 2fr); + gap: 16px; + align-items: start; + } + .usage-mosaic-section { + background: var(--bg-subtle); + border: 1px solid var(--border); + border-radius: 10px; + padding: 12px; + } + .usage-mosaic-section-title { + font-size: 12px; + font-weight: 600; + margin-bottom: 10px; + display: flex; + align-items: center; + justify-content: space-between; + } + .usage-mosaic-total { + font-size: 20px; + font-weight: 700; + } + .usage-daypart-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(90px, 1fr)); + gap: 8px; + } + .usage-daypart-cell { + border-radius: 8px; + padding: 10px; + color: var(--text); + background: rgba(255, 77, 77, 0.08); + border: 1px solid rgba(255, 77, 77, 0.2); + display: flex; + flex-direction: column; + gap: 4px; + } + .usage-daypart-label { + font-size: 12px; + font-weight: 600; + } + .usage-daypart-value { + font-size: 14px; + } + .usage-hour-grid { + display: grid; + grid-template-columns: repeat(24, minmax(6px, 1fr)); + gap: 4px; + } + .usage-hour-cell { + height: 28px; + border-radius: 6px; + background: rgba(255, 77, 77, 0.1); + border: 1px solid rgba(255, 77, 77, 0.2); + cursor: pointer; + transition: border-color 0.15s, box-shadow 0.15s; + } + .usage-hour-cell.selected { + border-color: rgba(255, 77, 77, 0.8); + box-shadow: 0 0 0 2px rgba(255, 77, 77, 0.2); + } + .usage-hour-labels { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 6px; + margin-top: 8px; + font-size: 11px; + color: var(--text-muted); + } + .usage-hour-legend { + display: flex; + gap: 8px; + align-items: center; + margin-top: 10px; + font-size: 11px; + color: var(--text-muted); + } + .usage-hour-legend span { + display: inline-block; + width: 14px; + height: 10px; + border-radius: 4px; + background: rgba(255, 77, 77, 0.15); + border: 1px solid rgba(255, 77, 77, 0.2); + } + .usage-calendar-labels { + display: grid; + grid-template-columns: repeat(7, minmax(10px, 1fr)); + gap: 6px; + font-size: 10px; + color: var(--text-muted); + margin-bottom: 6px; + } + .usage-calendar { + display: grid; + grid-template-columns: repeat(7, minmax(10px, 1fr)); + gap: 6px; + } + .usage-calendar-cell { + height: 18px; + border-radius: 4px; + border: 1px solid rgba(255, 77, 77, 0.2); + background: rgba(255, 77, 77, 0.08); + } + .usage-calendar-cell.empty { + background: transparent; + border-color: transparent; + } + .usage-summary-title { + font-size: 11px; + color: var(--text-muted); + margin-bottom: 6px; + display: inline-flex; + align-items: center; + gap: 6px; + } + .usage-info { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + margin-left: 6px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--bg); + font-size: 10px; + color: var(--text-muted); + cursor: help; + } + .usage-summary-value { + font-size: 16px; + font-weight: 600; + color: var(--text-strong); + } + .usage-summary-value.good { + color: #1f8f4e; + } + .usage-summary-value.warn { + color: #c57a00; + } + .usage-summary-value.bad { + color: #c9372c; + } + .usage-summary-hint { + font-size: 10px; + color: var(--text-muted); + cursor: help; + border: 1px solid var(--border); + border-radius: 999px; + padding: 0 6px; + line-height: 16px; + height: 16px; + display: inline-flex; + align-items: center; + justify-content: center; + } + .usage-summary-sub { + font-size: 11px; + color: var(--text-muted); + margin-top: 4px; + } + .usage-list { + display: flex; + flex-direction: column; + gap: 8px; + } + .usage-list-item { + display: flex; + justify-content: space-between; + gap: 12px; + font-size: 12px; + color: var(--text); + align-items: flex-start; + } + .usage-list-value { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + text-align: right; + } + .usage-list-sub { + font-size: 11px; + color: var(--text-muted); + } + .usage-list-item.button { + border: none; + background: transparent; + padding: 0; + text-align: left; + cursor: pointer; + } + .usage-list-item.button:hover { + color: var(--text-strong); + } + .usage-list-item .muted { + font-size: 11px; + } + .usage-error-list { + display: flex; + flex-direction: column; + gap: 10px; + } + .usage-error-row { + display: grid; + grid-template-columns: 1fr auto; + gap: 8px; + align-items: center; + font-size: 12px; + } + .usage-error-date { + font-weight: 600; + } + .usage-error-rate { + font-variant-numeric: tabular-nums; + } + .usage-error-sub { + grid-column: 1 / -1; + font-size: 11px; + color: var(--text-muted); + } + .usage-badges { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin-bottom: 8px; + } + .usage-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 8px; + border: 1px solid var(--border); + border-radius: 999px; + font-size: 11px; + background: var(--bg); + color: var(--text); + } + .usage-meta-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 12px; + } + .usage-meta-item { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; + } + .usage-meta-item span { + color: var(--text-muted); + font-size: 11px; + } + .usage-insights-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin-top: 12px; + } + .usage-insight-card { + padding: 14px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--bg-secondary); + } + .usage-insight-title { + font-size: 12px; + font-weight: 600; + margin-bottom: 10px; + } + .usage-insight-subtitle { + font-size: 11px; + color: var(--text-muted); + margin-top: 6px; + } + /* ===== CHART TOGGLE ===== */ + .chart-toggle { + display: flex; + background: var(--bg); + border-radius: 6px; + overflow: hidden; + border: 1px solid var(--border); + } + .chart-toggle .toggle-btn { + padding: 6px 14px; + font-size: 13px; + background: transparent; + border: none; + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s; + } + .chart-toggle .toggle-btn:hover { + color: var(--text); + } + .chart-toggle .toggle-btn.active { + background: #ff4d4d; + color: white; + } + .chart-toggle.small .toggle-btn { + padding: 4px 8px; + font-size: 11px; + } + .sessions-toggle { + border-radius: 4px; + } + .sessions-toggle .toggle-btn { + border-radius: 4px; + } + .daily-chart-header { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + margin-bottom: 6px; + } + + /* ===== DAILY BAR CHART ===== */ + .daily-chart { + margin-top: 12px; + } + .daily-chart-bars { + display: flex; + align-items: flex-end; + height: 200px; + gap: 4px; + padding: 8px 4px 36px; + } + .daily-bar-wrapper { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + height: 100%; + justify-content: flex-end; + cursor: pointer; + position: relative; + border-radius: 4px 4px 0 0; + transition: background 0.15s; + min-width: 0; + } + .daily-bar-wrapper:hover { + background: var(--bg-hover); + } + .daily-bar-wrapper.selected { + background: var(--accent-subtle); + } + .daily-bar-wrapper.selected .daily-bar { + background: var(--accent); + } + .daily-bar { + width: 100%; + max-width: var(--bar-max-width, 32px); + background: #ff4d4d; + border-radius: 3px 3px 0 0; + min-height: 2px; + transition: all 0.15s; + overflow: hidden; + } + .daily-bar-wrapper:hover .daily-bar { + background: #cc3d3d; + } + .daily-bar-label { + position: absolute; + bottom: -28px; + font-size: 10px; + color: var(--text-muted); + white-space: nowrap; + text-align: center; + transform: rotate(-35deg); + transform-origin: top center; + } + .daily-bar-total { + position: absolute; + top: -16px; + left: 50%; + transform: translateX(-50%); + font-size: 10px; + color: var(--text-muted); + white-space: nowrap; + } + .daily-bar-tooltip { + position: absolute; + bottom: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + background: var(--bg); + border: 1px solid var(--border); + border-radius: 6px; + padding: 8px 12px; + font-size: 12px; + white-space: nowrap; + z-index: 100; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + pointer-events: none; + opacity: 0; + transition: opacity 0.15s; + } + .daily-bar-wrapper:hover .daily-bar-tooltip { + opacity: 1; + } + + /* ===== COST/TOKEN BREAKDOWN BAR ===== */ + .cost-breakdown { + margin-top: 18px; + padding: 16px; + background: var(--bg-secondary); + border-radius: 8px; + } + .cost-breakdown-header { + font-weight: 600; + font-size: 15px; + letter-spacing: -0.02em; + margin-bottom: 12px; + color: var(--text-strong); + } + .cost-breakdown-bar { + height: 28px; + background: var(--bg); + border-radius: 6px; + overflow: hidden; + display: flex; + } + .cost-segment { + height: 100%; + transition: width 0.3s ease; + position: relative; + } + .cost-segment.output { + background: #ef4444; + } + .cost-segment.input { + background: #f59e0b; + } + .cost-segment.cache-write { + background: #10b981; + } + .cost-segment.cache-read { + background: #06b6d4; + } + .cost-breakdown-legend { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 12px; + } + .cost-breakdown-total { + margin-top: 10px; + font-size: 12px; + color: var(--text-muted); + } + .legend-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text); + cursor: help; + } + .legend-dot { + width: 10px; + height: 10px; + border-radius: 2px; + flex-shrink: 0; + } + .legend-dot.output { + background: #ef4444; + } + .legend-dot.input { + background: #f59e0b; + } + .legend-dot.cache-write { + background: #10b981; + } + .legend-dot.cache-read { + background: #06b6d4; + } + .legend-dot.system { + background: #ff4d4d; + } + .legend-dot.skills { + background: #8b5cf6; + } + .legend-dot.tools { + background: #ec4899; + } + .legend-dot.files { + background: #f59e0b; + } + .cost-breakdown-note { + margin-top: 10px; + font-size: 11px; + color: var(--text-muted); + line-height: 1.4; + } + + /* ===== SESSION BARS (scrollable list) ===== */ + .session-bars { + margin-top: 16px; + max-height: 400px; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg); + } + .session-bar-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + border-bottom: 1px solid var(--border); + cursor: pointer; + transition: background 0.15s; + } + .session-bar-row:last-child { + border-bottom: none; + } + .session-bar-row:hover { + background: var(--bg-hover); + } + .session-bar-row.selected { + background: var(--accent-subtle); + } + .session-bar-label { + flex: 1 1 auto; + min-width: 0; + font-size: 13px; + color: var(--text); + display: flex; + flex-direction: column; + gap: 2px; + } + .session-bar-title { + /* Prefer showing the full name; wrap instead of truncating. */ + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; + } + .session-bar-meta { + font-size: 10px; + color: var(--text-muted); + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .session-bar-track { + flex: 0 0 90px; + height: 6px; + background: var(--bg-secondary); + border-radius: 4px; + overflow: hidden; + opacity: 0.6; + } + .session-bar-fill { + height: 100%; + background: rgba(255, 77, 77, 0.7); + border-radius: 4px; + transition: width 0.3s ease; + } + .session-bar-value { + flex: 0 0 70px; + text-align: right; + font-size: 12px; + font-family: var(--font-mono); + color: var(--text-muted); + } + .session-bar-actions { + display: inline-flex; + align-items: center; + gap: 8px; + flex: 0 0 auto; + } + .session-copy-btn { + height: 26px; + padding: 0 10px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--bg-secondary); + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + cursor: pointer; + transition: background 0.15s, border-color 0.15s, color 0.15s; + } + .session-copy-btn:hover { + background: var(--bg); + border-color: var(--border-strong); + color: var(--text); + } + + /* ===== TIME SERIES CHART ===== */ + .session-timeseries { + margin-top: 24px; + padding: 16px; + background: var(--bg-secondary); + border-radius: 8px; + } + .timeseries-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + } + .timeseries-controls { + display: flex; + gap: 6px; + align-items: center; + } + .timeseries-header { + font-weight: 600; + color: var(--text); + } + .timeseries-chart { + width: 100%; + overflow: hidden; + } + .timeseries-svg { + width: 100%; + height: auto; + display: block; + } + .timeseries-svg .axis-label { + font-size: 10px; + fill: var(--text-muted); + } + .timeseries-svg .ts-area { + fill: #ff4d4d; + fill-opacity: 0.1; + } + .timeseries-svg .ts-line { + fill: none; + stroke: #ff4d4d; + stroke-width: 2; + } + .timeseries-svg .ts-dot { + fill: #ff4d4d; + transition: r 0.15s, fill 0.15s; + } + .timeseries-svg .ts-dot:hover { + r: 5; + } + .timeseries-svg .ts-bar { + fill: #ff4d4d; + transition: fill 0.15s; + } + .timeseries-svg .ts-bar:hover { + fill: #cc3d3d; + } + .timeseries-svg .ts-bar.output { fill: #ef4444; } + .timeseries-svg .ts-bar.input { fill: #f59e0b; } + .timeseries-svg .ts-bar.cache-write { fill: #10b981; } + .timeseries-svg .ts-bar.cache-read { fill: #06b6d4; } + .timeseries-summary { + margin-top: 12px; + font-size: 13px; + color: var(--text-muted); + display: flex; + flex-wrap: wrap; + gap: 8px; + } + .timeseries-loading { + padding: 24px; + text-align: center; + color: var(--text-muted); + } + + /* ===== SESSION LOGS ===== */ + .session-logs { + margin-top: 24px; + background: var(--bg-secondary); + border-radius: 8px; + overflow: hidden; + } + .session-logs-header { + padding: 10px 14px; + font-weight: 600; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; + font-size: 13px; + background: var(--bg-secondary); + } + .session-logs-loading { + padding: 24px; + text-align: center; + color: var(--text-muted); + } + .session-logs-list { + max-height: 400px; + overflow-y: auto; + } + .session-log-entry { + padding: 10px 14px; + border-bottom: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 6px; + background: var(--bg); + } + .session-log-entry:last-child { + border-bottom: none; + } + .session-log-entry.user { + border-left: 3px solid var(--accent); + } + .session-log-entry.assistant { + border-left: 3px solid var(--border-strong); + } + .session-log-meta { + display: flex; + gap: 8px; + align-items: center; + font-size: 11px; + color: var(--text-muted); + flex-wrap: wrap; + } + .session-log-role { + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + font-size: 10px; + padding: 2px 6px; + border-radius: 999px; + background: var(--bg-secondary); + border: 1px solid var(--border); + } + .session-log-entry.user .session-log-role { + color: var(--accent); + } + .session-log-entry.assistant .session-log-role { + color: var(--text-muted); + } + .session-log-content { + font-size: 13px; + line-height: 1.5; + color: var(--text); + white-space: pre-wrap; + word-break: break-word; + background: var(--bg-secondary); + border-radius: 8px; + padding: 8px 10px; + border: 1px solid var(--border); + max-height: 220px; + overflow-y: auto; + } + + /* ===== CONTEXT WEIGHT BREAKDOWN ===== */ + .context-weight-breakdown { + margin-top: 24px; + padding: 16px; + background: var(--bg-secondary); + border-radius: 8px; + } + .context-weight-breakdown .context-weight-header { + font-weight: 600; + font-size: 13px; + margin-bottom: 4px; + color: var(--text); + } + .context-weight-desc { + font-size: 12px; + color: var(--text-muted); + margin: 0 0 12px 0; + } + .context-stacked-bar { + height: 24px; + background: var(--bg); + border-radius: 6px; + overflow: hidden; + display: flex; + } + .context-segment { + height: 100%; + transition: width 0.3s ease; + } + .context-segment.system { + background: #ff4d4d; + } + .context-segment.skills { + background: #8b5cf6; + } + .context-segment.tools { + background: #ec4899; + } + .context-segment.files { + background: #f59e0b; + } + .context-legend { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 12px; + } + .context-total { + margin-top: 10px; + font-size: 12px; + font-weight: 600; + color: var(--text-muted); + } + .context-details { + margin-top: 12px; + border: 1px solid var(--border); + border-radius: 6px; + overflow: hidden; + } + .context-details summary { + padding: 10px 14px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + background: var(--bg); + border-bottom: 1px solid var(--border); + } + .context-details[open] summary { + border-bottom: 1px solid var(--border); + } + .context-list { + max-height: 200px; + overflow-y: auto; + } + .context-list-header { + display: flex; + justify-content: space-between; + padding: 8px 14px; + font-size: 11px; + text-transform: uppercase; + color: var(--text-muted); + background: var(--bg-secondary); + border-bottom: 1px solid var(--border); + } + .context-list-item { + display: flex; + justify-content: space-between; + padding: 8px 14px; + font-size: 12px; + border-bottom: 1px solid var(--border); + } + .context-list-item:last-child { + border-bottom: none; + } + .context-list-item .mono { + font-family: var(--font-mono); + color: var(--text); + } + .context-list-item .muted { + color: var(--text-muted); + font-family: var(--font-mono); + } + + /* ===== NO CONTEXT NOTE ===== */ + .no-context-note { + margin-top: 24px; + padding: 16px; + background: var(--bg-secondary); + border-radius: 8px; + font-size: 13px; + color: var(--text-muted); + line-height: 1.5; + } + + /* ===== TWO COLUMN LAYOUT ===== */ + .usage-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; + margin-top: 18px; + align-items: stretch; + } + .usage-grid-left { + display: flex; + flex-direction: column; + } + .usage-grid-right { + display: flex; + flex-direction: column; + } + + /* ===== LEFT CARD (Daily + Breakdown) ===== */ + .usage-left-card { + /* inherits background, border, shadow from .card */ + flex: 1; + display: flex; + flex-direction: column; + } + .usage-left-card .daily-chart-bars { + flex: 1; + min-height: 200px; + } + .usage-left-card .sessions-panel-title { + font-weight: 600; + font-size: 14px; + margin-bottom: 12px; + } + + /* ===== COMPACT DAILY CHART ===== */ + .daily-chart-compact { + margin-bottom: 16px; + } + .daily-chart-compact .sessions-panel-title { + margin-bottom: 8px; + } + .daily-chart-compact .daily-chart-bars { + height: 100px; + padding-bottom: 20px; + } + + /* ===== COMPACT COST BREAKDOWN ===== */ + .cost-breakdown-compact { + padding: 0; + margin: 0; + background: transparent; + border-top: 1px solid var(--border); + padding-top: 12px; + } + .cost-breakdown-compact .cost-breakdown-header { + margin-bottom: 8px; + } + .cost-breakdown-compact .cost-breakdown-legend { + gap: 12px; + } + .cost-breakdown-compact .cost-breakdown-note { + display: none; + } + + /* ===== SESSIONS CARD ===== */ + .sessions-card { + /* inherits background, border, shadow from .card */ + flex: 1; + display: flex; + flex-direction: column; + } + .sessions-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + .sessions-card-title { + font-weight: 600; + font-size: 14px; + } + .sessions-card-count { + font-size: 12px; + color: var(--text-muted); + } + .sessions-card-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin: 8px 0 10px; + font-size: 12px; + color: var(--text-muted); + } + .sessions-card-stats { + display: inline-flex; + gap: 12px; + } + .sessions-sort { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text-muted); + } + .sessions-sort select { + padding: 4px 8px; + border-radius: 6px; + border: 1px solid var(--border); + background: var(--bg); + color: var(--text); + font-size: 12px; + } + .sessions-action-btn { + height: 28px; + padding: 0 10px; + border-radius: 8px; + font-size: 12px; + line-height: 1; + } + .sessions-action-btn.icon { + width: 32px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + } + .sessions-card-hint { + font-size: 11px; + color: var(--text-muted); + margin-bottom: 8px; + } + .sessions-card .session-bars { + max-height: 280px; + background: var(--bg); + border-radius: 6px; + border: 1px solid var(--border); + margin: 0; + overflow-y: auto; + padding: 8px; + } + .sessions-card .session-bar-row { + padding: 6px 8px; + border-radius: 6px; + margin-bottom: 3px; + border: 1px solid transparent; + transition: all 0.15s; + } + .sessions-card .session-bar-row:hover { + border-color: var(--border); + background: var(--bg-hover); + } + .sessions-card .session-bar-row.selected { + border-color: var(--accent); + background: var(--accent-subtle); + box-shadow: inset 0 0 0 1px rgba(255, 77, 77, 0.15); + } + .sessions-card .session-bar-label { + flex: 1 1 auto; + min-width: 140px; + font-size: 12px; + } + .sessions-card .session-bar-value { + flex: 0 0 60px; + font-size: 11px; + font-weight: 600; + } + .sessions-card .session-bar-track { + flex: 0 0 70px; + height: 5px; + opacity: 0.5; + } + .sessions-card .session-bar-fill { + background: rgba(255, 77, 77, 0.55); + } + .sessions-clear-btn { + margin-left: auto; + } + + /* ===== EMPTY DETAIL STATE ===== */ + .session-detail-empty { + margin-top: 18px; + background: var(--bg-secondary); + border-radius: 8px; + border: 2px dashed var(--border); + padding: 32px; + text-align: center; + } + .session-detail-empty-title { + font-size: 15px; + font-weight: 600; + color: var(--text); + margin-bottom: 8px; + } + .session-detail-empty-desc { + font-size: 13px; + color: var(--text-muted); + margin-bottom: 16px; + line-height: 1.5; + } + .session-detail-empty-features { + display: flex; + justify-content: center; + gap: 24px; + flex-wrap: wrap; + } + .session-detail-empty-feature { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--text-muted); + } + .session-detail-empty-feature .icon { + font-size: 16px; + } + + /* ===== SESSION DETAIL PANEL ===== */ + .session-detail-panel { + margin-top: 12px; + /* inherits background, border-radius, shadow from .card */ + border: 2px solid var(--accent) !important; + } + .session-detail-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 12px; + border-bottom: 1px solid var(--border); + cursor: pointer; + } + .session-detail-header:hover { + background: var(--bg-hover); + } + .session-detail-title { + font-weight: 600; + font-size: 14px; + display: flex; + align-items: center; + gap: 8px; + } + .session-detail-header-left { + display: flex; + align-items: center; + gap: 8px; + } + .session-close-btn { + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + cursor: pointer; + padding: 2px 8px; + font-size: 16px; + line-height: 1; + border-radius: 4px; + transition: background 0.15s, color 0.15s; + } + .session-close-btn:hover { + background: var(--bg-hover); + color: var(--text); + border-color: var(--accent); + } + .session-detail-stats { + display: flex; + gap: 10px; + font-size: 12px; + color: var(--text-muted); + } + .session-detail-stats strong { + color: var(--text); + font-family: var(--font-mono); + } + .session-detail-content { + padding: 12px; + } + .session-summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 8px; + margin-bottom: 12px; + } + .session-summary-card { + border: 1px solid var(--border); + border-radius: 8px; + padding: 8px; + background: var(--bg-secondary); + } + .session-summary-title { + font-size: 11px; + color: var(--text-muted); + margin-bottom: 4px; + } + .session-summary-value { + font-size: 14px; + font-weight: 600; + } + .session-summary-meta { + font-size: 11px; + color: var(--text-muted); + margin-top: 4px; + } + .session-detail-row { + display: grid; + grid-template-columns: 1fr; + gap: 10px; + /* Separate "Usage Over Time" from the summary + Top Tools/Model Mix cards above. */ + margin-top: 12px; + margin-bottom: 10px; + } + .session-detail-bottom { + display: grid; + grid-template-columns: minmax(0, 1.8fr) minmax(0, 1fr); + gap: 10px; + align-items: stretch; + } + .session-detail-bottom .session-logs-compact { + margin: 0; + display: flex; + flex-direction: column; + } + .session-detail-bottom .session-logs-compact .session-logs-list { + flex: 1 1 auto; + max-height: none; + } + .context-details-panel { + display: flex; + flex-direction: column; + gap: 8px; + background: var(--bg); + border-radius: 6px; + border: 1px solid var(--border); + padding: 12px; + } + .context-breakdown-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 10px; + margin-top: 8px; + } + .context-breakdown-card { + border: 1px solid var(--border); + border-radius: 8px; + padding: 8px; + background: var(--bg-secondary); + } + .context-breakdown-title { + font-size: 11px; + font-weight: 600; + margin-bottom: 6px; + } + .context-breakdown-list { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 11px; + } + .context-breakdown-item { + display: flex; + justify-content: space-between; + gap: 8px; + } + .context-breakdown-more { + font-size: 10px; + color: var(--text-muted); + margin-top: 4px; + } + .context-breakdown-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + .context-expand-btn { + border: 1px solid var(--border); + background: var(--bg-secondary); + color: var(--text-muted); + font-size: 11px; + padding: 4px 8px; + border-radius: 999px; + cursor: pointer; + transition: all 0.15s; + } + .context-expand-btn:hover { + color: var(--text); + border-color: var(--border-strong); + background: var(--bg); + } + + /* ===== COMPACT TIMESERIES ===== */ + .session-timeseries-compact { + background: var(--bg); + border-radius: 6px; + border: 1px solid var(--border); + padding: 12px; + margin: 0; + } + .session-timeseries-compact .timeseries-header-row { + margin-bottom: 8px; + } + .session-timeseries-compact .timeseries-header { + font-size: 12px; + } + .session-timeseries-compact .timeseries-summary { + font-size: 11px; + margin-top: 8px; + } + + /* ===== COMPACT CONTEXT ===== */ + .context-weight-compact { + background: var(--bg); + border-radius: 6px; + border: 1px solid var(--border); + padding: 12px; + margin: 0; + } + .context-weight-compact .context-weight-header { + font-size: 12px; + margin-bottom: 4px; + } + .context-weight-compact .context-weight-desc { + font-size: 11px; + margin-bottom: 8px; + } + .context-weight-compact .context-stacked-bar { + height: 16px; + } + .context-weight-compact .context-legend { + font-size: 11px; + gap: 10px; + margin-top: 8px; + } + .context-weight-compact .context-total { + font-size: 11px; + margin-top: 6px; + } + .context-weight-compact .context-details { + margin-top: 8px; + } + .context-weight-compact .context-details summary { + font-size: 12px; + padding: 6px 10px; + } + + /* ===== COMPACT LOGS ===== */ + .session-logs-compact { + background: var(--bg); + border-radius: 10px; + border: 1px solid var(--border); + overflow: hidden; + margin: 0; + display: flex; + flex-direction: column; + } + .session-logs-compact .session-logs-header { + padding: 10px 12px; + font-size: 12px; + } + .session-logs-compact .session-logs-list { + max-height: none; + flex: 1 1 auto; + overflow: auto; + } + .session-logs-compact .session-log-entry { + padding: 8px 12px; + } + .session-logs-compact .session-log-content { + font-size: 12px; + max-height: 160px; + } + .session-log-tools { + margin-top: 6px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-secondary); + padding: 6px 8px; + font-size: 11px; + color: var(--text); + } + .session-log-tools summary { + cursor: pointer; + list-style: none; + display: flex; + align-items: center; + gap: 6px; + font-weight: 600; + } + .session-log-tools summary::-webkit-details-marker { + display: none; + } + .session-log-tools-list { + margin-top: 6px; + display: flex; + flex-wrap: wrap; + gap: 6px; + } + .session-log-tools-pill { + border: 1px solid var(--border); + border-radius: 999px; + padding: 2px 8px; + font-size: 10px; + background: var(--bg); + color: var(--text); + } + + /* ===== RESPONSIVE ===== */ + @media (max-width: 900px) { + .usage-grid { + grid-template-columns: 1fr; + } + .session-detail-row { + grid-template-columns: 1fr; + } + } + @media (max-width: 600px) { + .session-bar-label { + flex: 0 0 100px; + } + .cost-breakdown-legend { + gap: 10px; + } + .legend-item { + font-size: 11px; + } + .daily-chart-bars { + height: 170px; + gap: 6px; + padding-bottom: 40px; + } + .daily-bar-label { + font-size: 8px; + bottom: -30px; + transform: rotate(-45deg); + } + .usage-mosaic-grid { + grid-template-columns: 1fr; + } + .usage-hour-grid { + grid-template-columns: repeat(12, minmax(10px, 1fr)); + } + .usage-hour-cell { + height: 22px; + } + } +`; diff --git a/ui/src/ui/views/usageTypes.ts b/ui/src/ui/views/usageTypes.ts new file mode 100644 index 0000000000..7b73ea902c --- /dev/null +++ b/ui/src/ui/views/usageTypes.ts @@ -0,0 +1,285 @@ +export type UsageSessionEntry = { + key: string; + label?: string; + sessionId?: string; + updatedAt?: number; + agentId?: string; + channel?: string; + chatType?: string; + origin?: { + label?: string; + provider?: string; + surface?: string; + chatType?: string; + from?: string; + to?: string; + accountId?: string; + threadId?: string | number; + }; + modelOverride?: string; + providerOverride?: string; + modelProvider?: string; + model?: string; + usage: { + input: number; + output: number; + cacheRead: number; + cacheWrite: number; + totalTokens: number; + totalCost: number; + inputCost?: number; + outputCost?: number; + cacheReadCost?: number; + cacheWriteCost?: number; + missingCostEntries: number; + firstActivity?: number; + lastActivity?: number; + durationMs?: number; + activityDates?: string[]; // YYYY-MM-DD dates when session had activity + dailyBreakdown?: Array<{ date: string; tokens: number; cost: number }>; // Per-day breakdown + dailyMessageCounts?: Array<{ + date: string; + total: number; + user: number; + assistant: number; + toolCalls: number; + toolResults: number; + errors: number; + }>; + dailyLatency?: Array<{ + date: string; + count: number; + avgMs: number; + p95Ms: number; + minMs: number; + maxMs: number; + }>; + dailyModelUsage?: Array<{ + date: string; + provider?: string; + model?: string; + tokens: number; + cost: number; + count: number; + }>; + messageCounts?: { + total: number; + user: number; + assistant: number; + toolCalls: number; + toolResults: number; + errors: number; + }; + toolUsage?: { + totalCalls: number; + uniqueTools: number; + tools: Array<{ name: string; count: number }>; + }; + modelUsage?: Array<{ + provider?: string; + model?: string; + count: number; + totals: UsageTotals; + }>; + latency?: { + count: number; + avgMs: number; + p95Ms: number; + minMs: number; + maxMs: number; + }; + } | null; + contextWeight?: { + systemPrompt: { chars: number; projectContextChars: number; nonProjectContextChars: number }; + skills: { promptChars: number; entries: Array<{ name: string; blockChars: number }> }; + tools: { + listChars: number; + schemaChars: number; + entries: Array<{ name: string; summaryChars: number; schemaChars: number }>; + }; + injectedWorkspaceFiles: Array<{ + name: string; + path: string; + rawChars: number; + injectedChars: number; + truncated: boolean; + }>; + } | null; +}; + +export type UsageTotals = { + input: number; + output: number; + cacheRead: number; + cacheWrite: number; + totalTokens: number; + totalCost: number; + inputCost: number; + outputCost: number; + cacheReadCost: number; + cacheWriteCost: number; + missingCostEntries: number; +}; + +export type CostDailyEntry = UsageTotals & { date: string }; + +export type UsageAggregates = { + messages: { + total: number; + user: number; + assistant: number; + toolCalls: number; + toolResults: number; + errors: number; + }; + tools: { + totalCalls: number; + uniqueTools: number; + tools: Array<{ name: string; count: number }>; + }; + byModel: Array<{ + provider?: string; + model?: string; + count: number; + totals: UsageTotals; + }>; + byProvider: Array<{ + provider?: string; + model?: string; + count: number; + totals: UsageTotals; + }>; + byAgent: Array<{ agentId: string; totals: UsageTotals }>; + byChannel: Array<{ channel: string; totals: UsageTotals }>; + latency?: { + count: number; + avgMs: number; + p95Ms: number; + minMs: number; + maxMs: number; + }; + dailyLatency?: Array<{ + date: string; + count: number; + avgMs: number; + p95Ms: number; + minMs: number; + maxMs: number; + }>; + modelDaily?: Array<{ + date: string; + provider?: string; + model?: string; + tokens: number; + cost: number; + count: number; + }>; + daily: Array<{ + date: string; + tokens: number; + cost: number; + messages: number; + toolCalls: number; + errors: number; + }>; +}; + +export type UsageColumnId = + | "channel" + | "agent" + | "provider" + | "model" + | "messages" + | "tools" + | "errors" + | "duration"; + +export type TimeSeriesPoint = { + timestamp: number; + input: number; + output: number; + cacheRead: number; + cacheWrite: number; + totalTokens: number; + cost: number; + cumulativeTokens: number; + cumulativeCost: number; +}; + +export type UsageProps = { + loading: boolean; + error: string | null; + startDate: string; + endDate: string; + sessions: UsageSessionEntry[]; + sessionsLimitReached: boolean; // True if 1000 session cap was hit + totals: UsageTotals | null; + aggregates: UsageAggregates | null; + costDaily: CostDailyEntry[]; + selectedSessions: string[]; // Support multiple session selection + selectedDays: string[]; // Support multiple day selection + selectedHours: number[]; // Support multiple hour selection + chartMode: "tokens" | "cost"; + dailyChartMode: "total" | "by-type"; + timeSeriesMode: "cumulative" | "per-turn"; + timeSeriesBreakdownMode: "total" | "by-type"; + timeSeries: { points: TimeSeriesPoint[] } | null; + timeSeriesLoading: boolean; + sessionLogs: SessionLogEntry[] | null; + sessionLogsLoading: boolean; + sessionLogsExpanded: boolean; + logFilterRoles: SessionLogRole[]; + logFilterTools: string[]; + logFilterHasTools: boolean; + logFilterQuery: string; + query: string; + queryDraft: string; + sessionSort: "tokens" | "cost" | "recent" | "messages" | "errors"; + sessionSortDir: "asc" | "desc"; + recentSessions: string[]; + sessionsTab: "all" | "recent"; + visibleColumns: UsageColumnId[]; + timeZone: "local" | "utc"; + contextExpanded: boolean; + headerPinned: boolean; + onStartDateChange: (date: string) => void; + onEndDateChange: (date: string) => void; + onRefresh: () => void; + onTimeZoneChange: (zone: "local" | "utc") => void; + onToggleContextExpanded: () => void; + onToggleHeaderPinned: () => void; + onToggleSessionLogsExpanded: () => void; + onLogFilterRolesChange: (next: SessionLogRole[]) => void; + onLogFilterToolsChange: (next: string[]) => void; + onLogFilterHasToolsChange: (next: boolean) => void; + onLogFilterQueryChange: (next: string) => void; + onLogFilterClear: () => void; + onSelectSession: (key: string, shiftKey: boolean) => void; + onChartModeChange: (mode: "tokens" | "cost") => void; + onDailyChartModeChange: (mode: "total" | "by-type") => void; + onTimeSeriesModeChange: (mode: "cumulative" | "per-turn") => void; + onTimeSeriesBreakdownChange: (mode: "total" | "by-type") => void; + onSelectDay: (day: string, shiftKey: boolean) => void; // Support shift-click + onSelectHour: (hour: number, shiftKey: boolean) => void; + onClearDays: () => void; + onClearHours: () => void; + onClearSessions: () => void; + onClearFilters: () => void; + onQueryDraftChange: (query: string) => void; + onApplyQuery: () => void; + onClearQuery: () => void; + onSessionSortChange: (sort: "tokens" | "cost" | "recent" | "messages" | "errors") => void; + onSessionSortDirChange: (dir: "asc" | "desc") => void; + onSessionsTabChange: (tab: "all" | "recent") => void; + onToggleColumn: (column: UsageColumnId) => void; +}; + +export type SessionLogEntry = { + timestamp: number; + role: "user" | "assistant" | "tool" | "toolResult"; + content: string; + tokens?: number; + cost?: number; +}; + +export type SessionLogRole = SessionLogEntry["role"];