diff --git a/src/bot/commands/definitions.ts b/src/bot/commands/definitions.ts index ac987fd0..c0d695e4 100644 --- a/src/bot/commands/definitions.ts +++ b/src/bot/commands/definitions.ts @@ -30,6 +30,7 @@ const COMMAND_DEFINITIONS: BotCommandI18nDefinition[] = [ { command: "worktree", descriptionKey: "cmd.description.worktree" }, { command: "task", descriptionKey: "cmd.description.task" }, { command: "tasklist", descriptionKey: "cmd.description.tasklist" }, + { command: "session_delete", descriptionKey: "cmd.description.session_delete" }, { command: "rename", descriptionKey: "cmd.description.rename" }, { command: "commands", descriptionKey: "cmd.description.commands" }, { command: "skills", descriptionKey: "cmd.description.skills" }, diff --git a/src/bot/commands/session-delete.ts b/src/bot/commands/session-delete.ts new file mode 100644 index 00000000..1038fb47 --- /dev/null +++ b/src/bot/commands/session-delete.ts @@ -0,0 +1,454 @@ +import { CommandContext, Context, InlineKeyboard } from "grammy"; +import { getDateLocale, t } from "../../i18n/index.js"; +import { opencodeClient } from "../../opencode/client.js"; +import { getCurrentProject } from "../../settings/manager.js"; +import { clearSession, getCurrentSession } from "../../session/manager.js"; +import { interactionManager } from "../../interaction/manager.js"; +import type { InteractionState } from "../../interaction/types.js"; +import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js"; +import { logger } from "../../utils/logger.js"; +import { config } from "../../config.js"; +import { detachAttachedSession } from "../../attach/service.js"; +import { pinnedMessageManager } from "../../pinned/manager.js"; +import { summaryAggregator } from "../../summary/aggregator.js"; + +const CALLBACK_PREFIX = "sesdel:"; +const OPEN_PREFIX = `${CALLBACK_PREFIX}open:`; +const CONFIRM_PREFIX = `${CALLBACK_PREFIX}confirm:`; +const CANCEL = `${CALLBACK_PREFIX}cancel`; +const PAGE_PREFIX = `${CALLBACK_PREFIX}page:`; +const SESSION_FETCH_EXTRA_COUNT = 1; + +type SessionListItem = { + id: string; + title: string; + directory: string; + time: { + created: number; + }; +}; + +type SessionPage = { + sessions: SessionListItem[]; + hasNext: boolean; + page: number; +}; + +interface SessionDeleteListMetadata { + flow: "session_delete"; + stage: "list"; + messageId: number; +} + +interface SessionDeleteDetailMetadata { + flow: "session_delete"; + stage: "detail"; + messageId: number; + sessionId: string; + title: string; +} + +type SessionDeleteMetadata = SessionDeleteListMetadata | SessionDeleteDetailMetadata; + +function getCallbackMessageId(ctx: Context): number | null { + const message = ctx.callbackQuery?.message; + if (!message || !("message_id" in message)) { + return null; + } + + const messageId = (message as { message_id?: number }).message_id; + return typeof messageId === "number" ? messageId : null; +} + +function parseSessionDeleteMetadata(state: InteractionState | null): SessionDeleteMetadata | null { + if (!state || state.kind !== "custom") { + return null; + } + + const flow = state.metadata.flow; + const stage = state.metadata.stage; + const messageId = state.metadata.messageId; + + if (flow !== "session_delete" || typeof messageId !== "number") { + return null; + } + + if (stage === "list") { + return { flow, stage, messageId }; + } + + if (stage === "detail") { + const sessionId = state.metadata.sessionId; + const title = state.metadata.title; + if (typeof sessionId !== "string" || !sessionId || typeof title !== "string" || !title) { + return null; + } + + return { flow, stage, messageId, sessionId, title }; + } + + return null; +} + +function clearSessionDeleteInteraction(reason: string): void { + const metadata = parseSessionDeleteMetadata(interactionManager.getSnapshot()); + if (metadata) { + interactionManager.clear(reason); + } +} + +function buildSessionPageCallback(page: number): string { + return `${PAGE_PREFIX}${page}`; +} + +function parseSessionPageCallback(data: string): number | null { + if (!data.startsWith(PAGE_PREFIX)) { + return null; + } + + const rawPage = data.slice(PAGE_PREFIX.length); + const page = Number(rawPage); + if (!Number.isInteger(page) || page < 0) { + return null; + } + + return page; +} + +function truncateTitle(title: string, maxLength: number = 120): string { + return title.length <= maxLength ? title : `${title.slice(0, maxLength - 3)}...`; +} + +async function loadSessionPage( + directory: string, + page: number, + pageSize: number, +): Promise { + const startIndex = page * pageSize; + const endExclusive = startIndex + pageSize; + + const { data: sessions, error } = await opencodeClient.session.list({ + directory, + limit: endExclusive + SESSION_FETCH_EXTRA_COUNT, + roots: true, + }); + + if (error || !sessions) { + throw error || new Error("No data received from server"); + } + + const hasNext = sessions.length > endExclusive; + const pagedSessions = sessions.slice(startIndex, endExclusive); + + return { + sessions: pagedSessions as SessionListItem[], + hasNext, + page, + }; +} + +function buildSessionDeleteKeyboard(pageData: SessionPage, pageSize: number): InlineKeyboard { + const keyboard = new InlineKeyboard(); + const localeForDate = getDateLocale(); + const pageStartIndex = pageData.page * pageSize; + + pageData.sessions.forEach((session, index) => { + const date = new Date(session.time.created).toLocaleDateString(localeForDate); + const label = `${pageStartIndex + index + 1}. ${session.title} (${date})`; + keyboard.text(label, `${OPEN_PREFIX}${session.id}`).row(); + }); + + if (pageData.page > 0) { + keyboard.text( + t("session_delete.button.prev_page"), + buildSessionPageCallback(pageData.page - 1), + ); + } + + if (pageData.hasNext) { + keyboard.text( + t("session_delete.button.next_page"), + buildSessionPageCallback(pageData.page + 1), + ); + } + + if (pageData.page > 0 || pageData.hasNext) { + keyboard.row(); + } + + keyboard.text(t("session_delete.button.cancel"), CANCEL); + return keyboard; +} + +function buildSessionDeleteConfirmKeyboard(sessionId: string): InlineKeyboard { + return new InlineKeyboard() + .text(t("session_delete.button.delete"), `${CONFIRM_PREFIX}${sessionId}`) + .text(t("session_delete.button.cancel"), CANCEL); +} + +function formatSessionSelectText(page: number): string { + if (page === 0) { + return t("session_delete.select"); + } + + return t("session_delete.select_page", { page: page + 1 }); +} + +export async function sessionDeleteCommand(ctx: CommandContext): Promise { + try { + if (isForegroundBusy()) { + await replyBusyBlocked(ctx); + return; + } + + const pageSize = config.bot.sessionsListLimit; + const currentProject = getCurrentProject(); + + if (!currentProject) { + await ctx.reply(t("session_delete.project_not_selected")); + return; + } + + const firstPage = await loadSessionPage(currentProject.worktree, 0, pageSize); + + if (firstPage.sessions.length === 0) { + await ctx.reply(t("session_delete.empty")); + return; + } + + const keyboard = buildSessionDeleteKeyboard(firstPage, pageSize); + const message = await ctx.reply(formatSessionSelectText(firstPage.page), { + reply_markup: keyboard, + }); + + interactionManager.start({ + kind: "custom", + expectedInput: "callback", + metadata: { + flow: "session_delete", + stage: "list", + messageId: message.message_id, + }, + }); + } catch (error) { + logger.error("[SessionDelete] Failed to open session delete list", error); + await ctx.reply(t("session_delete.load_error")); + } +} + +export async function handleSessionDeleteCallback(ctx: Context): Promise { + if (isForegroundBusy()) { + await replyBusyBlocked(ctx); + return true; + } + + const data = ctx.callbackQuery?.data; + if (!data || !data.startsWith(CALLBACK_PREFIX)) { + return false; + } + + const metadata = parseSessionDeleteMetadata(interactionManager.getSnapshot()); + const callbackMessageId = getCallbackMessageId(ctx); + + if (!metadata || callbackMessageId === null || metadata.messageId !== callbackMessageId) { + await ctx + .answerCallbackQuery({ text: t("session_delete.inactive_callback"), show_alert: true }) + .catch(() => {}); + return true; + } + + try { + if (data === CANCEL) { + clearSessionDeleteInteraction("session_delete_cancelled"); + await ctx.answerCallbackQuery({ text: t("session_delete.cancelled_callback") }); + await ctx.deleteMessage().catch(() => {}); + return true; + } + + if (data.startsWith(OPEN_PREFIX)) { + if (metadata.stage !== "list") { + await ctx + .answerCallbackQuery({ text: t("session_delete.inactive_callback"), show_alert: true }) + .catch(() => {}); + return true; + } + + const sessionId = data.slice(OPEN_PREFIX.length); + if (!sessionId) { + await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => {}); + return true; + } + + const currentProject = getCurrentProject(); + if (!currentProject) { + clearSessionDeleteInteraction("session_delete_no_project"); + await ctx.answerCallbackQuery({ text: t("session_delete.inactive_callback"), show_alert: true }).catch(() => {}); + await ctx.deleteMessage().catch(() => {}); + return true; + } + + const { data: session, error } = await opencodeClient.session.get({ + sessionID: sessionId, + directory: currentProject.worktree, + }); + + if (error) { + logger.error("[SessionDelete] Failed to fetch session details:", error); + await ctx.answerCallbackQuery({ text: t("session_delete.load_error") }); + return true; + } + + if (!session) { + clearSessionDeleteInteraction("session_delete_not_found"); + await ctx.answerCallbackQuery({ text: t("session_delete.inactive_callback"), show_alert: true }).catch(() => {}); + await ctx.deleteMessage().catch(() => {}); + return true; + } + + const localeForDate = getDateLocale(); + const date = new Date(session.time.created).toLocaleDateString(localeForDate); + + await ctx.answerCallbackQuery(); + await ctx.editMessageText( + t("session_delete.details", { + title: session.title, + directory: session.directory, + date, + }), + { + reply_markup: buildSessionDeleteConfirmKeyboard(session.id), + }, + ); + + interactionManager.transition({ + expectedInput: "callback", + metadata: { + flow: "session_delete", + stage: "detail", + messageId: metadata.messageId, + sessionId: session.id, + title: session.title, + }, + }); + + return true; + } + + // Pagination (list stage only) + const page = parseSessionPageCallback(data); + if (page !== null) { + if (metadata.stage !== "list") { + await ctx.answerCallbackQuery({ text: t("session_delete.inactive_callback"), show_alert: true }).catch(() => {}); + return true; + } + + const currentProject = getCurrentProject(); + if (!currentProject) { + clearSessionDeleteInteraction("session_delete_page_no_project"); + await ctx.answerCallbackQuery({ text: t("session_delete.cancelled_callback") }).catch(() => {}); + await ctx.deleteMessage().catch(() => {}); + return true; + } + + try { + const pageSize = config.bot.sessionsListLimit; + const pageData = await loadSessionPage(currentProject.worktree, page, pageSize); + + if (pageData.sessions.length === 0) { + await ctx.answerCallbackQuery({ text: t("session_delete.page_empty_callback") }); + return true; + } + + const keyboard = buildSessionDeleteKeyboard(pageData, pageSize); + await ctx.editMessageText(formatSessionSelectText(pageData.page), { + reply_markup: keyboard, + }); + + interactionManager.transition({ + expectedInput: "callback", + metadata: { + flow: "session_delete", + stage: "list", + messageId: metadata.messageId, + }, + }); + await ctx.answerCallbackQuery(); + } catch (error) { + logger.error("[SessionDelete] Error loading sessions page:", error); + await ctx.answerCallbackQuery({ text: t("session_delete.page_load_error_callback") }); + } + + return true; + } + + if (data.startsWith(CONFIRM_PREFIX)) { + if (metadata.stage !== "detail") { + await ctx.answerCallbackQuery({ text: t("session_delete.inactive_callback"), show_alert: true }).catch(() => {}); + return true; + } + + const sessionId = data.slice(CONFIRM_PREFIX.length); + if (sessionId !== metadata.sessionId) { + await ctx.answerCallbackQuery({ text: t("session_delete.inactive_callback"), show_alert: true }).catch(() => {}); + return true; + } + + const currentProject = getCurrentProject(); + if (!currentProject) { + clearSessionDeleteInteraction("session_delete_confirm_no_project"); + await ctx.answerCallbackQuery({ text: t("session_delete.cancelled_callback") }).catch(() => {}); + await ctx.deleteMessage().catch(() => {}); + return true; + } + + try { + const deleteResult = await opencodeClient.session.delete({ + sessionID: sessionId, + }); + + if (deleteResult.error) { + logger.error("[SessionDelete] Delete failed:", deleteResult.error); + await ctx.answerCallbackQuery({ text: t("session_delete.delete_error") }); + return true; + } + + const currentSession = getCurrentSession(); + const isCurrent = currentSession?.id === sessionId; + const shortTitle = truncateTitle(metadata.title); + + logger.info(`[SessionDelete] Session deleted: id=${sessionId}, title="${metadata.title}", wasCurrent=${isCurrent}`); + + if (isCurrent) { + detachAttachedSession("session_delete_deleted_current"); + clearSession(); + summaryAggregator.clear(); + clearSessionDeleteInteraction("session_delete_deleted_current"); + try { + await pinnedMessageManager.clear(); + } catch (err) { + logger.error("[SessionDelete] Failed to clear pinned message:", err); + } + await ctx.answerCallbackQuery({ text: t("session_delete.deleted_current", { title: shortTitle }) }); + } else { + clearSessionDeleteInteraction("session_delete_deleted"); + await ctx.answerCallbackQuery({ text: t("session_delete.deleted", { title: shortTitle }) }); + } + + await ctx.deleteMessage().catch(() => {}); + } catch (error) { + logger.error("[SessionDelete] Delete failed:", error); + await ctx.answerCallbackQuery({ text: t("session_delete.delete_error") }); + } + + return true; + } + + await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => {}); + return true; + } catch (error) { + logger.error("[SessionDelete] Failed to handle callback", error); + clearSessionDeleteInteraction("session_delete_callback_error"); + await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => {}); + return true; + } +} diff --git a/src/bot/index.ts b/src/bot/index.ts index b631b015..05b30516 100644 --- a/src/bot/index.ts +++ b/src/bot/index.ts @@ -28,6 +28,7 @@ import { opencodeStopCommand } from "./commands/opencode-stop.js"; import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./commands/rename.js"; import { handleTaskCallback, handleTaskTextInput, taskCommand } from "./commands/task.js"; import { handleTaskListCallback, taskListCommand } from "./commands/tasklist.js"; +import { handleSessionDeleteCallback, sessionDeleteCommand } from "./commands/session-delete.js"; import { commandsCommand, handleCommandsCallback, @@ -1081,6 +1082,7 @@ export function createBot(): Bot { bot.command("abort", abortCommand); bot.command("task", taskCommand); bot.command("tasklist", taskListCommand); + bot.command("session_delete", sessionDeleteCommand); bot.command("rename", renameCommand); bot.command("commands", commandsCommand); bot.command("skills", skillsCommand); @@ -1115,13 +1117,14 @@ export function createBot(): Bot { const handledCompactConfirm = await handleCompactConfirm(ctx); const handledTask = await handleTaskCallback(ctx); const handledTaskList = await handleTaskListCallback(ctx); + const handledSessionDelete = await handleSessionDeleteCallback(ctx); const handledRenameCancel = await handleRenameCancel(ctx); const handledCommands = await handleCommandsCallback(ctx, { bot, ensureEventSubscription }); const handledSkills = await handleSkillsCallback(ctx, { bot, ensureEventSubscription }); const handledMcps = await handleMcpsCallback(ctx); logger.debug( - `[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, worktree=${handledWorktree}, open=${handledOpen}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, rename=${handledRenameCancel}, commands=${handledCommands}, skills=${handledSkills}, mcps=${handledMcps}`, + `[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, worktree=${handledWorktree}, open=${handledOpen}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, sessionDelete=${handledSessionDelete}, rename=${handledRenameCancel}, commands=${handledCommands}, skills=${handledSkills}`, ); if ( @@ -1138,6 +1141,7 @@ export function createBot(): Bot { !handledCompactConfirm && !handledTask && !handledTaskList && + !handledSessionDelete && !handledRenameCancel && !handledCommands && !handledSkills && diff --git a/src/i18n/de.ts b/src/i18n/de.ts index 4d75e341..ec464fcb 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -554,4 +554,27 @@ export const de: I18nDictionary = { "open.no_subfolders": "📭 Keine Unterordner", "open.subfolder_count": "{count} Unterordner", "open.subfolders_count": "{count} Unterordner", + + "cmd.description.session_delete": "Sitzungen löschen", + + "session_delete.project_not_selected": + "🏗 Es ist kein Projekt ausgewählt.\n\nWähle zuerst ein Projekt mit /projects.", + "session_delete.empty": "📭 Keine Sitzungen gefunden.\n\nErstelle eine neue Sitzung mit /new.", + "session_delete.select": "Wähle eine Sitzung zum Löschen:", + "session_delete.select_page": "Wähle eine Sitzung zum Löschen (Seite {page}):", + "session_delete.button.delete": "🗑 Löschen", + "session_delete.button.cancel": "❌ Abbrechen", + "session_delete.button.prev_page": "⬅️ Zurück", + "session_delete.button.next_page": "Weiter ➡️", + "session_delete.details": + "Sitzung: {title}\nVerzeichnis: {directory}\nErstellt: {date}\n\nDiese Sitzung löschen?", + "session_delete.deleted": "🗑 Sitzung gelöscht: {title}", + "session_delete.deleted_current": + "🗑 Aktuelle Sitzung gelöscht: {title}\n\nVerwende /sessions oder /new zum Fortfahren.", + "session_delete.cancelled_callback": "Abgebrochen", + "session_delete.inactive_callback": "Dieses Menü ist inaktiv", + "session_delete.load_error": "🔴 Sitzungen konnten nicht geladen werden.", + "session_delete.delete_error": "🔴 Sitzung konnte nicht gelöscht werden.", + "session_delete.page_empty_callback": "Keine Sitzungen auf dieser Seite", + "session_delete.page_load_error_callback": "Diese Seite kann nicht geladen werden. Bitte versuche es erneut.", }; diff --git a/src/i18n/en.ts b/src/i18n/en.ts index ff525677..c143d7d1 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -532,6 +532,29 @@ export const en = { "open.no_subfolders": "📭 No subfolders", "open.subfolder_count": "{count} subfolder", "open.subfolders_count": "{count} subfolders", + + "cmd.description.session_delete": "Delete sessions", + + "session_delete.project_not_selected": + "🏗 Project is not selected.\n\nFirst select a project with /projects.", + "session_delete.empty": "📭 No sessions found.\n\nCreate a new session with /new.", + "session_delete.select": "Select a session to delete:", + "session_delete.select_page": "Select a session to delete (page {page}):", + "session_delete.button.delete": "🗑 Delete", + "session_delete.button.cancel": "❌ Cancel", + "session_delete.button.prev_page": "⬅️ Prev", + "session_delete.button.next_page": "Next ➡️", + "session_delete.details": + "Session: {title}\nDirectory: {directory}\nCreated: {date}\n\nDelete this session?", + "session_delete.deleted": "🗑 Session deleted: {title}", + "session_delete.deleted_current": + "🗑 Deleted current session: {title}\n\nUse /sessions or /new to continue.", + "session_delete.cancelled_callback": "Cancelled", + "session_delete.inactive_callback": "This menu is inactive", + "session_delete.load_error": "🔴 Failed to load sessions.", + "session_delete.delete_error": "🔴 Failed to delete session.", + "session_delete.page_empty_callback": "No sessions on this page", + "session_delete.page_load_error_callback": "Cannot load this page. Please try again.", } as const; export type I18nKey = keyof typeof en; diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 6e2ac312..6369b26a 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -554,4 +554,27 @@ export const es: I18nDictionary = { "open.no_subfolders": "📭 Sin subcarpetas", "open.subfolder_count": "{count} subcarpeta", "open.subfolders_count": "{count} subcarpetas", + + "cmd.description.session_delete": "Eliminar sesiones", + + "session_delete.project_not_selected": + "🏗 No hay un proyecto seleccionado.\n\nPrimero selecciona un proyecto con /projects.", + "session_delete.empty": "📭 No se encontraron sesiones.\n\nCrea una nueva sesión con /new.", + "session_delete.select": "Selecciona una sesión para eliminar:", + "session_delete.select_page": "Selecciona una sesión para eliminar (página {page}):", + "session_delete.button.delete": "🗑 Eliminar", + "session_delete.button.cancel": "❌ Cancelar", + "session_delete.button.prev_page": "⬅️ Anterior", + "session_delete.button.next_page": "Siguiente ➡️", + "session_delete.details": + "Sesión: {title}\nDirectorio: {directory}\nCreada: {date}\n\n¿Eliminar esta sesión?", + "session_delete.deleted": "🗑 Sesión eliminada: {title}", + "session_delete.deleted_current": + "🗑 Sesión actual eliminada: {title}\n\nUsa /sessions o /new para continuar.", + "session_delete.cancelled_callback": "Cancelado", + "session_delete.inactive_callback": "Este menú está inactivo", + "session_delete.load_error": "🔴 No se pudieron cargar las sesiones.", + "session_delete.delete_error": "🔴 No se pudo eliminar la sesión.", + "session_delete.page_empty_callback": "No hay sesiones en esta página", + "session_delete.page_load_error_callback": "No se puede cargar esta página. Inténtalo de nuevo.", }; diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts index bda5b5f5..38c2dddb 100644 --- a/src/i18n/fr.ts +++ b/src/i18n/fr.ts @@ -553,4 +553,27 @@ export const fr: I18nDictionary = { "open.no_subfolders": "📭 Aucun sous-dossier", "open.subfolder_count": "{count} sous-dossier", "open.subfolders_count": "{count} sous-dossiers", + + "cmd.description.session_delete": "Supprimer des sessions", + + "session_delete.project_not_selected": + "🏗 Aucun projet sélectionné.\n\nSélectionnez d'abord un projet avec /projects.", + "session_delete.empty": "📭 Aucune session trouvée.\n\nCréez une nouvelle session avec /new.", + "session_delete.select": "Sélectionnez une session à supprimer :", + "session_delete.select_page": "Sélectionnez une session à supprimer (page {page}) :", + "session_delete.button.delete": "🗑 Supprimer", + "session_delete.button.cancel": "❌ Annuler", + "session_delete.button.prev_page": "⬅️ Précédent", + "session_delete.button.next_page": "Suivant ➡️", + "session_delete.details": + "Session : {title}\nRépertoire : {directory}\nCréée : {date}\n\nSupprimer cette session ?", + "session_delete.deleted": "🗑 Session supprimée : {title}", + "session_delete.deleted_current": + "🗑 Session actuelle supprimée : {title}\n\nUtilisez /sessions ou /new pour continuer.", + "session_delete.cancelled_callback": "Annulé", + "session_delete.inactive_callback": "Ce menu est inactif", + "session_delete.load_error": "🔴 Impossible de charger les sessions.", + "session_delete.delete_error": "🔴 Impossible de supprimer la session.", + "session_delete.page_empty_callback": "Aucune session sur cette page", + "session_delete.page_load_error_callback": "Impossible de charger cette page. Veuillez réessayer.", }; diff --git a/src/i18n/ru.ts b/src/i18n/ru.ts index 66c732fd..afc248b8 100644 --- a/src/i18n/ru.ts +++ b/src/i18n/ru.ts @@ -540,4 +540,27 @@ export const ru: I18nDictionary = { "open.no_subfolders": "📭 Нет подпапок", "open.subfolder_count": "{count} подпапка", "open.subfolders_count": "{count} подпапок", + + "cmd.description.session_delete": "Удалить сессии", + + "session_delete.project_not_selected": + "🏗 Проект не выбран.\n\nСначала выберите проект командой /projects.", + "session_delete.empty": "📭 Сессии не найдены.\n\nСоздайте новую сессию командой /new.", + "session_delete.select": "Выберите сессию для удаления:", + "session_delete.select_page": "Выберите сессию для удаления (страница {page}):", + "session_delete.button.delete": "🗑 Удалить", + "session_delete.button.cancel": "❌ Отмена", + "session_delete.button.prev_page": "⬅️ Назад", + "session_delete.button.next_page": "Далее ➡️", + "session_delete.details": + "Сессия: {title}\nКаталог: {directory}\nСоздана: {date}\n\nУдалить эту сессию?", + "session_delete.deleted": "🗑 Сессия удалена: {title}", + "session_delete.deleted_current": + "🗑 Текущая сессия удалена: {title}\n\nИспользуйте /sessions или /new для продолжения.", + "session_delete.cancelled_callback": "Отменено", + "session_delete.inactive_callback": "Это меню неактивно", + "session_delete.load_error": "🔴 Не удалось загрузить сессии.", + "session_delete.delete_error": "🔴 Не удалось удалить сессию.", + "session_delete.page_empty_callback": "На этой странице нет сессий", + "session_delete.page_load_error_callback": "Не удалось загрузить эту страницу. Пожалуйста, попробуйте еще раз.", }; diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 3d3c966d..69049dbb 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -489,4 +489,27 @@ export const zh: I18nDictionary = { "open.no_subfolders": "📭 无子文件夹", "open.subfolder_count": "{count} 个子文件夹", "open.subfolders_count": "{count} 个子文件夹", + + "cmd.description.session_delete": "删除会话", + + "session_delete.project_not_selected": + "🏗 未选择项目。\n\n请先使用 /projects 选择一个项目。", + "session_delete.empty": "📭 未找到会话。\n\n使用 /new 创建新会话。", + "session_delete.select": "选择要删除的会话:", + "session_delete.select_page": "选择要删除的会话(第 {page} 页):", + "session_delete.button.delete": "🗑 删除", + "session_delete.button.cancel": "❌ 取消", + "session_delete.button.prev_page": "⬅️ 上一页", + "session_delete.button.next_page": "下一页 ➡️", + "session_delete.details": + "会话:{title}\n目录:{directory}\n创建时间:{date}\n\n删除此会话?", + "session_delete.deleted": "🗑 会话已删除:{title}", + "session_delete.deleted_current": + "🗑 当前会话已删除:{title}\n\n使用 /sessions 或 /new 继续。", + "session_delete.cancelled_callback": "已取消", + "session_delete.inactive_callback": "此菜单已失效", + "session_delete.load_error": "🔴 加载会话失败。", + "session_delete.delete_error": "🔴 删除会话失败。", + "session_delete.page_empty_callback": "此页没有会话", + "session_delete.page_load_error_callback": "无法加载此页,请重试。", };