Merge 612488348b81683417980bb21ea77a9f5e8a8e50 into 6b4c24c2e55b5b4013277bd799525086f6a0c40f

This commit is contained in:
monk0bot0 2026-03-21 04:45:11 +00:00 committed by GitHub
commit aba064aa17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 155 additions and 9 deletions

View File

@ -136,11 +136,12 @@ function renderCronFilterIcon(hiddenCount: number) {
export function renderChatSessionSelect(state: AppViewState) {
const sessionGroups = resolveSessionOptionGroups(state, state.sessionKey, state.sessionsResult);
const modelSelect = renderChatModelSelect(state);
const selectedSessionKey = state.sessionKey === "main" ? "agent:main:main" : state.sessionKey;
return html`
<div class="chat-controls__session-row">
<label class="field chat-controls__session">
<select
.value=${state.sessionKey}
.value=${selectedSessionKey}
?disabled=${!state.connected || sessionGroups.length === 0}
@change=${(e: Event) => {
const next = (e.target as HTMLSelectElement).value;
@ -732,23 +733,66 @@ export function resolveSessionDisplayName(
key: string,
row?: SessionsListResult["sessions"][number],
): string {
const label = row?.label?.trim() || "";
const displayName = row?.displayName?.trim() || "";
const rawLabel = row?.label?.trim() || "";
const rawDisplayName = row?.displayName?.trim() || "";
const originLabel = row?.origin?.label?.trim() || "";
const { prefix, fallbackName } = parseSessionKey(key);
const cleanValue = (value: string): string => {
if (!value) {
return value;
}
return value
.replace(/^[a-z0-9_-]+:/i, "")
.replace(/\sid:\d+$/i, "")
.trim();
};
const cleanPersonLabel = (...values: string[]): string => {
for (const value of values) {
const trimmed = value.trim();
if (!trimmed) {
continue;
}
const match = trimmed.match(/^(.+?)\s+id:\d+$/i);
if (match?.[1]?.trim()) {
return match[1].trim();
}
const cleaned = cleanValue(trimmed);
if (cleaned && !/^(main|dm|slash|direct:\d+|slash:\d+)$/i.test(cleaned)) {
return cleaned;
}
}
return "";
};
const applyTypedPrefix = (name: string): string => {
if (!prefix) {
return name;
}
const prefixPattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}\\s*`, "i");
const prefixPattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*`, "i");
return prefixPattern.test(name) ? name : `${prefix} ${name}`;
};
if (label && label !== key) {
return applyTypedPrefix(label);
if (key === "main" || key.endsWith(":main")) {
return "main";
}
if (displayName && displayName !== key) {
return applyTypedPrefix(displayName);
const loweredKey = key.toLowerCase();
if (loweredKey.includes(":telegram:direct:") || loweredKey.includes("telegram:direct:")) {
const person = cleanPersonLabel(rawLabel, originLabel, rawDisplayName, fallbackName);
return person ? `${person} DM` : "DM";
}
if (loweredKey.includes(":telegram:slash:") || loweredKey.includes("telegram:slash:")) {
const person = cleanPersonLabel(rawLabel, originLabel, rawDisplayName, fallbackName);
return person ? `${person} slash` : "slash";
}
if (rawLabel && rawLabel !== key) {
return applyTypedPrefix(cleanValue(rawLabel));
}
if (rawDisplayName && rawDisplayName !== key) {
return applyTypedPrefix(cleanValue(rawDisplayName));
}
return fallbackName;
}
@ -817,6 +861,9 @@ export function resolveSessionOptionGroups(
if (!key || seenKeys.has(key)) {
return;
}
if (key === "main" && byKey.has("agent:main:main")) {
return;
}
seenKeys.add(key);
const row = byKey.get(key);
const parsed = parseAgentSessionKey(key);
@ -848,6 +895,12 @@ export function resolveSessionOptionGroups(
addOption(sessionKey);
for (const group of groups.values()) {
group.options = group.options.filter((option, index, all) => {
if (option.label !== "main") {
return true;
}
return all.findIndex((entry) => entry.label === "main") === index;
});
const counts = new Map<string, number>();
for (const option of group.options) {
counts.set(option.label, (counts.get(option.label) ?? 0) + 1);
@ -892,7 +945,14 @@ function resolveSessionScopedOptionLabel(
const label = row.label?.trim() || "";
const displayName = row.displayName?.trim() || "";
if ((label && label !== key) || (displayName && displayName !== key)) {
// Also delegate for channel-keyed sessions (e.g. telegram:direct/slash/group)
// which may carry display info only in origin.label, not label/displayName.
const loweredKey = key.toLowerCase();
const isChannelSession =
loweredKey.includes(":telegram:direct:") ||
loweredKey.includes(":telegram:slash:") ||
loweredKey.includes(":telegram:group:");
if ((label && label !== key) || (displayName && displayName !== key) || isChannelSession) {
return resolveSessionDisplayName(key, row);
}

View File

@ -399,6 +399,7 @@ export type GatewaySessionRow = {
model?: string;
modelProvider?: string;
contextTokens?: number;
origin?: { label?: string };
};
export type SessionsListResult = SessionsListResultBase<GatewaySessionsDefaults, GatewaySessionRow>;

View File

@ -916,4 +916,89 @@ describe("chat view", () => {
);
expect(labels).not.toContain("Subagent: cron-config-check");
});
it("renders clean Telegram DM and topic labels in the grouped session selector", () => {
const { state } = createChatHeaderState({ omitSessionFromList: true });
state.sessionKey = "agent:main:telegram:group:-1009999999999:topic:3";
state.settings.sessionKey = state.sessionKey;
state.sessionsResult = {
ts: 0,
path: "",
count: 4,
defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null },
sessions: [
{
key: "agent:main:main",
kind: "direct",
updatedAt: null,
},
{
key: "agent:main:telegram:direct:12345678",
kind: "direct",
updatedAt: null,
origin: { label: "Alice id:12345678" },
},
{
key: "agent:main:telegram:group:-1009999999999:topic:3",
kind: "group",
updatedAt: null,
displayName: "telegram:Monk-tech",
origin: { label: "Test Group id:-1009999999999 topic:3" },
},
{
key: "agent:main:telegram:slash:12345678",
kind: "direct",
updatedAt: null,
origin: { label: "Alice" },
},
],
};
const container = document.createElement("div");
render(renderChatSessionSelect(state), container);
const [sessionSelect] = Array.from(container.querySelectorAll<HTMLSelectElement>("select"));
const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) =>
option.textContent?.trim(),
);
expect(labels).toContain("main");
expect(labels).toContain("Alice DM");
expect(labels).toContain("Monk-tech");
expect(labels).toContain("Alice slash");
expect(labels).not.toContain("telegram:Monk-tech");
expect(labels).not.toContain("Alice id:12345678");
});
it("dedupes visible main when both canonical and alias keys are present", () => {
const { state } = createChatHeaderState({ omitSessionFromList: true });
state.sessionKey = "main";
state.settings.sessionKey = state.sessionKey;
state.sessionsResult = {
ts: 0,
path: "",
count: 1,
defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null },
sessions: [
{
key: "agent:main:main",
kind: "direct",
updatedAt: null,
},
],
};
const container = document.createElement("div");
render(renderChatSessionSelect(state), container);
const [sessionSelect] = Array.from(container.querySelectorAll<HTMLSelectElement>("select"));
const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) =>
option.textContent?.trim(),
);
const values = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) =>
option.getAttribute("value"),
);
expect(labels.filter((label) => label === "main")).toHaveLength(1);
expect(values.filter((value) => value === "agent:main:main")).toHaveLength(1);
expect(sessionSelect?.value).toBe("agent:main:main");
});
});