mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(ui): derive gateway types from protocol and consolidate navigation/error helpers (#120348)
* refactor(protocol): derive shared gateway types * refactor(ui): consolidate navigation click handling * refactor(ui): centralize UI state and error helpers
This commit is contained in:
committed by
GitHub
parent
fb0812c857
commit
8bf62e7007
@@ -122,7 +122,7 @@ aa8a411ad37c1d1143b67376bf2d20255b9eedff61d80815f42e4f8ed7bd8e58 module/secret-
|
||||
57dcb1462d4c4f9a98d934c4ca975b163d704758af9821a64001ff3ac05637c3 module/secret-input-runtime
|
||||
dc0ee07d392a85c218939000b28c0138f139215da00f5592b34a68ba8e29a25d module/secret-ref-runtime
|
||||
f97549081955e412d8eb64070c64bb5921bb1324744d824db05767d3adcce403 module/security-runtime
|
||||
1d4a6e7e97a21c95c47be75e0e1745daa42d6ea37d956dce3c114277f42c9906 module/session-catalog
|
||||
4bfa843c91a04a4cff0496ed89d9b9481c07b8dbdce6de784e2743f4f66afc1d module/session-catalog
|
||||
50f5e344f98c27570b7a30e32a906b612e2383d21f102e88cd93e1d5425a6de9 module/session-discussion
|
||||
f112bdabc51ba8659b37d0a6f6a32a2b1d471e5b49b56e108bf750ec55a7ea71 module/session-store-runtime
|
||||
36affbe151431a6141664b6838e20f2d121ff210d57a3c1b4b41a8818b5c81d8 module/setup
|
||||
|
||||
@@ -29,6 +29,7 @@ export {
|
||||
SessionToolOverridesSchema,
|
||||
type SessionCreatedActor,
|
||||
type SessionRow,
|
||||
type SessionRunStatus,
|
||||
type SessionToolOverrides,
|
||||
} from "./schema/sessions-row.js";
|
||||
export * from "./schema/session-classification.js";
|
||||
|
||||
@@ -109,6 +109,9 @@ const RunToolBindingsSchema = Type.Record(
|
||||
{ maxProperties: 16 },
|
||||
);
|
||||
|
||||
const QUEUE_MODES = ["steer", "followup", "collect", "interrupt"] as const;
|
||||
export type QueueMode = (typeof QUEUE_MODES)[number];
|
||||
|
||||
/** User-to-agent send request; idempotency key lets clients safely retry transport failures. */
|
||||
export const ChatSendParamsSchema = closedObject({
|
||||
sessionKey: ChatSendSessionKeyString,
|
||||
@@ -120,7 +123,7 @@ export const ChatSendParamsSchema = closedObject({
|
||||
// One-turn override for auto fast-mode cutoff seconds.
|
||||
fastAutoOnSeconds: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
// One-turn override for active-run queue admission.
|
||||
queueMode: Type.Optional(Type.String({ enum: ["steer", "followup", "collect", "interrupt"] })),
|
||||
queueMode: Type.Optional(Type.String({ enum: [...QUEUE_MODES] })),
|
||||
deliver: Type.Optional(Type.Boolean()),
|
||||
originatingChannel: Type.Optional(Type.String()),
|
||||
originatingTo: Type.Optional(Type.String()),
|
||||
|
||||
@@ -135,3 +135,4 @@ export const SessionRowSchema = Type.Object(
|
||||
export type SessionCreatedActor = Static<typeof SessionCreatedActorSchema>;
|
||||
export type SessionToolOverrides = Static<typeof SessionToolOverridesSchema>;
|
||||
export type SessionRow = Static<typeof SessionRowSchema>;
|
||||
export type SessionRunStatus = NonNullable<SessionRow["status"]>;
|
||||
|
||||
@@ -17,6 +17,7 @@ export {
|
||||
SessionToolOverridesSchema,
|
||||
type SessionCreatedActor,
|
||||
type SessionRow,
|
||||
type SessionRunStatus,
|
||||
type SessionToolOverrides,
|
||||
} from "./sessions-row.js";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Shared sessions_spawn test harness for gateway, registry, and lifecycle mocks.
|
||||
import { vi, type Mock } from "vitest";
|
||||
import type { SessionRunStatus } from "../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||
import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js";
|
||||
import { resolveRequesterStoreKey } from "./subagent-requester-store-key.js";
|
||||
|
||||
@@ -22,7 +23,7 @@ type TestSessionEntry = {
|
||||
updatedAt: number;
|
||||
startedAt?: number;
|
||||
endedAt?: number;
|
||||
status?: "running" | "done" | "failed" | "killed" | "timeout";
|
||||
status?: SessionRunStatus;
|
||||
};
|
||||
type SessionsSpawnGatewayMockOptions = {
|
||||
includeSessionsList?: boolean;
|
||||
|
||||
@@ -21,6 +21,7 @@ export {
|
||||
shouldResolveSessionIdInput,
|
||||
} from "./sessions-resolution.js";
|
||||
import { normalizeOptionalString, type FastMode } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { SessionRunStatus } from "../../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||
import { getRuntimeConfig } from "../../config/config.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { parseRawSessionConversationRef } from "../../sessions/session-key-utils.js";
|
||||
@@ -37,9 +38,6 @@ type SessionListDeliveryContext = {
|
||||
threadId?: string | number;
|
||||
};
|
||||
|
||||
/** Compact run status shown by session tools. */
|
||||
export type SessionRunStatus = "running" | "done" | "failed" | "killed" | "timeout";
|
||||
|
||||
/** Full Gateway session row consumed by session orchestration internals. */
|
||||
export type GatewaySessionListRow = {
|
||||
key: string;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@openclaw/normalization-core/string-coerce";
|
||||
import pMap from "p-map";
|
||||
import { Type } from "typebox";
|
||||
import type { SessionRunStatus } from "../../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||
import { getRuntimeConfig } from "../../config/config.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
@@ -47,7 +48,6 @@ import {
|
||||
resolveSandboxedSessionToolContext,
|
||||
type GatewaySessionListRow,
|
||||
type SessionListRow,
|
||||
type SessionRunStatus,
|
||||
} from "./sessions-helpers.js";
|
||||
|
||||
const SessionsListToolSchema = Type.Object({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { FastMode } from "@openclaw/normalization-core/string-coerce";
|
||||
// Parses inline reply directives into typed execution and routing options.
|
||||
import type { QueueMode } from "../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import type { ExecAsk, ExecSecurity, ExecTarget } from "../../infra/exec-approvals.js";
|
||||
import { extractModelDirective } from "../model.js";
|
||||
import { isSessionDefaultDirectiveValue } from "../thinking.js";
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
extractVerboseDirective,
|
||||
} from "./directives.js";
|
||||
import { extractQueueDirective } from "./queue/directive.js";
|
||||
import type { QueueDropPolicy, QueueMode } from "./queue/types.js";
|
||||
import type { QueueDropPolicy } from "./queue/types.js";
|
||||
|
||||
const NATIVE_REPLY_DIRECTIVE_COMMANDS = {
|
||||
think: true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { QueueMode } from "../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import type { SessionToolOverrides } from "../../config/sessions/types.js";
|
||||
// Shared get-reply type contracts for command, directive, and runtime layers.
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
@@ -5,7 +6,7 @@ import type { ReplyOptionsWithHeartbeatRunScope } from "../../infra/heartbeat-ru
|
||||
import type { GetReplyOptions } from "../get-reply-options.types.js";
|
||||
import type { ReplyPayload } from "../reply-payload.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import type { FollowupQueueDisposition, QueueMode } from "./queue/types.js";
|
||||
import type { FollowupQueueDisposition } from "./queue/types.js";
|
||||
import type { ReplyOptionsWithOperationRunState } from "./reply-operation-run-state.js";
|
||||
import type { ReplyOperation } from "./reply-run-registry.js";
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Converts queue directives into normalized queue settings.
|
||||
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { QueueMode } from "../../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import { parseDurationMs } from "../../../cli/parse-duration.js";
|
||||
import { parseStrictPositiveInteger } from "../../../infra/parse-finite-number.js";
|
||||
import { skipDirectiveArgPrefix, takeDirectiveToken } from "../directive-parsing.js";
|
||||
import { normalizeQueueDropPolicy, normalizeQueueMode } from "./normalize.js";
|
||||
import type { QueueDropPolicy, QueueMode } from "./types.js";
|
||||
import type { QueueDropPolicy } from "./types.js";
|
||||
|
||||
/** Parses debounce durations in `/queue` directives. */
|
||||
function parseQueueDebounce(raw?: string): number | undefined {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Normalizes queue config values from user and persisted settings.
|
||||
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { QueueDropPolicy, QueueMode } from "./types.js";
|
||||
import type { QueueMode } from "../../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import type { QueueDropPolicy } from "./types.js";
|
||||
|
||||
/** Normalizes user-entered queue mode aliases from directives/config. */
|
||||
export function normalizeQueueMode(raw?: string): QueueMode | undefined {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Tracks queue state for active, pending, and recently deduped reply runs.
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { QueueMode } from "../../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import type { ModelFallbackRouteResolution } from "../../../agents/model-fallback.types.js";
|
||||
import { resolveGlobalMap } from "../../../shared/global-singleton.js";
|
||||
import { applyQueueRuntimeSettings } from "../../../utils/queue-helpers.js";
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
completeFollowupRunLifecycle,
|
||||
type FollowupRun,
|
||||
type QueueDropPolicy,
|
||||
type QueueMode,
|
||||
type QueueSettings,
|
||||
} from "./types.js";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { FastMode } from "@openclaw/normalization-core/string-coerce";
|
||||
// Shared queue type contracts for admission, drain, and fallback handling.
|
||||
import type { QueueMode } from "../../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import type { AutoFallbackPrimaryProbe } from "../../../agents/agent-scope.js";
|
||||
import type { ExecToolDefaults } from "../../../agents/bash-tools.js";
|
||||
import type { CliSessionBindingFacts } from "../../../agents/cli-runner/types.js";
|
||||
@@ -28,8 +29,6 @@ import type { ThinkingCatalogEntry } from "../../thinking.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../directives.js";
|
||||
import { releaseRecentQueueMessageId } from "./recent-message-ids.js";
|
||||
|
||||
export type QueueMode = "steer" | "followup" | "collect" | "interrupt";
|
||||
|
||||
export type QueueDropPolicy = "old" | "new" | "summarize";
|
||||
|
||||
export type QueueSettings = {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { SessionRunStatus } from "../../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||
import type { SessionRestartRecoveryState } from "./restart-recovery-types.js";
|
||||
import type { InternalSessionEntry as SessionEntry } from "./types.js";
|
||||
|
||||
type SessionRunStatus = "running" | "done" | "failed" | "killed" | "timeout";
|
||||
|
||||
/** Authoritative lifecycle snapshot required for an atomic transcript admission. */
|
||||
export type SessionTranscriptTurnExpectedState = {
|
||||
abortedLastRun: boolean | undefined;
|
||||
|
||||
@@ -6,6 +6,8 @@ import type {
|
||||
SessionAcpMeta,
|
||||
} from "@openclaw/acp-core/types";
|
||||
import { normalizeOptionalString, type FastMode } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { QueueMode } from "../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import type { SessionRunStatus } from "../../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||
import type { SessionObserverDigest } from "../../../packages/gateway-protocol/src/schema/sessions.js";
|
||||
import type { SessionAgentStatus } from "../../../packages/gateway-protocol/src/session-icon.js";
|
||||
import type { ChatType } from "../../channels/chat-type.js";
|
||||
@@ -420,7 +422,7 @@ type SessionEntryCore = SessionRestartRecoveryState &
|
||||
/** Accumulated runtime across subagent follow-up runs, persisted after completion. */
|
||||
runtimeMs?: number;
|
||||
/** Final persisted subagent run status, used after in-memory run archival. */
|
||||
status?: "running" | "done" | "failed" | "killed" | "timeout";
|
||||
status?: SessionRunStatus;
|
||||
/** Compact user-facing reason for the latest failed or timed-out run. */
|
||||
lastRunError?: string;
|
||||
/**
|
||||
@@ -512,7 +514,7 @@ type SessionEntryCore = SessionRestartRecoveryState &
|
||||
groupActivation?: "mention" | "always";
|
||||
groupActivationNeedsSystemIntro?: boolean;
|
||||
sendPolicy?: "allow" | "deny";
|
||||
queueMode?: "steer" | "followup" | "collect" | "interrupt";
|
||||
queueMode?: QueueMode;
|
||||
queueDebounceMs?: number;
|
||||
queueCap?: number;
|
||||
queueDrop?: "old" | "new" | "summarize";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Defines message queue and delivery configuration types.
|
||||
import type { QueueDropPolicy, QueueMode, QueueModeByProvider } from "./types.queue.js";
|
||||
import type { QueueMode } from "../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import type { QueueDropPolicy, QueueModeByProvider } from "./types.queue.js";
|
||||
|
||||
export type MentionPatternsMode = "allow" | "deny";
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** Queue handling mode for inbound channel messages. */
|
||||
export type QueueMode = "steer" | "followup" | "collect" | "interrupt";
|
||||
import type { QueueMode } from "../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
|
||||
/** Queue overflow policy for inbound channel messages. */
|
||||
export type QueueDropPolicy = "old" | "new" | "summarize";
|
||||
|
||||
export type QueueModeByProvider = {
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
formatValidationErrors,
|
||||
validateChatSendParams,
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import type { QueueMode } from "../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import { isBtwRequestText } from "../../auto-reply/reply/btw-command.js";
|
||||
import type { QueueMode } from "../../auto-reply/reply/queue/types.js";
|
||||
import type { InputProvenance } from "../../sessions/input-provenance.js";
|
||||
import { normalizeInputProvenance } from "../../sessions/input-provenance.js";
|
||||
import { isBrowserCopilotClient, isOperatorUiClient } from "../../utils/message-channel.js";
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Gateway session lifecycle state projection.
|
||||
// Converts agent run lifecycle events into session row/store status updates.
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import type { SessionRunStatus } from "../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||
import { isAgentLifecycleYieldedWaiting } from "../agents/agent-lifecycle-parent-state.js";
|
||||
import {
|
||||
buildAgentRunTerminalOutcomeFromLifecycleEvent,
|
||||
@@ -17,7 +18,7 @@ import { updateSessionEntry } from "../config/sessions/session-accessor.js";
|
||||
import { getAgentEventLifecycleGeneration, type AgentEventPayload } from "../infra/agent-events.js";
|
||||
import { parseCronRunScopeSuffix } from "../sessions/session-key-utils.js";
|
||||
import { loadSessionEntry } from "./session-utils.js";
|
||||
import type { GatewaySessionRow, SessionRunStatus } from "./session-utils.types.js";
|
||||
import type { GatewaySessionRow } from "./session-utils.types.js";
|
||||
|
||||
type LifecyclePhase = "start" | "end" | "error";
|
||||
|
||||
|
||||
@@ -7,11 +7,12 @@ import type {
|
||||
SessionPeerKind,
|
||||
SessionPlacement,
|
||||
SessionRow,
|
||||
SessionRunStatus,
|
||||
SessionSharingRole,
|
||||
SessionVisibility,
|
||||
} from "../../packages/gateway-protocol/src/index.js";
|
||||
import type { QueueMode } from "../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import type { SessionObserverDigest } from "../../packages/gateway-protocol/src/schema/sessions.js";
|
||||
import type { QueueMode } from "../auto-reply/reply/queue/types.js";
|
||||
import type { ChatType } from "../channels/chat-type.js";
|
||||
import type {
|
||||
SessionCompactionCheckpoint,
|
||||
@@ -43,9 +44,6 @@ export type GatewaySessionsDefaults = {
|
||||
thinkingDefault?: string;
|
||||
};
|
||||
|
||||
/** Runtime status surfaced for the latest session run. */
|
||||
export type SessionRunStatus = "running" | "done" | "failed" | "killed" | "timeout";
|
||||
|
||||
type SubagentRunState = "active" | "interrupted" | "historical";
|
||||
|
||||
type SessionCompactionCheckpointPreview = Pick<
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { SessionRunStatus } from "../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||
import {
|
||||
ensureMemoryChunkProvenance,
|
||||
ensureMemoryRecallMetadataSchema,
|
||||
@@ -356,9 +357,7 @@ function migratedChatType(value: unknown): "direct" | "group" | "channel" | null
|
||||
return null;
|
||||
}
|
||||
|
||||
function migratedStatus(
|
||||
value: unknown,
|
||||
): "running" | "done" | "failed" | "killed" | "timeout" | null {
|
||||
function migratedStatus(value: unknown): SessionRunStatus | null {
|
||||
if (
|
||||
value === "running" ||
|
||||
value === "done" ||
|
||||
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
PROTOCOL_VERSION,
|
||||
} from "@openclaw/gateway-client/browser";
|
||||
// Control UI module implements gateway behavior.
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import {
|
||||
CONTROL_UI_OWNER_BOOTSTRAP_PROFILE_HINT,
|
||||
type ControlUiBootstrapProfileHint,
|
||||
@@ -40,7 +39,7 @@ import {
|
||||
BOOTSTRAP_HANDOFF_OPERATOR_SCOPES,
|
||||
CONTROL_UI_OWNER_BOOTSTRAP_OPERATOR_SCOPES,
|
||||
} from "../../../src/shared/device-bootstrap-profile.js";
|
||||
import { redactToolDetail } from "../lib/browser-redact.ts";
|
||||
import { formatUiError } from "../lib/format-error.ts";
|
||||
import {
|
||||
clearDeviceAuthToken,
|
||||
loadDeviceAuthToken,
|
||||
@@ -225,7 +224,7 @@ function getErrorName(err: unknown): string | undefined {
|
||||
|
||||
function isBrowserWebSocketSecurityError(err: unknown): boolean {
|
||||
const name = getErrorName(err)?.toLowerCase();
|
||||
const message = formatErrorMessage(err, { redact: redactToolDetail }).toLowerCase();
|
||||
const message = formatUiError(err).toLowerCase();
|
||||
return (
|
||||
name === "securityerror" ||
|
||||
message.includes("security error") ||
|
||||
@@ -236,7 +235,7 @@ function isBrowserWebSocketSecurityError(err: unknown): boolean {
|
||||
|
||||
function formatBrowserWebSocketConstructorError(err: unknown, url: string): GatewayErrorInfo {
|
||||
const securityError = isBrowserWebSocketSecurityError(err);
|
||||
const browserMessage = formatErrorMessage(err, { redact: redactToolDetail });
|
||||
const browserMessage = formatUiError(err);
|
||||
const isPlaintextWs = url.trim().toLowerCase().startsWith("ws://");
|
||||
const details = {
|
||||
code: securityError
|
||||
|
||||
+10
-101
@@ -1,7 +1,11 @@
|
||||
export type UpdateAvailable = import("../../../src/infra/update-startup.js").UpdateAvailable;
|
||||
import type { FastMode } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { AgentsListResult as ProtocolAgentsListResult } from "../../../packages/gateway-protocol/src/schema/agents-models-skills.js";
|
||||
import type { ChannelsStatusResult } from "../../../packages/gateway-protocol/src/schema/channels.js";
|
||||
import type { QueueMode } from "../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import type { SessionRow } from "../../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||
import type { SessionObserverDigest } from "../../../packages/gateway-protocol/src/schema/sessions.js";
|
||||
import type { PresenceEntry as ProtocolPresenceEntry } from "../../../packages/gateway-protocol/src/schema/snapshot.js";
|
||||
import type { SessionAgentStatus } from "../../../packages/gateway-protocol/src/session-icon.js";
|
||||
import type { SessionGoal } from "../../../src/config/sessions/types.js";
|
||||
import type { CronJobBase } from "../../../src/cron/types-shared.js";
|
||||
@@ -11,7 +15,6 @@ import type { FastModeSource } from "../../../src/shared/fast-mode.js";
|
||||
import type {
|
||||
GatewayAgentRuntime,
|
||||
GatewayAgentRow as SharedGatewayAgentRow,
|
||||
SessionBoardFace,
|
||||
SessionsListResultBase,
|
||||
SessionsPatchResultBase,
|
||||
} from "../../../src/shared/session-types.js";
|
||||
@@ -28,10 +31,9 @@ export type ChannelsPairingRequest =
|
||||
import("../../../packages/gateway-protocol/src/index.js").ChannelsPairingRequest;
|
||||
export type SessionVisibility =
|
||||
import("../../../packages/gateway-protocol/src/index.js").SessionVisibility;
|
||||
type SessionSharingRole =
|
||||
import("../../../packages/gateway-protocol/src/index.js").SessionSharingRole;
|
||||
export type SessionMembersListResult =
|
||||
import("../../../packages/gateway-protocol/src/index.js").SessionMembersListResult;
|
||||
export type { SessionRunStatus } from "../../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||
export type ChannelsStatusSnapshot = ChannelsStatusResult;
|
||||
export type ChannelUiMetaEntry = NonNullable<ChannelsStatusResult["channelMeta"]>[number];
|
||||
export type ChannelAccountSnapshot = ChannelsStatusResult["channelAccounts"][string][number];
|
||||
@@ -253,30 +255,7 @@ export type ConfigSchemaResponse = {
|
||||
generatedAt: string;
|
||||
};
|
||||
|
||||
export type PresenceEntry = {
|
||||
deviceId?: string | null;
|
||||
instanceId?: string | null;
|
||||
host?: string | null;
|
||||
ip?: string | null;
|
||||
version?: string | null;
|
||||
platform?: string | null;
|
||||
deviceFamily?: string | null;
|
||||
modelIdentifier?: string | null;
|
||||
roles?: string[] | null;
|
||||
scopes?: string[] | null;
|
||||
mode?: string | null;
|
||||
lastInputSeconds?: number | null;
|
||||
reason?: string | null;
|
||||
text?: string | null;
|
||||
ts?: number | null;
|
||||
user?: {
|
||||
id: string;
|
||||
email?: string | null;
|
||||
name?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
} | null;
|
||||
watchedSessions?: string[] | null;
|
||||
};
|
||||
export type PresenceEntry = ProtocolPresenceEntry;
|
||||
|
||||
export type GatewaySessionsDefaults = {
|
||||
modelProvider: string | null;
|
||||
@@ -295,12 +274,7 @@ export type GatewayThinkingLevelOption = {
|
||||
|
||||
export type GatewayAgentRow = SharedGatewayAgentRow;
|
||||
|
||||
export type AgentsListResult = {
|
||||
defaultId: string;
|
||||
mainKey: string;
|
||||
scope: string;
|
||||
agents: GatewayAgentRow[];
|
||||
};
|
||||
export type AgentsListResult = ProtocolAgentsListResult;
|
||||
|
||||
export type AgentIdentityResult = {
|
||||
agentId: string;
|
||||
@@ -417,7 +391,6 @@ export type ArtifactDownloadResult = {
|
||||
expiresAt?: string;
|
||||
};
|
||||
|
||||
export type SessionRunStatus = "running" | "done" | "failed" | "killed" | "timeout";
|
||||
type SubagentRunState = "active" | "interrupted" | "historical";
|
||||
|
||||
type SessionCompactionCheckpointReason =
|
||||
@@ -452,68 +425,19 @@ type SessionCompactionCheckpointPreview = Pick<
|
||||
"checkpointId" | "createdAt" | "reason"
|
||||
>;
|
||||
|
||||
export type GatewaySessionRow = {
|
||||
key: string;
|
||||
classification?: import("../../../packages/gateway-protocol/src/index.js").SessionClassification;
|
||||
agentId?: string;
|
||||
accountId?: string;
|
||||
peerKind?: import("../../../packages/gateway-protocol/src/index.js").SessionPeerKind;
|
||||
isMain?: boolean;
|
||||
isBackground?: boolean;
|
||||
visibility?: SessionVisibility;
|
||||
sharingRole?: SessionSharingRole;
|
||||
incognito?: true;
|
||||
spawnedBy?: string;
|
||||
controlOwnerSessionKey?: string;
|
||||
/** Collector swarm group that owns this child session, when applicable. */
|
||||
swarmGroupId?: string;
|
||||
parentSessionKey?: string;
|
||||
/** Managed worktree bound to this session (repo checkout + branch). */
|
||||
worktree?: { id: string; branch: string; repoRoot: string };
|
||||
/** Session-scoped exec node binding (exec host=node routing). */
|
||||
execNode?: string;
|
||||
spawnedWorkspaceDir?: string;
|
||||
spawnedCwd?: string;
|
||||
execCwd?: string;
|
||||
forkedFromParent?: boolean;
|
||||
spawnDepth?: number;
|
||||
subagentRole?: "orchestrator" | "leaf";
|
||||
subagentControlScope?: "children" | "none";
|
||||
createdVia?: "operator" | "spawn" | "channel" | "cron" | "talk" | "run" | "plugin" | "internal";
|
||||
createdActor?: import("../../../packages/gateway-protocol/src/schema/sessions.js").SessionCreatedActor;
|
||||
createdAt?: number;
|
||||
forkSource?: { sessionKey: string; sessionId: string; entryId?: string };
|
||||
previousSessionId?: string;
|
||||
export type GatewaySessionRow = SessionRow & {
|
||||
placement?: import("../../../packages/gateway-protocol/src/index.js").SessionPlacement;
|
||||
kind: "direct" | "group" | "global" | "unknown";
|
||||
label?: string;
|
||||
/** User-defined organization bucket; unrelated to chat-group kind/groupChannel. */
|
||||
category?: string;
|
||||
/** Preferred Control UI face for generic session navigation. */
|
||||
boardFace?: SessionBoardFace;
|
||||
displayName?: string;
|
||||
derivedTitle?: string;
|
||||
channel?: string;
|
||||
surface?: string;
|
||||
subject?: string;
|
||||
room?: string;
|
||||
space?: string;
|
||||
updatedAt: number | null;
|
||||
unread?: boolean;
|
||||
lastReadAt?: number;
|
||||
agentStatus?: SessionAgentStatus;
|
||||
observerDigest?: Pick<
|
||||
SessionObserverDigest,
|
||||
"agentId" | "runId" | "headline" | "health" | "updatedAt" | "revision"
|
||||
>;
|
||||
lastActivityAt?: number;
|
||||
archived?: boolean;
|
||||
archivedAt?: number;
|
||||
archivedBy?: import("../../../packages/gateway-protocol/src/schema/sessions.js").SessionCreatedActor;
|
||||
pinned?: boolean;
|
||||
pinnedAt?: number;
|
||||
icon?: string;
|
||||
sessionId?: string;
|
||||
systemSent?: boolean;
|
||||
abortedLastRun?: boolean;
|
||||
thinkingLevel?: string;
|
||||
@@ -521,25 +445,14 @@ export type GatewaySessionRow = {
|
||||
thinkingOptions?: string[];
|
||||
thinkingDefault?: string;
|
||||
fastMode?: FastMode;
|
||||
toolOverrides?: import("../lib/sessions/patch.js").SessionToolOverrides;
|
||||
effectiveFastMode?: FastMode;
|
||||
effectiveFastModeSource?: FastModeSource;
|
||||
fastAutoOnSeconds?: number;
|
||||
verboseLevel?: string;
|
||||
reasoningLevel?: string;
|
||||
elevatedLevel?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
totalTokensFresh?: boolean;
|
||||
estimatedCostUsd?: number;
|
||||
status?: SessionRunStatus;
|
||||
/** Compact user-facing reason for the latest failed or timed-out run. */
|
||||
lastRunError?: string;
|
||||
hasActiveRun?: boolean;
|
||||
activeRunIds?: string[];
|
||||
/** Active transcript-branch leaf returned with chat history. */
|
||||
activeLeafEntryId?: string | null;
|
||||
/** An enabled cron job is bound to this session (runs in it or delivers to it). */
|
||||
hasAutomation?: boolean;
|
||||
subagentRunState?: SubagentRunState;
|
||||
@@ -549,15 +462,11 @@ export type GatewaySessionRow = {
|
||||
runtimeMs?: number;
|
||||
/** UI-local timestamp for the runtimeMs sample; absent on raw Gateway rows. */
|
||||
runtimeSampledAt?: number;
|
||||
childSessions?: string[];
|
||||
model?: string;
|
||||
modelProvider?: string;
|
||||
modelSelectionLocked?: boolean;
|
||||
effectiveResponseUsage?: "on" | "off" | "tokens" | "full";
|
||||
queueMode?: "steer" | "followup" | "collect" | "interrupt";
|
||||
effectiveQueueMode?: "steer" | "followup" | "collect" | "interrupt";
|
||||
queueMode?: QueueMode;
|
||||
effectiveQueueMode?: QueueMode;
|
||||
agentRuntime?: GatewayAgentRuntime;
|
||||
contextTokens?: number;
|
||||
compactionCheckpointCount?: number;
|
||||
latestCompactionCheckpoint?: SessionCompactionCheckpointPreview;
|
||||
goal?: SessionGoal;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { promoteToPopoverTopLayer } from "../components/menu-surface.ts";
|
||||
import { NativeLinkMenu, type NativeLinkMenuAction } from "../components/native-link-menu.ts";
|
||||
import { copyToClipboard } from "../lib/clipboard.ts";
|
||||
import { shouldHandleNavigationClick } from "../lib/navigation-click.ts";
|
||||
|
||||
type NativeLinkTarget = "inline" | "external";
|
||||
|
||||
@@ -179,14 +180,7 @@ export function startNativeLinkRouting(): NativeLinkRouting {
|
||||
};
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
event.button !== 0 ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey
|
||||
) {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
}
|
||||
const appLink = trustedExternalAppUrl(event);
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
describe("connection user profile helpers", () => {
|
||||
it("resolves identity only from the current live presence entry", () => {
|
||||
const entries = [
|
||||
{ instanceId: "other", user: { id: "other-profile", name: "Other" } },
|
||||
{ instanceId: "self", user: { id: "old", name: "Old" }, reason: "disconnect" },
|
||||
{ instanceId: "self", user: { id: "profile-1", name: "Ada" } },
|
||||
{ instanceId: "other", user: { id: "other-profile", name: "Other" }, ts: 1 },
|
||||
{ instanceId: "self", user: { id: "old", name: "Old" }, reason: "disconnect", ts: 2 },
|
||||
{ instanceId: "self", user: { id: "profile-1", name: "Ada" }, ts: 3 },
|
||||
];
|
||||
|
||||
expect(resolveSelfPresenceUser(entries, "self")).toEqual({ id: "profile-1", name: "Ada" });
|
||||
@@ -21,7 +21,7 @@ describe("connection user profile helpers", () => {
|
||||
});
|
||||
|
||||
it("prefers locally refreshed identity state over the presence snapshot", () => {
|
||||
const presenceEntries = [{ instanceId: "self", user: { id: "profile-1", name: "Ada" } }];
|
||||
const presenceEntries = [{ instanceId: "self", user: { id: "profile-1", name: "Ada" }, ts: 1 }];
|
||||
|
||||
expect(
|
||||
resolveCurrentSelfUser({
|
||||
@@ -44,7 +44,7 @@ describe("connection user profile helpers", () => {
|
||||
});
|
||||
|
||||
it("reads presence payloads and builds scoped cache-busted avatar URLs", () => {
|
||||
const entries = [{ instanceId: "self", user: { id: "profile/1" } }];
|
||||
const entries = [{ instanceId: "self", user: { id: "profile/1" }, ts: 1 }];
|
||||
expect(readPresenceEntries({ presence: entries })).toEqual(entries);
|
||||
expect(readPresenceEntries({ presence: null })).toBeUndefined();
|
||||
expect(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Application-owned browser push subscription lifecycle.
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import type { GatewayBrowserClient } from "../api/gateway.ts";
|
||||
import { redactToolDetail } from "../lib/browser-redact.ts";
|
||||
import { formatUiError } from "../lib/format-error.ts";
|
||||
import type { ApplicationGateway } from "./gateway.ts";
|
||||
|
||||
type WebPushSnapshot = {
|
||||
@@ -89,7 +88,7 @@ export function createWebPushCapability(gateway: ApplicationGateway): WebPushCap
|
||||
publish({ loading: true, error: null });
|
||||
operation = action(client)
|
||||
.catch((error: unknown) => {
|
||||
publish({ error: formatErrorMessage(error, { redact: redactToolDetail }) });
|
||||
publish({ error: formatUiError(error) });
|
||||
})
|
||||
.finally(() => {
|
||||
operation = null;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "../app-navigation.ts";
|
||||
import { pathForRoute } from "../app-route-paths.ts";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { shouldHandleNavigationClick } from "../lib/navigation-click.ts";
|
||||
import { pluginTabSearch } from "../pages/plugin/route.ts";
|
||||
import type { SidebarWorkboardBoard, SidebarWorkboardRenderers } from "./app-sidebar-workboard.ts";
|
||||
import { icons, type IconName } from "./icons.ts";
|
||||
@@ -23,18 +24,6 @@ import { consumeDropdownKeyboardDismissal, trackDropdownKeyboardDismissal } from
|
||||
|
||||
type SidebarMenuPosition = { x: number; y: number };
|
||||
|
||||
/** Ordinary primary click without modifiers; anything else keeps native link behavior. */
|
||||
export function shouldHandleNavigationClick(event: MouseEvent): boolean {
|
||||
return (
|
||||
!event.defaultPrevented &&
|
||||
event.button === 0 &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.shiftKey &&
|
||||
!event.altKey
|
||||
);
|
||||
}
|
||||
|
||||
/** Settings routes highlight Settings; hub tabs highlight their hub entry. */
|
||||
export function isSidebarRouteActive(
|
||||
activeRouteId: NavigationRouteId | undefined,
|
||||
@@ -160,6 +149,7 @@ function renderMoreMenuRoute(params: SidebarMoreMenuParams, routeId: SidebarNavR
|
||||
@pointerleave=${params.onCancelPreload}
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
// wa-select also fires for native clicks; mark them so it does not add SPA navigation.
|
||||
(event.currentTarget as HTMLElement).dataset.nativeNavigation = "true";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { t } from "../i18n/index.ts";
|
||||
import { normalizeAgentLabel, resolveAgentTextAvatar } from "../lib/agents/display.ts";
|
||||
import { deriveAvatarInitial, resolveAgentAvatarUrl } from "../lib/avatar.ts";
|
||||
import { sessionHasBoard } from "../lib/board/provider.ts";
|
||||
import { shouldHandleNavigationClick } from "../lib/navigation-click.ts";
|
||||
import {
|
||||
resolveSessionPreferredFace,
|
||||
sessionNavigationTarget,
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
parseAgentSessionKey,
|
||||
} from "../lib/sessions/session-key.ts";
|
||||
import { pluginTabKey } from "../pages/plugin/route.ts";
|
||||
import { renderSidebarPluginTab, shouldHandleNavigationClick } from "./app-sidebar-nav-menus.ts";
|
||||
import { renderSidebarPluginTab } from "./app-sidebar-nav-menus.ts";
|
||||
import type { AppSidebarSessionNavigationElement } from "./app-sidebar-session-navigation.ts";
|
||||
import type { SidebarRecentSession } from "./app-sidebar-session-types.ts";
|
||||
import type { SidebarWorkboardBoard } from "./app-sidebar-workboard.ts";
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { GatewaySessionRow } from "../api/types.ts";
|
||||
import type { NavigationRouteId } from "../app-navigation.ts";
|
||||
import type { ApplicationNavigationOptions } from "../app/context.ts";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { shouldHandleNavigationClick } from "../lib/navigation-click.ts";
|
||||
import type { CatalogSessionKey } from "../lib/sessions/catalog-key.ts";
|
||||
import { buildCatalogSessionKey } from "../lib/sessions/catalog-key.ts";
|
||||
import {
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
} from "../lib/sessions/catalog-project-grouping.ts";
|
||||
import { sessionNavigationTarget } from "../lib/sessions/route-navigation.ts";
|
||||
import type { NewSessionTarget } from "../pages/new-session/location.ts";
|
||||
import { shouldHandleNavigationClick } from "./app-sidebar-nav-menus.ts";
|
||||
import {
|
||||
formatSidebarTimestamp,
|
||||
type CatalogBackingSessionDisplay,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { serializeSidebarEntry } from "../app-navigation.ts";
|
||||
import { isSessionRouteId } from "../app-route-paths.ts";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { listSelectableAgents } from "../lib/agents/display.ts";
|
||||
import { shouldHandleNavigationClick } from "../lib/navigation-click.ts";
|
||||
import { isCronSessionKey, resolveSessionDisplayName } from "../lib/session-display.ts";
|
||||
import type { SidebarSessionsGrouping } from "../lib/sessions/grouping.ts";
|
||||
import {
|
||||
@@ -107,8 +108,7 @@ export class AppSidebarSessionNavigationElement extends AppSidebarBase {
|
||||
private readonly runtimeSampledAtByRow = new WeakMap<GatewaySessionRow, number>();
|
||||
private readonly attention = new SessionAttentionController(this);
|
||||
|
||||
// These controllers initialize on AppSidebar after the navigation-owned
|
||||
// controllers, matching the former inheritance-chain field order.
|
||||
// Controller order preserves the former inheritance-chain field initialization order.
|
||||
declare readonly sessionOrganizer: SessionOrganizerController;
|
||||
declare readonly sidebarMenus: SidebarMenusController;
|
||||
|
||||
@@ -200,8 +200,7 @@ export class AppSidebarSessionNavigationElement extends AppSidebarBase {
|
||||
void this.sessionData.loadChildSessions(session.key);
|
||||
}
|
||||
}
|
||||
// The main session hides behind the identity card, so nothing in the list
|
||||
// triggers its child fetch; load eagerly or its threads never surface.
|
||||
// The hidden main row needs an eager child fetch or its threads never surface.
|
||||
const mainRow = this.mainSessionRow();
|
||||
if (
|
||||
mainRow &&
|
||||
@@ -373,20 +372,16 @@ export class AppSidebarSessionNavigationElement extends AppSidebarBase {
|
||||
}
|
||||
|
||||
handleSessionRowClick(event: MouseEvent, session: SidebarRecentSession) {
|
||||
if (event.defaultPrevented || event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
if (session.isChild) {
|
||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
||||
return;
|
||||
}
|
||||
if (session.isChild && shouldHandleNavigationClick(event)) {
|
||||
event.preventDefault();
|
||||
this.clearSessionSelection();
|
||||
this.selectSession(session.key);
|
||||
return;
|
||||
}
|
||||
// Cmd/Ctrl and Shift clicks build the multi-select instead of the browser's
|
||||
// open-in-new-tab default; middle-click still opens the row in a new tab.
|
||||
if (session.isChild || event.defaultPrevented || event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
// Modified parent clicks build multi-select; middle-click keeps native new-tab behavior.
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
this.toggleSessionSelected(session.key);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { html } from "lit";
|
||||
import type { GatewayBrowserClient } from "../api/gateway.ts";
|
||||
import { pathForWorkboardBoard } from "../app-route-paths.ts";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { shouldHandleNavigationClick } from "../lib/navigation-click.ts";
|
||||
import { workboardBoardLabel } from "../lib/workboard/board-presentation.ts";
|
||||
import { normalizeBoardsPayload } from "../lib/workboard/normalization.ts";
|
||||
import { getWorkboardState } from "../lib/workboard/runtime.ts";
|
||||
@@ -191,14 +192,7 @@ export const renderSidebarWorkboardEntry: SidebarWorkboardRenderers["renderEntry
|
||||
class="nav-item nav-item--workboard-board ${params.active ? "nav-item--active" : ""}"
|
||||
aria-current=${params.active ? "page" : undefined}
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
event.button !== 0 ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey
|
||||
) {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { SessionRunStatus } from "../../../packages/gateway-protocol/src/schema/sessions-row.js";
|
||||
|
||||
export type LobsterPetMode = "idle" | "busy" | "offline";
|
||||
|
||||
export type LobsterRunOutcome = "ok" | "error" | "aborted";
|
||||
@@ -125,7 +127,7 @@ export function lobsterPetSeed(sessionKey: string): number {
|
||||
export function resolveLobsterRunOutcome(
|
||||
sessions:
|
||||
| ReadonlyArray<{
|
||||
status?: "running" | "done" | "failed" | "killed" | "timeout";
|
||||
status?: SessionRunStatus;
|
||||
endedAt?: number | null;
|
||||
lastActivityAt?: number | null;
|
||||
updatedAt?: number | null;
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { pathForRoute, type RouteId } from "../app-route-paths.ts";
|
||||
import type { ApplicationNavigationOptions } from "../app/context.ts";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { shouldHandleNavigationClick } from "../lib/navigation-click.ts";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../lib/string-coerce.ts";
|
||||
import { icons } from "./icons.ts";
|
||||
import { redactLoginFailureError } from "./login-gate.ts";
|
||||
@@ -163,14 +164,7 @@ function renderItem(props: SettingsSidebarProps, routeId: RouteId, label?: strin
|
||||
@touchstart=${(event: TouchEvent) =>
|
||||
scheduleRoutePreload(props.preloadTimers, routeId, event, props.onPreload, active, true)}
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
event.button !== 0 ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey
|
||||
) {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
@@ -201,14 +195,7 @@ function renderBlockItem(props: SettingsSidebarProps, block: SettingsSearchBlock
|
||||
class="settings-sidebar__subitem ${active ? "settings-sidebar__subitem--active" : ""}"
|
||||
aria-current=${active ? "location" : nothing}
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
event.button !== 0 ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey
|
||||
) {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { property } from "lit/decorators.js";
|
||||
import { pathForRoute } from "../app-route-paths.ts";
|
||||
import { CONTROL_UI_BUILD_INFO } from "../build-info.ts";
|
||||
import { t } from "../i18n/index.ts";
|
||||
import { shouldHandleNavigationClick } from "../lib/navigation-click.ts";
|
||||
import { OpenClawLightDomContentsElement } from "../lit/openclaw-element.ts";
|
||||
import {
|
||||
formatBuildChipText,
|
||||
@@ -11,18 +12,6 @@ import {
|
||||
} from "./sidebar-build-chip-format.ts";
|
||||
import "./tooltip.ts";
|
||||
|
||||
function shouldHandleNavigationClick(event: MouseEvent): boolean {
|
||||
// Preserve browser behavior for modified clicks and non-primary buttons.
|
||||
return (
|
||||
!event.defaultPrevented &&
|
||||
event.button === 0 &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.shiftKey &&
|
||||
!event.altKey
|
||||
);
|
||||
}
|
||||
|
||||
class SidebarBuildChip extends OpenClawLightDomContentsElement {
|
||||
@property({ attribute: false }) basePath = "";
|
||||
@property({ attribute: false }) gatewayVersion: string | null = null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { QueueMode } from "../../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import { normalizeQueueMode } from "../../../../src/auto-reply/reply/queue/normalize.js";
|
||||
import type { QueueMode } from "../../../../src/auto-reply/reply/queue/types.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL } from "../../../../src/utils/message-channel-constants.js";
|
||||
import { normalizeChatFollowUpModeOverride, type ChatFollowUpMode } from "../../app/settings.js";
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatUiError } from "./format-error.ts";
|
||||
|
||||
describe("formatUiError", () => {
|
||||
it("formats structured causes through the browser-safe redactor", () => {
|
||||
const cause = Object.assign(new Error("OPENAI_API_KEY=sk-1234567890abcdef"), {
|
||||
code: "AUTH_FAILED",
|
||||
});
|
||||
|
||||
expect(formatUiError(new Error("request failed", { cause }))).toBe(
|
||||
"request failed | OPENAI_API_KEY=sk-123...cdef | AUTH_FAILED",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the fallback only when formatting produces an empty message", () => {
|
||||
expect(formatUiError("", "Request failed")).toBe("Request failed");
|
||||
expect(formatUiError("")).toBe("");
|
||||
expect(formatUiError(undefined, "Request failed")).toBe("undefined");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import { redactToolDetail } from "./browser-redact.ts";
|
||||
|
||||
export function formatUiError(error: unknown, fallback = ""): string {
|
||||
return formatErrorMessage(error, { redact: redactToolDetail }) || fallback;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shouldHandleNavigationClick } from "./navigation-click.ts";
|
||||
|
||||
function clickEvent(overrides: Partial<MouseEvent> = {}): MouseEvent {
|
||||
return {
|
||||
defaultPrevented: false,
|
||||
button: 0,
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
...overrides,
|
||||
} as MouseEvent;
|
||||
}
|
||||
|
||||
const nativeBehaviorCases = [
|
||||
["a prevented click", { defaultPrevented: true }],
|
||||
["a non-primary click", { button: 1 }],
|
||||
["a Meta-modified click", { metaKey: true }],
|
||||
["a Control-modified click", { ctrlKey: true }],
|
||||
["a Shift-modified click", { shiftKey: true }],
|
||||
["an Alt-modified click", { altKey: true }],
|
||||
] satisfies Array<[string, Partial<MouseEvent>]>;
|
||||
|
||||
describe("shouldHandleNavigationClick", () => {
|
||||
it("handles an ordinary primary click", () => {
|
||||
expect(shouldHandleNavigationClick(clickEvent())).toBe(true);
|
||||
});
|
||||
|
||||
it.each(nativeBehaviorCases)("preserves native behavior for %s", (_, event) => {
|
||||
expect(shouldHandleNavigationClick(clickEvent(event))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
/** Ordinary primary click without modifiers; anything else keeps native link behavior. */
|
||||
export function shouldHandleNavigationClick(event: MouseEvent): boolean {
|
||||
return (
|
||||
!event.defaultPrevented &&
|
||||
event.button === 0 &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.shiftKey &&
|
||||
!event.altKey
|
||||
);
|
||||
}
|
||||
@@ -102,7 +102,7 @@ describe("buildNodesInventory", () => {
|
||||
}),
|
||||
],
|
||||
nodes: [],
|
||||
presence: [{ instanceId: "BROWSER-1", reason: "disconnect" }],
|
||||
presence: [{ instanceId: "BROWSER-1", reason: "disconnect", ts: 1_000 }],
|
||||
});
|
||||
|
||||
expect(firstGroup(groups).primary.connected).toBe(true);
|
||||
@@ -304,8 +304,10 @@ describe("listStaleInventoryEntries", () => {
|
||||
|
||||
describe("findGatewayPresence", () => {
|
||||
it("returns the Gateway self beacon", () => {
|
||||
const gateway = { instanceId: "gateway-1", mode: " GATEWAY " };
|
||||
expect(findGatewayPresence([{ instanceId: "node-1", mode: "node" }, gateway])).toBe(gateway);
|
||||
const gateway = { instanceId: "gateway-1", mode: " GATEWAY ", ts: 2_000 };
|
||||
expect(findGatewayPresence([{ instanceId: "node-1", mode: "node", ts: 1_000 }, gateway])).toBe(
|
||||
gateway,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -315,11 +317,16 @@ describe("listUnpairedPresence", () => {
|
||||
paired: [device({ deviceId: "node-1", displayName: "megaclaw" })],
|
||||
nodes: [],
|
||||
});
|
||||
const joined = { deviceId: "NODE-1", mode: "node" };
|
||||
const gateway = { instanceId: "gateway-1", mode: "gateway" };
|
||||
const disconnected = { instanceId: "left-1", mode: "webchat", reason: "disconnect" };
|
||||
const joined = { deviceId: "NODE-1", mode: "node", ts: 1_000 };
|
||||
const gateway = { instanceId: "gateway-1", mode: "gateway", ts: 2_000 };
|
||||
const disconnected = {
|
||||
instanceId: "left-1",
|
||||
mode: "webchat",
|
||||
reason: "disconnect",
|
||||
ts: 3_000,
|
||||
};
|
||||
const textOnly = { text: "note from test", ts: 1_000 };
|
||||
const live = { instanceId: "webchat-1", mode: "webchat", host: "browser" };
|
||||
const live = { instanceId: "webchat-1", mode: "webchat", host: "browser", ts: 4_000 };
|
||||
|
||||
expect(listUnpairedPresence([joined, gateway, disconnected, textOnly, live], groups)).toEqual([
|
||||
live,
|
||||
|
||||
@@ -1385,7 +1385,7 @@ describe("reconcileSkillsAgentId", () => {
|
||||
reconcileSkillsAgentId(state, {
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "project",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }],
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import {
|
||||
ClawHubTrustErrorCodes,
|
||||
readClawHubTrustErrorDetails,
|
||||
@@ -10,7 +9,7 @@ import type {
|
||||
SkillStatusEntry,
|
||||
SkillStatusReport,
|
||||
} from "../../api/types.ts";
|
||||
import { redactToolDetail } from "../browser-redact.ts";
|
||||
import { formatUiError } from "../format-error.ts";
|
||||
import type { ClawHubSearchResult } from "./clawhub-search.ts";
|
||||
import {
|
||||
normalizeSkillApiKeyReplacement,
|
||||
@@ -335,7 +334,7 @@ export async function loadSkills(
|
||||
if (!isCurrent()) {
|
||||
return;
|
||||
}
|
||||
state.skillsError = formatErrorMessage(err, { redact: redactToolDetail });
|
||||
state.skillsError = formatUiError(err);
|
||||
} finally {
|
||||
// A transient disconnect invalidates the result, not this invocation's
|
||||
// loading ownership. Source/scope identity still protects newer loads.
|
||||
@@ -448,7 +447,7 @@ export async function loadSkillCard(state: SkillsState, skillKey: string) {
|
||||
if (isSkillsAgentScopeCurrent(state, agentScope)) {
|
||||
state.skillCardErrors = {
|
||||
...state.skillCardErrors,
|
||||
[skillKey]: formatErrorMessage(err, { redact: redactToolDetail }),
|
||||
[skillKey]: formatUiError(err),
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
@@ -493,7 +492,7 @@ async function loadClawHubSecurityVerdicts(state: SkillsState, report: SkillStat
|
||||
return;
|
||||
}
|
||||
state.clawhubVerdicts = {};
|
||||
state.clawhubVerdictsError = formatErrorMessage(err, { redact: redactToolDetail });
|
||||
state.clawhubVerdictsError = formatUiError(err);
|
||||
} finally {
|
||||
if (isSkillsAgentScopeCurrent(state, agentScope)) {
|
||||
state.clawhubVerdictsLoading = false;
|
||||
@@ -546,7 +545,7 @@ async function runSkillMutation(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const message = formatErrorMessage(err, { redact: redactToolDetail });
|
||||
const message = formatUiError(err);
|
||||
state.skillsError = message;
|
||||
setSkillMessage(state, skillKey, {
|
||||
kind: "error",
|
||||
@@ -659,7 +658,7 @@ export async function loadClawHubDetail(state: SkillsState, slug: string) {
|
||||
state.clawhubDetail = res ?? null;
|
||||
},
|
||||
(err) => {
|
||||
state.clawhubDetailError = formatErrorMessage(err, { redact: redactToolDetail });
|
||||
state.clawhubDetailError = formatUiError(err);
|
||||
},
|
||||
() => {
|
||||
state.clawhubDetailLoading = false;
|
||||
@@ -725,10 +724,7 @@ export async function installFromClawHub(
|
||||
kind: "error",
|
||||
text: needsAcknowledgement
|
||||
? formatClawHubAcknowledgementMessage(trustDetails?.warning)
|
||||
: formatClawHubInstallMessage(
|
||||
formatErrorMessage(err, { redact: redactToolDetail }),
|
||||
trustDetails?.warning,
|
||||
),
|
||||
: formatClawHubInstallMessage(formatUiError(err), trustDetails?.warning),
|
||||
...(needsAcknowledgement ? { acknowledgeSlug: slug } : {}),
|
||||
...(needsAcknowledgement && trustDetails?.version
|
||||
? { acknowledgeVersion: trustDetails.version }
|
||||
|
||||
@@ -680,7 +680,7 @@ describe("AgentsPage gateway lifecycle", () => {
|
||||
it("preserves matching initial route data, then resets it on provider replacement", () => {
|
||||
const client = {} as GatewayBrowserClient;
|
||||
const currentGateway = gateway(snapshot(client, false));
|
||||
const preloadedAgents = {
|
||||
const preloadedAgents: AgentsListResult = {
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "per-sender",
|
||||
|
||||
@@ -21,7 +21,7 @@ export function createAgentViewTestProps(
|
||||
agentsList: {
|
||||
defaultId: "alpha",
|
||||
mainKey: "main",
|
||||
scope: "workspace",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "alpha", name: "Alpha" } as never, { id: "beta", name: "Beta" } as never],
|
||||
},
|
||||
selectedAgentId: "beta",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Agent identity draft state and persistence, split out of agents-page.ts.
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import type { ApplicationContext, ApplicationNavigationPreferences } from "../../app/context.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { updateAgentIdentity } from "../../lib/agents/index.ts";
|
||||
import { redactToolDetail } from "../../lib/browser-redact.ts";
|
||||
import { formatUiError } from "../../lib/format-error.ts";
|
||||
import { fileToAvatarDataUrl } from "./avatar-image.ts";
|
||||
import type { AgentIdentityDraft } from "./panels-overview.ts";
|
||||
|
||||
@@ -104,14 +103,14 @@ export async function saveIdentityDraft(params: {
|
||||
await agents.refreshList();
|
||||
} catch (error) {
|
||||
refreshErrors.push(
|
||||
`Agent identity was saved, but the agent list refresh failed: ${formatErrorMessage(error, { redact: redactToolDetail })}`,
|
||||
`Agent identity was saved, but the agent list refresh failed: ${formatUiError(error)}`,
|
||||
);
|
||||
}
|
||||
try {
|
||||
await agentIdentity.ensure([agentId]);
|
||||
} catch (error) {
|
||||
refreshErrors.push(
|
||||
`Agent identity was saved, but the identity refresh failed: ${formatErrorMessage(error, { redact: redactToolDetail })}`,
|
||||
`Agent identity was saved, but the identity refresh failed: ${formatUiError(error)}`,
|
||||
);
|
||||
}
|
||||
if (params.isCurrent()) {
|
||||
|
||||
@@ -549,7 +549,7 @@ describe("renderAgents", () => {
|
||||
agentsList: {
|
||||
defaultId: "alpha",
|
||||
mainKey: "main",
|
||||
scope: "workspace",
|
||||
scope: "per-sender",
|
||||
agents: [
|
||||
{ id: "alpha", name: "Alpha", thinkingDefault: "off" } as never,
|
||||
{ id: "beta", name: "Beta", thinkingDefault: "xhigh" } as never,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import { asNullableRecord as asRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { html, nothing } from "lit";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
buildToolsEffectiveRequestKey,
|
||||
loadToolsEffective,
|
||||
} from "../../lib/agents/tools-effective.ts";
|
||||
import { redactToolDetail } from "../../lib/browser-redact.ts";
|
||||
import {
|
||||
buildAddMcpServerPatch,
|
||||
MCP_SERVER_NAME_PATTERN,
|
||||
@@ -26,6 +24,7 @@ import {
|
||||
patchMcpServers,
|
||||
summarizeMcpServers,
|
||||
} from "../../lib/config/mcp-servers.ts";
|
||||
import { formatUiError } from "../../lib/format-error.ts";
|
||||
import { isGatewayMethodAdvertised } from "../../lib/gateway-methods.ts";
|
||||
import { readSessionMethodAccess } from "../../lib/session-method-access.ts";
|
||||
import {
|
||||
@@ -39,22 +38,8 @@ import { loadSkillStatusReport } from "../../lib/skills/index.ts";
|
||||
import { refreshCurrentChatSessionList } from "./chat-session.ts";
|
||||
import { patchChatSessionSettings } from "./chat-settings-patches.ts";
|
||||
import type { ChatPageHost } from "./chat-state-host.ts";
|
||||
import type {
|
||||
ChatComposerMenuSkill,
|
||||
ChatComposerPlusMenuProps,
|
||||
} from "./components/chat-composer-plus-menu.ts";
|
||||
|
||||
type CapabilityMenuProps = Omit<
|
||||
ChatComposerPlusMenuProps,
|
||||
| "attachments"
|
||||
| "disabled"
|
||||
| "open"
|
||||
| "view"
|
||||
| "toolOverrides"
|
||||
| "onOpenChange"
|
||||
| "onViewChange"
|
||||
| "showCapabilities"
|
||||
>;
|
||||
import type { ChatComposerMenuSkill } from "./components/chat-composer-plus-menu.ts";
|
||||
import type { CapabilityMenuProps } from "./components/chat-composer-types.ts";
|
||||
|
||||
type ComposerMcpServerScope = "session" | "everywhere";
|
||||
|
||||
@@ -130,7 +115,7 @@ export class ChatComposerCapabilityHost {
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
error: formatUiError(error),
|
||||
stage: "config",
|
||||
};
|
||||
}
|
||||
@@ -146,7 +131,7 @@ export class ChatComposerCapabilityHost {
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
error: formatUiError(error),
|
||||
stage: "session",
|
||||
};
|
||||
}
|
||||
@@ -166,7 +151,7 @@ export class ChatComposerCapabilityHost {
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
error: formatUiError(error),
|
||||
stage: "session",
|
||||
};
|
||||
}
|
||||
@@ -410,7 +395,7 @@ export class ChatComposerCapabilityHost {
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false as const,
|
||||
error: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
error: formatUiError(error),
|
||||
};
|
||||
}
|
||||
if (!identityMatches()) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueueMode } from "../../../../src/auto-reply/reply/queue/types.js";
|
||||
import type { QueueMode } from "../../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import { GatewayRequestError } from "../../api/gateway.ts";
|
||||
import type { ChatAttachment } from "../../lib/chat/chat-types.ts";
|
||||
import { canCallGatewayMethod } from "../../lib/gateway-methods.ts";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { GatewayRequestError } from "../../api/gateway.ts";
|
||||
import type { GatewaySessionRow, SessionsListResult } from "../../api/types.ts";
|
||||
import type { AgentsListResult, GatewaySessionRow, SessionsListResult } from "../../api/types.ts";
|
||||
import { SLASH_COMMANDS } from "../../lib/chat/commands.ts";
|
||||
import { createResolvedModelPatch } from "../../test-helpers/chat-model.ts";
|
||||
import { createStorageMock } from "../../test-helpers/storage.ts";
|
||||
@@ -1916,7 +1916,7 @@ describe("handleSendChat", () => {
|
||||
])("gates $sendKey on its default-main alias patch", async ({ patchKey, sendKey }) => {
|
||||
const settingsPatch = createDeferred<boolean>();
|
||||
|
||||
const agentsList = {
|
||||
const agentsList: AgentsListResult = {
|
||||
defaultId: "ops",
|
||||
mainKey: "work",
|
||||
scope: "per-sender",
|
||||
@@ -1951,7 +1951,7 @@ describe("handleSendChat", () => {
|
||||
it("keeps a real main agent patch separate from a non-main default agent", async () => {
|
||||
const settingsPatch = createDeferred<boolean>();
|
||||
|
||||
const agentsList = {
|
||||
const agentsList: AgentsListResult = {
|
||||
defaultId: "ops",
|
||||
mainKey: "work",
|
||||
scope: "per-sender",
|
||||
@@ -8909,7 +8909,7 @@ describe("handleAbortChat", () => {
|
||||
agentId: "work",
|
||||
},
|
||||
},
|
||||
])("$name", async ({ scope, expected }) => {
|
||||
] as const)("$name", async ({ scope, expected }) => {
|
||||
const request = vi.fn(async () => ({ abortedRunId: null, status: "aborted" }));
|
||||
const sessionKey = "agent:work:main";
|
||||
const host = makeChatHost({
|
||||
|
||||
@@ -42,8 +42,10 @@ import {
|
||||
renderBackgroundTasksRail,
|
||||
type BackgroundTasksProps,
|
||||
} from "./components/chat-background-tasks.ts";
|
||||
import type { ChatComposerPlusMenuProps } from "./components/chat-composer-plus-menu.ts";
|
||||
import type { ChatComposerDisabledBanner } from "./components/chat-composer-types.ts";
|
||||
import type {
|
||||
CapabilityMenuProps,
|
||||
ChatComposerDisabledBanner,
|
||||
} from "./components/chat-composer-types.ts";
|
||||
import { isChatRunWorking, renderChatComposer } from "./components/chat-composer.ts";
|
||||
import { inlineChatImageFromEvent, openInlineChatImage } from "./components/chat-image-lightbox.ts";
|
||||
import type { ArtifactDownloadResolver } from "./components/chat-message-media.ts";
|
||||
@@ -166,17 +168,7 @@ export type ChatProps = {
|
||||
onDismissWorkspaceConflict?: () => void;
|
||||
sessions: SessionsListResult | null;
|
||||
toolOverrides?: SessionToolOverrides;
|
||||
capabilityMenu?: Omit<
|
||||
ChatComposerPlusMenuProps,
|
||||
| "attachments"
|
||||
| "disabled"
|
||||
| "open"
|
||||
| "view"
|
||||
| "toolOverrides"
|
||||
| "onOpenChange"
|
||||
| "onViewChange"
|
||||
| "showCapabilities"
|
||||
>;
|
||||
capabilityMenu?: CapabilityMenuProps;
|
||||
swarmSessions?: readonly GatewaySessionRow[];
|
||||
/** Host context resolving global-alias session keys (scope=global fleets). */
|
||||
sessionHost?: UiSessionDefaultsHost | null;
|
||||
|
||||
@@ -21,6 +21,18 @@ import type {
|
||||
ChatComposerPlusMenuView,
|
||||
} from "./chat-composer-plus-menu.ts";
|
||||
|
||||
export type CapabilityMenuProps = Omit<
|
||||
ChatComposerPlusMenuProps,
|
||||
| "attachments"
|
||||
| "disabled"
|
||||
| "open"
|
||||
| "view"
|
||||
| "toolOverrides"
|
||||
| "onOpenChange"
|
||||
| "onViewChange"
|
||||
| "showCapabilities"
|
||||
>;
|
||||
|
||||
type ChatComposerDisabledBannerContent = {
|
||||
text: string;
|
||||
actionLabel: string;
|
||||
@@ -56,17 +68,7 @@ export type ChatComposerProps = {
|
||||
draft: string;
|
||||
sessions: SessionsListResult | null;
|
||||
toolOverrides?: SessionToolOverrides;
|
||||
capabilityMenu?: Omit<
|
||||
ChatComposerPlusMenuProps,
|
||||
| "attachments"
|
||||
| "disabled"
|
||||
| "open"
|
||||
| "view"
|
||||
| "toolOverrides"
|
||||
| "onOpenChange"
|
||||
| "onViewChange"
|
||||
| "showCapabilities"
|
||||
>;
|
||||
capabilityMenu?: CapabilityMenuProps;
|
||||
providerUsage?: ProviderUsageDisplayProps;
|
||||
assistantName: string;
|
||||
sendShortcut?: ChatSendShortcut;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueueMode } from "../../../../src/auto-reply/reply/queue/types.js";
|
||||
import type { QueueMode } from "../../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import type { SessionsListResult } from "../../api/types.ts";
|
||||
import { setLastActiveSessionKey } from "../../app/settings.ts";
|
||||
import type { ChatAttachment, ChatQueueItem } from "../../lib/chat/chat-types.ts";
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import { html, nothing, type PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
import type { AgentsWorkspaceGetResult } from "../../../../packages/gateway-protocol/src/index.js";
|
||||
import type { MemorySearchResponse } from "../../../../src/gateway/server-methods/memory-search.ts";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { redactToolDetail } from "../../lib/browser-redact.ts";
|
||||
import { formatUiError } from "../../lib/format-error.ts";
|
||||
import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts";
|
||||
import "../../styles/memory-memories.css";
|
||||
|
||||
@@ -114,7 +113,7 @@ class MemoryMemoriesElement extends OpenClawLightDomElement {
|
||||
this.searchState = {
|
||||
kind: "error",
|
||||
query: normalizedQuery,
|
||||
message: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
message: formatUiError(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -159,7 +158,7 @@ class MemoryMemoriesElement extends OpenClawLightDomElement {
|
||||
}
|
||||
this.details = new Map(this.details).set(key, {
|
||||
kind: "error",
|
||||
message: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
message: formatUiError(error),
|
||||
});
|
||||
} finally {
|
||||
if (this.detailRequests.get(key) === request) {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// this element owns the shared agent selection, Overview status, and global
|
||||
// configuration controllers used by Settings.
|
||||
import { consume } from "@lit/context";
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import { asNullableRecord as asConfigRecord } from "@openclaw/normalization-core/record-coerce";
|
||||
import { html, type PropertyValues, type TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
@@ -15,8 +14,8 @@ import type { AgentSelectOption } from "../../components/agent-select.ts";
|
||||
import { renderDocsLink } from "../../components/settings-ui.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { listSelectableAgents, normalizeAgentLabel } from "../../lib/agents/display.ts";
|
||||
import { redactToolDetail } from "../../lib/browser-redact.ts";
|
||||
import { currentConfigObject } from "../../lib/config/index.ts";
|
||||
import { formatUiError } from "../../lib/format-error.ts";
|
||||
import { isGatewayMethodAdvertised } from "../../lib/gateway-methods.ts";
|
||||
import {
|
||||
loadPluginCatalog,
|
||||
@@ -383,7 +382,7 @@ class MemorySettingsPage extends OpenClawLightDomElement {
|
||||
}
|
||||
this.overviewStatus = {
|
||||
kind: "error",
|
||||
message: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
message: formatUiError(error),
|
||||
};
|
||||
} finally {
|
||||
if (this.overviewRequest === request) {
|
||||
@@ -497,10 +496,7 @@ class MemorySettingsPage extends OpenClawLightDomElement {
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.connection === connection) {
|
||||
this.addonErrors = new Map(this.addonErrors).set(
|
||||
pluginId,
|
||||
formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
);
|
||||
this.addonErrors = new Map(this.addonErrors).set(pluginId, formatUiError(error));
|
||||
}
|
||||
} finally {
|
||||
if (this.addonNoticeOperations.get(pluginId) === noticeOperation) {
|
||||
@@ -553,7 +549,7 @@ class MemorySettingsPage extends OpenClawLightDomElement {
|
||||
if (this.connection === connection) {
|
||||
this.engineOutcome = {
|
||||
kind: "error",
|
||||
message: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
message: formatUiError(error),
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
renderSettingsToggleRow,
|
||||
} from "../../components/settings-ui.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { shouldHandleNavigationClick } from "../../lib/navigation-click.ts";
|
||||
import { languageLabel, renderLanguageSelect } from "./language-select.ts";
|
||||
import { renderSessionObserverSettings } from "./session-observer-settings.ts";
|
||||
import { renderSettingsSelectRow } from "./settings-select-row.ts";
|
||||
@@ -416,16 +417,11 @@ export function renderLobsterPetSection(props: ConfigProps) {
|
||||
class="btn btn--sm lobsterdex__open"
|
||||
href=${props.lobsterdexHref}
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (
|
||||
event.button === 0 &&
|
||||
!event.metaKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.shiftKey &&
|
||||
!event.altKey
|
||||
) {
|
||||
event.preventDefault();
|
||||
props.onOpenLobsterdex?.();
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
props.onOpenLobsterdex?.();
|
||||
}}
|
||||
>${t("quickSettings.appearance.lobsterdexOpen")}</a
|
||||
>`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SystemInfoResult } from "../../../../packages/gateway-protocol/src/index.js";
|
||||
import type { QueueMode } from "../../../../src/auto-reply/reply/queue/types.js";
|
||||
import type { QueueMode } from "../../../../packages/gateway-protocol/src/schema/logs-chat.js";
|
||||
import type { ConfigUiHints, ModelCatalogEntry } from "../../api/types.ts";
|
||||
import type { NativeNotificationsPermission } from "../../app/native-notifications.ts";
|
||||
import type { ServerUiPrefProvenance } from "../../app/server-prefs.ts";
|
||||
|
||||
@@ -2169,7 +2169,15 @@ describe("config view", () => {
|
||||
unseen?.closest("openclaw-tooltip")?.querySelector('[slot="content"]')?.textContent,
|
||||
).toContain("Ripe when thumped.");
|
||||
|
||||
container.querySelector<HTMLAnchorElement>(".lobsterdex__open")?.click();
|
||||
const openLink = container.querySelector<HTMLAnchorElement>(".lobsterdex__open");
|
||||
openLink?.addEventListener("click", (event) => event.preventDefault(), {
|
||||
capture: true,
|
||||
once: true,
|
||||
});
|
||||
openLink?.click();
|
||||
expect(onOpenLobsterdex).not.toHaveBeenCalled();
|
||||
|
||||
openLink?.click();
|
||||
expect(onOpenLobsterdex).toHaveBeenCalledOnce();
|
||||
} finally {
|
||||
localStorage.removeItem("openclaw.control.lobsterdex.v1");
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
formatMs,
|
||||
formatTokens,
|
||||
} from "../../lib/format.ts";
|
||||
import { shouldHandleNavigationClick } from "../../lib/navigation-click.ts";
|
||||
import { sessionNavigationTarget } from "../../lib/sessions/route-navigation.ts";
|
||||
|
||||
// Leaf contract: the slice of the cron view props this module needs. Keeping
|
||||
@@ -349,14 +350,7 @@ function renderRun(
|
||||
class="session-link"
|
||||
href=${chatUrl}
|
||||
@click=${(e: MouseEvent) => {
|
||||
if (
|
||||
e.defaultPrevented ||
|
||||
e.button !== 0 ||
|
||||
e.metaKey ||
|
||||
e.ctrlKey ||
|
||||
e.shiftKey ||
|
||||
e.altKey
|
||||
) {
|
||||
if (!shouldHandleNavigationClick(e)) {
|
||||
return;
|
||||
}
|
||||
if (onNavigateToChat && entry.sessionKey) {
|
||||
|
||||
@@ -88,7 +88,7 @@ describe("CustodianSessionStore", () => {
|
||||
agentsList: {
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,13 +6,10 @@ import type { EventLogEntry } from "../../api/event-log.ts";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import type { HealthSnapshot, StatusSummary } from "../../api/types.ts";
|
||||
import { titleForRoute } from "../../app-navigation.ts";
|
||||
import {
|
||||
applicationContext,
|
||||
type ApplicationContext,
|
||||
type ApplicationGatewaySnapshot,
|
||||
} from "../../app/context.ts";
|
||||
import { applicationContext, type ApplicationContext } from "../../app/context.ts";
|
||||
import { renderSettingsWorkspace } from "../../components/settings-workspace.ts";
|
||||
import { loadGatewayDiagnostics } from "../../lib/gateway-diagnostics.ts";
|
||||
import { GatewayPageController } from "../../lit/gateway-page-controller.ts";
|
||||
import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts";
|
||||
import { PollController } from "../../lit/poll-controller.ts";
|
||||
import { SubscriptionsController } from "../../lit/subscriptions-controller.ts";
|
||||
@@ -24,8 +21,6 @@ class DebugPage extends OpenClawLightDomElement {
|
||||
@consume({ context: applicationContext, subscribe: true })
|
||||
private context!: ApplicationContext;
|
||||
|
||||
@state() private client: GatewayBrowserClient | null = null;
|
||||
@state() private connected = false;
|
||||
@state() private debugStatus: StatusSummary | null = null;
|
||||
@state() private debugHealth: HealthSnapshot | null = null;
|
||||
@state() private debugModels: unknown[] = [];
|
||||
@@ -45,13 +40,11 @@ class DebugPage extends OpenClawLightDomElement {
|
||||
},
|
||||
false,
|
||||
);
|
||||
private hasBoundGatewaySource = false;
|
||||
private gatewaySource: ApplicationContext["gateway"] | null = null;
|
||||
private callEpoch = 0;
|
||||
private diagnosticsTaskActiveClient: GatewayBrowserClient | null = null;
|
||||
private readonly diagnosticsTask = new Task(this, {
|
||||
autoRun: false,
|
||||
args: () => [this.connected ? this.client : null] as const,
|
||||
args: () => [this.gateway.connected ? this.gateway.client : null] as const,
|
||||
task: ([client], { signal }) =>
|
||||
client ? loadGatewayDiagnostics(client, signal) : initialState,
|
||||
onComplete: (result) => {
|
||||
@@ -67,68 +60,45 @@ class DebugPage extends OpenClawLightDomElement {
|
||||
this.debugDiagnosticsError = String(error);
|
||||
},
|
||||
});
|
||||
private readonly subscriptions = new SubscriptionsController(this)
|
||||
.effect(
|
||||
() => this.context?.gateway,
|
||||
(gateway) => {
|
||||
const resetForSourceBind = this.hasBoundGatewaySource;
|
||||
this.hasBoundGatewaySource = true;
|
||||
this.gatewaySource = gateway;
|
||||
const cleanup = gateway.subscribe((snapshot) => {
|
||||
if (this.gatewaySource === gateway && this.context.gateway === gateway) {
|
||||
this.applyGatewaySnapshot(snapshot);
|
||||
}
|
||||
});
|
||||
this.applyGatewaySnapshot(gateway.snapshot, resetForSourceBind);
|
||||
return cleanup;
|
||||
},
|
||||
)
|
||||
.watch(
|
||||
() => this.context?.gateway,
|
||||
(gateway, notify) => gateway.subscribeEventLog(notify),
|
||||
(gateway) => {
|
||||
this.eventLog = gateway.eventLog;
|
||||
},
|
||||
);
|
||||
private readonly gateway = new GatewayPageController(this, {
|
||||
getGateway: () => this.context?.gateway,
|
||||
onIdentityChange: () => {
|
||||
this.debugStatus = null;
|
||||
this.debugHealth = null;
|
||||
this.debugModels = [];
|
||||
this.debugHeartbeat = null;
|
||||
this.debugCallResult = null;
|
||||
this.debugCallError = null;
|
||||
this.debugDiagnosticsError = null;
|
||||
},
|
||||
invalidateRequests: () => {
|
||||
void this.diagnosticsTask.run([null]);
|
||||
this.diagnosticsTaskActiveClient = null;
|
||||
this.callEpoch += 1;
|
||||
},
|
||||
onSnapshot: () => {
|
||||
this.syncPolling();
|
||||
this.ensureInitialDebug();
|
||||
},
|
||||
});
|
||||
private readonly subscriptions = new SubscriptionsController(this).watch(
|
||||
() => this.context?.gateway,
|
||||
(gateway, notify) => gateway.subscribeEventLog(notify),
|
||||
(gateway) => {
|
||||
this.eventLog = gateway.eventLog;
|
||||
},
|
||||
);
|
||||
|
||||
override disconnectedCallback() {
|
||||
this.subscriptions.clear();
|
||||
void this.diagnosticsTask.run([null]);
|
||||
this.diagnosticsTaskActiveClient = null;
|
||||
this.callEpoch += 1;
|
||||
this.gatewaySource = null;
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private applyGatewaySnapshot(snapshot: ApplicationGatewaySnapshot, resetForSourceBind = false) {
|
||||
const connectionChanged = (snapshot.phase === "connected") !== this.connected;
|
||||
const clientChanged = resetForSourceBind || snapshot.client !== this.client;
|
||||
if (clientChanged || connectionChanged) {
|
||||
void this.diagnosticsTask.run([null]);
|
||||
this.diagnosticsTaskActiveClient = null;
|
||||
this.callEpoch += 1;
|
||||
}
|
||||
this.client = snapshot.client;
|
||||
this.connected = snapshot.phase === "connected";
|
||||
if (clientChanged) {
|
||||
this.resetServerState();
|
||||
}
|
||||
this.syncPolling();
|
||||
this.ensureInitialDebug();
|
||||
}
|
||||
|
||||
private resetServerState() {
|
||||
this.debugStatus = null;
|
||||
this.debugHealth = null;
|
||||
this.debugModels = [];
|
||||
this.debugHeartbeat = null;
|
||||
this.debugCallResult = null;
|
||||
this.debugCallError = null;
|
||||
this.debugDiagnosticsError = null;
|
||||
}
|
||||
|
||||
private syncPolling() {
|
||||
if (!this.connected || !this.client) {
|
||||
if (!this.gateway.connected || !this.gateway.client) {
|
||||
this.polling.stop();
|
||||
return;
|
||||
}
|
||||
@@ -136,14 +106,19 @@ class DebugPage extends OpenClawLightDomElement {
|
||||
}
|
||||
|
||||
private ensureInitialDebug() {
|
||||
if (!this.connected || !this.client || this.debugStatus || this.diagnosticsTaskActiveClient) {
|
||||
if (
|
||||
!this.gateway.connected ||
|
||||
!this.gateway.client ||
|
||||
this.debugStatus ||
|
||||
this.diagnosticsTaskActiveClient
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void this.loadDiagnostics();
|
||||
}
|
||||
|
||||
private loadDiagnostics(): Promise<void> {
|
||||
const client = this.connected ? this.client : null;
|
||||
const client = this.gateway.connected ? this.gateway.client : null;
|
||||
if (!client || this.diagnosticsTaskActiveClient) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -152,18 +127,18 @@ class DebugPage extends OpenClawLightDomElement {
|
||||
}
|
||||
|
||||
private async callDebugMethod() {
|
||||
const client = this.connected ? this.client : null;
|
||||
const client = this.gateway.connected ? this.gateway.client : null;
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
this.debugCallError = null;
|
||||
this.debugCallResult = null;
|
||||
const gateway = this.gatewaySource;
|
||||
const gateway = this.gateway.gateway;
|
||||
const epoch = ++this.callEpoch;
|
||||
const isCurrent = () =>
|
||||
this.connected &&
|
||||
this.client === client &&
|
||||
this.gatewaySource === gateway &&
|
||||
this.gateway.connected &&
|
||||
this.gateway.client === client &&
|
||||
this.gateway.gateway === gateway &&
|
||||
this.context.gateway === gateway &&
|
||||
this.callEpoch === epoch;
|
||||
try {
|
||||
|
||||
@@ -633,16 +633,15 @@ describe("gateway source replacement across reconnect with a reused client", ()
|
||||
const pending = deferred<unknown>();
|
||||
const request = vi.fn(() => pending.promise);
|
||||
const client = { request } as unknown as GatewayBrowserClient;
|
||||
const context = contextWithClient(client);
|
||||
const context = contextWithClient(client, { connected: true });
|
||||
const page = createPage("openclaw-debug-page", context) as TestPage & {
|
||||
connected: boolean;
|
||||
debugStatus: unknown;
|
||||
diagnosticsTask: { run: () => Promise<void>; status: TaskStatus };
|
||||
};
|
||||
page.debugStatus = { seeded: true };
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
(context.gateway.snapshot as ApplicationGatewaySnapshot).phase = "connected";
|
||||
page.connected = true;
|
||||
page.debugStatus = null;
|
||||
|
||||
const load = page.diagnosticsTask.run();
|
||||
await waitForFast(() => expect(request).toHaveBeenCalledTimes(4));
|
||||
|
||||
@@ -7,7 +7,6 @@ import "./logs-page.ts";
|
||||
|
||||
type TestLogsPage = HTMLElement & {
|
||||
context: ApplicationContext;
|
||||
connected: boolean;
|
||||
logsAutoFollow: boolean;
|
||||
logsEntries: unknown[];
|
||||
logsStatus: { error: string | null; hasLoaded: boolean; stale: boolean };
|
||||
@@ -16,11 +15,16 @@ type TestLogsPage = HTMLElement & {
|
||||
schedule: (force?: boolean) => void;
|
||||
};
|
||||
readonly updateComplete: Promise<boolean>;
|
||||
applyGatewaySnapshot: (snapshot: ApplicationGatewaySnapshot) => void;
|
||||
loadLogs: (opts?: { reset?: boolean; quiet?: boolean }) => Promise<boolean>;
|
||||
requestUpdate: () => void;
|
||||
};
|
||||
|
||||
type TestGateway = ApplicationContext["gateway"] & {
|
||||
publish: (snapshot: ApplicationGatewaySnapshot) => void;
|
||||
};
|
||||
|
||||
type TestApplicationContext = ApplicationContext & { gateway: TestGateway };
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
const promise = new Promise<T>((resolvePromise) => {
|
||||
@@ -29,16 +33,35 @@ function deferred<T>() {
|
||||
return { promise, resolve };
|
||||
}
|
||||
|
||||
function contextWithClient(client: GatewayBrowserClient): ApplicationContext {
|
||||
function contextWithClient(
|
||||
client: GatewayBrowserClient,
|
||||
connected = false,
|
||||
): TestApplicationContext {
|
||||
let snapshot = {
|
||||
client,
|
||||
phase: connected ? "connected" : "stopped",
|
||||
} as ApplicationGatewaySnapshot;
|
||||
const listeners = new Set<(snapshot: ApplicationGatewaySnapshot) => void>();
|
||||
return {
|
||||
basePath: "",
|
||||
gateway: {
|
||||
snapshot: { client, phase: "stopped" },
|
||||
subscribe: () => () => undefined,
|
||||
get snapshot() {
|
||||
return snapshot;
|
||||
},
|
||||
subscribe: (listener: (snapshot: ApplicationGatewaySnapshot) => void) => {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
publish: (next: ApplicationGatewaySnapshot) => {
|
||||
snapshot = next;
|
||||
for (const listener of listeners) {
|
||||
listener(next);
|
||||
}
|
||||
},
|
||||
},
|
||||
navigate: vi.fn(),
|
||||
preload: vi.fn(async () => undefined),
|
||||
} as unknown as ApplicationContext;
|
||||
} as unknown as TestApplicationContext;
|
||||
}
|
||||
|
||||
describe("LogsPage lifecycle", () => {
|
||||
@@ -103,10 +126,13 @@ describe("LogsPage lifecycle", () => {
|
||||
request: vi.fn(() => pending.promise),
|
||||
} as unknown as GatewayBrowserClient;
|
||||
const page = document.createElement("openclaw-logs-page") as TestLogsPage;
|
||||
page.context = contextWithClient(client);
|
||||
const context = contextWithClient(client);
|
||||
page.context = context;
|
||||
page.logsEntries = [{ raw: "seed" }];
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
page.connected = true;
|
||||
context.gateway.publish({ client, phase: "connected" } as ApplicationGatewaySnapshot);
|
||||
page.logsEntries = [];
|
||||
|
||||
const load = page.loadLogs({ reset: true });
|
||||
page.context = contextWithClient(client);
|
||||
@@ -124,10 +150,13 @@ describe("LogsPage lifecycle", () => {
|
||||
request: vi.fn(() => pending.promise),
|
||||
} as unknown as GatewayBrowserClient;
|
||||
const page = document.createElement("openclaw-logs-page") as TestLogsPage;
|
||||
page.context = contextWithClient(client);
|
||||
const context = contextWithClient(client);
|
||||
page.context = context;
|
||||
page.logsEntries = [{ raw: "seed" }];
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
page.connected = true;
|
||||
context.gateway.publish({ client, phase: "connected" } as ApplicationGatewaySnapshot);
|
||||
page.logsEntries = [];
|
||||
|
||||
const load = page.loadLogs({ reset: true });
|
||||
page.remove();
|
||||
@@ -143,13 +172,16 @@ describe("LogsPage lifecycle", () => {
|
||||
request: vi.fn(() => pending.promise),
|
||||
} as unknown as GatewayBrowserClient;
|
||||
const page = document.createElement("openclaw-logs-page") as TestLogsPage;
|
||||
page.context = contextWithClient(client);
|
||||
const context = contextWithClient(client);
|
||||
page.context = context;
|
||||
page.logsEntries = [{ raw: "seed" }];
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
page.connected = true;
|
||||
context.gateway.publish({ client, phase: "connected" } as ApplicationGatewaySnapshot);
|
||||
page.logsEntries = [];
|
||||
|
||||
const load = page.loadLogs({ reset: true });
|
||||
page.applyGatewaySnapshot({ client, phase: "stopped" } as ApplicationGatewaySnapshot);
|
||||
context.gateway.publish({ client, phase: "stopped" } as ApplicationGatewaySnapshot);
|
||||
pending.resolve({ cursor: 1, lines: ["stale"], reset: true });
|
||||
await load;
|
||||
|
||||
@@ -163,10 +195,12 @@ describe("LogsPage lifecycle", () => {
|
||||
request,
|
||||
} as unknown as GatewayBrowserClient;
|
||||
const page = document.createElement("openclaw-logs-page") as TestLogsPage;
|
||||
page.context = contextWithClient(client);
|
||||
const context = contextWithClient(client, true);
|
||||
page.context = context;
|
||||
page.logsEntries = [{ raw: "seed" }];
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
page.connected = true;
|
||||
page.logsEntries = [];
|
||||
|
||||
const first = page.loadLogs({ quiet: true });
|
||||
const second = page.loadLogs({ quiet: true });
|
||||
@@ -186,10 +220,13 @@ describe("LogsPage lifecycle", () => {
|
||||
.mockResolvedValueOnce({ cursor: 2, lines: ["fresh"], reset: true });
|
||||
const client = { request } as unknown as GatewayBrowserClient;
|
||||
const page = document.createElement("openclaw-logs-page") as TestLogsPage;
|
||||
page.context = contextWithClient(client);
|
||||
const context = contextWithClient(client);
|
||||
page.context = context;
|
||||
page.logsEntries = [{ raw: "seed" }];
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
page.connected = true;
|
||||
context.gateway.publish({ client, phase: "connected" } as ApplicationGatewaySnapshot);
|
||||
page.logsEntries = [];
|
||||
|
||||
await page.loadLogs({ reset: true });
|
||||
await page.loadLogs({ reset: true });
|
||||
@@ -215,16 +252,17 @@ describe("LogsPage lifecycle", () => {
|
||||
),
|
||||
} as unknown as GatewayBrowserClient;
|
||||
const page = document.createElement("openclaw-logs-page") as TestLogsPage;
|
||||
page.context = contextWithClient(client);
|
||||
const context = contextWithClient(client);
|
||||
page.context = context;
|
||||
const requestFrame = vi.spyOn(window, "requestAnimationFrame").mockReturnValue(1);
|
||||
document.body.append(page);
|
||||
await page.updateComplete;
|
||||
page.applyGatewaySnapshot({ client, phase: "connected" } as ApplicationGatewaySnapshot);
|
||||
context.gateway.publish({ client, phase: "connected" } as ApplicationGatewaySnapshot);
|
||||
requestFrame.mockClear();
|
||||
|
||||
page.streamFollow.schedule();
|
||||
page.applyGatewaySnapshot({ client, phase: "stopped" } as ApplicationGatewaySnapshot);
|
||||
page.applyGatewaySnapshot({ client, phase: "connected" } as ApplicationGatewaySnapshot);
|
||||
context.gateway.publish({ client, phase: "stopped" } as ApplicationGatewaySnapshot);
|
||||
context.gateway.publish({ client, phase: "connected" } as ApplicationGatewaySnapshot);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(requestFrame).not.toHaveBeenCalled();
|
||||
|
||||
@@ -3,13 +3,8 @@ import { consume } from "@lit/context";
|
||||
import { initialState, Task, TaskStatus } from "@lit/task";
|
||||
import { html, type PropertyValues } from "lit";
|
||||
import { state } from "lit/decorators.js";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import { titleForRoute } from "../../app-navigation.ts";
|
||||
import {
|
||||
applicationContext,
|
||||
type ApplicationContext,
|
||||
type ApplicationGatewaySnapshot,
|
||||
} from "../../app/context.ts";
|
||||
import { applicationContext, type ApplicationContext } from "../../app/context.ts";
|
||||
import {
|
||||
beginPanelRefresh,
|
||||
completePanelRefresh,
|
||||
@@ -21,10 +16,10 @@ import {
|
||||
formatMissingOperatorReadScopeMessage,
|
||||
isMissingOperatorReadScopeError,
|
||||
} from "../../lib/gateway-errors.ts";
|
||||
import { GatewayPageController } from "../../lit/gateway-page-controller.ts";
|
||||
import { OpenClawLightDomElement } from "../../lit/openclaw-element.ts";
|
||||
import { PollController } from "../../lit/poll-controller.ts";
|
||||
import { StreamAutoFollowController } from "../../lit/stream-auto-follow-controller.ts";
|
||||
import { SubscriptionsController } from "../../lit/subscriptions-controller.ts";
|
||||
import {
|
||||
DEFAULT_LOG_LEVEL_FILTERS,
|
||||
parseLogLine,
|
||||
@@ -40,8 +35,6 @@ class LogsPage extends OpenClawLightDomElement {
|
||||
@consume({ context: applicationContext, subscribe: true })
|
||||
private context!: ApplicationContext;
|
||||
|
||||
@state() private client: GatewayBrowserClient | null = null;
|
||||
@state() private connected = false;
|
||||
@state() private logsStatus = createPanelRefreshStatus();
|
||||
@state() private logsFile: string | null = null;
|
||||
@state() private logsEntries: LogEntry[] = [];
|
||||
@@ -62,13 +55,11 @@ class LogsPage extends OpenClawLightDomElement {
|
||||
false,
|
||||
);
|
||||
private contentScrollFrame: number | null = null;
|
||||
private hasBoundGatewaySource = false;
|
||||
private gatewaySource: ApplicationContext["gateway"] | null = null;
|
||||
private logsTaskQuiet = false;
|
||||
private logsTaskArgs(opts?: { reset?: boolean; quiet?: boolean }) {
|
||||
return [
|
||||
this.connected ? this.gatewaySource : null,
|
||||
this.connected ? this.client : null,
|
||||
this.gateway.connected ? this.gateway.gateway : null,
|
||||
this.gateway.connected ? this.gateway.client : null,
|
||||
opts?.reset ? null : this.logsCursor,
|
||||
opts?.reset === true,
|
||||
opts?.quiet === true,
|
||||
@@ -131,35 +122,40 @@ class LogsPage extends OpenClawLightDomElement {
|
||||
this.logsStatus = completePanelRefresh();
|
||||
},
|
||||
});
|
||||
private readonly subscriptions = new SubscriptionsController(this).effect(
|
||||
() => this.context?.gateway,
|
||||
(gateway) => {
|
||||
const resetForSourceBind = this.hasBoundGatewaySource;
|
||||
this.hasBoundGatewaySource = true;
|
||||
this.gatewaySource = gateway;
|
||||
const cleanup = gateway.subscribe((snapshot) => {
|
||||
if (this.gatewaySource === gateway && this.context.gateway === gateway) {
|
||||
this.applyGatewaySnapshot(snapshot);
|
||||
}
|
||||
});
|
||||
this.applyGatewaySnapshot(gateway.snapshot, resetForSourceBind);
|
||||
private readonly gateway = new GatewayPageController(this, {
|
||||
getGateway: () => this.context?.gateway,
|
||||
onIdentityChange: () => {
|
||||
this.logsStatus = createPanelRefreshStatus();
|
||||
this.logsFile = null;
|
||||
this.logsEntries = [];
|
||||
this.logsTruncated = false;
|
||||
this.logsCursor = null;
|
||||
this.streamFollow.atBottom = true;
|
||||
return cleanup;
|
||||
},
|
||||
);
|
||||
invalidateRequests: () => {
|
||||
this.logsTaskQuiet = false;
|
||||
void this.logsTask.run([null, null, null, false, false]);
|
||||
},
|
||||
onSnapshot: () => {
|
||||
this.syncPolling();
|
||||
this.ensureInitialLogs();
|
||||
},
|
||||
});
|
||||
private readonly streamFollow = new StreamAutoFollowController(this, {
|
||||
selector: ".log-stream",
|
||||
isEnabled: () => this.logsAutoFollow,
|
||||
captureCurrent: () => {
|
||||
const gateway = this.gatewaySource;
|
||||
const client = this.client;
|
||||
const gateway = this.gateway.gateway;
|
||||
const epoch = this.gateway.epoch;
|
||||
// Same-client reconnects retain object identity; the epoch keeps queued
|
||||
// scroll work bound to the connection that scheduled it.
|
||||
return () =>
|
||||
this.isConnected &&
|
||||
this.connected &&
|
||||
this.gateway.connected &&
|
||||
gateway !== null &&
|
||||
this.gatewaySource === gateway &&
|
||||
this.gateway.gateway === gateway &&
|
||||
this.context.gateway === gateway &&
|
||||
this.client === client;
|
||||
this.gateway.epoch === epoch;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -182,10 +178,8 @@ class LogsPage extends OpenClawLightDomElement {
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
this.subscriptions.clear();
|
||||
this.logsTaskQuiet = false;
|
||||
void this.logsTask.run([null, null, null, false, false]);
|
||||
this.gatewaySource = null;
|
||||
if (this.contentScrollFrame !== null) {
|
||||
cancelAnimationFrame(this.contentScrollFrame);
|
||||
this.contentScrollFrame = null;
|
||||
@@ -201,33 +195,8 @@ class LogsPage extends OpenClawLightDomElement {
|
||||
}
|
||||
}
|
||||
|
||||
private applyGatewaySnapshot(snapshot: ApplicationGatewaySnapshot, resetForSourceBind = false) {
|
||||
const connectionChanged = (snapshot.phase === "connected") !== this.connected;
|
||||
const clientChanged = resetForSourceBind || snapshot.client !== this.client;
|
||||
if (clientChanged || connectionChanged) {
|
||||
this.logsTaskQuiet = false;
|
||||
void this.logsTask.run([null, null, null, false, false]);
|
||||
}
|
||||
this.client = snapshot.client;
|
||||
this.connected = snapshot.phase === "connected";
|
||||
if (clientChanged) {
|
||||
this.resetServerState();
|
||||
}
|
||||
this.syncPolling();
|
||||
this.ensureInitialLogs();
|
||||
}
|
||||
|
||||
private resetServerState() {
|
||||
this.logsStatus = createPanelRefreshStatus();
|
||||
this.logsFile = null;
|
||||
this.logsEntries = [];
|
||||
this.logsTruncated = false;
|
||||
this.logsCursor = null;
|
||||
this.streamFollow.atBottom = true;
|
||||
}
|
||||
|
||||
private syncPolling() {
|
||||
if (!this.connected || !this.client) {
|
||||
if (!this.gateway.connected || !this.gateway.client) {
|
||||
this.polling.stop();
|
||||
return;
|
||||
}
|
||||
@@ -235,7 +204,7 @@ class LogsPage extends OpenClawLightDomElement {
|
||||
}
|
||||
|
||||
private ensureInitialLogs() {
|
||||
if (!this.connected || !this.client || this.logsEntries.length > 0) {
|
||||
if (!this.gateway.connected || !this.gateway.client || this.logsEntries.length > 0) {
|
||||
return;
|
||||
}
|
||||
void this.loadLogs({ reset: true }).then((current) => {
|
||||
@@ -247,11 +216,12 @@ class LogsPage extends OpenClawLightDomElement {
|
||||
|
||||
private async loadLogs(opts?: { reset?: boolean; quiet?: boolean }): Promise<boolean> {
|
||||
const quiet = opts?.quiet === true;
|
||||
const gateway = this.gateway.gateway;
|
||||
if (
|
||||
!this.gatewaySource ||
|
||||
!this.client ||
|
||||
!this.connected ||
|
||||
this.context.gateway !== this.gatewaySource ||
|
||||
!gateway ||
|
||||
!this.gateway.client ||
|
||||
!this.gateway.connected ||
|
||||
this.context.gateway !== gateway ||
|
||||
(this.logsTask.status === TaskStatus.PENDING && opts?.reset !== true)
|
||||
) {
|
||||
return false;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import { html, nothing } from "lit";
|
||||
import type {
|
||||
EnvironmentsListResult,
|
||||
@@ -7,7 +6,7 @@ import type {
|
||||
import { GatewayRequestError, type GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import { icons } from "../../components/icons.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { redactToolDetail } from "../../lib/browser-redact.ts";
|
||||
import { formatUiError } from "../../lib/format-error.ts";
|
||||
import { generateUUID } from "../../lib/uuid.ts";
|
||||
import type { DraftCloudProfile } from "./discovery.ts";
|
||||
import { readDraftCloudProfiles } from "./discovery.ts";
|
||||
@@ -70,7 +69,7 @@ async function readPlacement(
|
||||
if (!isAmbiguousDispatchError(error)) {
|
||||
return {
|
||||
status: "rejected",
|
||||
error: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
error: formatUiError(error),
|
||||
};
|
||||
}
|
||||
return { status: "unavailable" };
|
||||
@@ -96,7 +95,7 @@ async function cancelActivePlacement(
|
||||
await client.request("environments.destroy", { environmentId });
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
return formatErrorMessage(error, { redact: redactToolDetail });
|
||||
return formatUiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +231,7 @@ export async function deleteCloudDraftSession(
|
||||
await client.request("sessions.delete", { key, agentId, deleteTranscript: true });
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
return formatErrorMessage(error, { redact: redactToolDetail });
|
||||
return formatUiError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,7 +333,7 @@ export async function startCloudInitialTurn(
|
||||
isCurrent,
|
||||
);
|
||||
} catch (error) {
|
||||
dispatchError = formatErrorMessage(error, { redact: redactToolDetail });
|
||||
dispatchError = formatUiError(error);
|
||||
if (!isAmbiguousDispatchError(error)) {
|
||||
return { status: "dispatch-rejected", error: dispatchError };
|
||||
}
|
||||
@@ -433,13 +432,13 @@ export async function startCloudInitialTurn(
|
||||
? { status: "cleanup-rejected", error: cleanupError, messageId }
|
||||
: {
|
||||
status: "send-definitive-rejected",
|
||||
error: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
error: formatUiError(error),
|
||||
messageId,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: "send-rejected",
|
||||
error: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
error: formatUiError(error),
|
||||
messageId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("new-session route catalog target", () => {
|
||||
agentsList: {
|
||||
defaultId: "roboclaw",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "roboclaw" }],
|
||||
},
|
||||
});
|
||||
@@ -104,7 +104,7 @@ describe("new-session route catalog target", () => {
|
||||
agentsList: {
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }],
|
||||
},
|
||||
staleRosterClient: true,
|
||||
@@ -119,7 +119,7 @@ describe("new-session route catalog target", () => {
|
||||
agentsState.agentsList = {
|
||||
defaultId: "roboclaw",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "roboclaw" }, { id: "research" }],
|
||||
};
|
||||
const data = await loadNewSessionData(context, "?agent=research&catalog=claude");
|
||||
|
||||
@@ -118,7 +118,7 @@ describe("NodesPage gateway lifecycle", () => {
|
||||
expect(page.nodeState.nodes).toBe(preloadedNodes);
|
||||
|
||||
page.context = { gateway: gateway(client) } as unknown as ApplicationContext;
|
||||
page.presence = [{ instanceId: "stale" }];
|
||||
page.presence = [{ instanceId: "stale", ts: 1_000 }];
|
||||
applyGatewaySnapshot(page, page.context.gateway.snapshot, true);
|
||||
expect(page.nodeState.nodes).toEqual([]);
|
||||
expect(page.presence).toEqual([]);
|
||||
|
||||
@@ -236,6 +236,7 @@ describe("nodes inventory rendering", () => {
|
||||
platform: "linux",
|
||||
version: "2026.7.11",
|
||||
lastInputSeconds: 5,
|
||||
ts: 1_000,
|
||||
},
|
||||
],
|
||||
devicesList: {
|
||||
@@ -269,7 +270,7 @@ describe("nodes inventory rendering", () => {
|
||||
],
|
||||
paired: [],
|
||||
},
|
||||
presence: [{ instanceId: "probe-1", host: "laptop", mode: "probe" }],
|
||||
presence: [{ instanceId: "probe-1", host: "laptop", mode: "probe", ts: 1_000 }],
|
||||
});
|
||||
|
||||
const section = getInventorySection(container);
|
||||
@@ -539,8 +540,15 @@ describe("nodes inventory rendering", () => {
|
||||
roles: ["operator"],
|
||||
platform: "macos 26.5.2",
|
||||
lastInputSeconds: 90,
|
||||
ts: 1_000,
|
||||
},
|
||||
{
|
||||
instanceId: "left-1",
|
||||
host: "gone",
|
||||
mode: "webchat",
|
||||
reason: "disconnect",
|
||||
ts: 2_000,
|
||||
},
|
||||
{ instanceId: "left-1", host: "gone", mode: "webchat", reason: "disconnect" },
|
||||
],
|
||||
});
|
||||
const section = getSection(container, "Connected without pairing");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { initialState, Task, TaskStatus } from "@lit/task";
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import type { RouteLocation } from "@openclaw/uirouter";
|
||||
import { html, type PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators.js";
|
||||
@@ -19,7 +18,6 @@ import type { McpServerForm } from "../../components/mcp-server-form.ts";
|
||||
import { renderDocsLink } from "../../components/settings-ui.ts";
|
||||
import { renderSettingsWorkspace } from "../../components/settings-workspace.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { redactToolDetail } from "../../lib/browser-redact.ts";
|
||||
import { resolveEditableSnapshotConfig } from "../../lib/config/index.ts";
|
||||
import {
|
||||
buildAddMcpServerPatch,
|
||||
@@ -32,6 +30,7 @@ import {
|
||||
type McpServerSummary,
|
||||
type McpServersPatchBuildResult,
|
||||
} from "../../lib/config/mcp-servers.ts";
|
||||
import { formatUiError } from "../../lib/format-error.ts";
|
||||
import {
|
||||
installPlugin,
|
||||
pluginInstallNeedsRiskAcknowledgement,
|
||||
@@ -175,7 +174,7 @@ class PluginsPage extends OpenClawLightDomElement {
|
||||
this.replaceResult(result);
|
||||
},
|
||||
onError: (error) => {
|
||||
this.error = formatErrorMessage(error, { redact: redactToolDetail });
|
||||
this.error = formatUiError(error);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -531,14 +530,14 @@ class PluginsPage extends OpenClawLightDomElement {
|
||||
private get searchError(): string | null {
|
||||
return this.searchTask.status === TaskStatus.ERROR &&
|
||||
this.debouncedSearchQuery === this.query.trim()
|
||||
? formatErrorMessage(this.searchTask.error, { redact: redactToolDetail })
|
||||
? formatUiError(this.searchTask.error)
|
||||
: null;
|
||||
}
|
||||
|
||||
private get configRefreshError(): string | null {
|
||||
const failure =
|
||||
this.configTask.status === TaskStatus.ERROR
|
||||
? formatErrorMessage(this.configTask.error, { redact: redactToolDetail })
|
||||
? formatUiError(this.configTask.error)
|
||||
: this.configTask.status === TaskStatus.COMPLETE
|
||||
? this.configTask.value
|
||||
: null;
|
||||
@@ -734,7 +733,7 @@ class PluginsPage extends OpenClawLightDomElement {
|
||||
onError: (error: unknown) => void = (error) => {
|
||||
this.setMessage(rowKey, {
|
||||
kind: "error",
|
||||
text: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
text: formatUiError(error),
|
||||
});
|
||||
},
|
||||
): Promise<void> {
|
||||
@@ -798,7 +797,7 @@ class PluginsPage extends OpenClawLightDomElement {
|
||||
}
|
||||
this.setMessage(rowKey, {
|
||||
kind: "error",
|
||||
text: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
text: formatUiError(error),
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -893,7 +892,7 @@ class PluginsPage extends OpenClawLightDomElement {
|
||||
this.mcpMessage = { kind: "success", text: params.successText };
|
||||
return true;
|
||||
} catch (error) {
|
||||
return fail(formatErrorMessage(error, { redact: redactToolDetail }));
|
||||
return fail(formatUiError(error));
|
||||
} finally {
|
||||
this.mcpBusy = false;
|
||||
if (params.busyKey) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import { definePage, type RouteLoaderOptions, type RouteLocation } from "@openclaw/uirouter";
|
||||
import { html } from "lit";
|
||||
import { routePageSpec } from "../../app-route-paths.ts";
|
||||
import type { ApplicationContext } from "../../app/context.ts";
|
||||
import { redactToolDetail } from "../../lib/browser-redact.ts";
|
||||
import { formatUiError } from "../../lib/format-error.ts";
|
||||
import { loadPluginCatalog } from "../../lib/plugins/index.ts";
|
||||
import type { PluginsRouteData } from "./plugins-page.ts";
|
||||
import { pluginsRouteLocation } from "./route-data.ts";
|
||||
@@ -27,7 +26,7 @@ async function loadPluginsRouteData(
|
||||
gateway,
|
||||
gatewaySnapshot,
|
||||
result: null,
|
||||
error: formatErrorMessage(error, { redact: redactToolDetail }),
|
||||
error: formatUiError(error),
|
||||
location,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
formatRelativeTimestamp,
|
||||
formatTokens,
|
||||
} from "../../lib/format.ts";
|
||||
import { shouldHandleNavigationClick } from "../../lib/navigation-click.ts";
|
||||
import { formatSessionTokens } from "../../lib/presenter.ts";
|
||||
import { isCronSessionKey } from "../../lib/session-display.ts";
|
||||
import { formatGoalDetail, formatGoalSummary } from "../../lib/session-goal.ts";
|
||||
@@ -1486,14 +1487,7 @@ function renderRows(row: GatewaySessionRow, props: SessionsProps) {
|
||||
href=${chatUrl}
|
||||
class="session-link"
|
||||
@click=${(e: MouseEvent) => {
|
||||
if (
|
||||
e.defaultPrevented ||
|
||||
e.button !== 0 ||
|
||||
e.metaKey ||
|
||||
e.ctrlKey ||
|
||||
e.shiftKey ||
|
||||
e.altKey
|
||||
) {
|
||||
if (!shouldHandleNavigationClick(e)) {
|
||||
return;
|
||||
}
|
||||
if (props.onNavigateToChat) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import { html, nothing } from "lit";
|
||||
import type { ApplicationGateway } from "../../app/context.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { redactToolDetail } from "../../lib/browser-redact.ts";
|
||||
import { formatUiError } from "../../lib/format-error.ts";
|
||||
import { canCallGatewayMethod } from "../../lib/gateway-methods.ts";
|
||||
import type { SkillWorkshopHistoryScanResult, SkillWorkshopHistoryScanState } from "./state.ts";
|
||||
|
||||
@@ -69,7 +68,7 @@ export async function loadSkillWorkshopHistoryScanStatus(
|
||||
);
|
||||
current.state.loaded = true;
|
||||
} catch (error) {
|
||||
current.state.error = formatErrorMessage(error, { redact: redactToolDetail });
|
||||
current.state.error = formatUiError(error);
|
||||
// Loaded means this scope attempted a read. A scan action can still
|
||||
// force a retry because the result remains absent.
|
||||
current.state.loaded = true;
|
||||
@@ -144,7 +143,7 @@ export async function runSkillWorkshopHistoryScan(params: {
|
||||
params.state.loaded = true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
const scanError = formatErrorMessage(error, { redact: redactToolDetail });
|
||||
const scanError = formatUiError(error);
|
||||
try {
|
||||
params.state.result = await client.request<SkillWorkshopHistoryScanResult>(
|
||||
"skills.proposals.historyStatus",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// Control UI controller manages skill workshop gateway state.
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import type { AgentSelectionCapability } from "../../app/agent-selection.ts";
|
||||
import type { ApplicationGateway } from "../../app/context.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { formatBytes } from "../../lib/agents/display.ts";
|
||||
import { redactToolDetail } from "../../lib/browser-redact.ts";
|
||||
import { formatUiError } from "../../lib/format-error.ts";
|
||||
import { canCallGatewayMethod } from "../../lib/gateway-methods.ts";
|
||||
import {
|
||||
normalizeAgentId,
|
||||
@@ -406,7 +405,7 @@ export async function loadSkillWorkshopProposals(
|
||||
await loadSkillWorkshopProposalDetail(state, context, state.skillWorkshopSelectedKey);
|
||||
}
|
||||
} catch (err) {
|
||||
state.skillWorkshopError = formatErrorMessage(err, { redact: redactToolDetail });
|
||||
state.skillWorkshopError = formatUiError(err);
|
||||
} finally {
|
||||
state.skillWorkshopLoading = false;
|
||||
if (skillWorkshopAgentParams(context).agentId !== requestAgentId) {
|
||||
@@ -456,7 +455,7 @@ async function loadSkillWorkshopProposalDetail(
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (state.skillWorkshopAgentId === requestAgentId) {
|
||||
state.skillWorkshopError = formatErrorMessage(err, { redact: redactToolDetail });
|
||||
state.skillWorkshopError = formatUiError(err);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
@@ -524,7 +523,7 @@ export async function runSkillWorkshopLifecycleAction(
|
||||
t(action === "apply" ? "skillWorkshop.notices.applied" : "skillWorkshop.notices.rejected"),
|
||||
);
|
||||
} catch (err) {
|
||||
state.skillWorkshopError = formatErrorMessage(err, { redact: redactToolDetail });
|
||||
state.skillWorkshopError = formatUiError(err);
|
||||
} finally {
|
||||
if (
|
||||
state.skillWorkshopActionBusy?.key === proposalId &&
|
||||
@@ -599,7 +598,7 @@ export async function runSkillWorkshopEvaluation(
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (state.skillWorkshopAgentId === requestAgentId) {
|
||||
state.skillWorkshopError = formatErrorMessage(err, { redact: redactToolDetail });
|
||||
state.skillWorkshopError = formatUiError(err);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
@@ -668,7 +667,7 @@ export async function requestSkillWorkshopRevision(
|
||||
showActionNotice(state, proposal, t("skillWorkshop.notices.revisionRequested"));
|
||||
return true;
|
||||
} catch (err) {
|
||||
state.skillWorkshopError = formatErrorMessage(err, { redact: redactToolDetail });
|
||||
state.skillWorkshopError = formatUiError(err);
|
||||
return false;
|
||||
} finally {
|
||||
if (
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { formatErrorMessage } from "@openclaw/normalization-core";
|
||||
import { definePage } from "@openclaw/uirouter";
|
||||
import { html } from "lit";
|
||||
import { routePageSpec } from "../../app-route-paths.ts";
|
||||
import type { ApplicationContext } from "../../app/context.ts";
|
||||
import { redactToolDetail } from "../../lib/browser-redact.ts";
|
||||
import { formatUiError } from "../../lib/format-error.ts";
|
||||
import { loadSkillStatusReport } from "../../lib/skills/index.ts";
|
||||
import type { SkillsRouteData } from "./skills-page.ts";
|
||||
|
||||
@@ -30,12 +29,12 @@ async function loadSkillsRouteData(context: ApplicationContext): Promise<SkillsR
|
||||
try {
|
||||
agentsList = await agents.ensureList();
|
||||
} catch (err) {
|
||||
error = formatErrorMessage(err, { redact: redactToolDetail });
|
||||
error = formatUiError(err);
|
||||
}
|
||||
try {
|
||||
report = (await loadSkillStatusReport(client, null)) ?? null;
|
||||
} catch (err) {
|
||||
error ??= formatErrorMessage(err, { redact: redactToolDetail });
|
||||
error ??= formatUiError(err);
|
||||
}
|
||||
return {
|
||||
gateway,
|
||||
|
||||
@@ -63,7 +63,7 @@ function createProps(overrides: Partial<SkillsProps> = {}): SkillsProps {
|
||||
const agentsList: AgentsListResult = {
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "project",
|
||||
scope: "per-sender",
|
||||
agents: [
|
||||
{ id: "main", name: "Main" },
|
||||
{ id: "research", identity: { name: "Research", avatar: "R" } },
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { GatewaySessionRow } from "../../api/types.ts";
|
||||
import { icon, type IconName } from "../../components/icons.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { formatMs, formatRelativeTimestamp } from "../../lib/format.ts";
|
||||
import { shouldHandleNavigationClick } from "../../lib/navigation-click.ts";
|
||||
import {
|
||||
resolveSessionPreferredFace,
|
||||
sessionNavigationTarget,
|
||||
@@ -56,14 +57,7 @@ function renderSessionLink(task: TaskSummary, props: TasksProps) {
|
||||
class="session-link"
|
||||
href=${href}
|
||||
@click=${(event: MouseEvent) => {
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
event.button !== 0 ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey
|
||||
) {
|
||||
if (!shouldHandleNavigationClick(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
@@ -877,7 +877,7 @@ describe("renderWorkboard", () => {
|
||||
agentsList: {
|
||||
defaultId: "main",
|
||||
mainKey: "agent:main:main",
|
||||
scope: "test",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main", name: "Main" }],
|
||||
},
|
||||
});
|
||||
@@ -907,7 +907,7 @@ describe("renderWorkboard", () => {
|
||||
agentsList: {
|
||||
defaultId: "main",
|
||||
mainKey: "agent:main:main",
|
||||
scope: "test",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }, { id: "writer" }, { id: "ops" }],
|
||||
},
|
||||
scopeAgentId: "writer",
|
||||
@@ -1808,7 +1808,7 @@ describe("renderWorkboard", () => {
|
||||
agentsList: {
|
||||
defaultId: "main",
|
||||
mainKey: "agent:main:main",
|
||||
scope: "test",
|
||||
scope: "per-sender",
|
||||
agents: [
|
||||
{ id: "main", name: "Main" },
|
||||
{ id: "writer", name: "Writer" },
|
||||
@@ -2141,10 +2141,10 @@ describe("renderWorkboard", () => {
|
||||
});
|
||||
|
||||
it("filters cards by linked agent", () => {
|
||||
const agentsList = {
|
||||
const agentsList: NonNullable<WorkboardRenderProps["agentsList"]> = {
|
||||
defaultId: "main",
|
||||
mainKey: "agent:main:main",
|
||||
scope: "test",
|
||||
scope: "per-sender",
|
||||
agents: [
|
||||
{ id: "main", name: "Main" },
|
||||
{ id: "ops", name: "Ops" },
|
||||
@@ -2228,7 +2228,7 @@ describe("renderWorkboard", () => {
|
||||
agentsList: {
|
||||
defaultId: "main",
|
||||
mainKey: "agent:main:main",
|
||||
scope: "test",
|
||||
scope: "per-sender",
|
||||
agents: [
|
||||
{ id: "main", name: "Main" },
|
||||
{ id: "main", name: "Main duplicate" },
|
||||
@@ -2298,7 +2298,7 @@ describe("renderWorkboard", () => {
|
||||
agentsList: {
|
||||
defaultId: "main",
|
||||
mainKey: "agent:main:main",
|
||||
scope: "test",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main", name: "Main", agentRuntime: { id: "codex", source: "agent" } }],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { WorktreeRecord } from "../../../../packages/gateway-protocol/src/i
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import { subtitleForRoute, titleForRoute } from "../../app-navigation.ts";
|
||||
import { applicationContext, type ApplicationContext } from "../../app/context.ts";
|
||||
import { shouldHandleNavigationClick } from "../../components/app-sidebar-nav-menus.ts";
|
||||
import { showConfirmDialog } from "../../components/confirm-dialog.ts";
|
||||
import { renderSessionsHubHeader } from "../../components/sessions-hub-header.ts";
|
||||
import {
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
import { renderSettingsWorkspace } from "../../components/settings-workspace.ts";
|
||||
import { t } from "../../i18n/index.ts";
|
||||
import { formatRelativeTimestamp } from "../../lib/format.ts";
|
||||
import { shouldHandleNavigationClick } from "../../lib/navigation-click.ts";
|
||||
import {
|
||||
resolveSessionPreferredFaceForKey,
|
||||
sessionNavigationTarget,
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("AppSidebar agent chip", () => {
|
||||
{
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }],
|
||||
},
|
||||
[],
|
||||
@@ -66,7 +66,7 @@ describe("AppSidebar agent chip", () => {
|
||||
{
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }],
|
||||
},
|
||||
[],
|
||||
@@ -262,7 +262,7 @@ describe("AppSidebar agent chip", () => {
|
||||
{
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main", identity: { name: "Molty", emoji: "🦞" } }],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -343,7 +343,7 @@ describe("AppSidebar brand actions", () => {
|
||||
const agentsList = {
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }, { id: "research" }],
|
||||
} as AgentsListResult;
|
||||
const { sidebar } = await mountSidebar(
|
||||
@@ -465,7 +465,7 @@ describe("AppSidebar agent chip", () => {
|
||||
const agents = {
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }, { id: "settings" }],
|
||||
} as AgentsListResult;
|
||||
const { sidebar } = await mountSidebar(
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("AppSidebar session catalog pagination", () => {
|
||||
{
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }, { id: "research" }],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -78,7 +78,7 @@ describe("AppSidebar session catalog request errors", () => {
|
||||
{
|
||||
defaultId: "roboclaw",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "roboclaw" }],
|
||||
},
|
||||
);
|
||||
@@ -117,7 +117,7 @@ describe("AppSidebar session catalog request errors", () => {
|
||||
{
|
||||
defaultId: "roboclaw",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }, { id: "roboclaw" }],
|
||||
},
|
||||
);
|
||||
@@ -155,7 +155,7 @@ describe("AppSidebar session catalog request errors", () => {
|
||||
{
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("AppSidebar outbox badges", () => {
|
||||
{
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main" }],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -489,7 +489,7 @@ export async function mountSidebar(
|
||||
export const TWO_AGENTS = {
|
||||
defaultId: "main",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: [{ id: "main", identity: { name: "Molty" } }, { id: "research" }],
|
||||
} as AgentsListResult;
|
||||
|
||||
@@ -497,7 +497,7 @@ export const manyAgents = (count: number) =>
|
||||
({
|
||||
defaultId: "agent-1",
|
||||
mainKey: "main",
|
||||
scope: "agent",
|
||||
scope: "per-sender",
|
||||
agents: Array.from({ length: count }, (_, index) => ({ id: `agent-${index + 1}` })),
|
||||
}) as AgentsListResult;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user