feat(ui): add session sidebar search and bulk selection

Adds an optional session filter bar to the left sidebar with title search across parent/child sessions and a scoped Select All. Introduces multi-select checkboxes, bulk delete with clear selection controls, and confirmation dialogs for both single and bulk deletions using the existing alert dialog flow. Updates session i18n strings across supported locales.
This commit is contained in:
Shantur Rathore
2026-01-30 17:34:25 +00:00
parent 67f5f830a3
commit 1af01680ee
8 changed files with 429 additions and 41 deletions

View File

@@ -911,6 +911,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
void result.catch((error) => log.error("Failed to create session:", error)) void result.catch((error) => log.error("Failed to create session:", error))
} }
}} }}
enableFilterBar
showHeader={false} showHeader={false}
showFooter={false} showFooter={false}
/> />

View File

@@ -2,12 +2,13 @@ import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCl
import type { SessionStatus } from "../types/session" import type { SessionStatus } from "../types/session"
import type { SessionThread } from "../stores/session-state" import type { SessionThread } from "../stores/session-state"
import { getSessionStatus } from "../stores/session-status" import { getSessionStatus } from "../stores/session-status"
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown } from "lucide-solid" import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown, Search, Square, CheckSquare, MinusSquare } from "lucide-solid"
import KeyboardHint from "./keyboard-hint" import KeyboardHint from "./keyboard-hint"
import SessionRenameDialog from "./session-rename-dialog" import SessionRenameDialog from "./session-rename-dialog"
import { keyboardRegistry } from "../lib/keyboard-registry" import { keyboardRegistry } from "../lib/keyboard-registry"
import { showToastNotification } from "../lib/notifications" import { showToastNotification } from "../lib/notifications"
import { useI18n } from "../lib/i18n" import { useI18n } from "../lib/i18n"
import { showConfirmDialog } from "../stores/alerts"
import { import {
deleteSession, deleteSession,
ensureSessionParentExpanded, ensureSessionParentExpanded,
@@ -35,6 +36,7 @@ interface SessionListProps {
showFooter?: boolean showFooter?: boolean
headerContent?: JSX.Element headerContent?: JSX.Element
footerContent?: JSX.Element footerContent?: JSX.Element
enableFilterBar?: boolean
} }
function formatSessionStatus(status: SessionStatus): string { function formatSessionStatus(status: SessionStatus): string {
@@ -46,6 +48,70 @@ const SessionList: Component<SessionListProps> = (props) => {
const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null) const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null)
const [isRenaming, setIsRenaming] = createSignal(false) const [isRenaming, setIsRenaming] = createSignal(false)
const [filterQuery, setFilterQuery] = createSignal("")
const normalizedQuery = createMemo(() => filterQuery().trim().toLowerCase())
const [selectedSessionIds, setSelectedSessionIds] = createSignal<Set<string>>(new Set())
const normalizeSessionLabel = (sessionId: string) => {
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
const title = (session?.title ?? "").trim()
return title || t("sessionList.session.untitled")
}
const sessionMatchesQuery = (sessionId: string, query: string) => {
if (!query) return true
const label = normalizeSessionLabel(sessionId).toLowerCase()
if (label.includes(query)) return true
return sessionId.toLowerCase().includes(query)
}
const filteredThreads = createMemo<SessionThread[]>(() => {
const query = normalizedQuery()
if (!query) return props.threads
const next: SessionThread[] = []
for (const thread of props.threads) {
const parentMatches = sessionMatchesQuery(thread.parent.id, query)
const matchingChildren = thread.children.filter((child) => sessionMatchesQuery(child.id, query))
if (!parentMatches && matchingChildren.length === 0) continue
next.push({
parent: thread.parent,
children: matchingChildren,
latestUpdated: thread.latestUpdated,
})
}
return next
})
const allMatchingSessionIds = createMemo<string[]>(() => {
const ids: string[] = []
for (const thread of filteredThreads()) {
ids.push(thread.parent.id)
for (const child of thread.children) ids.push(child.id)
}
return ids
})
const selectedCount = createMemo(() => selectedSessionIds().size)
const isAllSelected = createMemo(() => {
const ids = allMatchingSessionIds()
if (ids.length === 0) return false
const selected = selectedSessionIds()
return ids.every((id) => selected.has(id))
})
const isSelectAllIndeterminate = createMemo(() => {
const ids = allMatchingSessionIds()
const total = ids.length
if (total === 0) return false
const count = selectedCount()
return count > 0 && count < total
})
const isSessionDeleting = (sessionId: string) => { const isSessionDeleting = (sessionId: string) => {
const deleting = loading().deletingSession.get(props.instanceId) const deleting = loading().deletingSession.get(props.instanceId)
return deleting ? deleting.has(sessionId) : false return deleting ? deleting.has(sessionId) : false
@@ -82,6 +148,17 @@ const SessionList: Component<SessionListProps> = (props) => {
event.stopPropagation() event.stopPropagation()
if (isSessionDeleting(sessionId)) return if (isSessionDeleting(sessionId)) return
const confirmed = await showConfirmDialog(
t("sessionList.delete.confirmMessage", { label: normalizeSessionLabel(sessionId) }),
{
title: t("sessionList.delete.title"),
variant: "warning",
confirmLabel: t("sessionList.delete.confirmLabel"),
cancelLabel: t("sessionList.delete.cancelLabel"),
},
)
if (!confirmed) return
const shouldSelectFallback = props.activeSessionId === sessionId const shouldSelectFallback = props.activeSessionId === sessionId
let fallbackSessionId: string | undefined let fallbackSessionId: string | undefined
@@ -153,6 +230,115 @@ const SessionList: Component<SessionListProps> = (props) => {
} }
} }
const setSelectedMany = (sessionIds: string[], checked: boolean) => {
if (sessionIds.length === 0) return
setSelectedSessionIds((prev) => {
const next = new Set(prev)
sessionIds.forEach((id) => {
if (checked) next.add(id)
else next.delete(id)
})
return next
})
}
const getSelectableThreadIds = (parentId: string): string[] => {
const query = normalizedQuery()
const source = query ? filteredThreads() : props.threads
const thread = source.find((t) => t.parent.id === parentId)
if (!thread) return [parentId]
return [thread.parent.id, ...thread.children.map((c) => c.id)]
}
const getAllSessionIdsInOrder = (threads: SessionThread[]): string[] => {
const ids: string[] = []
threads.forEach((thread) => {
ids.push(thread.parent.id)
thread.children.forEach((child) => ids.push(child.id))
})
return ids
}
const handleToggleSelectAll = (checked: boolean) => {
const ids = allMatchingSessionIds()
setSelectedMany(ids, checked)
}
const toggleSelectAll = () => {
if (isAllSelected()) {
handleToggleSelectAll(false)
return
}
handleToggleSelectAll(true)
}
const handleBulkDelete = async () => {
const selected = Array.from(selectedSessionIds())
if (selected.length === 0) return
const confirmed = await showConfirmDialog(
t("sessionList.bulkDelete.confirmMessage", { count: selected.length }),
{
title: t("sessionList.bulkDelete.title"),
variant: "warning",
confirmLabel: t("sessionList.bulkDelete.confirmLabel"),
cancelLabel: t("sessionList.bulkDelete.cancelLabel"),
},
)
if (!confirmed) return
const deletedSet = new Set(selected)
const currentActiveId = props.activeSessionId
let fallbackSessionId: string | undefined
if (currentActiveId && deletedSet.has(currentActiveId)) {
const ordered = getAllSessionIdsInOrder(props.threads)
const currentIndex = ordered.indexOf(currentActiveId)
for (let i = Math.max(0, currentIndex); i < ordered.length; i++) {
const candidate = ordered[i]
if (candidate && !deletedSet.has(candidate)) {
fallbackSessionId = candidate
break
}
}
if (!fallbackSessionId) {
for (let i = currentIndex - 1; i >= 0; i--) {
const candidate = ordered[i]
if (candidate && !deletedSet.has(candidate)) {
fallbackSessionId = candidate
break
}
}
}
}
let failed = 0
for (const sessionId of selected) {
try {
// eslint-disable-next-line no-await-in-loop
await deleteSession(props.instanceId, sessionId)
} catch (error) {
failed += 1
log.error(`Failed to delete session ${sessionId}:`, error)
}
}
setSelectedSessionIds(new Set<string>())
if (fallbackSessionId) {
setActiveSessionFromList(props.instanceId, fallbackSessionId)
}
if (failed > 0) {
showToastNotification({
message: t("sessionList.bulkDelete.error", { count: failed }),
variant: "error",
})
}
}
const SessionRow: Component<{ const SessionRow: Component<{
sessionId: string sessionId: string
@@ -190,9 +376,31 @@ const SessionList: Component<SessionListProps> = (props) => {
? t("sessionList.status.needsInput") ? t("sessionList.status.needsInput")
: statusLabel() : statusLabel()
return ( const isSelected = () => selectedSessionIds().has(rowProps.sessionId)
<div class="session-list-item group">
const parentGroupState = createMemo(() => {
if (rowProps.isChild) {
return { checked: isSelected(), indeterminate: false, ids: [rowProps.sessionId] }
}
const ids = getSelectableThreadIds(rowProps.sessionId)
const selected = selectedSessionIds()
const selectedInGroup = ids.reduce((count, id) => (selected.has(id) ? count + 1 : count), 0)
return {
checked: selectedInGroup > 0 && selectedInGroup === ids.length,
indeterminate: selectedInGroup > 0 && selectedInGroup < ids.length,
ids,
}
})
let rowCheckboxEl: HTMLInputElement | null = null
createEffect(() => {
if (!rowCheckboxEl) return
rowCheckboxEl.indeterminate = parentGroupState().indeterminate
})
return (
<div class="session-list-item group">
<button <button
class={`session-item-base ${rowProps.isChild ? `session-item-child${rowProps.isLastChild ? " session-item-child-last" : ""} session-item-border-assistant session-item-kind-assistant` : "session-item-border-user session-item-kind-user"} ${isActive() ? "session-item-active" : "session-item-inactive"}`} class={`session-item-base ${rowProps.isChild ? `session-item-child${rowProps.isLastChild ? " session-item-child-last" : ""} session-item-border-assistant session-item-kind-assistant` : "session-item-border-user session-item-kind-user"} ${isActive() ? "session-item-active" : "session-item-inactive"}`}
data-session-id={rowProps.sessionId} data-session-id={rowProps.sessionId}
@@ -204,11 +412,23 @@ const SessionList: Component<SessionListProps> = (props) => {
> >
<div class="session-item-row session-item-header"> <div class="session-item-row session-item-header">
<div class="session-item-title-row"> <div class="session-item-title-row">
{rowProps.isChild ? ( <Show when={props.enableFilterBar}>
<Bot class="w-4 h-4 flex-shrink-0" /> <input
) : ( ref={(el) => {
<User class="w-4 h-4 flex-shrink-0" /> rowCheckboxEl = el
)} }}
type="checkbox"
checked={parentGroupState().checked}
onClick={(event) => event.stopPropagation()}
onChange={(event) => {
event.stopPropagation()
setSelectedMany(parentGroupState().ids, event.currentTarget.checked)
}}
aria-label={t("sessionList.selection.checkboxAriaLabel")}
/>
</Show>
{rowProps.isChild ? <Bot class="w-4 h-4 flex-shrink-0" /> : <User class="w-4 h-4 flex-shrink-0" />}
<span class="session-item-title session-item-title--clamp">{title()}</span> <span class="session-item-title session-item-title--clamp">{title()}</span>
</div> </div>
</div> </div>
@@ -216,9 +436,7 @@ const SessionList: Component<SessionListProps> = (props) => {
<div class="flex items-center gap-2 min-w-0"> <div class="flex items-center gap-2 min-w-0">
<Show <Show
when={rowProps.hasChildren && !rowProps.isChild} when={rowProps.hasChildren && !rowProps.isChild}
fallback={ fallback={rowProps.isChild ? null : <span class="session-item-expander session-item-expander--spacer" aria-hidden="true" />}
rowProps.isChild ? null : <span class="session-item-expander session-item-expander--spacer" aria-hidden="true" />
}
> >
<span <span
class={`session-item-expander opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`} class={`session-item-expander opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
@@ -228,18 +446,16 @@ const SessionList: Component<SessionListProps> = (props) => {
}} }}
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label={rowProps.expanded ? t("sessionList.expand.collapseAriaLabel") : t("sessionList.expand.expandAriaLabel")} aria-label={
rowProps.expanded ? t("sessionList.expand.collapseAriaLabel") : t("sessionList.expand.expandAriaLabel")
}
title={rowProps.expanded ? t("sessionList.expand.collapseTitle") : t("sessionList.expand.expandTitle")} title={rowProps.expanded ? t("sessionList.expand.collapseTitle") : t("sessionList.expand.expandTitle")}
> >
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} /> <ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
</span> </span>
</Show> </Show>
<span class={`status-indicator session-status session-status-list ${statusClassName()}`}> <span class={`status-indicator session-status session-status-list ${statusClassName()}`}>
{needsInput() ? ( {needsInput() ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
<ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" />
) : (
<span class="status-dot" />
)}
{statusText()} {statusText()}
</span> </span>
</div> </div>
@@ -365,6 +581,63 @@ const SessionList: Component<SessionListProps> = (props) => {
<div <div
class="session-list-container bg-surface-secondary border-r border-base flex flex-col w-full" class="session-list-container bg-surface-secondary border-r border-base flex flex-col w-full"
> >
<Show when={props.enableFilterBar}>
<div class="p-3 border-b border-base">
<div class="flex items-center gap-2">
<div class="relative flex-1 min-w-0">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-muted" aria-hidden="true">
<Search class="w-4 h-4" />
</span>
<input
type="text"
class="form-input pl-9"
value={filterQuery()}
onInput={(e) => setFilterQuery(e.currentTarget.value)}
placeholder={t("sessionList.filter.placeholder")}
aria-label={t("sessionList.filter.ariaLabel")}
/>
</div>
<button
type="button"
class="button-tertiary p-2 inline-flex items-center justify-center"
onClick={toggleSelectAll}
disabled={allMatchingSessionIds().length === 0}
aria-label={t("sessionList.selection.selectAllAriaLabel")}
title={t("sessionList.selection.selectAllLabel")}
>
<Show
when={isSelectAllIndeterminate()}
fallback={isAllSelected() ? <CheckSquare class="w-4 h-4" /> : <Square class="w-4 h-4" />}
>
<MinusSquare class="w-4 h-4" />
</Show>
</button>
</div>
<Show when={selectedCount() > 0}>
<div class="mt-2 flex items-center justify-end gap-2">
<button
type="button"
class="button-tertiary"
onClick={handleBulkDelete}
aria-label={t("sessionList.bulkDelete.ariaLabel", { count: selectedCount() })}
>
{t("sessionList.bulkDelete.button", { count: selectedCount() })}
</button>
<button
type="button"
class="button-tertiary"
onClick={() => setSelectedSessionIds(new Set<string>())}
aria-label={t("sessionList.selection.clearAriaLabel")}
>
{t("sessionList.selection.clearLabel")}
</button>
</div>
</Show>
</div>
</Show>
<Show when={props.showHeader !== false}> <Show when={props.showHeader !== false}>
<div class="session-list-header p-3 border-b border-base"> <div class="session-list-header p-3 border-b border-base">
{props.headerContent ?? ( {props.headerContent ?? (
@@ -378,33 +651,33 @@ const SessionList: Component<SessionListProps> = (props) => {
</div> </div>
</Show> </Show>
<div class="session-list flex-1 overflow-y-auto" ref={(el) => listEl[1](el)}> <div class="session-list flex-1 overflow-y-auto" ref={(el) => listEl[1](el)}>
<Show when={props.threads.length > 0}> <Show when={filteredThreads().length > 0}>
<div class="session-section"> <div class="session-section">
<For each={props.threads}> <For each={filteredThreads()}>
{(thread) => { {(thread) => {
const expanded = () => isSessionParentExpanded(props.instanceId, thread.parent.id) const expanded = () => (normalizedQuery() ? true : isSessionParentExpanded(props.instanceId, thread.parent.id))
return ( return (
<> <>
<SessionRow <SessionRow
sessionId={thread.parent.id} sessionId={thread.parent.id}
hasChildren={thread.children.length > 0} hasChildren={thread.children.length > 0}
expanded={expanded()} expanded={expanded()}
onToggleExpand={() => toggleSessionParentExpanded(props.instanceId, thread.parent.id)} onToggleExpand={() => toggleSessionParentExpanded(props.instanceId, thread.parent.id)}
/> />
<Show when={expanded() && thread.children.length > 0}> <Show when={expanded() && thread.children.length > 0}>
<For each={thread.children}> <For each={thread.children}>
{(child, index) => ( {(child, index) => (
<SessionRow sessionId={child.id} isChild isLastChild={index() === thread.children.length - 1} /> <SessionRow sessionId={child.id} isChild isLastChild={index() === thread.children.length - 1} />
)} )}
</For> </For>
</Show> </Show>
</> </>
) )
}} }}
</For> </For>
</div> </div>
</Show> </Show>

View File

@@ -30,8 +30,27 @@ export const sessionMessages = {
"sessionList.copyId.success": "Session ID copied", "sessionList.copyId.success": "Session ID copied",
"sessionList.copyId.error": "Unable to copy session ID", "sessionList.copyId.error": "Unable to copy session ID",
"sessionList.delete.error": "Unable to delete session", "sessionList.delete.error": "Unable to delete session",
"sessionList.delete.title": "Delete session",
"sessionList.delete.confirmMessage": "Delete \"{label}\"? This cannot be undone.",
"sessionList.delete.confirmLabel": "Delete",
"sessionList.delete.cancelLabel": "Cancel",
"sessionList.rename.error": "Unable to rename session", "sessionList.rename.error": "Unable to rename session",
"sessionList.filter.placeholder": "Search sessions…",
"sessionList.filter.ariaLabel": "Search sessions",
"sessionList.selection.selectAllLabel": "Select all",
"sessionList.selection.selectAllAriaLabel": "Select all sessions",
"sessionList.selection.clearLabel": "Clear",
"sessionList.selection.clearAriaLabel": "Clear selection",
"sessionList.selection.checkboxAriaLabel": "Select session",
"sessionList.bulkDelete.button": "Delete {count}",
"sessionList.bulkDelete.ariaLabel": "Delete {count} selected sessions",
"sessionList.bulkDelete.title": "Delete sessions",
"sessionList.bulkDelete.confirmMessage": "Delete {count} selected sessions? This cannot be undone.",
"sessionList.bulkDelete.confirmLabel": "Delete",
"sessionList.bulkDelete.cancelLabel": "Cancel",
"sessionList.bulkDelete.error": "Unable to delete {count} sessions",
"sessionRenameDialog.title": "Rename Session", "sessionRenameDialog.title": "Rename Session",
"sessionRenameDialog.description.withLabel": "Update the title for \"{label}\".", "sessionRenameDialog.description.withLabel": "Update the title for \"{label}\".",
"sessionRenameDialog.description.default": "Set a new title for this session.", "sessionRenameDialog.description.default": "Set a new title for this session.",

View File

@@ -30,8 +30,27 @@ export const sessionMessages = {
"sessionList.copyId.success": "ID de sesión copiado", "sessionList.copyId.success": "ID de sesión copiado",
"sessionList.copyId.error": "No se pudo copiar el ID de sesión", "sessionList.copyId.error": "No se pudo copiar el ID de sesión",
"sessionList.delete.error": "No se pudo eliminar la sesión", "sessionList.delete.error": "No se pudo eliminar la sesión",
"sessionList.delete.title": "Eliminar sesión",
"sessionList.delete.confirmMessage": "¿Eliminar \"{label}\"? Esto no se puede deshacer.",
"sessionList.delete.confirmLabel": "Eliminar",
"sessionList.delete.cancelLabel": "Cancelar",
"sessionList.rename.error": "No se pudo renombrar la sesión", "sessionList.rename.error": "No se pudo renombrar la sesión",
"sessionList.filter.placeholder": "Buscar sesiones…",
"sessionList.filter.ariaLabel": "Buscar sesiones",
"sessionList.selection.selectAllLabel": "Seleccionar todo",
"sessionList.selection.selectAllAriaLabel": "Seleccionar todas las sesiones",
"sessionList.selection.clearLabel": "Limpiar",
"sessionList.selection.clearAriaLabel": "Limpiar selección",
"sessionList.selection.checkboxAriaLabel": "Seleccionar sesión",
"sessionList.bulkDelete.button": "Eliminar {count}",
"sessionList.bulkDelete.ariaLabel": "Eliminar {count} sesiones seleccionadas",
"sessionList.bulkDelete.title": "Eliminar sesiones",
"sessionList.bulkDelete.confirmMessage": "¿Eliminar {count} sesiones seleccionadas? Esto no se puede deshacer.",
"sessionList.bulkDelete.confirmLabel": "Eliminar",
"sessionList.bulkDelete.cancelLabel": "Cancelar",
"sessionList.bulkDelete.error": "No se pudieron eliminar {count} sesiones",
"sessionRenameDialog.title": "Renombrar sesión", "sessionRenameDialog.title": "Renombrar sesión",
"sessionRenameDialog.description.withLabel": "Actualiza el título de \"{label}\".", "sessionRenameDialog.description.withLabel": "Actualiza el título de \"{label}\".",
"sessionRenameDialog.description.default": "Establece un nuevo título para esta sesión.", "sessionRenameDialog.description.default": "Establece un nuevo título para esta sesión.",

View File

@@ -30,8 +30,27 @@ export const sessionMessages = {
"sessionList.copyId.success": "ID de session copié", "sessionList.copyId.success": "ID de session copié",
"sessionList.copyId.error": "Impossible de copier l'ID de session", "sessionList.copyId.error": "Impossible de copier l'ID de session",
"sessionList.delete.error": "Impossible de supprimer la session", "sessionList.delete.error": "Impossible de supprimer la session",
"sessionList.delete.title": "Supprimer la session",
"sessionList.delete.confirmMessage": "Supprimer \"{label}\" ? Cette action est irréversible.",
"sessionList.delete.confirmLabel": "Supprimer",
"sessionList.delete.cancelLabel": "Annuler",
"sessionList.rename.error": "Impossible de renommer la session", "sessionList.rename.error": "Impossible de renommer la session",
"sessionList.filter.placeholder": "Rechercher des sessions…",
"sessionList.filter.ariaLabel": "Rechercher des sessions",
"sessionList.selection.selectAllLabel": "Tout sélectionner",
"sessionList.selection.selectAllAriaLabel": "Sélectionner toutes les sessions",
"sessionList.selection.clearLabel": "Effacer",
"sessionList.selection.clearAriaLabel": "Effacer la sélection",
"sessionList.selection.checkboxAriaLabel": "Sélectionner la session",
"sessionList.bulkDelete.button": "Supprimer {count}",
"sessionList.bulkDelete.ariaLabel": "Supprimer {count} sessions sélectionnées",
"sessionList.bulkDelete.title": "Supprimer des sessions",
"sessionList.bulkDelete.confirmMessage": "Supprimer {count} sessions sélectionnées ? Cette action est irréversible.",
"sessionList.bulkDelete.confirmLabel": "Supprimer",
"sessionList.bulkDelete.cancelLabel": "Annuler",
"sessionList.bulkDelete.error": "Impossible de supprimer {count} sessions",
"sessionRenameDialog.title": "Renommer la session", "sessionRenameDialog.title": "Renommer la session",
"sessionRenameDialog.description.withLabel": "Mettre à jour le titre de \"{label}\".", "sessionRenameDialog.description.withLabel": "Mettre à jour le titre de \"{label}\".",
"sessionRenameDialog.description.default": "Définir un nouveau titre pour cette session.", "sessionRenameDialog.description.default": "Définir un nouveau titre pour cette session.",

View File

@@ -30,8 +30,27 @@ export const sessionMessages = {
"sessionList.copyId.success": "セッション ID をコピーしました", "sessionList.copyId.success": "セッション ID をコピーしました",
"sessionList.copyId.error": "セッション ID をコピーできません", "sessionList.copyId.error": "セッション ID をコピーできません",
"sessionList.delete.error": "セッションを削除できません", "sessionList.delete.error": "セッションを削除できません",
"sessionList.delete.title": "セッションを削除",
"sessionList.delete.confirmMessage": "\"{label}\" を削除しますか?この操作は元に戻せません。",
"sessionList.delete.confirmLabel": "削除",
"sessionList.delete.cancelLabel": "キャンセル",
"sessionList.rename.error": "セッション名を変更できません", "sessionList.rename.error": "セッション名を変更できません",
"sessionList.filter.placeholder": "セッションを検索…",
"sessionList.filter.ariaLabel": "セッションを検索",
"sessionList.selection.selectAllLabel": "すべて選択",
"sessionList.selection.selectAllAriaLabel": "すべてのセッションを選択",
"sessionList.selection.clearLabel": "クリア",
"sessionList.selection.clearAriaLabel": "選択をクリア",
"sessionList.selection.checkboxAriaLabel": "セッションを選択",
"sessionList.bulkDelete.button": "{count} 件を削除",
"sessionList.bulkDelete.ariaLabel": "選択した {count} 件のセッションを削除",
"sessionList.bulkDelete.title": "セッションを削除",
"sessionList.bulkDelete.confirmMessage": "選択した {count} 件のセッションを削除しますか?この操作は元に戻せません。",
"sessionList.bulkDelete.confirmLabel": "削除",
"sessionList.bulkDelete.cancelLabel": "キャンセル",
"sessionList.bulkDelete.error": "{count} 件のセッションを削除できません",
"sessionRenameDialog.title": "セッション名を変更", "sessionRenameDialog.title": "セッション名を変更",
"sessionRenameDialog.description.withLabel": "\"{label}\" のタイトルを更新します。", "sessionRenameDialog.description.withLabel": "\"{label}\" のタイトルを更新します。",
"sessionRenameDialog.description.default": "このセッションの新しいタイトルを設定します。", "sessionRenameDialog.description.default": "このセッションの新しいタイトルを設定します。",

View File

@@ -30,8 +30,27 @@ export const sessionMessages = {
"sessionList.copyId.success": "ID сессии скопирован", "sessionList.copyId.success": "ID сессии скопирован",
"sessionList.copyId.error": "Не удалось скопировать ID сессии", "sessionList.copyId.error": "Не удалось скопировать ID сессии",
"sessionList.delete.error": "Не удалось удалить сессию", "sessionList.delete.error": "Не удалось удалить сессию",
"sessionList.delete.title": "Удалить сессию",
"sessionList.delete.confirmMessage": "Удалить \"{label}\"? Это действие нельзя отменить.",
"sessionList.delete.confirmLabel": "Удалить",
"sessionList.delete.cancelLabel": "Отмена",
"sessionList.rename.error": "Не удалось переименовать сессию", "sessionList.rename.error": "Не удалось переименовать сессию",
"sessionList.filter.placeholder": "Поиск сессий…",
"sessionList.filter.ariaLabel": "Поиск сессий",
"sessionList.selection.selectAllLabel": "Выбрать все",
"sessionList.selection.selectAllAriaLabel": "Выбрать все сессии",
"sessionList.selection.clearLabel": "Очистить",
"sessionList.selection.clearAriaLabel": "Очистить выбор",
"sessionList.selection.checkboxAriaLabel": "Выбрать сессию",
"sessionList.bulkDelete.button": "Удалить {count}",
"sessionList.bulkDelete.ariaLabel": "Удалить {count} выбранных сессий",
"sessionList.bulkDelete.title": "Удалить сессии",
"sessionList.bulkDelete.confirmMessage": "Удалить {count} выбранных сессий? Это действие нельзя отменить.",
"sessionList.bulkDelete.confirmLabel": "Удалить",
"sessionList.bulkDelete.cancelLabel": "Отмена",
"sessionList.bulkDelete.error": "Не удалось удалить {count} сессий",
"sessionRenameDialog.title": "Переименовать сессию", "sessionRenameDialog.title": "Переименовать сессию",
"sessionRenameDialog.description.withLabel": "Обновите название для \"{label}\".", "sessionRenameDialog.description.withLabel": "Обновите название для \"{label}\".",
"sessionRenameDialog.description.default": "Установите новое название для этой сессии.", "sessionRenameDialog.description.default": "Установите новое название для этой сессии.",

View File

@@ -30,8 +30,27 @@ export const sessionMessages = {
"sessionList.copyId.success": "已复制会话 ID", "sessionList.copyId.success": "已复制会话 ID",
"sessionList.copyId.error": "无法复制会话 ID", "sessionList.copyId.error": "无法复制会话 ID",
"sessionList.delete.error": "无法删除会话", "sessionList.delete.error": "无法删除会话",
"sessionList.delete.title": "删除会话",
"sessionList.delete.confirmMessage": "删除“{label}”?此操作无法撤销。",
"sessionList.delete.confirmLabel": "删除",
"sessionList.delete.cancelLabel": "取消",
"sessionList.rename.error": "无法重命名会话", "sessionList.rename.error": "无法重命名会话",
"sessionList.filter.placeholder": "搜索会话…",
"sessionList.filter.ariaLabel": "搜索会话",
"sessionList.selection.selectAllLabel": "全选",
"sessionList.selection.selectAllAriaLabel": "选择所有会话",
"sessionList.selection.clearLabel": "清除",
"sessionList.selection.clearAriaLabel": "清除选择",
"sessionList.selection.checkboxAriaLabel": "选择会话",
"sessionList.bulkDelete.button": "删除 {count}",
"sessionList.bulkDelete.ariaLabel": "删除已选择的 {count} 个会话",
"sessionList.bulkDelete.title": "删除会话",
"sessionList.bulkDelete.confirmMessage": "删除已选择的 {count} 个会话?此操作无法撤销。",
"sessionList.bulkDelete.confirmLabel": "删除",
"sessionList.bulkDelete.cancelLabel": "取消",
"sessionList.bulkDelete.error": "无法删除 {count} 个会话",
"sessionRenameDialog.title": "重命名会话", "sessionRenameDialog.title": "重命名会话",
"sessionRenameDialog.description.withLabel": "更新“{label}”的标题。", "sessionRenameDialog.description.withLabel": "更新“{label}”的标题。",
"sessionRenameDialog.description.default": "为此会话设置新标题。", "sessionRenameDialog.description.default": "为此会话设置新标题。",