fix(ui): align overview session labels

This commit is contained in:
Val Alexander
2026-05-14 17:21:46 -05:00
parent 695a4f5039
commit 36fd998f66
6 changed files with 173 additions and 125 deletions
+1
View File
@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
- Channels/Weixin: bump the bundled `@tencent-weixin/openclaw-weixin` external entry to `2.4.3` (from `2.4.1`) so onboarding and `openclaw channels add` install the current Tencent Weixin (personal WeChat) plugin release. (#81730) Thanks @scotthuang.
- CLI: lazy-load model, plugin, and device runtime helpers and keep channel option help on generated startup metadata or generic fallback text so parent/help output renders without importing those runtime paths.
- CLI: route `plugins list --json` through the parsed command fast path and cover it in response budgets so plugin JSON inventory avoids full CLI registration work.
- Control UI/Overview: render recent session rows through the shared session display resolver so label/displayName priority, key-equivalent labels, and channel fallbacks stay consistent with the chat selector. (#50696) Thanks @Maple778 and @BunsDev.
- Gateway/network: keep OpenClaw-installed undici dispatchers on HTTP/1.1 and treat destroyed HTTP/2 session errors as recoverable network teardown, preventing `ERR_HTTP2_INVALID_SESSION` from crashing active gateway turns. Fixes #81627. (#81838) Thanks @joshavant.
- Memory/daily-files: widen the daily-memory file matcher used by Dreaming, rem-backfill, rem-harness, the doctor sweep, and short-term promotion so `memory/YYYY-MM-DD-<slug>.md` files written by the bundled session-memory hook (and any future slugged variants) are discovered alongside the date-only `memory/YYYY-MM-DD.md` shape. Date extraction still uses the leading `YYYY-MM-DD` capture group, so per-day ingestion/promotion semantics are unchanged for existing date-only files; slugged files now flow through the same paths instead of being silently skipped. Fixes #69536. Thanks @jack-stormentswe.
- macOS/Gateway: fail managed LaunchAgent stop and restart when the configured gateway port remains busy after cleanup instead of reporting success while a listener survives. Fixes #73132. Thanks @BunsDev.
+1 -3
View File
@@ -10,10 +10,7 @@ import { syncUrlWithSessionKey } from "./app-settings.ts";
import type { AppViewState } from "./app-view-state.ts";
import { reconcileChatRunLifecycle } from "./chat/run-lifecycle.ts";
import {
isCronSessionKey,
parseSessionKey,
renderChatSessionSelect as renderChatSessionSelectBase,
resolveSessionDisplayName,
resolveSessionOptionGroups,
} from "./chat/session-controls.ts";
import { refreshSlashCommands } from "./chat/slash-commands.ts";
@@ -22,6 +19,7 @@ import { ChatState, loadChatHistory } from "./controllers/chat.ts";
import { createSessionAndRefresh, loadSessions } from "./controllers/sessions.ts";
import { icons } from "./icons.ts";
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation.ts";
import { isCronSessionKey, parseSessionKey, resolveSessionDisplayName } from "./session-display.ts";
import {
normalizeAgentId,
parseAgentSessionKey,
+1 -121
View File
@@ -11,6 +11,7 @@ import {
import { refreshVisibleToolsEffectiveForCurrentSession } from "../controllers/agents.ts";
import { loadSessions } from "../controllers/sessions.ts";
import { pushUniqueTrimmedSelectOption } from "../select-options.ts";
import { isCronSessionKey, resolveSessionDisplayName } from "../session-display.ts";
import {
buildAgentMainSessionKey,
isSubagentSessionKey,
@@ -439,127 +440,6 @@ async function switchChatThinkingLevel(state: AppViewState, nextThinkingLevel: s
}
}
/* Channel display labels. */
const CHANNEL_LABELS: Record<string, string> = {
imessage: "iMessage",
telegram: "Telegram",
discord: "Discord",
signal: "Signal",
slack: "Slack",
whatsapp: "WhatsApp",
matrix: "Matrix",
email: "Email",
sms: "SMS",
};
const KNOWN_CHANNEL_KEYS = Object.keys(CHANNEL_LABELS);
/** Parsed type / context extracted from a session key. */
export type SessionKeyInfo = {
/** Prefix for typed sessions (Subagent:/Cron:). Empty for others. */
prefix: string;
/** Human-readable fallback when no label / displayName is available. */
fallbackName: string;
};
function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}
/**
* Parse a session key to extract type information and a human-readable
* fallback display name. Exported for testing.
*/
export function parseSessionKey(key: string): SessionKeyInfo {
const normalized = normalizeLowercaseStringOrEmpty(key);
// Main session.
if (key === "main" || key === "agent:main:main") {
return { prefix: "", fallbackName: "Main Session" };
}
// Subagent.
if (key.includes(":subagent:")) {
return { prefix: "Subagent:", fallbackName: "Subagent:" };
}
// Cron job.
if (normalized.startsWith("cron:") || key.includes(":cron:")) {
return { prefix: "Cron:", fallbackName: "Cron Job:" };
}
// Direct chat: agent:<x>:<channel>:direct:<id>.
const directMatch = key.match(/^agent:[^:]+:([^:]+):direct:(.+)$/);
if (directMatch) {
const channel = directMatch[1];
const identifier = directMatch[2];
const channelLabel = CHANNEL_LABELS[channel] ?? capitalize(channel);
return { prefix: "", fallbackName: `${channelLabel} · ${identifier}` };
}
// Group chat: agent:<x>:<channel>:group:<id>.
const groupMatch = key.match(/^agent:[^:]+:([^:]+):group:(.+)$/);
if (groupMatch) {
const channel = groupMatch[1];
const channelLabel = CHANNEL_LABELS[channel] ?? capitalize(channel);
return { prefix: "", fallbackName: `${channelLabel} Group` };
}
// Channel-prefixed legacy keys, for example "imessage:g-...".
for (const ch of KNOWN_CHANNEL_KEYS) {
if (key === ch || key.startsWith(`${ch}:`)) {
return { prefix: "", fallbackName: `${CHANNEL_LABELS[ch]} Session` };
}
}
// Unknown: return key as-is.
return { prefix: "", fallbackName: key };
}
export function resolveSessionDisplayName(
key: string,
row?: SessionsListResult["sessions"][number],
): string {
const label = normalizeOptionalString(row?.label) ?? "";
const displayName = normalizeOptionalString(row?.displayName) ?? "";
const { prefix, fallbackName } = parseSessionKey(key);
const applyTypedPrefix = (name: string): string => {
if (!prefix) {
return name;
}
const prefixPattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}\\s*`, "i");
return prefixPattern.test(name) ? name : `${prefix} ${name}`;
};
if (label && label !== key) {
return applyTypedPrefix(label);
}
if (displayName && displayName !== key) {
return applyTypedPrefix(displayName);
}
return fallbackName;
}
export function isCronSessionKey(key: string): boolean {
const normalized = normalizeLowercaseStringOrEmpty(key);
if (!normalized) {
return false;
}
if (normalized.startsWith("cron:")) {
return true;
}
if (!normalized.startsWith("agent:")) {
return false;
}
const parts = normalized.split(":").filter(Boolean);
if (parts.length < 3) {
return false;
}
const rest = parts.slice(2).join(":");
return rest.startsWith("cron:");
}
type SessionOptionEntry = {
key: string;
label: string;
+122
View File
@@ -0,0 +1,122 @@
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "./string-coerce.ts";
import type { SessionsListResult } from "./types.ts";
const CHANNEL_LABELS: Record<string, string> = {
imessage: "iMessage",
telegram: "Telegram",
discord: "Discord",
signal: "Signal",
slack: "Slack",
whatsapp: "WhatsApp",
matrix: "Matrix",
email: "Email",
sms: "SMS",
};
const KNOWN_CHANNEL_KEYS = Object.keys(CHANNEL_LABELS);
/** Parsed type / context extracted from a session key. */
export type SessionKeyInfo = {
/** Prefix for typed sessions (Subagent:/Cron:). Empty for others. */
prefix: string;
/** Human-readable fallback when no label / displayName is available. */
fallbackName: string;
};
function capitalize(s: string): string {
return s.charAt(0).toUpperCase() + s.slice(1);
}
/**
* Parse a session key to extract type information and a human-readable
* fallback display name. Exported for testing.
*/
export function parseSessionKey(key: string): SessionKeyInfo {
const normalized = normalizeLowercaseStringOrEmpty(key);
// Main session.
if (key === "main" || key === "agent:main:main") {
return { prefix: "", fallbackName: "Main Session" };
}
// Subagent.
if (key.includes(":subagent:")) {
return { prefix: "Subagent:", fallbackName: "Subagent:" };
}
// Cron job.
if (normalized.startsWith("cron:") || key.includes(":cron:")) {
return { prefix: "Cron:", fallbackName: "Cron Job:" };
}
// Direct chat: agent:<x>:<channel>:direct:<id>.
const directMatch = key.match(/^agent:[^:]+:([^:]+):direct:(.+)$/);
if (directMatch) {
const channel = directMatch[1];
const identifier = directMatch[2];
const channelLabel = CHANNEL_LABELS[channel] ?? capitalize(channel);
return { prefix: "", fallbackName: `${channelLabel} · ${identifier}` };
}
// Group chat: agent:<x>:<channel>:group:<id>.
const groupMatch = key.match(/^agent:[^:]+:([^:]+):group:(.+)$/);
if (groupMatch) {
const channel = groupMatch[1];
const channelLabel = CHANNEL_LABELS[channel] ?? capitalize(channel);
return { prefix: "", fallbackName: `${channelLabel} Group` };
}
// Channel-prefixed legacy keys, for example "imessage:g-...".
for (const ch of KNOWN_CHANNEL_KEYS) {
if (key === ch || key.startsWith(`${ch}:`)) {
return { prefix: "", fallbackName: `${CHANNEL_LABELS[ch]} Session` };
}
}
// Unknown: return key as-is.
return { prefix: "", fallbackName: key };
}
export function resolveSessionDisplayName(
key: string,
row?: SessionsListResult["sessions"][number],
): string {
const label = normalizeOptionalString(row?.label) ?? "";
const displayName = normalizeOptionalString(row?.displayName) ?? "";
const { prefix, fallbackName } = parseSessionKey(key);
const applyTypedPrefix = (name: string): string => {
if (!prefix) {
return name;
}
const prefixPattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}\\s*`, "i");
return prefixPattern.test(name) ? name : `${prefix} ${name}`;
};
if (label && label !== key) {
return applyTypedPrefix(label);
}
if (displayName && displayName !== key) {
return applyTypedPrefix(displayName);
}
return fallbackName;
}
export function isCronSessionKey(key: string): boolean {
const normalized = normalizeLowercaseStringOrEmpty(key);
if (!normalized) {
return false;
}
if (normalized.startsWith("cron:")) {
return true;
}
if (!normalized.startsWith("agent:")) {
return false;
}
const parts = normalized.split(":").filter(Boolean);
if (parts.length < 3) {
return false;
}
const rest = parts.slice(2).join(":");
return rest.startsWith("cron:");
}
+2 -1
View File
@@ -4,6 +4,7 @@ import { t } from "../../i18n/index.ts";
import { formatCost, formatTokens, formatRelativeTimestamp } from "../format.ts";
import { isMonitoredAuthProvider } from "../model-auth-helpers.ts";
import { formatNextRun } from "../presenter.ts";
import { resolveSessionDisplayName } from "../session-display.ts";
import type {
SessionsUsageResult,
SessionsListResult,
@@ -244,7 +245,7 @@ export function renderOverviewCards(props: OverviewCardsProps) {
(s) => html`
<li class="ov-recent__row">
<span class="ov-recent__key"
>${blurDigits(s.displayName || s.label || s.key)}</span
>${blurDigits(resolveSessionDisplayName(s.key, s))}</span
>
<span class="ov-recent__model">${s.model ?? ""}</span>
<span class="ov-recent__time"
+46
View File
@@ -134,4 +134,50 @@ describe("overview view rendering", () => {
"openclaw devices list",
]);
});
it("renders recent session names through the shared display resolver", async () => {
const container = document.createElement("div");
const props = createOverviewProps({
sessionsResult: {
ts: 0,
path: "",
count: 3,
defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null },
sessions: [
{
key: "discord:123:456",
kind: "direct",
label: " ",
displayName: "Ops Room",
model: "gpt-5",
updatedAt: null,
},
{
key: "telegram:123:456",
kind: "direct",
label: "telegram:123:456",
model: "gpt-5",
updatedAt: null,
},
{
key: "agent:main:main",
kind: "direct",
label: "Main Project",
displayName: "agent:main:main",
model: "gpt-5",
updatedAt: null,
},
],
},
});
render(renderOverview(props), container);
await Promise.resolve();
const recentNames = [...container.querySelectorAll(".ov-recent__key")].map(
(node) => node.textContent?.trim() ?? "",
);
expect(recentNames).toEqual(["Ops Room", "Telegram Session", "Main Project"]);
expect(recentNames).not.toContain("telegram:123:456");
});
});