@silicaclaw/cli 2026.3.20-2 → 2026.3.20-4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/INSTALL.md +13 -7
  3. package/README.md +60 -12
  4. package/VERSION +1 -1
  5. package/apps/local-console/dist/apps/local-console/src/server.d.ts +39 -0
  6. package/apps/local-console/dist/apps/local-console/src/server.js +220 -0
  7. package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +1 -0
  8. package/apps/local-console/dist/packages/network/src/relayPreview.js +17 -0
  9. package/apps/local-console/public/app/app.js +274 -3
  10. package/apps/local-console/public/app/events.js +21 -0
  11. package/apps/local-console/public/app/network.js +111 -30
  12. package/apps/local-console/public/app/overview.js +49 -21
  13. package/apps/local-console/public/app/social.js +315 -93
  14. package/apps/local-console/public/app/styles.css +86 -0
  15. package/apps/local-console/public/app/template.js +56 -35
  16. package/apps/local-console/public/app/translations.js +394 -312
  17. package/apps/local-console/src/server.ts +251 -1
  18. package/apps/public-explorer/public/app/template.js +2 -2
  19. package/apps/public-explorer/public/app/translations.js +36 -36
  20. package/docs/NEW_USER_OPERATIONS.md +5 -5
  21. package/docs/OPENCLAW_BRIDGE.md +7 -7
  22. package/docs/OPENCLAW_BRIDGE_ZH.md +6 -6
  23. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +1 -0
  24. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +17 -0
  25. package/node_modules/@silicaclaw/network/src/relayPreview.ts +17 -0
  26. package/openclaw-skills/silicaclaw-bridge-setup/SKILL.md +18 -0
  27. package/openclaw-skills/silicaclaw-bridge-setup/VERSION +1 -1
  28. package/openclaw-skills/silicaclaw-bridge-setup/manifest.json +2 -2
  29. package/openclaw-skills/silicaclaw-broadcast/SKILL.md +18 -0
  30. package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
  31. package/openclaw-skills/silicaclaw-broadcast/manifest.json +2 -2
  32. package/openclaw-skills/silicaclaw-network-config/SKILL.md +158 -0
  33. package/openclaw-skills/silicaclaw-network-config/VERSION +1 -0
  34. package/openclaw-skills/silicaclaw-network-config/agents/openai.yaml +6 -0
  35. package/openclaw-skills/silicaclaw-network-config/manifest.json +27 -0
  36. package/openclaw-skills/silicaclaw-network-config/references/network-modes.md +22 -0
  37. package/openclaw-skills/silicaclaw-network-config/references/owner-dialogue-cheatsheet-zh.md +47 -0
  38. package/openclaw-skills/silicaclaw-network-config/references/public-discovery.md +22 -0
  39. package/openclaw-skills/silicaclaw-owner-push/SKILL.md +18 -0
  40. package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
  41. package/openclaw-skills/silicaclaw-owner-push/manifest.json +2 -2
  42. package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +3 -0
  43. package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +67 -8
  44. package/package.json +1 -1
  45. package/packages/network/dist/packages/network/src/relayPreview.d.ts +1 -0
  46. package/packages/network/dist/packages/network/src/relayPreview.js +17 -0
  47. package/packages/network/src/relayPreview.ts +17 -0
  48. package/scripts/silicaclaw-cli.mjs +4 -1
  49. package/scripts/silicaclaw-gateway.mjs +108 -0
  50. package/scripts/validate-openclaw-skill.mjs +19 -0
@@ -24,6 +24,134 @@ export function createSocialController({
24
24
  toPrettyJson,
25
25
  writeUiCache,
26
26
  }) {
27
+ const SKILLS_SECTION_LIMIT = 4;
28
+ const SKILLS_DIALOGUE_LIMIT = 1;
29
+ let lastMessagesRenderKey = "";
30
+ let lastLogsRenderKey = "";
31
+ const sectionRenderCache = new Map();
32
+ let skillsQuery = "";
33
+ let skillsFilter = "all";
34
+ const skillsExpanded = {
35
+ bundled: false,
36
+ installed: false,
37
+ dialogue: false,
38
+ };
39
+
40
+ function skillModeLabel(mode) {
41
+ if (mode === "workspace") return t("labels.skillsModeWorkspace");
42
+ if (mode === "legacy") return t("labels.skillsModeLegacy");
43
+ if (mode === "bundled") return t("labels.skillsModeBundled");
44
+ if (mode === "installed") return t("labels.skillsModeInstalled");
45
+ return mode ? String(mode) : t("labels.skillsModeGeneric");
46
+ }
47
+
48
+ function normalizeSkillSearchText(value) {
49
+ return String(value || "").trim().toLowerCase();
50
+ }
51
+
52
+ function setSkillsQuery(value) {
53
+ skillsQuery = normalizeSkillSearchText(value);
54
+ }
55
+
56
+ function setSkillsFilter(value) {
57
+ const next = String(value || "").trim();
58
+ skillsFilter = ["all", "attention", "updates", "installed"].includes(next) ? next : "all";
59
+ }
60
+
61
+ function toggleSkillsExpanded(section) {
62
+ if (!(section in skillsExpanded)) return;
63
+ skillsExpanded[section] = !skillsExpanded[section];
64
+ }
65
+
66
+ function skillMatchesSearch(skill) {
67
+ if (!skillsQuery) return true;
68
+ const haystack = [
69
+ skill.display_name,
70
+ skill.name,
71
+ skill.description,
72
+ skill.version,
73
+ ...(Array.isArray(skill.capabilities) ? skill.capabilities : []),
74
+ ...(Array.isArray(skill.owner_dialogue_examples_zh) ? skill.owner_dialogue_examples_zh : []),
75
+ ].map((item) => normalizeSkillSearchText(item)).join(" ");
76
+ return haystack.includes(skillsQuery);
77
+ }
78
+
79
+ function skillMatchesFilter(skill, section) {
80
+ if (skillsFilter === "all") return true;
81
+ if (skillsFilter === "updates") return Boolean(skill.update_available);
82
+ if (skillsFilter === "installed") {
83
+ return section === "installed" ? true : Boolean(skill.installed_in_openclaw);
84
+ }
85
+ if (skillsFilter === "attention") {
86
+ return section === "installed"
87
+ ? Boolean(skill.update_available)
88
+ : Boolean(skill.update_available || !skill.installed_in_openclaw);
89
+ }
90
+ return true;
91
+ }
92
+
93
+ function renderSkillsSectionFooter({ footerId, section, totalCount, visibleCount, limit }) {
94
+ const footer = document.getElementById(footerId);
95
+ if (!footer) return;
96
+ if (totalCount <= limit) {
97
+ footer.innerHTML = "";
98
+ return;
99
+ }
100
+ const expanded = skillsExpanded[section];
101
+ const hiddenCount = Math.max(totalCount - visibleCount, 0);
102
+ footer.innerHTML = `
103
+ <button class="secondary skills-section__toggle" type="button" data-skills-toggle="${section}">
104
+ ${expanded ? t("actions.showLess") : t("actions.showMoreCount", { count: String(hiddenCount) })}
105
+ </button>
106
+ `;
107
+ }
108
+
109
+ function renderFilteredSkillCards({ skills, section, gridId, footerId, renderer, limit }) {
110
+ const filtered = skills.filter((skill) => skillMatchesSearch(skill) && skillMatchesFilter(skill, section));
111
+ const expanded = skillsExpanded[section];
112
+ const visible = expanded ? filtered : filtered.slice(0, limit);
113
+ document.getElementById(gridId).innerHTML = visible.length
114
+ ? visible.map((skill) => renderer(skill)).join("")
115
+ : `<div class="skills-empty">${t("hints.skillsNoFilterMatch")}</div>`;
116
+ renderSkillsSectionFooter({
117
+ footerId,
118
+ section,
119
+ totalCount: filtered.length,
120
+ visibleCount: visible.length,
121
+ limit,
122
+ });
123
+ return filtered.length;
124
+ }
125
+
126
+ function renderSkillsFilterMeta({ bundledCount, installedCount, dialogueCount }) {
127
+ const matchedTotal = bundledCount + installedCount + dialogueCount;
128
+ const filterLabel = t(`labels.skillsFilter${skillsFilter.charAt(0).toUpperCase()}${skillsFilter.slice(1)}`);
129
+ document.getElementById("skillsFilterMeta").textContent = t("hints.skillsFilterMeta", {
130
+ count: String(matchedTotal),
131
+ filter: filterLabel,
132
+ });
133
+ document.querySelectorAll("[data-skills-filter]").forEach((btn) => {
134
+ btn.classList.toggle("active", btn.getAttribute("data-skills-filter") === skillsFilter);
135
+ });
136
+ }
137
+
138
+ function setCachedContent(id, value, mode = "html") {
139
+ const cacheKey = `${mode}:${id}`;
140
+ if (sectionRenderCache.get(cacheKey) === value) {
141
+ return;
142
+ }
143
+ const el = document.getElementById(id);
144
+ if (!el) return;
145
+ if (mode === "text") {
146
+ el.textContent = value;
147
+ } else if (mode === "class") {
148
+ el.className = value;
149
+ } else {
150
+ el.innerHTML = value;
151
+ }
152
+ sectionRenderCache.set(cacheKey, value);
153
+ }
154
+
27
155
  function renderSocialMessages() {
28
156
  const listEl = document.getElementById("socialMessageList");
29
157
  const metaEl = document.getElementById("socialMessageMeta");
@@ -37,10 +165,16 @@ export function createSocialController({
37
165
  seconds: String(Math.floor((governance.send_limit?.window_ms || 60000) / 1000)),
38
166
  })}`
39
167
  : t("overview.messageHint");
40
- hintEl.textContent = governanceHint;
41
168
  if (!socialMessagesCache.length) {
42
- metaEl.textContent = t("overview.noMessagesMeta");
43
- listEl.innerHTML = `<div class="empty-state">${t("overview.noMessagesEmpty")}</div>`;
169
+ const nextMeta = t("overview.noMessagesMeta");
170
+ const nextHtml = `<div class="empty-state">${t("overview.noMessagesEmpty")}</div>`;
171
+ const renderKey = JSON.stringify({ hint: governanceHint, meta: nextMeta, html: nextHtml });
172
+ if (renderKey !== lastMessagesRenderKey) {
173
+ hintEl.textContent = governanceHint;
174
+ metaEl.textContent = nextMeta;
175
+ listEl.innerHTML = nextHtml;
176
+ lastMessagesRenderKey = renderKey;
177
+ }
44
178
  return;
45
179
  }
46
180
 
@@ -59,16 +193,28 @@ export function createSocialController({
59
193
  seconds: String(Math.floor((governance.send_limit?.window_ms || 60000) / 1000)),
60
194
  })}`
61
195
  : "";
62
- metaEl.textContent = `${baseMeta}${governanceMeta}`;
196
+ const nextMeta = `${baseMeta}${governanceMeta}`;
63
197
 
64
198
  if (!filteredMessages.length) {
65
- listEl.innerHTML = `<div class="empty-state">${t("overview.noMessagesEmpty")}</div>`;
199
+ const nextHtml = `<div class="empty-state">${t("overview.noMessagesEmpty")}</div>`;
200
+ const renderKey = JSON.stringify({ hint: governanceHint, meta: nextMeta, html: nextHtml });
201
+ if (renderKey !== lastMessagesRenderKey) {
202
+ hintEl.textContent = governanceHint;
203
+ metaEl.textContent = nextMeta;
204
+ listEl.innerHTML = nextHtml;
205
+ lastMessagesRenderKey = renderKey;
206
+ }
66
207
  return;
67
208
  }
68
209
 
69
- listEl.innerHTML = filteredMessages
210
+ const nextHtml = filteredMessages
70
211
  .map((item) => {
71
212
  const visibleRemoteCount = getVisibleRemotePublicCount();
213
+ const avatarUrl = String(item.avatar_url || "").trim();
214
+ const displayName = String(item.display_name || t("overview.unnamed")).trim() || t("overview.unnamed");
215
+ const avatar = avatarUrl
216
+ ? `<img class="agent-card__avatar" src="${escapeHtml(avatarUrl)}" alt="${escapeHtml(displayName)}" loading="lazy" />`
217
+ : `<div class="agent-card__avatar-fallback">${escapeHtml((displayName[0] || "?").toUpperCase())}</div>`;
72
218
  const selfStatusChips = item.is_self
73
219
  ? `
74
220
  <span class="tag-chip" style="margin-left:8px;">${t("overview.selfMessagePublished")}</span>
@@ -101,14 +247,17 @@ export function createSocialController({
101
247
  return `
102
248
  <div class="log-item">
103
249
  <div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
104
- <div>
105
- <strong>${escapeHtml(item.display_name || t("overview.unnamed"))}</strong>
106
- <span class="mono" style="color:#90a2c3; margin-left:8px;">${escapeHtml(shortId(item.agent_id || ""))}</span>
107
- <span class="tag-chip" style="margin-left:8px;">${escapeHtml(item.topic || t("labels.globalTopic"))}</span>
108
- ${item.is_self ? `<span class="tag-chip" style="margin-left:8px;">${t("overview.messageFilterSelf")}</span>` : ""}
109
- <span class="tag-chip" style="margin-left:8px;">${item.online ? t("overview.online") : t("overview.offline")}</span>
110
- ${observationChip}
111
- ${selfStatusChips}
250
+ <div style="display:flex; align-items:flex-start; gap:10px; min-width:0;">
251
+ ${avatar}
252
+ <div style="min-width:0;">
253
+ <strong>${escapeHtml(displayName)}</strong>
254
+ <span class="mono" style="color:#90a2c3; margin-left:8px;">${escapeHtml(shortId(item.agent_id || ""))}</span>
255
+ <span class="tag-chip" style="margin-left:8px;">${escapeHtml(item.topic || t("labels.globalTopic"))}</span>
256
+ ${item.is_self ? `<span class="tag-chip" style="margin-left:8px;">${t("overview.messageFilterSelf")}</span>` : ""}
257
+ <span class="tag-chip" style="margin-left:8px;">${item.online ? t("overview.online") : t("overview.offline")}</span>
258
+ ${observationChip}
259
+ ${selfStatusChips}
260
+ </div>
112
261
  </div>
113
262
  <div class="mono" style="color:#90a2c3;">${new Date(item.created_at).toLocaleString()}</div>
114
263
  </div>
@@ -118,6 +267,25 @@ export function createSocialController({
118
267
  `;
119
268
  })
120
269
  .join("");
270
+ const renderKey = JSON.stringify({
271
+ hint: governanceHint,
272
+ meta: nextMeta,
273
+ filter: socialMessageFilter,
274
+ messages: filteredMessages.map((item) => [
275
+ item.message_id,
276
+ item.updated_at || item.created_at,
277
+ item.remote_observation_count || 0,
278
+ item.online ? 1 : 0,
279
+ ]),
280
+ visibleRemoteCount: getVisibleRemotePublicCount(),
281
+ });
282
+ if (renderKey === lastMessagesRenderKey) {
283
+ return;
284
+ }
285
+ hintEl.textContent = governanceHint;
286
+ metaEl.textContent = nextMeta;
287
+ listEl.innerHTML = nextHtml;
288
+ lastMessagesRenderKey = renderKey;
121
289
  }
122
290
 
123
291
  async function refreshMessages() {
@@ -162,19 +330,29 @@ export function createSocialController({
162
330
  const publicDiscoveryText = status.public_enabled ? t("social.publicDiscoveryEnabled") : t("social.publicDiscoveryDisabled");
163
331
 
164
332
  const namespaceText = `${status.connected_to_silicaclaw ? t("social.connectedToSilicaClaw") : t("social.notConnected")} · ${publicDiscoveryText} · ${t("social.mode")} ${mode}`;
165
- document.getElementById("socialStatusLine").textContent = summaryLine;
166
- document.getElementById("socialStatusSubline").textContent = namespaceText;
333
+ setCachedContent("socialStatusLine", summaryLine, "text");
334
+ setCachedContent("socialStatusSubline", namespaceText, "text");
167
335
  const bar = document.getElementById("integrationStatusBar");
168
- bar.className = `integration-strip ${status.connected_to_silicaclaw && status.public_enabled ? "ok" : "warn"}${getActiveTab() === "overview" ? "" : " hidden"}`;
169
- bar.textContent = t("social.barStatus", {
336
+ const barClassName = `integration-strip ${status.connected_to_silicaclaw && status.public_enabled ? "ok" : "warn"}${getActiveTab() === "overview" ? "" : " hidden"}`;
337
+ const barText = t("social.barStatus", {
170
338
  connected: status.connected_to_silicaclaw ? t("common.yes") : t("common.no"),
171
339
  mode,
172
340
  public: status.public_enabled ? t("common.on") : t("common.off"),
173
341
  });
174
- document.getElementById("brandStatusDot").className = `sidebar-version__status ${status.connected_to_silicaclaw ? "ok" : "warn"}`;
342
+ if (bar.className !== barClassName) {
343
+ bar.className = barClassName;
344
+ }
345
+ if (bar.textContent !== barText) {
346
+ bar.textContent = barText;
347
+ }
348
+ const brandStatusDot = document.getElementById("brandStatusDot");
349
+ const brandStatusClassName = `sidebar-version__status ${status.connected_to_silicaclaw ? "ok" : "warn"}`;
350
+ if (brandStatusDot.className !== brandStatusClassName) {
351
+ brandStatusDot.className = brandStatusClassName;
352
+ }
175
353
  writeUiCache("silicaclaw_ui_social", {
176
- integrationStatusText: bar.textContent,
177
- integrationStatusClassName: bar.className,
354
+ integrationStatusText: barText,
355
+ integrationStatusClassName: barClassName,
178
356
  socialStatusLineText: summaryLine,
179
357
  socialStatusSublineText: namespaceText,
180
358
  });
@@ -182,14 +360,14 @@ export function createSocialController({
182
360
  if (!status.configured && status.configured_reason) reasons.push(t("social.configuredReason", { reason: status.configured_reason }));
183
361
  if (!status.running && status.running_reason) reasons.push(t("social.runningReason", { reason: status.running_reason }));
184
362
  if (!status.discoverable && status.discoverable_reason) reasons.push(t("social.discoverableReasonFull", { reason: status.discoverable_reason }));
185
- document.getElementById("socialStateHint").textContent = reasons.length ? reasons.join(" · ") : t("hints.allIntegrationChecksPassed");
363
+ setCachedContent("socialStateHint", reasons.length ? reasons.join(" · ") : t("hints.allIntegrationChecksPassed"), "text");
186
364
  const modeSelect = document.getElementById("socialModeSelect");
187
365
  const displayedSelectedMode = getSocialModeDirty() && getSocialModePending() ? getSocialModePending() : selectedMode;
188
366
  if (modeSelect && displayedSelectedMode !== "-") modeSelect.value = displayedSelectedMode;
189
367
  renderSocialModeHint(displayedSelectedMode, mode, !!social.network_requires_restart, getSocialModeDirty());
190
368
  setSocialModePendingState(getSocialModeDirty());
191
369
 
192
- document.getElementById("socialPrimaryCards").innerHTML = [
370
+ setCachedContent("socialPrimaryCards", [
193
371
  [t("social.configured"), status.configured ? t("common.yes") : t("common.no")],
194
372
  [t("social.running"), status.running ? t("common.yes") : t("common.no")],
195
373
  [t("social.discoverable"), discoverable ? t("common.yes") : t("common.no")],
@@ -197,9 +375,9 @@ export function createSocialController({
197
375
  [t("social.networkMode"), mode],
198
376
  [t("labels.adapter"), effectiveAdapter],
199
377
  [t("social.discoverableReason"), status.discoverable_reason || "-"],
200
- ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join("");
378
+ ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join(""));
201
379
 
202
- document.getElementById("socialIntegrationCards").innerHTML = [
380
+ setCachedContent("socialIntegrationCards", [
203
381
  [t("social.connected"), bridge.connected_to_silicaclaw ? t("common.yes") : t("common.no")],
204
382
  [t("social.messageBroadcast"), bridge.message_broadcast_enabled ? t("common.on") : t("common.off")],
205
383
  [t("social.displayName"), status.display_name || t("overview.unnamed")],
@@ -207,9 +385,9 @@ export function createSocialController({
207
385
  [t("social.socialFound"), summary.social_md_found ? t("common.yes") : t("common.no")],
208
386
  [t("social.socialSource"), summary.social_md_source_path || "-"],
209
387
  [t("social.reuseOpenClawIdentity"), summary.reused_openclaw_identity ? t("common.yes") : t("common.no")],
210
- ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join("");
388
+ ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join(""));
211
389
 
212
- document.getElementById("socialMessagePathCards").innerHTML = [
390
+ setCachedContent("socialMessagePathCards", [
213
391
  [t("social.messageBroadcast"), bridge.message_broadcast_enabled ? t("common.on") : t("common.off")],
214
392
  [t("social.publicDiscovery"), status.public_enabled ? t("common.on") : t("common.off")],
215
393
  [t("social.namespace"), effectiveNamespace],
@@ -218,7 +396,7 @@ export function createSocialController({
218
396
  [t("network.lastPoll"), networkDiag.last_poll_at ? new Date(networkDiag.last_poll_at).toLocaleTimeString() : "-"],
219
397
  [t("network.lastPublish"), networkDiag.last_publish_at ? new Date(networkDiag.last_publish_at).toLocaleTimeString() : "-"],
220
398
  [t("network.lastError"), networkDiag.last_error || t("network.none")],
221
- ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${escapeHtml(String(v))}</div></div>`).join("");
399
+ ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${escapeHtml(String(v))}</div></div>`).join(""));
222
400
 
223
401
  const skillLearning = bridge.skill_learning || {};
224
402
  const ownerDelivery = bridge.owner_delivery || {};
@@ -246,21 +424,21 @@ export function createSocialController({
246
424
  ownerDeliveryHeadline = t("feedback.openclawRoleNotRunning");
247
425
  ownerDeliveryBody = ownerDelivery.reason || "-";
248
426
  }
249
- document.getElementById("socialOwnerDeliveryStatus").className = `feedback ${ownerDeliveryTone}`;
250
- document.getElementById("socialOwnerDeliveryStatus").textContent = ownerDeliveryHeadline;
251
- document.getElementById("socialOwnerDeliverySubline").textContent = [
427
+ setCachedContent("socialOwnerDeliveryStatus", `feedback ${ownerDeliveryTone}`, "class");
428
+ setCachedContent("socialOwnerDeliveryStatus", ownerDeliveryHeadline, "text");
429
+ setCachedContent("socialOwnerDeliverySubline", [
252
430
  `${t("social.broadcastReadable")}: ${ownerDelivery.bridge_messages_readable ? t("common.yes") : t("common.no")}`,
253
431
  `${t("social.ownerForwardCommand")}: ${ownerDelivery.forward_command_configured ? t("common.yes") : t("common.no")}`,
254
432
  `${t("social.ownerForwardReady")}: ${ownerDelivery.ready ? t("common.yes") : t("common.no")}`,
255
- ].join(" · ");
256
- document.getElementById("socialOwnerDeliveryReason").textContent = ownerDeliveryBody;
257
- document.getElementById("socialCapabilityCards").innerHTML = [
433
+ ].join(" · "), "text");
434
+ setCachedContent("socialOwnerDeliveryReason", ownerDeliveryBody, "text");
435
+ setCachedContent("socialCapabilityCards", [
258
436
  [t("socialCapability.publicBroadcast"), bridge.message_broadcast_enabled ? t("common.yes") : t("common.no")],
259
437
  [t("socialCapability.monitorBroadcasts"), ownerDelivery.bridge_messages_readable ? t("common.yes") : t("common.no")],
260
438
  [t("socialCapability.autoPushToOwner"), ownerDelivery.ready ? t("common.yes") : t("common.no")],
261
439
  [t("socialCapability.ownerPrivateBoundary"), t("socialCapability.ownerPrivateBoundaryValue")],
262
- ].map(([k, v]) => `<div class="card"><div class="label">${escapeHtml(String(k))}</div><div class="value" style="font-size:17px;">${escapeHtml(String(v))}</div></div>`).join("");
263
- document.getElementById("openclawSkillCards").innerHTML = [
440
+ ].map(([k, v]) => `<div class="card"><div class="label">${escapeHtml(String(k))}</div><div class="value" style="font-size:17px;">${escapeHtml(String(v))}</div></div>`).join(""));
441
+ setCachedContent("openclawSkillCards", [
264
442
  [t("social.openclawInstalled"), openclawDetected ? t("common.yes") : t("common.no")],
265
443
  [t("social.running"), openclawRunning ? t("common.yes") : t("common.no")],
266
444
  [t("social.skillInstalled"), skillInstalled ? t("common.yes") : t("common.no")],
@@ -271,13 +449,13 @@ export function createSocialController({
271
449
  [t("social.openclawGateway"), bridge.openclaw_runtime?.gateway_url || "-"],
272
450
  [t("social.installMode"), skillLearning.install_mode || "-"],
273
451
  [t("social.installedPath"), skillInstalled ? installedSkillPath : "-"],
274
- ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${escapeHtml(v)}</div></div>`).join("");
275
- document.getElementById("openclawSkillPath").textContent = ownerDelivery.forward_command
452
+ ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${escapeHtml(v)}</div></div>`).join(""));
453
+ setCachedContent("openclawSkillPath", ownerDelivery.forward_command
276
454
  ? `${ownerDelivery.forward_command}${ownerDelivery.owner_channel ? ` · ${ownerDelivery.owner_channel}` : ""}${ownerDelivery.owner_target ? ` · ${ownerDelivery.owner_target}` : ""}`
277
455
  : skillInstalled
278
456
  ? installedSkillPath
279
- : `${installAction.recommended_command || "-"}${bridge.openclaw_runtime?.gateway_url ? ` · detect ${bridge.openclaw_runtime.gateway_url}` : ""}`;
280
- document.getElementById("openclawSkillHint").textContent = !openclawDetected
457
+ : `${installAction.recommended_command || "-"}${bridge.openclaw_runtime?.gateway_url ? ` · detect ${bridge.openclaw_runtime.gateway_url}` : ""}`, "text");
458
+ setCachedContent("openclawSkillHint", !openclawDetected
281
459
  ? t("feedback.openclawRoleBroadcasterOnly")
282
460
  : !openclawRunning
283
461
  ? t("feedback.openclawRoleNotRunning")
@@ -285,11 +463,11 @@ export function createSocialController({
285
463
  ? t("feedback.openclawRoleReadyToLearn")
286
464
  : ownerDelivery.ready
287
465
  ? t("feedback.openclawRoleOwnerReady")
288
- : ownerDelivery.bridge_messages_readable && !ownerDelivery.forward_command_configured
289
- ? t("feedback.openclawRoleLearningOnly")
290
- : ownerDelivery.bridge_messages_readable
291
- ? t("feedback.openclawRoleNeedsOwnerRoute")
292
- : t("feedback.openclawRoleLearned");
466
+ : ownerDelivery.bridge_messages_readable && !ownerDelivery.forward_command_configured
467
+ ? t("feedback.openclawRoleLearningOnly")
468
+ : ownerDelivery.bridge_messages_readable
469
+ ? t("feedback.openclawRoleNeedsOwnerRoute")
470
+ : t("feedback.openclawRoleLearned"), "text");
293
471
  const skillInstallBtn = document.getElementById("openclawSkillInstallBtn");
294
472
  skillInstallBtn.textContent = !openclawDetected
295
473
  ? t("actions.openclawNotInstalled")
@@ -310,33 +488,33 @@ export function createSocialController({
310
488
  document.getElementById("governanceDuplicateWindowInput").value = String(Math.floor((policy.duplicate_window_ms ?? 180000) / 1000));
311
489
  document.getElementById("governanceBlockedAgentsInput").value = blockedAgentIds.join(", ");
312
490
  document.getElementById("governanceBlockedTermsInput").value = blockedTerms.join(", ");
313
- document.getElementById("socialGovernanceCards").innerHTML = [
491
+ setCachedContent("socialGovernanceCards", [
314
492
  [t("labels.sendLimit"), `${policy.send_limit?.max ?? "-"} / ${Math.floor((policy.send_limit?.window_ms ?? 60000) / 1000)}s`],
315
493
  [t("labels.receiveLimit"), `${policy.receive_limit?.max ?? "-"} / ${Math.floor((policy.receive_limit?.window_ms ?? 60000) / 1000)}s`],
316
494
  [t("labels.duplicateWindowSeconds"), `${Math.floor((policy.duplicate_window_ms ?? 0) / 1000)}s`],
317
495
  [t("labels.blockedAgentIds"), blockedAgentIds.length],
318
496
  [t("labels.blockedTerms"), blockedTerms.length],
319
- ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join("");
497
+ ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join(""));
320
498
 
321
499
  const moderationEvents = Array.isArray(governance.recent_events) ? governance.recent_events : [];
322
- document.getElementById("socialModerationList").innerHTML = moderationEvents.length === 0
500
+ setCachedContent("socialModerationList", moderationEvents.length === 0
323
501
  ? `<div class="empty-state">${t("network.noModerationEvents")}</div>`
324
502
  : moderationEvents.map((event) => `
325
503
  <div class="log-item">
326
504
  <div class="log-${event.level || "warn"}">[${String(event.level || "warn").toUpperCase()}] ${escapeHtml(event.message || "-")}</div>
327
505
  <div class="mono" style="color:#90a2c3;">${new Date(event.timestamp).toLocaleString()}</div>
328
506
  </div>
329
- `).join("");
507
+ `).join(""));
330
508
 
331
- document.getElementById("socialAdvancedCards").innerHTML = [
509
+ setCachedContent("socialAdvancedCards", [
332
510
  [t("labels.adapter"), effectiveAdapter],
333
511
  [t("social.namespace"), effectiveNamespace],
334
512
  [t("labels.room"), effectiveRoom],
335
513
  [t("social.bridgeStatus"), bridge.connected_to_silicaclaw ? t("common.yes") : t("common.no")],
336
514
  [t("social.messageBroadcast"), bridge.message_broadcast_enabled ? t("common.on") : t("common.off")],
337
515
  [t("social.restartRequired"), social.network_requires_restart ? t("common.yes") : t("common.no")],
338
- ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join("");
339
- document.getElementById("socialAdvancedWrap").textContent = toPrettyJson({
516
+ ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join(""));
517
+ setCachedContent("socialAdvancedWrap", toPrettyJson({
340
518
  effective_runtime: {
341
519
  mode: effectiveMode,
342
520
  adapter: effectiveAdapter,
@@ -358,17 +536,17 @@ export function createSocialController({
358
536
  social_md_found: summary.social_md_found,
359
537
  social_md_source_path: summary.social_md_source_path,
360
538
  },
361
- });
539
+ }), "text");
362
540
 
363
- document.getElementById("socialSourceWrap").textContent = toPrettyJson({
541
+ setCachedContent("socialSourceWrap", toPrettyJson({
364
542
  found: social.found,
365
543
  source_path: social.source_path,
366
544
  parse_error: social.parse_error,
367
- });
368
- document.getElementById("socialRawWrap").textContent = toPrettyJson({
545
+ }), "text");
546
+ setCachedContent("socialRawWrap", toPrettyJson({
369
547
  raw_frontmatter: social.raw_frontmatter || null,
370
- });
371
- document.getElementById("socialRuntimeWrap").textContent = toPrettyJson(runtime);
548
+ }), "text");
549
+ setCachedContent("socialRuntimeWrap", toPrettyJson(runtime), "text");
372
550
  }
373
551
 
374
552
  async function exportSocialTemplate() {
@@ -383,23 +561,43 @@ export function createSocialController({
383
561
  const logLevelFilter = getLogLevelFilter();
384
562
  const el = document.getElementById("logList");
385
563
  if (!logsCache.length) {
386
- el.innerHTML = `<div class="empty-state">${t("network.noLogsYet")}</div>`;
564
+ const nextHtml = `<div class="empty-state">${t("network.noLogsYet")}</div>`;
565
+ if (nextHtml !== lastLogsRenderKey) {
566
+ el.innerHTML = nextHtml;
567
+ lastLogsRenderKey = nextHtml;
568
+ }
387
569
  return;
388
570
  }
389
571
  const filtered = logLevelFilter === "all" ? logsCache : logsCache.filter((item) => String(item.level || "").toLowerCase() === logLevelFilter);
390
572
  if (!filtered.length) {
391
- el.innerHTML = `<div class="empty-state">${t("network.noLogsForLevel", { level: logLevelFilter })}</div>`;
573
+ const nextHtml = `<div class="empty-state">${t("network.noLogsForLevel", { level: logLevelFilter })}</div>`;
574
+ if (nextHtml !== lastLogsRenderKey) {
575
+ el.innerHTML = nextHtml;
576
+ lastLogsRenderKey = nextHtml;
577
+ }
392
578
  return;
393
579
  }
394
- el.innerHTML = filtered.map((item) => `
580
+ const nextHtml = filtered.map((item) => `
395
581
  <div class="log-item">
396
582
  <div class="log-${item.level}">[${String(item.level).toUpperCase()}] ${item.message}</div>
397
583
  <div class="mono" style="color:#90a2c3;">${new Date(item.timestamp).toLocaleString()}</div>
398
584
  </div>
399
585
  `).join("");
586
+ const renderKey = JSON.stringify({
587
+ level: logLevelFilter,
588
+ items: filtered.map((item) => [item.timestamp, item.level, item.message]),
589
+ });
590
+ if (renderKey === lastLogsRenderKey) {
591
+ return;
592
+ }
593
+ el.innerHTML = nextHtml;
594
+ lastLogsRenderKey = renderKey;
400
595
  }
401
596
 
402
597
  async function refreshLogs() {
598
+ if (getActiveTab() !== "network") {
599
+ return;
600
+ }
403
601
  setLogsCache((await api("/api/logs")).data || []);
404
602
  renderLogs();
405
603
  }
@@ -425,8 +623,8 @@ export function createSocialController({
425
623
  <div class="skill-card">
426
624
  <div class="skill-card__top">
427
625
  <div>
428
- <div class="skill-card__eyebrow">${escapeHtml(options.eyebrow || skill.install_mode || "skill")}</div>
429
- <div class="skill-card__title">${escapeHtml(skill.display_name || skill.name || "Skill")}</div>
626
+ <div class="skill-card__eyebrow">${escapeHtml(skillModeLabel(options.eyebrow || skill.install_mode || "skill"))}</div>
627
+ <div class="skill-card__title">${escapeHtml(skill.display_name || skill.name || t("labels.skillsModeGeneric"))}</div>
430
628
  </div>
431
629
  <div class="skill-card__version mono">${escapeHtml(versionText)}</div>
432
630
  </div>
@@ -476,7 +674,7 @@ export function createSocialController({
476
674
  <div class="skill-card">
477
675
  <div class="skill-card__top">
478
676
  <div>
479
- <div class="skill-card__eyebrow">${escapeHtml(skill.display_name || skill.name || "Skill")}</div>
677
+ <div class="skill-card__eyebrow">${escapeHtml(skill.display_name || skill.name || t("labels.skillsModeGeneric"))}</div>
480
678
  <div class="skill-card__title">${escapeHtml(t("labels.skillsDialogueExamples"))}</div>
481
679
  </div>
482
680
  <div class="skill-card__version mono">${escapeHtml(skill.version || "-")}</div>
@@ -604,36 +802,57 @@ export function createSocialController({
604
802
  `).join("")
605
803
  : `<div class="skills-empty">${t("hints.skillsNoBundled")}</div>`;
606
804
 
607
- document.getElementById("skillsBundledGrid").innerHTML = bundledSorted.length
608
- ? bundledSorted.map((skill) => renderSkillCard(skill, {
609
- eyebrow: skill.install_mode === "workspace" || skill.install_mode === "legacy" ? skill.install_mode : "bundled",
610
- statusText: skill.update_available
611
- ? t("labels.skillsUpdateAvailable")
612
- : skill.installed_in_openclaw
613
- ? `${t("labels.skillsStatus")}: ${t("common.yes")}`
614
- : t("hints.skillsNotInstalled"),
615
- installable: true,
616
- updateAvailable: skill.update_available,
617
- installedVersion: skill.installed_version,
618
- bundledVersion: skill.version,
619
- })).join("")
620
- : `<div class="skills-empty">${t("hints.skillsNoBundled")}</div>`;
805
+ const bundledMatchCount = renderFilteredSkillCards({
806
+ skills: bundledSorted,
807
+ section: "bundled",
808
+ gridId: "skillsBundledGrid",
809
+ footerId: "skillsBundledFooter",
810
+ limit: SKILLS_SECTION_LIMIT,
811
+ renderer: (skill) => renderSkillCard(skill, {
812
+ eyebrow: skill.install_mode === "workspace" || skill.install_mode === "legacy" ? skill.install_mode : "bundled",
813
+ statusText: skill.update_available
814
+ ? t("labels.skillsUpdateAvailable")
815
+ : skill.installed_in_openclaw
816
+ ? `${t("labels.skillsStatus")}: ${t("common.yes")}`
817
+ : t("hints.skillsNotInstalled"),
818
+ installable: true,
819
+ updateAvailable: skill.update_available,
820
+ installedVersion: skill.installed_version,
821
+ bundledVersion: skill.version,
822
+ }),
823
+ });
824
+
825
+ const installedMatchCount = renderFilteredSkillCards({
826
+ skills: installedSorted,
827
+ section: "installed",
828
+ gridId: "skillsInstalledGrid",
829
+ footerId: "skillsInstalledFooter",
830
+ limit: SKILLS_SECTION_LIMIT,
831
+ renderer: (skill) => renderSkillCard(skill, {
832
+ eyebrow: skill.install_mode || "installed",
833
+ statusText: skill.update_available
834
+ ? t("labels.skillsUpdateAvailable")
835
+ : `${t("labels.skillsStatus")}: ${escapeHtml(skillModeLabel(skill.install_mode || "installed"))}`,
836
+ updateAvailable: skill.update_available,
837
+ installedVersion: skill.version,
838
+ bundledVersion: skill.bundled_version,
839
+ }),
840
+ });
841
+
842
+ const dialogueMatchCount = renderFilteredSkillCards({
843
+ skills: featuredSkills,
844
+ section: "dialogue",
845
+ gridId: "skillsDialogueGrid",
846
+ footerId: "skillsDialogueFooter",
847
+ limit: SKILLS_DIALOGUE_LIMIT,
848
+ renderer: (skill) => renderDialogueCard(skill),
849
+ });
621
850
 
622
- document.getElementById("skillsInstalledGrid").innerHTML = installedSorted.length
623
- ? installedSorted.map((skill) => renderSkillCard(skill, {
624
- eyebrow: skill.install_mode || "installed",
625
- statusText: skill.update_available
626
- ? t("labels.skillsUpdateAvailable")
627
- : `${t("labels.skillsStatus")}: ${escapeHtml(String(skill.install_mode || "-"))}`,
628
- updateAvailable: skill.update_available,
629
- installedVersion: skill.version,
630
- bundledVersion: skill.bundled_version,
631
- })).join("")
632
- : `<div class="skills-empty">${t("hints.skillsNoInstalled")}</div>`;
633
-
634
- document.getElementById("skillsDialogueGrid").innerHTML = featuredSkills.length
635
- ? featuredSkills.map((skill) => renderDialogueCard(skill)).join("")
636
- : `<div class="skills-empty">${t("hints.skillsNoDialogueExamples")}</div>`;
851
+ renderSkillsFilterMeta({
852
+ bundledCount: bundledMatchCount,
853
+ installedCount: installedMatchCount,
854
+ dialogueCount: dialogueMatchCount,
855
+ });
637
856
 
638
857
  const installBtn = document.getElementById("skillsInstallBtn");
639
858
  installBtn.textContent = !openclawDetected
@@ -659,6 +878,9 @@ export function createSocialController({
659
878
  refreshSocial,
660
879
  renderLogs,
661
880
  renderSocialMessages,
881
+ setSkillsFilter,
882
+ setSkillsQuery,
662
883
  setLogLevelFilter,
884
+ toggleSkillsExpanded,
663
885
  };
664
886
  }