@silicaclaw/cli 2026.3.20-3 → 2026.3.20-5

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 (31) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/INSTALL.md +2 -2
  3. package/README.md +2 -2
  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 +229 -12
  7. package/apps/local-console/dist/packages/network/src/relayPreview.d.ts +4 -0
  8. package/apps/local-console/dist/packages/network/src/relayPreview.js +37 -6
  9. package/apps/local-console/public/app/app.js +293 -2
  10. package/apps/local-console/public/app/network.js +144 -32
  11. package/apps/local-console/public/app/overview.js +43 -15
  12. package/apps/local-console/public/app/social.js +135 -53
  13. package/apps/local-console/public/app/styles.css +86 -0
  14. package/apps/local-console/public/app/template.js +7 -1
  15. package/apps/local-console/public/app/translations.js +44 -0
  16. package/apps/local-console/src/server.ts +262 -14
  17. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.d.ts +4 -0
  18. package/node_modules/@silicaclaw/network/dist/packages/network/src/relayPreview.js +37 -6
  19. package/node_modules/@silicaclaw/network/src/relayPreview.ts +41 -6
  20. package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -1
  21. package/openclaw-skills/silicaclaw-broadcast/manifest.json +1 -1
  22. package/openclaw-skills/silicaclaw-owner-push/VERSION +1 -1
  23. package/openclaw-skills/silicaclaw-owner-push/manifest.json +1 -1
  24. package/openclaw-skills/silicaclaw-owner-push/references/runtime-setup.md +3 -0
  25. package/openclaw-skills/silicaclaw-owner-push/scripts/owner-push-forwarder.mjs +67 -8
  26. package/package.json +1 -1
  27. package/packages/network/dist/packages/network/src/relayPreview.d.ts +4 -0
  28. package/packages/network/dist/packages/network/src/relayPreview.js +37 -6
  29. package/packages/network/src/relayPreview.ts +41 -6
  30. package/scripts/silicaclaw-cli.mjs +4 -1
  31. package/scripts/silicaclaw-gateway.mjs +108 -0
@@ -7,6 +7,8 @@ export function createOverviewController({
7
7
  t,
8
8
  writeUiCache,
9
9
  }) {
10
+ let lastAgentsRenderKey = "";
11
+
10
12
  function renderOverviewGuide(overview, profile) {
11
13
  const hasDisplayName = Boolean(String(profile?.display_name || overview?.display_name || "").trim());
12
14
  const hasBio = Boolean(String(profile?.bio || "").trim());
@@ -221,8 +223,17 @@ export function createOverviewController({
221
223
  if (!filtered.length) {
222
224
  const agentsCountHintText = t("overview.agentsZero");
223
225
  const agentsWrapHtml = `<div class="label">${t("overview.noDiscoveredAgents")}</div>`;
224
- document.getElementById("agentsCountHint").textContent = agentsCountHintText;
225
- document.getElementById("agentsWrap").innerHTML = agentsWrapHtml;
226
+ const renderKey = JSON.stringify({
227
+ state: "empty",
228
+ hint: agentsCountHintText,
229
+ page: agentsPage,
230
+ onlineOnly: getOnlyShowOnline(),
231
+ });
232
+ if (renderKey !== lastAgentsRenderKey) {
233
+ document.getElementById("agentsCountHint").textContent = agentsCountHintText;
234
+ document.getElementById("agentsWrap").innerHTML = agentsWrapHtml;
235
+ lastAgentsRenderKey = renderKey;
236
+ }
226
237
  writeUiCache("silicaclaw_ui_overview", {
227
238
  overviewCardsHtml,
228
239
  brandVersionText,
@@ -282,20 +293,37 @@ export function createOverviewController({
282
293
  </div>
283
294
  </div>
284
295
  `;
285
- document.getElementById("agentsCountHint").textContent = agentsCountHintText;
286
- document.getElementById("agentsWrap").innerHTML = agentsWrapHtml;
287
- document.getElementById("agentsPrevPageBtn")?.addEventListener("click", async () => {
288
- if (agentsPage <= 1) return;
289
- agentsPage -= 1;
290
- onPageChange(agentsPage);
291
- await refreshOverview({ getAgentsPage, getOnlyShowOnline, onPageChange, setOverviewMode, setVisibleRemotePublicCount });
292
- });
293
- document.getElementById("agentsNextPageBtn")?.addEventListener("click", async () => {
294
- if (agentsPage >= totalAgentPages) return;
295
- agentsPage += 1;
296
- onPageChange(agentsPage);
297
- await refreshOverview({ getAgentsPage, getOnlyShowOnline, onPageChange, setOverviewMode, setVisibleRemotePublicCount });
296
+ const renderKey = JSON.stringify({
297
+ state: "list",
298
+ hint: agentsCountHintText,
299
+ page: agentsPage,
300
+ totalPages: totalAgentPages,
301
+ onlineOnly: getOnlyShowOnline(),
302
+ items: pagedAgents.map((agent) => [
303
+ agent.agent_id,
304
+ agent.updated_at,
305
+ agent.online ? 1 : 0,
306
+ agent.display_name || "",
307
+ agent.bio || "",
308
+ ]),
298
309
  });
310
+ if (renderKey !== lastAgentsRenderKey) {
311
+ document.getElementById("agentsCountHint").textContent = agentsCountHintText;
312
+ document.getElementById("agentsWrap").innerHTML = agentsWrapHtml;
313
+ document.getElementById("agentsPrevPageBtn")?.addEventListener("click", async () => {
314
+ if (agentsPage <= 1) return;
315
+ agentsPage -= 1;
316
+ onPageChange(agentsPage);
317
+ await refreshOverview({ getAgentsPage, getOnlyShowOnline, onPageChange, setOverviewMode, setVisibleRemotePublicCount });
318
+ });
319
+ document.getElementById("agentsNextPageBtn")?.addEventListener("click", async () => {
320
+ if (agentsPage >= totalAgentPages) return;
321
+ agentsPage += 1;
322
+ onPageChange(agentsPage);
323
+ await refreshOverview({ getAgentsPage, getOnlyShowOnline, onPageChange, setOverviewMode, setVisibleRemotePublicCount });
324
+ });
325
+ lastAgentsRenderKey = renderKey;
326
+ }
299
327
  writeUiCache("silicaclaw_ui_overview", {
300
328
  overviewCardsHtml,
301
329
  brandVersionText,
@@ -26,6 +26,9 @@ export function createSocialController({
26
26
  }) {
27
27
  const SKILLS_SECTION_LIMIT = 4;
28
28
  const SKILLS_DIALOGUE_LIMIT = 1;
29
+ let lastMessagesRenderKey = "";
30
+ let lastLogsRenderKey = "";
31
+ const sectionRenderCache = new Map();
29
32
  let skillsQuery = "";
30
33
  let skillsFilter = "all";
31
34
  const skillsExpanded = {
@@ -132,6 +135,23 @@ export function createSocialController({
132
135
  });
133
136
  }
134
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
+
135
155
  function renderSocialMessages() {
136
156
  const listEl = document.getElementById("socialMessageList");
137
157
  const metaEl = document.getElementById("socialMessageMeta");
@@ -145,10 +165,16 @@ export function createSocialController({
145
165
  seconds: String(Math.floor((governance.send_limit?.window_ms || 60000) / 1000)),
146
166
  })}`
147
167
  : t("overview.messageHint");
148
- hintEl.textContent = governanceHint;
149
168
  if (!socialMessagesCache.length) {
150
- metaEl.textContent = t("overview.noMessagesMeta");
151
- 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
+ }
152
178
  return;
153
179
  }
154
180
 
@@ -167,14 +193,21 @@ export function createSocialController({
167
193
  seconds: String(Math.floor((governance.send_limit?.window_ms || 60000) / 1000)),
168
194
  })}`
169
195
  : "";
170
- metaEl.textContent = `${baseMeta}${governanceMeta}`;
196
+ const nextMeta = `${baseMeta}${governanceMeta}`;
171
197
 
172
198
  if (!filteredMessages.length) {
173
- 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
+ }
174
207
  return;
175
208
  }
176
209
 
177
- listEl.innerHTML = filteredMessages
210
+ const nextHtml = filteredMessages
178
211
  .map((item) => {
179
212
  const visibleRemoteCount = getVisibleRemotePublicCount();
180
213
  const avatarUrl = String(item.avatar_url || "").trim();
@@ -234,6 +267,25 @@ export function createSocialController({
234
267
  `;
235
268
  })
236
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;
237
289
  }
238
290
 
239
291
  async function refreshMessages() {
@@ -278,19 +330,29 @@ export function createSocialController({
278
330
  const publicDiscoveryText = status.public_enabled ? t("social.publicDiscoveryEnabled") : t("social.publicDiscoveryDisabled");
279
331
 
280
332
  const namespaceText = `${status.connected_to_silicaclaw ? t("social.connectedToSilicaClaw") : t("social.notConnected")} · ${publicDiscoveryText} · ${t("social.mode")} ${mode}`;
281
- document.getElementById("socialStatusLine").textContent = summaryLine;
282
- document.getElementById("socialStatusSubline").textContent = namespaceText;
333
+ setCachedContent("socialStatusLine", summaryLine, "text");
334
+ setCachedContent("socialStatusSubline", namespaceText, "text");
283
335
  const bar = document.getElementById("integrationStatusBar");
284
- bar.className = `integration-strip ${status.connected_to_silicaclaw && status.public_enabled ? "ok" : "warn"}${getActiveTab() === "overview" ? "" : " hidden"}`;
285
- 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", {
286
338
  connected: status.connected_to_silicaclaw ? t("common.yes") : t("common.no"),
287
339
  mode,
288
340
  public: status.public_enabled ? t("common.on") : t("common.off"),
289
341
  });
290
- 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
+ }
291
353
  writeUiCache("silicaclaw_ui_social", {
292
- integrationStatusText: bar.textContent,
293
- integrationStatusClassName: bar.className,
354
+ integrationStatusText: barText,
355
+ integrationStatusClassName: barClassName,
294
356
  socialStatusLineText: summaryLine,
295
357
  socialStatusSublineText: namespaceText,
296
358
  });
@@ -298,14 +360,14 @@ export function createSocialController({
298
360
  if (!status.configured && status.configured_reason) reasons.push(t("social.configuredReason", { reason: status.configured_reason }));
299
361
  if (!status.running && status.running_reason) reasons.push(t("social.runningReason", { reason: status.running_reason }));
300
362
  if (!status.discoverable && status.discoverable_reason) reasons.push(t("social.discoverableReasonFull", { reason: status.discoverable_reason }));
301
- document.getElementById("socialStateHint").textContent = reasons.length ? reasons.join(" · ") : t("hints.allIntegrationChecksPassed");
363
+ setCachedContent("socialStateHint", reasons.length ? reasons.join(" · ") : t("hints.allIntegrationChecksPassed"), "text");
302
364
  const modeSelect = document.getElementById("socialModeSelect");
303
365
  const displayedSelectedMode = getSocialModeDirty() && getSocialModePending() ? getSocialModePending() : selectedMode;
304
366
  if (modeSelect && displayedSelectedMode !== "-") modeSelect.value = displayedSelectedMode;
305
367
  renderSocialModeHint(displayedSelectedMode, mode, !!social.network_requires_restart, getSocialModeDirty());
306
368
  setSocialModePendingState(getSocialModeDirty());
307
369
 
308
- document.getElementById("socialPrimaryCards").innerHTML = [
370
+ setCachedContent("socialPrimaryCards", [
309
371
  [t("social.configured"), status.configured ? t("common.yes") : t("common.no")],
310
372
  [t("social.running"), status.running ? t("common.yes") : t("common.no")],
311
373
  [t("social.discoverable"), discoverable ? t("common.yes") : t("common.no")],
@@ -313,9 +375,9 @@ export function createSocialController({
313
375
  [t("social.networkMode"), mode],
314
376
  [t("labels.adapter"), effectiveAdapter],
315
377
  [t("social.discoverableReason"), status.discoverable_reason || "-"],
316
- ].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(""));
317
379
 
318
- document.getElementById("socialIntegrationCards").innerHTML = [
380
+ setCachedContent("socialIntegrationCards", [
319
381
  [t("social.connected"), bridge.connected_to_silicaclaw ? t("common.yes") : t("common.no")],
320
382
  [t("social.messageBroadcast"), bridge.message_broadcast_enabled ? t("common.on") : t("common.off")],
321
383
  [t("social.displayName"), status.display_name || t("overview.unnamed")],
@@ -323,9 +385,9 @@ export function createSocialController({
323
385
  [t("social.socialFound"), summary.social_md_found ? t("common.yes") : t("common.no")],
324
386
  [t("social.socialSource"), summary.social_md_source_path || "-"],
325
387
  [t("social.reuseOpenClawIdentity"), summary.reused_openclaw_identity ? t("common.yes") : t("common.no")],
326
- ].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(""));
327
389
 
328
- document.getElementById("socialMessagePathCards").innerHTML = [
390
+ setCachedContent("socialMessagePathCards", [
329
391
  [t("social.messageBroadcast"), bridge.message_broadcast_enabled ? t("common.on") : t("common.off")],
330
392
  [t("social.publicDiscovery"), status.public_enabled ? t("common.on") : t("common.off")],
331
393
  [t("social.namespace"), effectiveNamespace],
@@ -334,7 +396,7 @@ export function createSocialController({
334
396
  [t("network.lastPoll"), networkDiag.last_poll_at ? new Date(networkDiag.last_poll_at).toLocaleTimeString() : "-"],
335
397
  [t("network.lastPublish"), networkDiag.last_publish_at ? new Date(networkDiag.last_publish_at).toLocaleTimeString() : "-"],
336
398
  [t("network.lastError"), networkDiag.last_error || t("network.none")],
337
- ].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(""));
338
400
 
339
401
  const skillLearning = bridge.skill_learning || {};
340
402
  const ownerDelivery = bridge.owner_delivery || {};
@@ -362,21 +424,21 @@ export function createSocialController({
362
424
  ownerDeliveryHeadline = t("feedback.openclawRoleNotRunning");
363
425
  ownerDeliveryBody = ownerDelivery.reason || "-";
364
426
  }
365
- document.getElementById("socialOwnerDeliveryStatus").className = `feedback ${ownerDeliveryTone}`;
366
- document.getElementById("socialOwnerDeliveryStatus").textContent = ownerDeliveryHeadline;
367
- document.getElementById("socialOwnerDeliverySubline").textContent = [
427
+ setCachedContent("socialOwnerDeliveryStatus", `feedback ${ownerDeliveryTone}`, "class");
428
+ setCachedContent("socialOwnerDeliveryStatus", ownerDeliveryHeadline, "text");
429
+ setCachedContent("socialOwnerDeliverySubline", [
368
430
  `${t("social.broadcastReadable")}: ${ownerDelivery.bridge_messages_readable ? t("common.yes") : t("common.no")}`,
369
431
  `${t("social.ownerForwardCommand")}: ${ownerDelivery.forward_command_configured ? t("common.yes") : t("common.no")}`,
370
432
  `${t("social.ownerForwardReady")}: ${ownerDelivery.ready ? t("common.yes") : t("common.no")}`,
371
- ].join(" · ");
372
- document.getElementById("socialOwnerDeliveryReason").textContent = ownerDeliveryBody;
373
- document.getElementById("socialCapabilityCards").innerHTML = [
433
+ ].join(" · "), "text");
434
+ setCachedContent("socialOwnerDeliveryReason", ownerDeliveryBody, "text");
435
+ setCachedContent("socialCapabilityCards", [
374
436
  [t("socialCapability.publicBroadcast"), bridge.message_broadcast_enabled ? t("common.yes") : t("common.no")],
375
437
  [t("socialCapability.monitorBroadcasts"), ownerDelivery.bridge_messages_readable ? t("common.yes") : t("common.no")],
376
438
  [t("socialCapability.autoPushToOwner"), ownerDelivery.ready ? t("common.yes") : t("common.no")],
377
439
  [t("socialCapability.ownerPrivateBoundary"), t("socialCapability.ownerPrivateBoundaryValue")],
378
- ].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("");
379
- 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", [
380
442
  [t("social.openclawInstalled"), openclawDetected ? t("common.yes") : t("common.no")],
381
443
  [t("social.running"), openclawRunning ? t("common.yes") : t("common.no")],
382
444
  [t("social.skillInstalled"), skillInstalled ? t("common.yes") : t("common.no")],
@@ -387,13 +449,13 @@ export function createSocialController({
387
449
  [t("social.openclawGateway"), bridge.openclaw_runtime?.gateway_url || "-"],
388
450
  [t("social.installMode"), skillLearning.install_mode || "-"],
389
451
  [t("social.installedPath"), skillInstalled ? installedSkillPath : "-"],
390
- ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${escapeHtml(v)}</div></div>`).join("");
391
- 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
392
454
  ? `${ownerDelivery.forward_command}${ownerDelivery.owner_channel ? ` · ${ownerDelivery.owner_channel}` : ""}${ownerDelivery.owner_target ? ` · ${ownerDelivery.owner_target}` : ""}`
393
455
  : skillInstalled
394
456
  ? installedSkillPath
395
- : `${installAction.recommended_command || "-"}${bridge.openclaw_runtime?.gateway_url ? ` · detect ${bridge.openclaw_runtime.gateway_url}` : ""}`;
396
- 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
397
459
  ? t("feedback.openclawRoleBroadcasterOnly")
398
460
  : !openclawRunning
399
461
  ? t("feedback.openclawRoleNotRunning")
@@ -401,11 +463,11 @@ export function createSocialController({
401
463
  ? t("feedback.openclawRoleReadyToLearn")
402
464
  : ownerDelivery.ready
403
465
  ? t("feedback.openclawRoleOwnerReady")
404
- : ownerDelivery.bridge_messages_readable && !ownerDelivery.forward_command_configured
405
- ? t("feedback.openclawRoleLearningOnly")
406
- : ownerDelivery.bridge_messages_readable
407
- ? t("feedback.openclawRoleNeedsOwnerRoute")
408
- : 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");
409
471
  const skillInstallBtn = document.getElementById("openclawSkillInstallBtn");
410
472
  skillInstallBtn.textContent = !openclawDetected
411
473
  ? t("actions.openclawNotInstalled")
@@ -426,33 +488,33 @@ export function createSocialController({
426
488
  document.getElementById("governanceDuplicateWindowInput").value = String(Math.floor((policy.duplicate_window_ms ?? 180000) / 1000));
427
489
  document.getElementById("governanceBlockedAgentsInput").value = blockedAgentIds.join(", ");
428
490
  document.getElementById("governanceBlockedTermsInput").value = blockedTerms.join(", ");
429
- document.getElementById("socialGovernanceCards").innerHTML = [
491
+ setCachedContent("socialGovernanceCards", [
430
492
  [t("labels.sendLimit"), `${policy.send_limit?.max ?? "-"} / ${Math.floor((policy.send_limit?.window_ms ?? 60000) / 1000)}s`],
431
493
  [t("labels.receiveLimit"), `${policy.receive_limit?.max ?? "-"} / ${Math.floor((policy.receive_limit?.window_ms ?? 60000) / 1000)}s`],
432
494
  [t("labels.duplicateWindowSeconds"), `${Math.floor((policy.duplicate_window_ms ?? 0) / 1000)}s`],
433
495
  [t("labels.blockedAgentIds"), blockedAgentIds.length],
434
496
  [t("labels.blockedTerms"), blockedTerms.length],
435
- ].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(""));
436
498
 
437
499
  const moderationEvents = Array.isArray(governance.recent_events) ? governance.recent_events : [];
438
- document.getElementById("socialModerationList").innerHTML = moderationEvents.length === 0
500
+ setCachedContent("socialModerationList", moderationEvents.length === 0
439
501
  ? `<div class="empty-state">${t("network.noModerationEvents")}</div>`
440
502
  : moderationEvents.map((event) => `
441
503
  <div class="log-item">
442
504
  <div class="log-${event.level || "warn"}">[${String(event.level || "warn").toUpperCase()}] ${escapeHtml(event.message || "-")}</div>
443
505
  <div class="mono" style="color:#90a2c3;">${new Date(event.timestamp).toLocaleString()}</div>
444
506
  </div>
445
- `).join("");
507
+ `).join(""));
446
508
 
447
- document.getElementById("socialAdvancedCards").innerHTML = [
509
+ setCachedContent("socialAdvancedCards", [
448
510
  [t("labels.adapter"), effectiveAdapter],
449
511
  [t("social.namespace"), effectiveNamespace],
450
512
  [t("labels.room"), effectiveRoom],
451
513
  [t("social.bridgeStatus"), bridge.connected_to_silicaclaw ? t("common.yes") : t("common.no")],
452
514
  [t("social.messageBroadcast"), bridge.message_broadcast_enabled ? t("common.on") : t("common.off")],
453
515
  [t("social.restartRequired"), social.network_requires_restart ? t("common.yes") : t("common.no")],
454
- ].map(([k, v]) => `<div class="card"><div class="label">${k}</div><div class="value" style="font-size:17px;">${v}</div></div>`).join("");
455
- 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({
456
518
  effective_runtime: {
457
519
  mode: effectiveMode,
458
520
  adapter: effectiveAdapter,
@@ -474,17 +536,17 @@ export function createSocialController({
474
536
  social_md_found: summary.social_md_found,
475
537
  social_md_source_path: summary.social_md_source_path,
476
538
  },
477
- });
539
+ }), "text");
478
540
 
479
- document.getElementById("socialSourceWrap").textContent = toPrettyJson({
541
+ setCachedContent("socialSourceWrap", toPrettyJson({
480
542
  found: social.found,
481
543
  source_path: social.source_path,
482
544
  parse_error: social.parse_error,
483
- });
484
- document.getElementById("socialRawWrap").textContent = toPrettyJson({
545
+ }), "text");
546
+ setCachedContent("socialRawWrap", toPrettyJson({
485
547
  raw_frontmatter: social.raw_frontmatter || null,
486
- });
487
- document.getElementById("socialRuntimeWrap").textContent = toPrettyJson(runtime);
548
+ }), "text");
549
+ setCachedContent("socialRuntimeWrap", toPrettyJson(runtime), "text");
488
550
  }
489
551
 
490
552
  async function exportSocialTemplate() {
@@ -499,23 +561,43 @@ export function createSocialController({
499
561
  const logLevelFilter = getLogLevelFilter();
500
562
  const el = document.getElementById("logList");
501
563
  if (!logsCache.length) {
502
- 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
+ }
503
569
  return;
504
570
  }
505
571
  const filtered = logLevelFilter === "all" ? logsCache : logsCache.filter((item) => String(item.level || "").toLowerCase() === logLevelFilter);
506
572
  if (!filtered.length) {
507
- 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
+ }
508
578
  return;
509
579
  }
510
- el.innerHTML = filtered.map((item) => `
580
+ const nextHtml = filtered.map((item) => `
511
581
  <div class="log-item">
512
582
  <div class="log-${item.level}">[${String(item.level).toUpperCase()}] ${item.message}</div>
513
583
  <div class="mono" style="color:#90a2c3;">${new Date(item.timestamp).toLocaleString()}</div>
514
584
  </div>
515
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;
516
595
  }
517
596
 
518
597
  async function refreshLogs() {
598
+ if (getActiveTab() !== "network") {
599
+ return;
600
+ }
519
601
  setLogsCache((await api("/api/logs")).data || []);
520
602
  renderLogs();
521
603
  }
@@ -586,6 +586,88 @@
586
586
  overflow: hidden;
587
587
  text-overflow: ellipsis;
588
588
  }
589
+ .sidebar-version__hint {
590
+ font-size: 10px;
591
+ line-height: 1.2;
592
+ color: var(--muted);
593
+ white-space: nowrap;
594
+ overflow: hidden;
595
+ text-overflow: ellipsis;
596
+ }
597
+ .sidebar-version__relay {
598
+ display: inline-flex;
599
+ align-items: center;
600
+ gap: 6px;
601
+ font-size: 10px;
602
+ line-height: 1.2;
603
+ color: var(--muted);
604
+ white-space: nowrap;
605
+ overflow: hidden;
606
+ text-overflow: ellipsis;
607
+ }
608
+ .sidebar-version__relay::before {
609
+ content: "";
610
+ width: 7px;
611
+ height: 7px;
612
+ border-radius: 999px;
613
+ background: var(--ok);
614
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--ok) 12%, transparent);
615
+ flex: 0 0 auto;
616
+ }
617
+ .sidebar-version__relay.warn {
618
+ color: color-mix(in srgb, var(--warn) 86%, var(--text));
619
+ }
620
+ .sidebar-version__relay.warn::before {
621
+ background: var(--warn);
622
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--warn) 12%, transparent);
623
+ }
624
+ .sidebar-version__relay.danger {
625
+ color: color-mix(in srgb, var(--danger) 86%, var(--text));
626
+ }
627
+ .sidebar-version__relay.danger::before {
628
+ background: var(--danger);
629
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--danger) 12%, transparent);
630
+ }
631
+ .sidebar-version__actions {
632
+ display: flex;
633
+ align-items: center;
634
+ gap: 10px;
635
+ flex: 0 0 auto;
636
+ }
637
+ .sidebar-version__btn {
638
+ appearance: none;
639
+ border: 1px solid color-mix(in srgb, var(--accent) 38%, var(--border));
640
+ background: color-mix(in srgb, var(--accent) 16%, transparent);
641
+ color: var(--text-strong);
642
+ border-radius: 999px;
643
+ padding: 6px 10px;
644
+ font-size: 11px;
645
+ line-height: 1;
646
+ font-weight: 700;
647
+ cursor: pointer;
648
+ transition: background .18s ease, border-color .18s ease, opacity .18s ease;
649
+ }
650
+ .sidebar-version__btn:hover:not(:disabled) {
651
+ background: color-mix(in srgb, var(--accent) 22%, transparent);
652
+ border-color: color-mix(in srgb, var(--accent) 52%, var(--border));
653
+ }
654
+ .sidebar-version__btn--ghost {
655
+ background: transparent;
656
+ border-color: color-mix(in srgb, var(--border) 88%, transparent);
657
+ color: var(--muted);
658
+ }
659
+ .sidebar-version__btn--ghost:hover:not(:disabled) {
660
+ background: color-mix(in srgb, var(--bg-elevated) 72%, transparent);
661
+ border-color: color-mix(in srgb, var(--border) 100%, transparent);
662
+ }
663
+ .sidebar-version__btn:disabled {
664
+ opacity: .55;
665
+ cursor: wait;
666
+ }
667
+ .sidebar-version__btn.hidden,
668
+ .sidebar-version__hint.hidden {
669
+ display: none;
670
+ }
589
671
  .sidebar-version__status {
590
672
  width: 8px;
591
673
  height: 8px;
@@ -609,6 +691,9 @@
609
691
  .app.nav-collapsed .sidebar-version__copy {
610
692
  display: none;
611
693
  }
694
+ .app.nav-collapsed .sidebar-version__btn {
695
+ display: none;
696
+ }
612
697
 
613
698
  .main {
614
699
  display: flex;
@@ -818,6 +903,7 @@
818
903
  .pill.ok { color: var(--ok); border-color: rgba(34, 197, 94, 0.45); background: rgba(34, 197, 94, 0.08); }
819
904
  .pill.warn { color: var(--warn); border-color: rgba(245, 158, 11, 0.45); background: rgba(245, 158, 11, 0.08); }
820
905
  .pill.danger { color: var(--danger); border-color: rgba(239, 68, 68, 0.42); background: rgba(239, 68, 68, 0.08); }
906
+ .pill.ok { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 42%, transparent); background: color-mix(in srgb, var(--ok) 10%, transparent); }
821
907
 
822
908
  .notice {
823
909
  display: none;
@@ -100,8 +100,14 @@ export const appTemplate = String.raw`<div class="app" id="appShell">
100
100
  <div class="sidebar-version__copy">
101
101
  <span class="sidebar-version__label">Version</span>
102
102
  <span class="sidebar-version__text" id="brandVersion">-</span>
103
+ <span class="sidebar-version__hint" id="brandUpdateHint">Checking for updates...</span>
104
+ <span class="sidebar-version__relay hidden" id="brandRelayHint">Relay queues are healthy.</span>
105
+ </div>
106
+ <div class="sidebar-version__actions">
107
+ <button class="sidebar-version__btn sidebar-version__btn--ghost hidden" id="brandCheckUpdateBtn" type="button">Check</button>
108
+ <button class="sidebar-version__btn hidden" id="brandUpdateBtn" type="button">Update</button>
109
+ <span class="sidebar-version__status" id="brandStatusDot" aria-hidden="true"></span>
103
110
  </div>
104
- <span class="sidebar-version__status" id="brandStatusDot" aria-hidden="true"></span>
105
111
  </div>
106
112
  </div>
107
113
  </div>