Route OpenClaw chat by HA user

This commit is contained in:
2026-05-06 08:08:18 +02:00
parent 90d525e842
commit 8221098ebc
12 changed files with 285 additions and 68 deletions
+86 -32
View File
@@ -47,9 +47,9 @@ from .const import (
ATTR_ACCOUNT_ID,
ATTR_SOURCE,
ATTR_TIMESTAMP,
ATTR_HA_USER_ID,
CONF_ADDON_CONFIG_PATH,
CONF_AGENT_ID,
CONF_VOICE_AGENT_ID,
CONF_GATEWAY_HOST,
CONF_GATEWAY_PORT,
CONF_GATEWAY_TOKEN,
@@ -67,7 +67,6 @@ from .const import (
CONF_THINKING_TIMEOUT,
CONTEXT_STRATEGY_TRUNCATE,
DEFAULT_AGENT_ID,
DEFAULT_VOICE_AGENT_ID,
DEFAULT_CONTEXT_MAX_CHARS,
DEFAULT_CONTEXT_STRATEGY,
DEFAULT_ENABLE_TOOL_CALLS,
@@ -90,6 +89,13 @@ from .const import (
from .coordinator import OpenClawCoordinator
from .exposure import apply_context_policy, build_exposed_entities_context
from .helpers import extract_text_recursive
from .routing import (
build_scoped_session_id,
normalize_ha_user_id,
normalize_optional_text,
resolve_agent_id,
resolve_model_override,
)
_LOGGER = logging.getLogger(__name__)
@@ -107,7 +113,7 @@ _CARD_PATH = Path(__file__).parent / "www" / _CARD_FILENAME
# URL at which the card JS is served (registered via register_static_path)
_CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}"
# Versioned URL used for Lovelace resource registration to avoid stale browser cache
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.62"
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.63"
OpenClawConfigEntry = ConfigEntry
@@ -125,6 +131,7 @@ SEND_MESSAGE_SCHEMA = vol.Schema(
CLEAR_HISTORY_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_SESSION_ID): cv.string,
vol.Optional(ATTR_AGENT_ID): cv.string,
}
)
@@ -399,18 +406,13 @@ async def _async_add_lovelace_resource(hass: HomeAssistant, url: str) -> bool:
def _async_register_services(hass: HomeAssistant) -> None:
"""Register openclaw.send_message and openclaw.clear_history services."""
def _normalize_optional_text(value: Any) -> str | None:
if not isinstance(value, str):
return None
cleaned = value.strip()
return cleaned or None
async def handle_send_message(call: ServiceCall) -> None:
"""Handle the openclaw.send_message service call."""
message: str = call.data[ATTR_MESSAGE]
source: str | None = call.data.get(ATTR_SOURCE)
session_id: str = call.data.get(ATTR_SESSION_ID) or "default"
call_agent_id = _normalize_optional_text(call.data.get(ATTR_AGENT_ID))
raw_session_id: str = call.data.get(ATTR_SESSION_ID) or "default"
ha_user_id = normalize_ha_user_id(call.context.user_id)
call_agent_id = normalize_optional_text(call.data.get(ATTR_AGENT_ID))
extra_headers = _VOICE_REQUEST_HEADERS if source == "voice" else None
entry_data = _get_first_entry_data(hass)
@@ -421,12 +423,12 @@ def _async_register_services(hass: HomeAssistant) -> None:
client: OpenClawApiClient = entry_data["client"]
coordinator: OpenClawCoordinator = entry_data["coordinator"]
options = _get_entry_options(hass, entry_data)
voice_agent_id = _normalize_optional_text(
options.get(CONF_VOICE_AGENT_ID, DEFAULT_VOICE_AGENT_ID)
resolved_agent_id = resolve_agent_id(ha_user_id, call_agent_id)
scoped_session_id = build_scoped_session_id(
raw_session_id,
ha_user_id,
resolved_agent_id,
)
resolved_agent_id = call_agent_id
if resolved_agent_id is None and source == "voice":
resolved_agent_id = voice_agent_id
try:
include_context = options.get(
@@ -447,13 +449,13 @@ def _async_register_services(hass: HomeAssistant) -> None:
)
system_prompt = apply_context_policy(raw_context, max_chars, strategy)
active_model = _normalize_optional_text(options.get("active_model"))
active_model = resolve_model_override(options.get("active_model"))
_append_chat_history(hass, session_id, "user", message)
_append_chat_history(hass, scoped_session_id, "user", message)
response = await client.async_send_message(
message=message,
session_id=session_id,
session_id=scoped_session_id,
system_prompt=system_prompt,
agent_id=resolved_agent_id,
model=active_model,
@@ -469,7 +471,7 @@ def _async_register_services(hass: HomeAssistant) -> None:
+ "\n".join(f"- {line}" for line in tool_results)
+ "\nRespond to the user based on these results."
),
session_id=session_id,
session_id=scoped_session_id,
system_prompt=system_prompt,
agent_id=resolved_agent_id,
model=active_model,
@@ -486,12 +488,15 @@ def _async_register_services(hass: HomeAssistant) -> None:
list(response.keys()),
)
_append_chat_history(hass, session_id, "assistant", assistant_message)
_append_chat_history(hass, scoped_session_id, "assistant", assistant_message)
hass.bus.async_fire(
EVENT_MESSAGE_RECEIVED,
{
ATTR_MESSAGE: assistant_message,
ATTR_SESSION_ID: session_id,
ATTR_SESSION_ID: raw_session_id,
ATTR_SESSION_KEY: scoped_session_id,
ATTR_HA_USER_ID: ha_user_id,
ATTR_AGENT_ID: resolved_agent_id,
ATTR_MODEL: model_used,
ATTR_TIMESTAMP: datetime.now(timezone.utc).isoformat(),
},
@@ -500,12 +505,20 @@ def _async_register_services(hass: HomeAssistant) -> None:
except OpenClawApiError as err:
_LOGGER.error("Failed to send message to OpenClaw: %s", err)
_append_chat_history(hass, session_id, "assistant", f"OpenClaw error: {err}")
_append_chat_history(
hass,
scoped_session_id,
"assistant",
f"OpenClaw error: {err}",
)
hass.bus.async_fire(
EVENT_MESSAGE_RECEIVED,
{
ATTR_MESSAGE: f"OpenClaw error: {err}",
ATTR_SESSION_ID: session_id,
ATTR_SESSION_ID: raw_session_id,
ATTR_SESSION_KEY: scoped_session_id,
ATTR_HA_USER_ID: ha_user_id,
ATTR_AGENT_ID: resolved_agent_id,
ATTR_MODEL: "unknown",
ATTR_TIMESTAMP: datetime.now(timezone.utc).isoformat(),
},
@@ -513,13 +526,32 @@ def _async_register_services(hass: HomeAssistant) -> None:
async def handle_clear_history(call: ServiceCall) -> None:
"""Handle the openclaw.clear_history service call."""
session_id: str | None = call.data.get(ATTR_SESSION_ID)
_LOGGER.info("Clear history requested (session=%s)", session_id or "all")
raw_session_id: str | None = call.data.get(ATTR_SESSION_ID)
ha_user_id = normalize_ha_user_id(call.context.user_id)
requested_agent_id = normalize_optional_text(call.data.get(ATTR_AGENT_ID))
agent_id = resolve_agent_id(ha_user_id, requested_agent_id)
_LOGGER.info(
"Clear history requested (user=%s, agent=%s, session=%s)",
ha_user_id,
agent_id,
raw_session_id or "all",
)
store = _get_chat_history_store(hass)
if session_id:
store.pop(session_id, None)
if raw_session_id:
scoped_session_id = build_scoped_session_id(
raw_session_id,
ha_user_id,
agent_id,
)
store.pop(scoped_session_id, None)
else:
store.clear()
scoped_prefix = build_scoped_session_id("", ha_user_id, agent_id).rsplit(
"__session_",
1,
)[0] + "__session_"
for key in list(store):
if key.startswith(scoped_prefix):
store.pop(key, None)
async def handle_invoke_tool(call: ServiceCall) -> None:
"""Handle the openclaw.invoke_tool service call."""
@@ -793,9 +825,26 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None:
msg: dict[str, Any],
) -> None:
"""Return chat history for a session."""
session_id = msg.get("session_id") or "default"
history = _get_chat_history_store(hass).get(session_id, [])
connection.send_result(msg["id"], {"session_id": session_id, "messages": history})
raw_session_id = msg.get("session_id") or "default"
connection_user = getattr(connection, "user", None)
ha_user_id = normalize_ha_user_id(getattr(connection_user, "id", None))
agent_id = resolve_agent_id(ha_user_id)
scoped_session_id = build_scoped_session_id(
raw_session_id,
ha_user_id,
agent_id,
)
history = _get_chat_history_store(hass).get(scoped_session_id, [])
connection.send_result(
msg["id"],
{
"session_id": raw_session_id,
ATTR_SESSION_KEY: scoped_session_id,
ATTR_HA_USER_ID: ha_user_id,
ATTR_AGENT_ID: agent_id,
"messages": history,
},
)
websocket_api.async_register_command(hass, websocket_get_history)
@@ -813,6 +862,9 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None:
"""Return frontend-related integration settings."""
entry_data = _get_first_entry_data(hass)
options = _get_entry_options(hass, entry_data) if entry_data else {}
connection_user = getattr(connection, "user", None)
ha_user_id = normalize_ha_user_id(getattr(connection_user, "id", None))
agent_id = resolve_agent_id(ha_user_id)
connection.send_result(
msg["id"],
{
@@ -837,6 +889,8 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None:
CONF_THINKING_TIMEOUT,
DEFAULT_THINKING_TIMEOUT,
),
ATTR_HA_USER_ID: ha_user_id,
ATTR_AGENT_ID: agent_id,
"language": hass.config.language,
},
)
+1
View File
@@ -138,6 +138,7 @@ ATTR_DRY_RUN = "dry_run"
ATTR_MESSAGE_CHANNEL = "message_channel"
ATTR_ACCOUNT_ID = "account_id"
ATTR_AGENT_ID = "agent_id"
ATTR_HA_USER_ID = "ha_user_id"
ATTR_OK = "ok"
ATTR_RESULT = "result"
ATTR_ERROR = "error"
+11 -14
View File
@@ -19,18 +19,18 @@ from homeassistant.helpers import intent
from .api import OpenClawApiClient, OpenClawApiError
from .const import (
ATTR_AGENT_ID,
ATTR_HA_USER_ID,
ATTR_MESSAGE,
ATTR_MODEL,
ATTR_SESSION_ID,
ATTR_SESSION_KEY,
ATTR_TIMESTAMP,
CONF_ASSIST_SESSION_ID,
CONF_AGENT_ID,
CONF_CONTEXT_MAX_CHARS,
CONF_CONTEXT_STRATEGY,
CONF_INCLUDE_EXPOSED_CONTEXT,
CONF_VOICE_AGENT_ID,
DEFAULT_ASSIST_SESSION_ID,
DEFAULT_AGENT_ID,
DEFAULT_CONTEXT_MAX_CHARS,
DEFAULT_CONTEXT_STRATEGY,
DEFAULT_INCLUDE_EXPOSED_CONTEXT,
@@ -41,6 +41,7 @@ from .const import (
from .coordinator import OpenClawCoordinator
from .exposure import apply_context_policy, build_exposed_entities_context
from .helpers import extract_text_recursive
from .routing import normalize_ha_user_id, resolve_agent_id, resolve_model_override
_LOGGER = logging.getLogger(__name__)
@@ -121,19 +122,12 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
message = user_input.text
assistant_id = "conversation"
context = getattr(user_input, "context", None)
ha_user_id = normalize_ha_user_id(getattr(context, "user_id", None))
options = self.entry.options
voice_agent_id = self._normalize_optional_text(
options.get(CONF_VOICE_AGENT_ID)
)
configured_agent_id = self._normalize_optional_text(
options.get(
CONF_AGENT_ID,
self.entry.data.get(CONF_AGENT_ID, DEFAULT_AGENT_ID),
)
)
resolved_agent_id = voice_agent_id or configured_agent_id
resolved_agent_id = resolve_agent_id(ha_user_id)
conversation_id = self._resolve_conversation_id(user_input, resolved_agent_id)
active_model = self._normalize_optional_text(options.get("active_model"))
active_model = resolve_model_override(options.get("active_model"))
include_context = options.get(
CONF_INCLUDE_EXPOSED_CONTEXT,
DEFAULT_INCLUDE_EXPOSED_CONTEXT,
@@ -203,6 +197,9 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent):
{
ATTR_MESSAGE: full_response,
ATTR_SESSION_ID: conversation_id,
ATTR_SESSION_KEY: conversation_id,
ATTR_HA_USER_ID: ha_user_id,
ATTR_AGENT_ID: resolved_agent_id,
ATTR_MODEL: coordinator.data.get(DATA_MODEL) if coordinator.data else None,
ATTR_TIMESTAMP: datetime.now(timezone.utc).isoformat(),
},
+1 -1
View File
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistantIntegration/issues",
"requirements": [],
"version": "0.1.62",
"version": "0.1.63",
"dependencies": ["conversation"],
"after_dependencies": ["hassio", "lovelace"]
}
+71
View File
@@ -0,0 +1,71 @@
"""Routing helpers for Home Assistant user-aware OpenClaw chat sessions."""
from __future__ import annotations
import re
from typing import Any
DEFAULT_CHAT_AGENT_ID = "tetra-home"
SYSTEM_HA_USER_ID = "system"
HA_USER_AGENT_MAP: dict[str, str] = {
"bd3f666f43f941e886ef007a794eefe8": "main",
"1d2be9a622584cf0abb1c243d12a9959": "family-krzysztof",
"eca5887898544556840878ab37a35fbc": "family-ewa",
"2f5bf9f09478400082380bf7f26966d9": "family-tosia",
"f613d90efe8b4afea63c4a5a7ef7b559": "family-domi",
}
def normalize_optional_text(value: Any) -> str | None:
"""Return a stripped string, or None for blank/non-string values."""
if not isinstance(value, str):
return None
cleaned = value.strip()
return cleaned or None
def normalize_ha_user_id(value: Any) -> str:
"""Return a stable HA user id for routing/history scope."""
return normalize_optional_text(value) or SYSTEM_HA_USER_ID
def resolve_agent_id(
ha_user_id: str | None,
explicit_agent_id: str | None = None,
) -> str:
"""Resolve the OpenClaw agent for a Home Assistant user."""
if explicit_agent_id:
return explicit_agent_id
normalized_user_id = normalize_ha_user_id(ha_user_id)
return HA_USER_AGENT_MAP.get(normalized_user_id, DEFAULT_CHAT_AGENT_ID)
def resolve_model_override(value: Any) -> str | None:
"""Return a provider model override, excluding OpenClaw pseudo-model ids."""
model = normalize_optional_text(value)
if model and model.startswith("openclaw/"):
return None
return model
def build_scoped_session_id(
raw_session_id: str | None,
ha_user_id: str | None,
agent_id: str | None,
) -> str:
"""Build the backend/gateway session id scoped by user, agent, and raw session."""
raw_session = normalize_optional_text(raw_session_id) or "default"
user = normalize_ha_user_id(ha_user_id)
agent = normalize_optional_text(agent_id) or DEFAULT_CHAT_AGENT_ID
return (
f"ha_user_{_sanitize_session_part(user)}"
f"__agent_{_sanitize_session_part(agent)}"
f"__session_{_sanitize_session_part(raw_session)}"
)
def _sanitize_session_part(value: str) -> str:
"""Keep scoped session ids header-safe and readable."""
cleaned = re.sub(r"[^A-Za-z0-9_.-]+", "_", value.strip())
return cleaned.strip("_") or "default"
+4 -2
View File
@@ -72,7 +72,8 @@ class OpenClawModelSelect(CoordinatorEntity[OpenClawCoordinator], SelectEntity):
# Initialise from coordinator cache
models = coordinator.available_models
self._attr_options = models if models else ["unknown"]
current = (coordinator.data or {}).get(DATA_MODEL)
selected = entry.options.get(description.key)
current = selected or (coordinator.data or {}).get(DATA_MODEL)
self._attr_current_option = current if current in self._attr_options else (
self._attr_options[0] if self._attr_options else None
)
@@ -83,7 +84,8 @@ class OpenClawModelSelect(CoordinatorEntity[OpenClawCoordinator], SelectEntity):
models = self.coordinator.available_models
if models:
self._attr_options = models
current = (self.coordinator.data or {}).get(DATA_MODEL)
selected = self._entry.options.get(self.entity_description.key)
current = selected or (self.coordinator.data or {}).get(DATA_MODEL)
if current and current in self._attr_options:
self._attr_current_option = current
self.async_write_ha_state()
+5
View File
@@ -120,6 +120,7 @@ class OpenClawSensor(CoordinatorEntity[OpenClawCoordinator], SensorEntity):
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._entry = entry
self._attr_unique_id = f"{entry.entry_id}_{description.key}"
self._attr_device_info = {
"identifiers": {(DOMAIN, entry.entry_id)},
@@ -134,6 +135,10 @@ class OpenClawSensor(CoordinatorEntity[OpenClawCoordinator], SensorEntity):
"""Return the state of the sensor."""
if not self.coordinator.data:
return None
if self.entity_description.key == DATA_MODEL:
selected = self._entry.options.get("active_model")
if selected:
return selected
return self.coordinator.data.get(self.entity_description.key)
@property
+10 -4
View File
@@ -20,8 +20,8 @@ send_message:
agent_id:
name: Agent ID
description: >
Optional OpenClaw agent ID to route this message to. Overrides the
agent ID configured in the integration options. Defaults to "main".
Optional OpenClaw agent ID to route this message to. When omitted,
the integration routes by the Home Assistant user who called the service.
required: false
example: "main"
selector:
@@ -29,11 +29,17 @@ send_message:
clear_history:
name: Clear History
description: Clear the conversation history for a session.
description: Clear scoped conversation history for the current Home Assistant user.
fields:
session_id:
name: Session ID
description: Session ID to clear. If omitted, clears the default session.
description: Session ID to clear. If omitted, clears this user's sessions for the resolved agent.
required: false
selector:
text:
agent_id:
name: Agent ID
description: Optional OpenClaw agent ID override for the history scope.
required: false
selector:
text:
+7 -3
View File
@@ -127,17 +127,21 @@
},
"agent_id": {
"name": "Agent ID",
"description": "Optional OpenClaw agent ID to route this message to. Overrides the configured default. Defaults to \"main\"."
"description": "Optional OpenClaw agent ID to route this message to. When omitted, OpenClaw routes by the Home Assistant user who called the service."
}
}
},
"clear_history": {
"name": "Clear History",
"description": "Clear conversation history.",
"description": "Clear scoped conversation history for the current Home Assistant user.",
"fields": {
"session_id": {
"name": "Session ID",
"description": "Session to clear."
"description": "Session to clear. If omitted, clears this user's sessions for the resolved agent."
},
"agent_id": {
"name": "Agent ID",
"description": "Optional OpenClaw agent ID override for the history scope."
}
}
},
@@ -129,17 +129,21 @@
},
"agent_id": {
"name": "Agent ID",
"description": "Optional OpenClaw agent ID to route this message to. Overrides the configured default. Defaults to \"main\"."
"description": "Optional OpenClaw agent ID to route this message to. When omitted, OpenClaw routes by the Home Assistant user who called the service."
}
}
},
"clear_history": {
"name": "Clear History",
"description": "Clear conversation history.",
"description": "Clear scoped conversation history for the current Home Assistant user.",
"fields": {
"session_id": {
"name": "Session ID",
"description": "Session to clear."
"description": "Session to clear. If omitted, clears this user's sessions for the resolved agent."
},
"agent_id": {
"name": "Agent ID",
"description": "Optional OpenClaw agent ID override for the history scope."
}
}
},
@@ -13,7 +13,7 @@
* + subscribes to openclaw_message_received events.
*/
const CARD_VERSION = "0.3.13";
const CARD_VERSION = "0.3.14";
// Max time (ms) to show the thinking indicator before falling back to an error (default; overridable via card config `thinking_timeout` in seconds)
const THINKING_TIMEOUT_MS = 120_000;
@@ -96,6 +96,9 @@ class OpenClawChatCard extends HTMLElement {
this._autoScrollPinned = true;
this._lastHassRenderSignature = null;
this._integrationThinkingTimeout = null;
this._backendHaUserId = null;
this._activeAgentId = null;
this._lastStorageIdentity = null;
}
// ── HA card interface ───────────────────────────────────────────────
@@ -128,8 +131,15 @@ class OpenClawChatCard extends HTMLElement {
set hass(hass) {
const firstSet = !this._hass;
const previousStorageIdentity = this._getStorageIdentity();
this._hass = hass;
const currentSignature = this._getHassRenderSignature(hass);
const currentStorageIdentity = this._getStorageIdentity();
if (currentStorageIdentity !== previousStorageIdentity) {
this._lastStorageIdentity = currentStorageIdentity;
this._resetTransientChatState();
this._restoreMessages();
}
if (firstSet) {
this._subscribeToEvents();
this._syncHistoryFromBackend();
@@ -252,11 +262,19 @@ class OpenClawChatCard extends HTMLElement {
const data = event.data;
if (!data || !data.message) return;
const haUserId = this._getHaUserId();
if (data.ha_user_id && haUserId === "unknown") {
this._syncHistoryFromBackend(1);
return;
}
if (data.ha_user_id && haUserId !== "unknown" && data.ha_user_id !== haUserId) return;
if (data.agent_id && this._activeAgentId && data.agent_id !== this._activeAgentId) return;
// Check if this event is for our session
const sessionId = this._getSessionId();
if (data.session_id && data.session_id !== sessionId) return;
const signature = `${sessionId}|${data.timestamp || ""}|${String(data.message)}`;
const signature = `${haUserId}|${data.agent_id || ""}|${sessionId}|${data.timestamp || ""}|${String(data.message)}`;
if (signature === this._lastAssistantEventSignature) {
return;
}
@@ -295,7 +313,16 @@ class OpenClawChatCard extends HTMLElement {
}
}
_clearChat() {
async _clearChat() {
if (this._hass) {
try {
await this._hass.callService("openclaw", "clear_history", {
session_id: this._getSessionId(),
});
} catch (err) {
console.warn("OpenClaw: backend history clear failed:", err);
}
}
this._messages = [];
this._isProcessing = false;
this._pendingResponses = 0;
@@ -304,11 +331,31 @@ class OpenClawChatCard extends HTMLElement {
this._render();
}
_resetTransientChatState() {
this._isProcessing = false;
this._pendingResponses = 0;
this._clearThinkingTimer();
this._lastAssistantEventSignature = null;
}
// ── Message persistence ──────────────────────────────────────────────
_getHaUserId() {
return (
this._hass?.user?.id ||
this._backendHaUserId ||
"unknown"
);
}
_getStorageIdentity() {
return `${this._getHaUserId()}|${this._getSessionId()}`;
}
_getStorageKey() {
const userId = this._getHaUserId();
const session = this._getSessionId();
return `${STORAGE_PREFIX}${session}`;
return `${STORAGE_PREFIX}${userId}_${session}`;
}
_getSessionId() {
@@ -329,9 +376,8 @@ class OpenClawChatCard extends HTMLElement {
_restoreMessages() {
try {
const stored = sessionStorage.getItem(this._getStorageKey());
if (stored) {
this._messages = JSON.parse(stored);
}
const parsed = stored ? JSON.parse(stored) : [];
this._messages = Array.isArray(parsed) ? parsed : [];
} catch (e) {
// Corrupted data — start fresh
this._messages = [];
@@ -357,6 +403,18 @@ class OpenClawChatCard extends HTMLElement {
});
}
const previousStorageIdentity = this._getStorageIdentity();
if (result?.ha_user_id) {
this._backendHaUserId = result.ha_user_id;
}
if (result?.agent_id) {
this._activeAgentId = result.agent_id;
}
if (this._getStorageIdentity() !== previousStorageIdentity) {
this._resetTransientChatState();
this._restoreMessages();
}
const serverMessages = Array.isArray(result?.messages) ? result.messages : [];
if (!serverMessages.length) return;
@@ -426,6 +484,17 @@ class OpenClawChatCard extends HTMLElement {
typeof result?.thinking_timeout === "number" && result.thinking_timeout >= 10
? result.thinking_timeout * 1000
: null;
const previousStorageIdentity = this._getStorageIdentity();
if (result?.ha_user_id) {
this._backendHaUserId = result.ha_user_id;
}
if (result?.agent_id) {
this._activeAgentId = result.agent_id;
}
if (this._getStorageIdentity() !== previousStorageIdentity) {
this._resetTransientChatState();
this._restoreMessages();
}
if (includePipeline) {
try {
@@ -2185,7 +2254,11 @@ class OpenClawChatCard extends HTMLElement {
const clearBtn = this.shadowRoot.getElementById("clear-btn");
if (clearBtn) {
clearBtn.addEventListener("click", () => {
if (confirm("Clear chat history?")) this._clearChat();
if (confirm("Clear chat history?")) {
this._clearChat().catch((err) => {
console.error("OpenClaw: clear chat failed", err);
});
}
});
}
+1 -1
View File
@@ -1,7 +1,7 @@
(async () => {
try {
if (!customElements.get("openclaw-chat-card")) {
const src = "/openclaw/openclaw-chat-card.js?v=0.1.62";
const src = "/openclaw/openclaw-chat-card.js?v=0.1.63";
console.info("OpenClaw loader importing", src);
await import(src);
}