mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(ui): align overview session labels
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:");
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user