Read cwd from session.header (not only meta) and title from sessionTitle service or session/title events; list details in the message body and richer inline buttons. Co-authored-by: Cursor <cursoragent@cursor.com>
108 lines
3.4 KiB
JavaScript
108 lines
3.4 KiB
JavaScript
const BUTTON_MAX = 64;
|
|
export function readSessionCwd(session) {
|
|
const s = session;
|
|
const cwd = s?.header?.cwd ?? s?.meta?.cwd;
|
|
if (typeof cwd !== 'string')
|
|
return undefined;
|
|
const trimmed = cwd.trim();
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
}
|
|
export function workspaceName(cwd) {
|
|
if (!cwd)
|
|
return undefined;
|
|
const parts = cwd.split(/[/\\]/).filter(Boolean);
|
|
return parts.length > 0 ? parts[parts.length - 1] : undefined;
|
|
}
|
|
export function readSessionTitle(ctx, session) {
|
|
if (!session)
|
|
return undefined;
|
|
try {
|
|
const svc = ctx
|
|
?.sessionTitle;
|
|
const snap = svc?.get?.(session);
|
|
if (snap?.title && String(snap.title).trim())
|
|
return String(snap.title).trim();
|
|
}
|
|
catch {
|
|
// optional service
|
|
}
|
|
try {
|
|
const projections = ctx?.sessionProjections;
|
|
const projected = projections?.get?.(session, 'title');
|
|
if (typeof projected === 'string' && projected.trim())
|
|
return projected.trim();
|
|
}
|
|
catch {
|
|
// optional service
|
|
}
|
|
const events = session.events;
|
|
if (Array.isArray(events)) {
|
|
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
const ev = events[i];
|
|
if (ev?.type === 'session/title' && typeof ev.data?.title === 'string') {
|
|
const t = ev.data.title.trim();
|
|
if (t)
|
|
return t;
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
export function describeAgent(agent, index, ctx) {
|
|
const sessionId = String(agent.id);
|
|
const idTail = sessionId.length > 12 ? sessionId.slice(-12) : sessionId;
|
|
const cwd = readSessionCwd(agent.session);
|
|
const title = readSessionTitle(ctx, agent.session);
|
|
return {
|
|
index,
|
|
title,
|
|
cwd,
|
|
workspace: workspaceName(cwd),
|
|
idTail,
|
|
sessionId,
|
|
};
|
|
}
|
|
/** Compact label for inline keyboard buttons (Telegram ≤64 chars). */
|
|
export function buttonLabel(parts) {
|
|
const n = parts.index + 1;
|
|
const primary = parts.title || parts.workspace || `session ${n}`;
|
|
let text = `${n}. ${primary}`;
|
|
if (parts.title && parts.workspace) {
|
|
const withWs = `${n}. ${parts.title} · ${parts.workspace}`;
|
|
if ([...withWs].length <= BUTTON_MAX)
|
|
text = withWs;
|
|
}
|
|
if ([...text].length <= BUTTON_MAX)
|
|
return text;
|
|
const chars = [...text];
|
|
return `${chars.slice(0, BUTTON_MAX - 1).join('')}…`;
|
|
}
|
|
/** Multi-line detail block for the picker message body. */
|
|
export function detailLines(parts) {
|
|
const lines = [`${parts.index + 1}. ${parts.title ?? '(无标题)'}`];
|
|
if (parts.cwd) {
|
|
lines.push(` 工作区:${parts.cwd}`);
|
|
}
|
|
else if (parts.workspace) {
|
|
lines.push(` 工作区:${parts.workspace}`);
|
|
}
|
|
else {
|
|
lines.push(' 工作区:(未知)');
|
|
}
|
|
lines.push(` ID:…${parts.idTail}`);
|
|
return lines.join('\n');
|
|
}
|
|
/** Human label stored on bind / status (not length-capped as hard as buttons). */
|
|
export function displayLabel(parts) {
|
|
const bits = [];
|
|
if (parts.title)
|
|
bits.push(parts.title);
|
|
if (parts.workspace)
|
|
bits.push(parts.workspace);
|
|
if (bits.length === 0)
|
|
bits.push(`session ${parts.index + 1}`);
|
|
bits.push(`…${parts.idTail}`);
|
|
const text = bits.join(' · ');
|
|
return text.length > 120 ? `${text.slice(0, 117)}...` : text;
|
|
}
|