Skip to content

Commit 2156b20

Browse files
authored
fix(ui): align overview session labels
Summary: - move session display-name resolution into a shared UI helper - reuse that resolver for Overview recent sessions and chat session controls - keep Telegram/channel fallback labels from leaking raw compound session keys into Overview Verification: - node scripts/run-vitest.mjs ui/src/ui/views/overview.render.test.ts ui/src/ui/app-render.helpers.node.test.ts - pnpm exec oxfmt --check --threads=1 ui/src/ui/app-render.helpers.ts ui/src/ui/chat/session-controls.ts ui/src/ui/session-display.ts ui/src/ui/views/overview-cards.ts ui/src/ui/views/overview.render.test.ts CHANGELOG.md - pnpm exec oxlint --tsconfig config/tsconfig/oxlint.core.json ui/src/ui/app-render.helpers.ts ui/src/ui/chat/session-controls.ts ui/src/ui/session-display.ts ui/src/ui/views/overview-cards.ts ui/src/ui/views/overview.render.test.ts src/commands/doctor/shared/legacy-config-core-normalizers.ts - git diff --check origin/main...HEAD - GitHub CI on 36fd998: check, check-lint, checks-node-core-ui, build-artifacts, Real behavior proof all passed
1 parent 695a4f5 commit 2156b20

6 files changed

Lines changed: 173 additions & 125 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
5757
- 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.
5858
- 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.
5959
- 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.
60+
- 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.
6061
- 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.
6162
- 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.
6263
- 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.

ui/src/ui/app-render.helpers.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ import { syncUrlWithSessionKey } from "./app-settings.ts";
1010
import type { AppViewState } from "./app-view-state.ts";
1111
import { reconcileChatRunLifecycle } from "./chat/run-lifecycle.ts";
1212
import {
13-
isCronSessionKey,
14-
parseSessionKey,
1513
renderChatSessionSelect as renderChatSessionSelectBase,
16-
resolveSessionDisplayName,
1714
resolveSessionOptionGroups,
1815
} from "./chat/session-controls.ts";
1916
import { refreshSlashCommands } from "./chat/slash-commands.ts";
@@ -22,6 +19,7 @@ import { ChatState, loadChatHistory } from "./controllers/chat.ts";
2219
import { createSessionAndRefresh, loadSessions } from "./controllers/sessions.ts";
2320
import { icons } from "./icons.ts";
2421
import { iconForTab, pathForTab, titleForTab, type Tab } from "./navigation.ts";
22+
import { isCronSessionKey, parseSessionKey, resolveSessionDisplayName } from "./session-display.ts";
2523
import {
2624
normalizeAgentId,
2725
parseAgentSessionKey,

ui/src/ui/chat/session-controls.ts

Lines changed: 1 addition & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import { refreshVisibleToolsEffectiveForCurrentSession } from "../controllers/agents.ts";
1212
import { loadSessions } from "../controllers/sessions.ts";
1313
import { pushUniqueTrimmedSelectOption } from "../select-options.ts";
14+
import { isCronSessionKey, resolveSessionDisplayName } from "../session-display.ts";
1415
import {
1516
buildAgentMainSessionKey,
1617
isSubagentSessionKey,
@@ -439,127 +440,6 @@ async function switchChatThinkingLevel(state: AppViewState, nextThinkingLevel: s
439440
}
440441
}
441442

442-
/* Channel display labels. */
443-
const CHANNEL_LABELS: Record<string, string> = {
444-
imessage: "iMessage",
445-
telegram: "Telegram",
446-
discord: "Discord",
447-
signal: "Signal",
448-
slack: "Slack",
449-
whatsapp: "WhatsApp",
450-
matrix: "Matrix",
451-
email: "Email",
452-
sms: "SMS",
453-
};
454-
455-
const KNOWN_CHANNEL_KEYS = Object.keys(CHANNEL_LABELS);
456-
457-
/** Parsed type / context extracted from a session key. */
458-
export type SessionKeyInfo = {
459-
/** Prefix for typed sessions (Subagent:/Cron:). Empty for others. */
460-
prefix: string;
461-
/** Human-readable fallback when no label / displayName is available. */
462-
fallbackName: string;
463-
};
464-
465-
function capitalize(s: string): string {
466-
return s.charAt(0).toUpperCase() + s.slice(1);
467-
}
468-
469-
/**
470-
* Parse a session key to extract type information and a human-readable
471-
* fallback display name. Exported for testing.
472-
*/
473-
export function parseSessionKey(key: string): SessionKeyInfo {
474-
const normalized = normalizeLowercaseStringOrEmpty(key);
475-
476-
// Main session.
477-
if (key === "main" || key === "agent:main:main") {
478-
return { prefix: "", fallbackName: "Main Session" };
479-
}
480-
481-
// Subagent.
482-
if (key.includes(":subagent:")) {
483-
return { prefix: "Subagent:", fallbackName: "Subagent:" };
484-
}
485-
486-
// Cron job.
487-
if (normalized.startsWith("cron:") || key.includes(":cron:")) {
488-
return { prefix: "Cron:", fallbackName: "Cron Job:" };
489-
}
490-
491-
// Direct chat: agent:<x>:<channel>:direct:<id>.
492-
const directMatch = key.match(/^agent:[^:]+:([^:]+):direct:(.+)$/);
493-
if (directMatch) {
494-
const channel = directMatch[1];
495-
const identifier = directMatch[2];
496-
const channelLabel = CHANNEL_LABELS[channel] ?? capitalize(channel);
497-
return { prefix: "", fallbackName: `${channelLabel} · ${identifier}` };
498-
}
499-
500-
// Group chat: agent:<x>:<channel>:group:<id>.
501-
const groupMatch = key.match(/^agent:[^:]+:([^:]+):group:(.+)$/);
502-
if (groupMatch) {
503-
const channel = groupMatch[1];
504-
const channelLabel = CHANNEL_LABELS[channel] ?? capitalize(channel);
505-
return { prefix: "", fallbackName: `${channelLabel} Group` };
506-
}
507-
508-
// Channel-prefixed legacy keys, for example "imessage:g-...".
509-
for (const ch of KNOWN_CHANNEL_KEYS) {
510-
if (key === ch || key.startsWith(`${ch}:`)) {
511-
return { prefix: "", fallbackName: `${CHANNEL_LABELS[ch]} Session` };
512-
}
513-
}
514-
515-
// Unknown: return key as-is.
516-
return { prefix: "", fallbackName: key };
517-
}
518-
519-
export function resolveSessionDisplayName(
520-
key: string,
521-
row?: SessionsListResult["sessions"][number],
522-
): string {
523-
const label = normalizeOptionalString(row?.label) ?? "";
524-
const displayName = normalizeOptionalString(row?.displayName) ?? "";
525-
const { prefix, fallbackName } = parseSessionKey(key);
526-
527-
const applyTypedPrefix = (name: string): string => {
528-
if (!prefix) {
529-
return name;
530-
}
531-
const prefixPattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}\\s*`, "i");
532-
return prefixPattern.test(name) ? name : `${prefix} ${name}`;
533-
};
534-
535-
if (label && label !== key) {
536-
return applyTypedPrefix(label);
537-
}
538-
if (displayName && displayName !== key) {
539-
return applyTypedPrefix(displayName);
540-
}
541-
return fallbackName;
542-
}
543-
544-
export function isCronSessionKey(key: string): boolean {
545-
const normalized = normalizeLowercaseStringOrEmpty(key);
546-
if (!normalized) {
547-
return false;
548-
}
549-
if (normalized.startsWith("cron:")) {
550-
return true;
551-
}
552-
if (!normalized.startsWith("agent:")) {
553-
return false;
554-
}
555-
const parts = normalized.split(":").filter(Boolean);
556-
if (parts.length < 3) {
557-
return false;
558-
}
559-
const rest = parts.slice(2).join(":");
560-
return rest.startsWith("cron:");
561-
}
562-
563443
type SessionOptionEntry = {
564444
key: string;
565445
label: string;

ui/src/ui/session-display.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "./string-coerce.ts";
2+
import type { SessionsListResult } from "./types.ts";
3+
4+
const CHANNEL_LABELS: Record<string, string> = {
5+
imessage: "iMessage",
6+
telegram: "Telegram",
7+
discord: "Discord",
8+
signal: "Signal",
9+
slack: "Slack",
10+
whatsapp: "WhatsApp",
11+
matrix: "Matrix",
12+
email: "Email",
13+
sms: "SMS",
14+
};
15+
16+
const KNOWN_CHANNEL_KEYS = Object.keys(CHANNEL_LABELS);
17+
18+
/** Parsed type / context extracted from a session key. */
19+
export type SessionKeyInfo = {
20+
/** Prefix for typed sessions (Subagent:/Cron:). Empty for others. */
21+
prefix: string;
22+
/** Human-readable fallback when no label / displayName is available. */
23+
fallbackName: string;
24+
};
25+
26+
function capitalize(s: string): string {
27+
return s.charAt(0).toUpperCase() + s.slice(1);
28+
}
29+
30+
/**
31+
* Parse a session key to extract type information and a human-readable
32+
* fallback display name. Exported for testing.
33+
*/
34+
export function parseSessionKey(key: string): SessionKeyInfo {
35+
const normalized = normalizeLowercaseStringOrEmpty(key);
36+
37+
// Main session.
38+
if (key === "main" || key === "agent:main:main") {
39+
return { prefix: "", fallbackName: "Main Session" };
40+
}
41+
42+
// Subagent.
43+
if (key.includes(":subagent:")) {
44+
return { prefix: "Subagent:", fallbackName: "Subagent:" };
45+
}
46+
47+
// Cron job.
48+
if (normalized.startsWith("cron:") || key.includes(":cron:")) {
49+
return { prefix: "Cron:", fallbackName: "Cron Job:" };
50+
}
51+
52+
// Direct chat: agent:<x>:<channel>:direct:<id>.
53+
const directMatch = key.match(/^agent:[^:]+:([^:]+):direct:(.+)$/);
54+
if (directMatch) {
55+
const channel = directMatch[1];
56+
const identifier = directMatch[2];
57+
const channelLabel = CHANNEL_LABELS[channel] ?? capitalize(channel);
58+
return { prefix: "", fallbackName: `${channelLabel} · ${identifier}` };
59+
}
60+
61+
// Group chat: agent:<x>:<channel>:group:<id>.
62+
const groupMatch = key.match(/^agent:[^:]+:([^:]+):group:(.+)$/);
63+
if (groupMatch) {
64+
const channel = groupMatch[1];
65+
const channelLabel = CHANNEL_LABELS[channel] ?? capitalize(channel);
66+
return { prefix: "", fallbackName: `${channelLabel} Group` };
67+
}
68+
69+
// Channel-prefixed legacy keys, for example "imessage:g-...".
70+
for (const ch of KNOWN_CHANNEL_KEYS) {
71+
if (key === ch || key.startsWith(`${ch}:`)) {
72+
return { prefix: "", fallbackName: `${CHANNEL_LABELS[ch]} Session` };
73+
}
74+
}
75+
76+
// Unknown: return key as-is.
77+
return { prefix: "", fallbackName: key };
78+
}
79+
80+
export function resolveSessionDisplayName(
81+
key: string,
82+
row?: SessionsListResult["sessions"][number],
83+
): string {
84+
const label = normalizeOptionalString(row?.label) ?? "";
85+
const displayName = normalizeOptionalString(row?.displayName) ?? "";
86+
const { prefix, fallbackName } = parseSessionKey(key);
87+
88+
const applyTypedPrefix = (name: string): string => {
89+
if (!prefix) {
90+
return name;
91+
}
92+
const prefixPattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}\\s*`, "i");
93+
return prefixPattern.test(name) ? name : `${prefix} ${name}`;
94+
};
95+
96+
if (label && label !== key) {
97+
return applyTypedPrefix(label);
98+
}
99+
if (displayName && displayName !== key) {
100+
return applyTypedPrefix(displayName);
101+
}
102+
return fallbackName;
103+
}
104+
105+
export function isCronSessionKey(key: string): boolean {
106+
const normalized = normalizeLowercaseStringOrEmpty(key);
107+
if (!normalized) {
108+
return false;
109+
}
110+
if (normalized.startsWith("cron:")) {
111+
return true;
112+
}
113+
if (!normalized.startsWith("agent:")) {
114+
return false;
115+
}
116+
const parts = normalized.split(":").filter(Boolean);
117+
if (parts.length < 3) {
118+
return false;
119+
}
120+
const rest = parts.slice(2).join(":");
121+
return rest.startsWith("cron:");
122+
}

ui/src/ui/views/overview-cards.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { t } from "../../i18n/index.ts";
44
import { formatCost, formatTokens, formatRelativeTimestamp } from "../format.ts";
55
import { isMonitoredAuthProvider } from "../model-auth-helpers.ts";
66
import { formatNextRun } from "../presenter.ts";
7+
import { resolveSessionDisplayName } from "../session-display.ts";
78
import type {
89
SessionsUsageResult,
910
SessionsListResult,
@@ -244,7 +245,7 @@ export function renderOverviewCards(props: OverviewCardsProps) {
244245
(s) => html`
245246
<li class="ov-recent__row">
246247
<span class="ov-recent__key"
247-
>${blurDigits(s.displayName || s.label || s.key)}</span
248+
>${blurDigits(resolveSessionDisplayName(s.key, s))}</span
248249
>
249250
<span class="ov-recent__model">${s.model ?? ""}</span>
250251
<span class="ov-recent__time"

ui/src/ui/views/overview.render.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,50 @@ describe("overview view rendering", () => {
134134
"openclaw devices list",
135135
]);
136136
});
137+
138+
it("renders recent session names through the shared display resolver", async () => {
139+
const container = document.createElement("div");
140+
const props = createOverviewProps({
141+
sessionsResult: {
142+
ts: 0,
143+
path: "",
144+
count: 3,
145+
defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null },
146+
sessions: [
147+
{
148+
key: "discord:123:456",
149+
kind: "direct",
150+
label: " ",
151+
displayName: "Ops Room",
152+
model: "gpt-5",
153+
updatedAt: null,
154+
},
155+
{
156+
key: "telegram:123:456",
157+
kind: "direct",
158+
label: "telegram:123:456",
159+
model: "gpt-5",
160+
updatedAt: null,
161+
},
162+
{
163+
key: "agent:main:main",
164+
kind: "direct",
165+
label: "Main Project",
166+
displayName: "agent:main:main",
167+
model: "gpt-5",
168+
updatedAt: null,
169+
},
170+
],
171+
},
172+
});
173+
174+
render(renderOverview(props), container);
175+
await Promise.resolve();
176+
177+
const recentNames = [...container.querySelectorAll(".ov-recent__key")].map(
178+
(node) => node.textContent?.trim() ?? "",
179+
);
180+
expect(recentNames).toEqual(["Ops Room", "Telegram Session", "Main Project"]);
181+
expect(recentNames).not.toContain("telegram:123:456");
182+
});
137183
});

0 commit comments

Comments
 (0)