refactor(web): extract ShellPage sidebar JSX into components

ShellPage's JSX carried the whole sidebar inline, including two near-copies
of the group header and of the main header's sidebar toggles, which made the
render hard to follow and easy to change in one place only.

Move-only: the sidebar header, group header (one component for the
conversation and project variants), the team and project roster rows, the
archived section, the user menu and the sidebar toggle buttons now live in
pages/shell. DOM, classNames, test ids, aria attributes, handlers and messages
are unchanged; the catalogs only update their file references, and the
desktop source checks for the header toggles now read the file they moved to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
Claude 2026-10-04 10:05:21 +00:00
parent a0060b8df7
commit 3c678519a6
No known key found for this signature in database
17 changed files with 1048 additions and 839 deletions

View File

@ -63,6 +63,7 @@ describe("window chrome", () => {
it("keeps conversation header controls clickable", () => {
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "../pages");
const shell = readFileSync(path.join(root, "Shell.tsx"), "utf8");
const toggles = readFileSync(path.join(root, "shell/sidebar-toggle-buttons.tsx"), "utf8");
expect(shell).toContain(
'className="app-drag flex items-center justify-between border-b border-sidebar-border',
);
@ -70,14 +71,16 @@ describe("window chrome", () => {
expect(shell).toContain('className="app-no-drag flex min-w-0 items-center gap-3"');
// Ensure header sidebar toggle controls stay clickable (not part of drag region)
expect(
shell.match(/app-no-drag .* size-control-lg .* place-items-center/g)?.length ?? 0,
toggles.match(/app-no-drag .* size-control-lg .* place-items-center/g)?.length ?? 0,
).toBeGreaterThanOrEqual(2);
});
it("moves window chrome into the conversation header when the bots sidebar is collapsed", () => {
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), "../pages");
const shell = readFileSync(path.join(root, "Shell.tsx"), "utf8");
expect(shell).toContain("{botsSidebarCollapsed && desktopBridge() ? <WindowChrome /> : null}");
const toggles = readFileSync(path.join(root, "shell/sidebar-toggle-buttons.tsx"), "utf8");
expect(shell).toContain("collapsed={botsSidebarCollapsed}");
expect(toggles).toContain("{collapsed && desktopBridge() ? <WindowChrome /> : null}");
});
});

View File

@ -14,7 +14,7 @@ msgstr ""
"Plural-Forms: \n"
"X-Generator: @lingui/cli\n"
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid " (unread)"
msgstr " (ungelesen)"
@ -126,17 +126,17 @@ msgid "{0}, {label}"
msgstr "{0}, {label}"
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, 1:1 conversation"
msgstr "{0}, 1:1-Unterhaltung"
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, group conversation"
msgstr "{0}, Gruppenunterhaltung"
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, main conversation"
msgstr "{0}, Hauptunterhaltung"
@ -339,6 +339,7 @@ msgstr "Aktionsbestätigungen"
#. placeholder {0}: sectionMenu.section.name
#: src/pages/BotContextMenu.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Actions for {0}"
msgstr "Aktionen für {0}"
@ -354,7 +355,7 @@ msgstr "Aktiv"
msgid "Active model"
msgstr "Aktives Modell"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Activity"
msgstr "Aktivität"
@ -647,7 +648,7 @@ msgstr "Archivieren"
msgid "archived"
msgstr "archiviert"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Archived"
msgstr "Archiviert"
@ -1159,7 +1160,7 @@ msgid "Coder"
msgstr "Coder"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Collapse {0}"
msgstr "{0} einklappen"
@ -1743,9 +1744,9 @@ msgstr "Avatare konnten nicht aktualisiert werden"
msgid "Couldn't update messaging settings"
msgstr "Messaging-Einstellungen konnten nicht aktualisiert werden"
#: src/pages/Shell.tsx
#: src/pages/shell/bot-panel.tsx
#: src/pages/shell/dialogs.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Create"
msgstr "Erstellen"
@ -1902,14 +1903,14 @@ msgstr "Standardbereich"
#: src/pages/KnowledgeSection.tsx
#: src/pages/McpServersOverlay.tsx
#: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
#: src/pages/shell/dialogs.tsx
msgid "Delete"
msgstr "Löschen"
#. placeholder {0}: bot.name
#. placeholder {0}: group.name
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Delete {0}"
msgstr "{0} löschen"
@ -2218,7 +2219,7 @@ msgid "Expand"
msgstr "Erweitern"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}"
msgstr "{0} ausklappen"
@ -2791,7 +2792,7 @@ msgstr "Lokales Docker"
msgid "Local Server Settings"
msgstr "Einstellungen des lokalen Servers"
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Log out"
msgstr "Abmelden"
@ -2974,7 +2975,7 @@ msgstr "Minimal"
msgid "Minimize"
msgstr "Minimieren"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Minimize bots"
msgstr "Bots minimieren"
@ -3396,7 +3397,7 @@ msgid "Open"
msgstr "Öffnen"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Open {0}"
msgstr "{0} öffnen"
@ -3416,7 +3417,7 @@ msgstr "Im Browser öffnen"
msgid "Open in full window"
msgstr "In vollständigem Fenster öffnen"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Open navigation"
msgstr "Navigation öffnen"
@ -3931,7 +3932,7 @@ msgstr "Wird zurückgesetzt…"
msgid "Restart to update"
msgstr "Zum Aktualisieren neu starten"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Restore"
msgstr "Wiederherstellen"
@ -4136,7 +4137,7 @@ msgid "Searching…"
msgstr "Suche…"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Select {0}"
msgstr "{0} auswählen"
@ -4202,6 +4203,7 @@ msgstr "Stimme einrichten"
#: src/pages/MessagingSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Settings"
msgstr "Einstellungen"
@ -4253,6 +4255,7 @@ msgid "Show all {0}"
msgstr "Alle {0} anzeigen"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Show bots"
msgstr "Bots anzeigen"
@ -4832,7 +4835,7 @@ msgstr "Hochladen"
#: src/pages/AccountSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Usage"
msgstr "Nutzung"

View File

@ -14,7 +14,7 @@ msgstr ""
"Plural-Forms: \n"
"X-Generator: @lingui/cli\n"
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid " (unread)"
msgstr " (unread)"
@ -126,17 +126,17 @@ msgid "{0}, {label}"
msgstr "{0}, {label}"
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, 1:1 conversation"
msgstr "{0}, 1:1 conversation"
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, group conversation"
msgstr "{0}, group conversation"
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, main conversation"
msgstr "{0}, main conversation"
@ -339,6 +339,7 @@ msgstr "Action confirmations"
#. placeholder {0}: sectionMenu.section.name
#: src/pages/BotContextMenu.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Actions for {0}"
msgstr "Actions for {0}"
@ -354,7 +355,7 @@ msgstr "Active"
msgid "Active model"
msgstr "Active model"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Activity"
msgstr "Activity"
@ -647,7 +648,7 @@ msgstr "Archive"
msgid "archived"
msgstr "archived"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Archived"
msgstr "Archived"
@ -1159,7 +1160,7 @@ msgid "Coder"
msgstr "Coder"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Collapse {0}"
msgstr "Collapse {0}"
@ -1743,9 +1744,9 @@ msgstr "Couldn't update avatars"
msgid "Couldn't update messaging settings"
msgstr "Couldn't update messaging settings"
#: src/pages/Shell.tsx
#: src/pages/shell/bot-panel.tsx
#: src/pages/shell/dialogs.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Create"
msgstr "Create"
@ -1902,14 +1903,14 @@ msgstr "Default scope"
#: src/pages/KnowledgeSection.tsx
#: src/pages/McpServersOverlay.tsx
#: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
#: src/pages/shell/dialogs.tsx
msgid "Delete"
msgstr "Delete"
#. placeholder {0}: bot.name
#. placeholder {0}: group.name
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Delete {0}"
msgstr "Delete {0}"
@ -2218,7 +2219,7 @@ msgid "Expand"
msgstr "Expand"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}"
msgstr "Expand {0}"
@ -2791,7 +2792,7 @@ msgstr "Local Docker"
msgid "Local Server Settings"
msgstr "Local Server Settings"
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Log out"
msgstr "Log out"
@ -2974,7 +2975,7 @@ msgstr "Minimal"
msgid "Minimize"
msgstr "Minimize"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Minimize bots"
msgstr "Minimize bots"
@ -3396,7 +3397,7 @@ msgid "Open"
msgstr "Open"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Open {0}"
msgstr "Open {0}"
@ -3416,7 +3417,7 @@ msgstr "Open in browser"
msgid "Open in full window"
msgstr "Open in full window"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Open navigation"
msgstr "Open navigation"
@ -3931,7 +3932,7 @@ msgstr "Resetting…"
msgid "Restart to update"
msgstr "Restart to update"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Restore"
msgstr "Restore"
@ -4136,7 +4137,7 @@ msgid "Searching…"
msgstr "Searching…"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Select {0}"
msgstr "Select {0}"
@ -4202,6 +4203,7 @@ msgstr "Set up voice"
#: src/pages/MessagingSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Settings"
msgstr "Settings"
@ -4253,6 +4255,7 @@ msgid "Show all {0}"
msgstr "Show all {0}"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Show bots"
msgstr "Show bots"
@ -4832,7 +4835,7 @@ msgstr "Upload"
#: src/pages/AccountSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Usage"
msgstr "Usage"

View File

@ -14,7 +14,7 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: @lingui/cli\n"
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid " (unread)"
msgstr " (sin leer)"
@ -126,17 +126,17 @@ msgid "{0}, {label}"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, 1:1 conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, main conversation"
msgstr ""
@ -339,6 +339,7 @@ msgstr "Confirmaciones de acción"
#. placeholder {0}: sectionMenu.section.name
#: src/pages/BotContextMenu.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Actions for {0}"
msgstr "Acciones para {0}"
@ -354,7 +355,7 @@ msgstr "Activo"
msgid "Active model"
msgstr "Modelo activo"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Activity"
msgstr "Actividad"
@ -647,7 +648,7 @@ msgstr "Archivar"
msgid "archived"
msgstr "archivado"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Archived"
msgstr "Archivado"
@ -1159,7 +1160,7 @@ msgid "Coder"
msgstr ""
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Collapse {0}"
msgstr "Contraer {0}"
@ -1743,9 +1744,9 @@ msgstr "No se pudieron actualizar los avatares"
msgid "Couldn't update messaging settings"
msgstr "No se pudo actualizar la configuración de mensajería"
#: src/pages/Shell.tsx
#: src/pages/shell/bot-panel.tsx
#: src/pages/shell/dialogs.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Create"
msgstr "Crear"
@ -1902,14 +1903,14 @@ msgstr "Alcance predeterminado"
#: src/pages/KnowledgeSection.tsx
#: src/pages/McpServersOverlay.tsx
#: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
#: src/pages/shell/dialogs.tsx
msgid "Delete"
msgstr "Eliminar"
#. placeholder {0}: bot.name
#. placeholder {0}: group.name
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Delete {0}"
msgstr "Eliminar {0}"
@ -2218,7 +2219,7 @@ msgid "Expand"
msgstr "Expandir"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}"
msgstr "Expandir {0}"
@ -2791,7 +2792,7 @@ msgstr ""
msgid "Local Server Settings"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Log out"
msgstr "Cerrar sesión"
@ -2974,7 +2975,7 @@ msgstr "Mínimo"
msgid "Minimize"
msgstr "Minimizar"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Minimize bots"
msgstr ""
@ -3396,7 +3397,7 @@ msgid "Open"
msgstr "Abrir"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Open {0}"
msgstr "Abrir {0}"
@ -3416,7 +3417,7 @@ msgstr ""
msgid "Open in full window"
msgstr "Abrir en ventana completa"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Open navigation"
msgstr "Abrir navegación"
@ -3931,7 +3932,7 @@ msgstr "Restableciendo…"
msgid "Restart to update"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Restore"
msgstr "Restaurar"
@ -4136,7 +4137,7 @@ msgid "Searching…"
msgstr "Buscando…"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Select {0}"
msgstr ""
@ -4202,6 +4203,7 @@ msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Settings"
msgstr "Configuración"
@ -4253,6 +4255,7 @@ msgid "Show all {0}"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Show bots"
msgstr ""
@ -4832,7 +4835,7 @@ msgstr "Subir"
#: src/pages/AccountSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Usage"
msgstr "Uso"

View File

@ -14,7 +14,7 @@ msgstr ""
"Plural-Forms: \n"
"X-Generator: @lingui/cli\n"
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid " (unread)"
msgstr " (अपठित)"
@ -126,17 +126,17 @@ msgid "{0}, {label}"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, 1:1 conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, main conversation"
msgstr ""
@ -339,6 +339,7 @@ msgstr "कार्रवाई की पुष्टि"
#. placeholder {0}: sectionMenu.section.name
#: src/pages/BotContextMenu.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Actions for {0}"
msgstr "{0} के लिए कार्रवाइयां"
@ -354,7 +355,7 @@ msgstr ""
msgid "Active model"
msgstr "सक्रिय मॉडल"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Activity"
msgstr "गतिविधि"
@ -647,7 +648,7 @@ msgstr "आर्काइव करें"
msgid "archived"
msgstr "आर्काइव किया गया"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Archived"
msgstr "आर्काइव किया गया"
@ -1159,7 +1160,7 @@ msgid "Coder"
msgstr ""
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Collapse {0}"
msgstr ""
@ -1743,9 +1744,9 @@ msgstr ""
msgid "Couldn't update messaging settings"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/bot-panel.tsx
#: src/pages/shell/dialogs.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Create"
msgstr "बनाएं"
@ -1902,14 +1903,14 @@ msgstr "डिफ़ॉल्ट स्कोप"
#: src/pages/KnowledgeSection.tsx
#: src/pages/McpServersOverlay.tsx
#: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
#: src/pages/shell/dialogs.tsx
msgid "Delete"
msgstr "डिलीट करें"
#. placeholder {0}: bot.name
#. placeholder {0}: group.name
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Delete {0}"
msgstr "{0} डिलीट करें"
@ -2218,7 +2219,7 @@ msgid "Expand"
msgstr "विस्तृत करें"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}"
msgstr ""
@ -2791,7 +2792,7 @@ msgstr ""
msgid "Local Server Settings"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Log out"
msgstr "लॉग आउट"
@ -2974,7 +2975,7 @@ msgstr "न्यूनतम"
msgid "Minimize"
msgstr "छोटा करें"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Minimize bots"
msgstr ""
@ -3396,7 +3397,7 @@ msgid "Open"
msgstr "खोलें"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Open {0}"
msgstr ""
@ -3416,7 +3417,7 @@ msgstr ""
msgid "Open in full window"
msgstr "पूरी विंडो में खोलें"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Open navigation"
msgstr "नेविगेशन खोलें"
@ -3931,7 +3932,7 @@ msgstr "रीसेट हो रहा है…"
msgid "Restart to update"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Restore"
msgstr "बहाल करें"
@ -4136,7 +4137,7 @@ msgid "Searching…"
msgstr "खोजा जा रहा है…"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Select {0}"
msgstr ""
@ -4202,6 +4203,7 @@ msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Settings"
msgstr "सेटिंग्स"
@ -4253,6 +4255,7 @@ msgid "Show all {0}"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Show bots"
msgstr ""
@ -4832,7 +4835,7 @@ msgstr "अपलोड"
#: src/pages/AccountSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Usage"
msgstr "उपयोग"

View File

@ -14,7 +14,7 @@ msgstr ""
"Plural-Forms: \n"
"X-Generator: @lingui/cli\n"
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid " (unread)"
msgstr " (읽지 않음)"
@ -126,17 +126,17 @@ msgid "{0}, {label}"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, 1:1 conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, main conversation"
msgstr ""
@ -339,6 +339,7 @@ msgstr "작업 실행 전 확인"
#. placeholder {0}: sectionMenu.section.name
#: src/pages/BotContextMenu.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Actions for {0}"
msgstr "{0} 작업 메뉴"
@ -354,7 +355,7 @@ msgstr "활성"
msgid "Active model"
msgstr "현재 모델"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Activity"
msgstr "활동"
@ -647,7 +648,7 @@ msgstr "보관"
msgid "archived"
msgstr "보관됨"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Archived"
msgstr "보관됨"
@ -1159,7 +1160,7 @@ msgid "Coder"
msgstr ""
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Collapse {0}"
msgstr "{0} 접기"
@ -1743,9 +1744,9 @@ msgstr "아바타를 업데이트할 수 없습니다"
msgid "Couldn't update messaging settings"
msgstr "메시징 설정을 업데이트할 수 없습니다"
#: src/pages/Shell.tsx
#: src/pages/shell/bot-panel.tsx
#: src/pages/shell/dialogs.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Create"
msgstr "만들기"
@ -1902,14 +1903,14 @@ msgstr "기본 기억 범위"
#: src/pages/KnowledgeSection.tsx
#: src/pages/McpServersOverlay.tsx
#: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
#: src/pages/shell/dialogs.tsx
msgid "Delete"
msgstr "삭제"
#. placeholder {0}: bot.name
#. placeholder {0}: group.name
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Delete {0}"
msgstr "{0} 삭제"
@ -2218,7 +2219,7 @@ msgid "Expand"
msgstr "펼치기"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}"
msgstr "{0} 펼치기"
@ -2791,7 +2792,7 @@ msgstr ""
msgid "Local Server Settings"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Log out"
msgstr "로그아웃"
@ -2974,7 +2975,7 @@ msgstr "최소"
msgid "Minimize"
msgstr "최소화"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Minimize bots"
msgstr ""
@ -3396,7 +3397,7 @@ msgid "Open"
msgstr "열기"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Open {0}"
msgstr "{0} 열기"
@ -3416,7 +3417,7 @@ msgstr ""
msgid "Open in full window"
msgstr "전체 창으로 열기"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Open navigation"
msgstr "메뉴 열기"
@ -3931,7 +3932,7 @@ msgstr "재설정 중…"
msgid "Restart to update"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Restore"
msgstr "복원"
@ -4136,7 +4137,7 @@ msgid "Searching…"
msgstr "검색 중…"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Select {0}"
msgstr ""
@ -4202,6 +4203,7 @@ msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Settings"
msgstr "설정"
@ -4253,6 +4255,7 @@ msgid "Show all {0}"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Show bots"
msgstr ""
@ -4832,7 +4835,7 @@ msgstr "업로드"
#: src/pages/AccountSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Usage"
msgstr "사용량"

View File

@ -14,7 +14,7 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: @lingui/cli\n"
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid " (unread)"
msgstr " (não lido)"
@ -126,17 +126,17 @@ msgid "{0}, {label}"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, 1:1 conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, main conversation"
msgstr ""
@ -339,6 +339,7 @@ msgstr "Confirmações de ação"
#. placeholder {0}: sectionMenu.section.name
#: src/pages/BotContextMenu.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Actions for {0}"
msgstr "Ações para {0}"
@ -354,7 +355,7 @@ msgstr ""
msgid "Active model"
msgstr "Modelo ativo"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Activity"
msgstr "Atividade"
@ -647,7 +648,7 @@ msgstr "Arquivar"
msgid "archived"
msgstr "arquivado"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Archived"
msgstr "Arquivado"
@ -1159,7 +1160,7 @@ msgid "Coder"
msgstr ""
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Collapse {0}"
msgstr ""
@ -1743,9 +1744,9 @@ msgstr ""
msgid "Couldn't update messaging settings"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/bot-panel.tsx
#: src/pages/shell/dialogs.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Create"
msgstr "Criar"
@ -1902,14 +1903,14 @@ msgstr "Escopo padrão"
#: src/pages/KnowledgeSection.tsx
#: src/pages/McpServersOverlay.tsx
#: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
#: src/pages/shell/dialogs.tsx
msgid "Delete"
msgstr "Excluir"
#. placeholder {0}: bot.name
#. placeholder {0}: group.name
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Delete {0}"
msgstr "Excluir {0}"
@ -2218,7 +2219,7 @@ msgid "Expand"
msgstr "Expandir"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}"
msgstr ""
@ -2791,7 +2792,7 @@ msgstr ""
msgid "Local Server Settings"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Log out"
msgstr "Sair"
@ -2974,7 +2975,7 @@ msgstr "Mínimo"
msgid "Minimize"
msgstr "Minimizar"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Minimize bots"
msgstr ""
@ -3396,7 +3397,7 @@ msgid "Open"
msgstr "Abrir"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Open {0}"
msgstr ""
@ -3416,7 +3417,7 @@ msgstr ""
msgid "Open in full window"
msgstr "Abrir em janela cheia"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Open navigation"
msgstr "Abrir a navegação"
@ -3931,7 +3932,7 @@ msgstr "Redefinindo…"
msgid "Restart to update"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Restore"
msgstr "Restaurar"
@ -4136,7 +4137,7 @@ msgid "Searching…"
msgstr "Pesquisando…"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Select {0}"
msgstr ""
@ -4202,6 +4203,7 @@ msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Settings"
msgstr "Configurações"
@ -4253,6 +4255,7 @@ msgid "Show all {0}"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Show bots"
msgstr ""
@ -4832,7 +4835,7 @@ msgstr "Enviar"
#: src/pages/AccountSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Usage"
msgstr "Uso"

View File

@ -14,7 +14,7 @@ msgstr ""
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: @lingui/cli\n"
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid " (unread)"
msgstr " (непрочитано)"
@ -126,17 +126,17 @@ msgid "{0}, {label}"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, 1:1 conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, main conversation"
msgstr ""
@ -339,6 +339,7 @@ msgstr "Подтверждения действий"
#. placeholder {0}: sectionMenu.section.name
#: src/pages/BotContextMenu.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Actions for {0}"
msgstr "Действия для {0}"
@ -354,7 +355,7 @@ msgstr "Активный"
msgid "Active model"
msgstr "Активная модель"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Activity"
msgstr "Активность"
@ -647,7 +648,7 @@ msgstr "Архивировать"
msgid "archived"
msgstr "в архиве"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Archived"
msgstr "В архиве"
@ -1159,7 +1160,7 @@ msgid "Coder"
msgstr ""
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Collapse {0}"
msgstr "Свернуть {0}"
@ -1743,9 +1744,9 @@ msgstr "Не удалось обновить аватары"
msgid "Couldn't update messaging settings"
msgstr "Не удалось обновить настройки обмена сообщениями."
#: src/pages/Shell.tsx
#: src/pages/shell/bot-panel.tsx
#: src/pages/shell/dialogs.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Create"
msgstr "Создать"
@ -1902,14 +1903,14 @@ msgstr "Область действия по умолчанию"
#: src/pages/KnowledgeSection.tsx
#: src/pages/McpServersOverlay.tsx
#: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
#: src/pages/shell/dialogs.tsx
msgid "Delete"
msgstr "Удалить"
#. placeholder {0}: bot.name
#. placeholder {0}: group.name
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Delete {0}"
msgstr "Удалить {0}"
@ -2218,7 +2219,7 @@ msgid "Expand"
msgstr "Развернуть"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}"
msgstr "Развернуть {0}"
@ -2791,7 +2792,7 @@ msgstr ""
msgid "Local Server Settings"
msgstr "Настройки локального сервера"
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Log out"
msgstr "Выйти"
@ -2974,7 +2975,7 @@ msgstr "Минимальный"
msgid "Minimize"
msgstr "Свернуть"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Minimize bots"
msgstr "Свернуть ботов"
@ -3396,7 +3397,7 @@ msgid "Open"
msgstr "Открыть"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Open {0}"
msgstr "Открыть {0}"
@ -3416,7 +3417,7 @@ msgstr ""
msgid "Open in full window"
msgstr "Открыть в полном окне"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Open navigation"
msgstr "Открыть навигацию"
@ -3931,7 +3932,7 @@ msgstr "Сброс…"
msgid "Restart to update"
msgstr "Перезапустить для обновления"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Restore"
msgstr "Восстановить"
@ -4136,7 +4137,7 @@ msgid "Searching…"
msgstr "Поиск…"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Select {0}"
msgstr ""
@ -4202,6 +4203,7 @@ msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Settings"
msgstr "Настройки"
@ -4253,6 +4255,7 @@ msgid "Show all {0}"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Show bots"
msgstr "Показать ботов"
@ -4832,7 +4835,7 @@ msgstr "Загрузить"
#: src/pages/AccountSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Usage"
msgstr "Использование"

View File

@ -14,7 +14,7 @@ msgstr ""
"Plural-Forms: \n"
"X-Generator: @lingui/cli\n"
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid " (unread)"
msgstr " (okunmadı)"
@ -126,17 +126,17 @@ msgid "{0}, {label}"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, 1:1 conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, main conversation"
msgstr ""
@ -339,6 +339,7 @@ msgstr "İşlem onayları"
#. placeholder {0}: sectionMenu.section.name
#: src/pages/BotContextMenu.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Actions for {0}"
msgstr "{0} için işlemler"
@ -354,7 +355,7 @@ msgstr ""
msgid "Active model"
msgstr "Etkin model"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Activity"
msgstr "Etkinlik"
@ -647,7 +648,7 @@ msgstr "Arşivle"
msgid "archived"
msgstr "arşivlendi"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Archived"
msgstr "Arşivlendi"
@ -1159,7 +1160,7 @@ msgid "Coder"
msgstr ""
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Collapse {0}"
msgstr ""
@ -1743,9 +1744,9 @@ msgstr ""
msgid "Couldn't update messaging settings"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/bot-panel.tsx
#: src/pages/shell/dialogs.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Create"
msgstr "Oluştur"
@ -1902,14 +1903,14 @@ msgstr "Varsayılan kapsam"
#: src/pages/KnowledgeSection.tsx
#: src/pages/McpServersOverlay.tsx
#: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
#: src/pages/shell/dialogs.tsx
msgid "Delete"
msgstr "Sil"
#. placeholder {0}: bot.name
#. placeholder {0}: group.name
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Delete {0}"
msgstr "{0} sil"
@ -2218,7 +2219,7 @@ msgid "Expand"
msgstr "Genişlet"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}"
msgstr ""
@ -2791,7 +2792,7 @@ msgstr ""
msgid "Local Server Settings"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Log out"
msgstr "Çıkış yap"
@ -2974,7 +2975,7 @@ msgstr "Minimal"
msgid "Minimize"
msgstr "Küçült"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Minimize bots"
msgstr ""
@ -3396,7 +3397,7 @@ msgid "Open"
msgstr "Aç"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Open {0}"
msgstr ""
@ -3416,7 +3417,7 @@ msgstr ""
msgid "Open in full window"
msgstr "Tam pencerede aç"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Open navigation"
msgstr "Gezinmeyi aç"
@ -3931,7 +3932,7 @@ msgstr ""
msgid "Restart to update"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Restore"
msgstr "Geri yükle"
@ -4136,7 +4137,7 @@ msgid "Searching…"
msgstr "Aranıyor…"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Select {0}"
msgstr ""
@ -4202,6 +4203,7 @@ msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Settings"
msgstr "Ayarlar"
@ -4253,6 +4255,7 @@ msgid "Show all {0}"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Show bots"
msgstr ""
@ -4832,7 +4835,7 @@ msgstr "Yükle"
#: src/pages/AccountSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Usage"
msgstr "Kullanım"

View File

@ -14,7 +14,7 @@ msgstr ""
"Plural-Forms: \n"
"X-Generator: @lingui/cli\n"
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid " (unread)"
msgstr "(未读)"
@ -126,17 +126,17 @@ msgid "{0}, {label}"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, 1:1 conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, group conversation"
msgstr ""
#. placeholder {0}: item.chat.name
#: src/pages/Shell.tsx
#: src/pages/shell/roster-rows.tsx
msgid "{0}, main conversation"
msgstr ""
@ -339,6 +339,7 @@ msgstr "操作确认"
#. placeholder {0}: sectionMenu.section.name
#: src/pages/BotContextMenu.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Actions for {0}"
msgstr "{0} 的操作"
@ -354,7 +355,7 @@ msgstr "启用"
msgid "Active model"
msgstr "当前模型"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Activity"
msgstr "动态"
@ -647,7 +648,7 @@ msgstr "归档"
msgid "archived"
msgstr "已归档"
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Archived"
msgstr "已归档"
@ -1159,7 +1160,7 @@ msgid "Coder"
msgstr ""
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Collapse {0}"
msgstr "折叠 {0}"
@ -1743,9 +1744,9 @@ msgstr "无法更新头像"
msgid "Couldn't update messaging settings"
msgstr "无法更新消息设置"
#: src/pages/Shell.tsx
#: src/pages/shell/bot-panel.tsx
#: src/pages/shell/dialogs.tsx
#: src/pages/shell/sidebar-header.tsx
msgid "Create"
msgstr "创建"
@ -1902,14 +1903,14 @@ msgstr "默认范围"
#: src/pages/KnowledgeSection.tsx
#: src/pages/McpServersOverlay.tsx
#: src/pages/RoutineEditor.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
#: src/pages/shell/dialogs.tsx
msgid "Delete"
msgstr "删除"
#. placeholder {0}: bot.name
#. placeholder {0}: group.name
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Delete {0}"
msgstr "删除 {0}"
@ -2218,7 +2219,7 @@ msgid "Expand"
msgstr "展开"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Expand {0}"
msgstr "展开 {0}"
@ -2791,7 +2792,7 @@ msgstr ""
msgid "Local Server Settings"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Log out"
msgstr "退出登录"
@ -2974,7 +2975,7 @@ msgstr "最低"
msgid "Minimize"
msgstr "最小化"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Minimize bots"
msgstr ""
@ -3396,7 +3397,7 @@ msgid "Open"
msgstr "打开"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Open {0}"
msgstr "打开 {0}"
@ -3416,7 +3417,7 @@ msgstr ""
msgid "Open in full window"
msgstr "在完整窗口中打开"
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Open navigation"
msgstr "打开导航"
@ -3931,7 +3932,7 @@ msgstr "正在重置…"
msgid "Restart to update"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/archived-section.tsx
msgid "Restore"
msgstr "恢复"
@ -4136,7 +4137,7 @@ msgid "Searching…"
msgstr "正在搜索…"
#. placeholder {0}: group.title
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-group-header.tsx
msgid "Select {0}"
msgstr ""
@ -4202,6 +4203,7 @@ msgstr ""
#: src/pages/MessagingSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Settings"
msgstr "设置"
@ -4253,6 +4255,7 @@ msgid "Show all {0}"
msgstr ""
#: src/pages/Shell.tsx
#: src/pages/shell/sidebar-toggle-buttons.tsx
msgid "Show bots"
msgstr ""
@ -4832,7 +4835,7 @@ msgstr "上传"
#: src/pages/AccountSettingsOverlay.tsx
#: src/pages/SettingsOverlay.tsx
#: src/pages/Shell.tsx
#: src/pages/shell/user-menu.tsx
msgid "Usage"
msgstr "用量"

View File

@ -84,25 +84,13 @@ import {
Popover,
PopoverContent,
PopoverTrigger,
Wordmark,
} from "@rakazo/ui-web";
import {
ArrowLeft,
Bell,
Calendar,
ChevronDown,
Folder,
Gauge,
Globe,
GripVertical,
LayoutGrid,
Lock,
LogOut,
Maximize2,
Menu,
MoreHorizontal,
PanelLeftClose,
PanelLeftOpen,
PanelRight,
Pencil,
Plus,
@ -140,7 +128,7 @@ import { TeachRecordingChrome, TeachStopButton } from "../components/teach/Teach
import { useSidebarResize } from "../hooks/useSidebarResize";
import { useWideLayout } from "../hooks/useWideLayout";
import { readActivityMode, writeActivityMode } from "../lib/activity-mode";
import { needsYouChipLabel, rosterActivitySubtitle, rosterStatusDot } from "../lib/agent-activity";
import { rosterActivitySubtitle } from "../lib/agent-activity";
import { agentPointerFeed } from "../lib/agent-pointer";
import type { ArtifactTarget } from "../lib/artifact-open";
import { attachmentMaxBytes, uploadAttachment } from "../lib/artifact-upload";
@ -157,7 +145,6 @@ import {
shouldNotifyBrowser,
} from "../lib/browser-notifications";
import { loadComputerScreen } from "../lib/computer-screen";
import { desktopBridge } from "../lib/desktop";
import { scheduleFocusPrompt } from "../lib/focus-prompt";
import { formatBytes } from "../lib/format-bytes";
import {
@ -216,8 +203,9 @@ import {
} from "./RoutineEditor";
import type { SettingsSection } from "./SettingsOverlay";
import { SpaceSearchResults } from "./SpaceSearch";
import { ArchivedSection } from "./shell/archived-section";
import { BotSettings, CreateBotForm } from "./shell/bot-panel";
import { BotCreatePicker, ConversationBotPicker } from "./shell/bot-picker";
import { ConversationBotPicker } from "./shell/bot-picker";
import {
collapsedSidebarSectionsStorageKey,
readCollapsedSidebarSections,
@ -243,15 +231,13 @@ import {
} from "./shell/dialogs";
import { MainConversationRow } from "./shell/main-conversation-row";
import { cleanRosterPreview, formatRosterTime, stripSpaceTitlePrefix } from "./shell/roster-format";
import {
rosterNameClass,
rosterPreviewClass,
rosterRowClass,
rosterTimeClass,
rosterUnreadDotClass,
} from "./shell/roster-row-style";
import { rosterRowClass } from "./shell/roster-row-style";
import { ProjectRosterRow, TeamRosterRow } from "./shell/roster-rows";
import type { PendingAttachment } from "./shell/shell-types";
import { FALLBACK_BOT_COLOR } from "./shell/shell-types";
import { SidebarGroupHeader } from "./shell/sidebar-group-header";
import { SidebarHeader } from "./shell/sidebar-header";
import { SidebarToggleButtons } from "./shell/sidebar-toggle-buttons";
import {
applyThreadEvent,
firstThreadRoute,
@ -259,7 +245,7 @@ import {
threadSnapshotSignal,
} from "./shell/thread-state";
import { Transcript } from "./shell/transcript";
import { WindowChrome } from "./WindowChrome";
import { UserMenu } from "./shell/user-menu";
const BotContextMenu = lazyWithRecovery("BotContextMenu", () =>
import("./BotContextMenu").then((module) => ({ default: module.BotContextMenu })),
@ -326,7 +312,7 @@ function voiceStatusRefreshTimeout(): Promise<never> {
export type NavPillar = "heute" | "team" | "projekte";
import { ProjectDragGrip, SortableGroup } from "../components/SortableGroup";
import { SortableGroup } from "../components/SortableGroup";
export function ShellPage() {
const { t } = useLingui();
@ -3384,85 +3370,20 @@ export function ShellPage() {
onPointerDown={startResizingLeft}
className="absolute -right-2 top-0 bottom-0 w-4 cursor-col-resize z-50 hidden md:block"
/>
<div className="app-drag flex items-center justify-between px-4 lg:px-6 2xl:px-8 pb-3 pt-4">
<WindowChrome />
<div className="flex-1 px-1">
<Wordmark />
</div>
<div className="relative flex items-center gap-2.5">
<button
type="button"
aria-label={t`Activity`}
aria-pressed={activityMode}
title={t`Activity`}
data-activity-mode={activityMode ? "on" : "off"}
onClick={toggleActivityMode}
className={`app-no-drag flex size-control-sm items-center justify-center rounded-full ${
activityMode
? "bg-primary text-primary-foreground"
: "rk-sidebar-muted hover:text-foreground/75"
}`}
>
<Bell
strokeWidth={1.8}
fill={activityMode ? "currentColor" : "none"}
aria-hidden="true"
className="size-icon-md"
/>
</button>
<Popover open={createMenuOpen} onOpenChange={setCreateMenuOpen}>
<PopoverTrigger
className="app-no-drag text-[21px] rk-sidebar-muted hover:text-foreground/75"
title={t`Create`}
data-testid="create-menu-trigger"
>
+
</PopoverTrigger>
{/* Unmount with the state change so the panel it opens never coexists with the menu. */}
{createMenuOpen ? (
<PopoverContent
align="end"
className="app-no-drag w-auto gap-0 overflow-hidden p-0 data-closed:animate-none"
>
<BotCreatePicker
bots={bots}
onCreateBot={() => {
setCreateMenuOpen(false);
setMobileSidebarOpen(false);
setPanel("create");
}}
onOpenBot={(id) => {
setCreateMenuOpen(false);
setMobileSidebarOpen(false);
navigate(`/app/${id}`);
}}
onCreateGroup={() => {
setCreateMenuOpen(false);
setMobileSidebarOpen(false);
setPanel("create-group");
}}
onCreateSpace={() => {
setCreateMenuOpen(false);
setMobileSidebarOpen(false);
openNewSpaceDialog("space");
}}
onShowGroupInfo={() => {
setCreateMenuOpen(false);
setMobileSidebarOpen(false);
setPickerInfoTopic("group");
}}
onShowSpaceInfo={() => {
setCreateMenuOpen(false);
setMobileSidebarOpen(false);
setPickerInfoTopic("space");
}}
/>
</PopoverContent>
) : null}
</Popover>
</div>
</div>
<SidebarHeader
activityMode={activityMode}
onToggleActivityMode={toggleActivityMode}
createMenuOpen={createMenuOpen}
onCreateMenuOpenChange={setCreateMenuOpen}
onCloseMobileSidebar={() => setMobileSidebarOpen(false)}
bots={bots}
onCreateBot={() => setPanel("create")}
onOpenBot={(id) => navigate(`/app/${id}`)}
onCreateGroup={() => setPanel("create-group")}
onCreateSpace={() => openNewSpaceDialog("space")}
onShowGroupInfo={() => setPickerInfoTopic("group")}
onShowSpaceInfo={() => setPickerInfoTopic("space")}
/>
<InputGroup
data-testid="sidebar-search"
@ -3598,229 +3519,41 @@ export function ShellPage() {
return (
<div key={group.key} data-sidebar-group={group.key}>
{group.title ? (
<div className="flex items-center pt-3 pb-0.5">
<button
type="button"
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg px-2.5 py-1 text-ui-label font-semibold tracking-wider uppercase text-foreground/65 dark:text-muted-foreground/80 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
onClick={() => {
if (group.emptySpaceId) {
openSpaceChat(group.emptySpaceId, "/onboarding");
return;
}
toggleSidebarSection(group.key);
}}
onContextMenu={
group.sectionId
? (event) => {
event.preventDefault();
// Prefer section rename over delete-space when both apply;
// the dedicated space-actions button still opens the space menu.
const sections =
group.spaceId === bootstrapMe?.spaceId
? botSections
: (spaces.find((space) => space.id === group.spaceId)
?.botSections ?? []);
const section = sections.find(
(item) => item.id === group.sectionId,
);
if (!section) return;
sectionMenuAnchor.current = event.currentTarget;
setSectionMenu({
section,
spaceId: group.spaceId,
position: { x: event.clientX, y: event.clientY },
});
}
: group.canDeleteSpace
? (event) => {
event.preventDefault();
spaceMenuAnchor.current = event.currentTarget;
setSpaceMenu({
id: group.spaceId,
position: { x: event.clientX, y: event.clientY },
});
}
: undefined
}
aria-expanded={group.emptySpaceId ? undefined : !collapsed}
aria-label={
group.emptySpaceId
? t`Open ${group.title}`
: collapsed
? t`Expand ${group.title}`
: t`Collapse ${group.title}`
}
>
<span className="flex min-w-0 items-center gap-1.5 truncate">
<Folder strokeWidth={2} aria-hidden="true" className="size-icon-sm" />
<span className="truncate">{group.title}</span>
</span>
{group.emptySpaceId ? null : (
<ChevronDown
strokeWidth={1.8}
className={`size-icon-md ${collapsed ? "-rotate-90 transition-transform" : "transition-transform"}`}
aria-hidden="true"
/>
)}
</button>
{group.canDeleteSpace ? (
<Button
variant="ghost"
size="icon-sm"
aria-label={t`Actions for ${group.spaceName}`}
onClick={(event) => {
const rect = event.currentTarget.getBoundingClientRect();
spaceMenuAnchor.current = event.currentTarget;
setSpaceMenu({
id: group.spaceId,
position: { x: rect.left, y: rect.bottom },
});
}}
>
<MoreHorizontal aria-hidden="true" className="size-icon-md" />
</Button>
) : null}
</div>
<SidebarGroupHeader
variant="conversation"
group={group}
collapsed={collapsed}
activeSpaceId={bootstrapMe?.spaceId}
botSections={botSections}
spaces={spaces}
onOpenEmptySpace={(spaceId) => openSpaceChat(spaceId, "/onboarding")}
onToggle={toggleSidebarSection}
sectionMenuAnchor={sectionMenuAnchor}
onSectionMenu={setSectionMenu}
spaceMenuAnchor={spaceMenuAnchor}
onSpaceMenu={setSpaceMenu}
/>
) : null}
{!collapsed &&
group.bots
.slice(0, expandedTeamGroups[group.key] ? undefined : 4)
.map((item) => (
<button
<TeamRosterRow
key={`${item.kind}:${item.chat.id}`}
type="button"
draggable={item.kind === "bot"}
data-roster-bot-id={item.kind === "bot" ? item.chat.id : undefined}
aria-keyshortcuts={
item.kind === "bot" ? "Alt+ArrowUp Alt+ArrowDown" : undefined
}
onDragStart={(event) => {
if (item.kind !== "bot") return;
setDraggedBotId(item.chat.id);
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", item.chat.id);
}}
onDragOver={(event) => {
if (
item.kind === "bot" &&
draggedBotId &&
groupBotIds.includes(draggedBotId)
) {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}
}}
onDrop={(event) => {
if (item.kind !== "bot" || !draggedBotId) return;
event.preventDefault();
reorderRosterBot(draggedBotId, item.chat.id, groupBotIds);
setDraggedBotId(null);
}}
onDragEnd={() => setDraggedBotId(null)}
onKeyDown={(event) => {
if (
item.kind !== "bot" ||
!event.altKey ||
(event.key !== "ArrowUp" && event.key !== "ArrowDown")
)
return;
const index = groupBotIds.indexOf(item.chat.id);
const target =
groupBotIds[index + (event.key === "ArrowUp" ? -1 : 1)];
if (!target) return;
event.preventDefault();
reorderRosterBot(item.chat.id, target, groupBotIds);
}}
aria-label={
item.kind === "bot"
? t`${item.chat.name}, 1:1 conversation`
: t`${item.chat.name}, group conversation`
}
onClick={() => {
item={item}
groupBotIds={groupBotIds}
draggedBotId={draggedBotId}
onDraggedBotIdChange={setDraggedBotId}
onReorder={reorderRosterBot}
selected={!inGroup && active?.id === item.chat.id}
onOpen={() => {
setActiveNavPillar("team");
openSpaceChat(item.chat.spaceId, `/app/${item.chat.id}`);
}}
onContextMenu={(event) => {
if (item.chat.spaceId !== bootstrapMe?.spaceId) return;
event.preventDefault();
botMenuAnchor.current = event.currentTarget;
setBotMenu({
kind: item.kind,
id: item.chat.id,
position: { x: event.clientX, y: event.clientY },
});
}}
className={`${rosterRowClass(
!inGroup && active?.id === item.chat.id,
)} cursor-grab active:cursor-grabbing`}
style={{
opacity: draggedBotId === item.chat.id ? 0.55 : 1,
}}
>
<div className="relative shrink-0">
<BotAvatar
color={item.chat.color}
identity={item.chat.id}
size={40}
status={item.chat.status}
/>
<span
className={`absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full border-2 border-sidebar ${
rosterStatusDot(item.chat.status) === "idle"
? "bg-success"
: "bg-warning animate-pulse"
}`}
/>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-1.5">
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
<span
dir="auto"
data-roster-bot-name=""
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${rosterNameClass(
item.chat.unread,
)}`}
>
{item.chat.name}
</span>
{needsYouChipLabel(item.chat.status) ? (
<span className="shrink-0 rounded-full border border-warning/30 bg-warning/10 px-2 py-0.5 text-ui-label font-medium text-foreground dark:text-warning">
{needsYouChipLabel(item.chat.status)}
</span>
) : item.chat.title ? (
<span className="min-w-0 truncate rounded-full border border-border bg-muted px-2 py-0.5 text-ui-label font-medium text-muted-foreground">
{item.chat.title}
</span>
) : null}
{item.chat.unread ? (
<span className="sr-only">
<Trans> (unread)</Trans>
</span>
) : null}
</div>
<div className="flex shrink-0 items-center gap-1.5">
<span className={rosterTimeClass}>
{formatRosterTime(item.chat.updatedAt)}
</span>
{item.chat.unread ? (
<span aria-hidden="true" className={rosterUnreadDotClass} />
) : null}
</div>
</div>
<div
dir="auto"
className={`mt-0.5 truncate text-ui-body leading-snug ${rosterPreviewClass(
item.chat.unread,
)}`}
>
{rosterActivitySubtitle(
item.chat.status,
cleanRosterPreview(item.chat.preview),
)}
</div>
</div>
</button>
activeSpaceId={bootstrapMe?.spaceId}
menuAnchor={botMenuAnchor}
onMenu={setBotMenu}
/>
))}
{!collapsed && group.bots.length > 4 && (
<button
@ -3894,98 +3627,20 @@ export function ShellPage() {
<SortableGroup key={`proj-${group.key}`} group={group}>
<div data-sidebar-group={group.key}>
{group.title ? (
<div className="flex items-center pt-3 pb-0.5 relative">
{/* Drag grip — positioned to the left of the title text */}
<ProjectDragGrip className="absolute left-1 top-1/2 -translate-y-1/2 w-6 h-6 rounded flex items-center justify-center text-muted-foreground/30 hover:text-muted-foreground/70 active:text-muted-foreground transition-colors cursor-grab active:cursor-grabbing">
<GripVertical className="size-icon-sm" strokeWidth={2} />
</ProjectDragGrip>
<button
type="button"
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg pl-8 py-1 text-ui-label font-semibold tracking-wider uppercase text-foreground/65 dark:text-muted-foreground/80 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
onClick={() => {
if (group.emptySpaceId) {
void startProject(group.emptySpaceId);
return;
}
toggleSidebarSection(group.key);
}}
onContextMenu={
group.sectionId
? (event) => {
event.preventDefault();
const sections =
group.spaceId === bootstrapMe?.spaceId
? botSections
: (spaces.find(
(space) => space.id === group.spaceId,
)?.botSections ?? []);
const section = sections.find(
(item) => item.id === group.sectionId,
);
if (!section) return;
sectionMenuAnchor.current = event.currentTarget;
setSectionMenu({
section,
spaceId: group.spaceId,
position: { x: event.clientX, y: event.clientY },
});
}
: group.canDeleteSpace
? (event) => {
event.preventDefault();
spaceMenuAnchor.current = event.currentTarget;
setSpaceMenu({
id: group.spaceId,
position: { x: event.clientX, y: event.clientY },
});
}
: undefined
}
aria-expanded={group.emptySpaceId ? undefined : !collapsed}
aria-label={
group.emptySpaceId
? t`Select ${group.title}`
: collapsed
? t`Expand ${group.title}`
: t`Collapse ${group.title}`
}
>
<span className="flex min-w-0 items-center gap-1.5 truncate">
{group.showLock ? (
<Lock
strokeWidth={2}
aria-hidden="true"
className="size-icon-sm"
/>
) : null}
<span className="truncate">{group.title}</span>
</span>
{group.emptySpaceId ? null : (
<ChevronDown
strokeWidth={1.8}
className={`size-icon-md ${collapsed ? "-rotate-90 transition-transform" : "transition-transform"}`}
aria-hidden="true"
/>
)}
</button>
{group.canDeleteSpace ? (
<Button
variant="ghost"
size="icon-sm"
aria-label={t`Actions for ${group.spaceName}`}
onClick={(event) => {
const rect = event.currentTarget.getBoundingClientRect();
spaceMenuAnchor.current = event.currentTarget;
setSpaceMenu({
id: group.spaceId,
position: { x: rect.left, y: rect.bottom },
});
}}
>
<MoreHorizontal aria-hidden="true" className="size-icon-md" />
</Button>
) : null}
</div>
<SidebarGroupHeader
variant="project"
group={group}
collapsed={collapsed}
activeSpaceId={bootstrapMe?.spaceId}
botSections={botSections}
spaces={spaces}
onOpenEmptySpace={(spaceId) => void startProject(spaceId)}
onToggle={toggleSidebarSection}
sectionMenuAnchor={sectionMenuAnchor}
onSectionMenu={setSectionMenu}
spaceMenuAnchor={spaceMenuAnchor}
onSpaceMenu={setSpaceMenu}
/>
) : null}
{!collapsed && group.startSpaceId ? (
<div className="px-3 pb-2">
@ -4003,17 +3658,15 @@ export function ShellPage() {
) : null}
{!collapsed &&
group.bots.map((item) => (
<button
<ProjectRosterRow
key={`${item.kind}:${item.chat.id}`}
type="button"
aria-label={
item={item}
selected={
item.kind === "bot"
? t`${item.chat.name}, 1:1 conversation`
: item.chat.kind === "main"
? t`${item.chat.name}, main conversation`
: t`${item.chat.name}, group conversation`
? !inGroup && active?.id === item.chat.id
: inGroup && activeGroup?.id === item.chat.id
}
onClick={() => {
onOpen={() => {
setActiveNavPillar("projekte");
openSpaceChat(
item.chat.spaceId,
@ -4022,71 +3675,10 @@ export function ShellPage() {
: `/app/g/${item.chat.id}`,
);
}}
onContextMenu={(event) => {
// A project's main conversation has no chat actions.
if (
item.chat.spaceId !== bootstrapMe?.spaceId ||
item.chat.kind === "main"
)
return;
event.preventDefault();
botMenuAnchor.current = event.currentTarget;
setBotMenu({
kind: item.kind,
id: item.chat.id,
position: { x: event.clientX, y: event.clientY },
});
}}
className={rosterRowClass(
item.kind === "bot"
? !inGroup && active?.id === item.chat.id
: inGroup && activeGroup?.id === item.chat.id,
)}
>
{item.kind === "bot" ? (
<BotAvatar
color={item.chat.color}
identity={item.chat.id}
size={40}
status={item.chat.status}
/>
) : (
<GroupAvatar members={(item.chat as any).members} size={40} />
)}
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-1.5">
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
<span
dir="auto"
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${rosterNameClass(
item.chat.unread,
)}`}
>
{item.chat.name}
</span>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<span className={rosterTimeClass}>
{formatRosterTime(item.chat.updatedAt)}
</span>
{item.chat.unread ? (
<span
aria-hidden="true"
className={rosterUnreadDotClass}
/>
) : null}
</div>
</div>
<div
dir="auto"
className={`mt-0.5 truncate text-ui-body leading-snug ${rosterPreviewClass(
item.chat.unread,
)}`}
>
{cleanRosterPreview(localizedPreview(item.chat))}
</div>
</div>
</button>
activeSpaceId={bootstrapMe?.spaceId}
menuAnchor={botMenuAnchor}
onMenu={setBotMenu}
/>
))}
</div>
</SortableGroup>
@ -4099,154 +3691,46 @@ export function ShellPage() {
</>
)}
{archivedBots.length + archivedGroups.length > 0 && !showSpaceSearch ? (
<div className="mt-2 border-t border-border pt-2">
<button
type="button"
aria-expanded={archivedOpen}
onClick={() => setArchivedOpen((open) => !open)}
className="flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-ui-body rk-sidebar-muted hover:bg-sidebar-accent"
>
<span>
<Trans>Archived</Trans>
</span>
<span>{archivedBots.length + archivedGroups.length}</span>
</button>
{archivedOpen ? (
<>
{archivedBots.map((bot) => (
<div key={bot.id} className="flex items-center gap-2 rounded-lg px-2.5 py-2">
<BotAvatar
color={bot.color}
identity={bot.id}
size={28}
status={bot.status}
/>
<span
className="min-w-0 flex-1 truncate text-ui-body text-foreground/75"
dir="auto"
>
{bot.name}
</span>
<Button
variant="ghost"
size="xs"
onClick={() =>
void rpc.bots.restore({ botId: bot.id }).then(() => refreshBots(true))
}
>
<Trans>Restore</Trans>
</Button>
<Button
variant="ghost"
size="xs"
className="text-destructive hover:text-destructive"
aria-label={t`Delete ${bot.name}`}
onClick={() => setDeleteTarget(bot)}
>
<Trans>Delete</Trans>
</Button>
</div>
))}
{archivedGroups.map((group) => (
<div key={group.id} className="flex items-center gap-2 rounded-lg px-2.5 py-2">
<GroupAvatar members={group.members} size={28} />
<span
className="min-w-0 flex-1 truncate text-ui-body text-foreground/75"
dir="auto"
>
{group.name}
</span>
<Button
variant="ghost"
size="xs"
onClick={() =>
void rpc.groups
.restore({ groupId: group.id })
.then(() => refreshBots(true))
}
>
<Trans>Restore</Trans>
</Button>
<Button
variant="ghost"
size="xs"
className="text-destructive hover:text-destructive"
aria-label={t`Delete ${group.name}`}
onClick={() => setDeleteGroupTarget(group)}
>
<Trans>Delete</Trans>
</Button>
</div>
))}
</>
) : null}
</div>
<ArchivedSection
bots={archivedBots}
groups={archivedGroups}
open={archivedOpen}
onToggle={() => setArchivedOpen((open) => !open)}
onRestoreBot={(bot) =>
void rpc.bots.restore({ botId: bot.id }).then(() => refreshBots(true))
}
onRestoreGroup={(group) =>
void rpc.groups.restore({ groupId: group.id }).then(() => refreshBots(true))
}
onDeleteBot={setDeleteTarget}
onDeleteGroup={setDeleteGroupTarget}
/>
) : null}
</div>
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
<PopoverTrigger
data-testid="user-menu-trigger"
className="flex items-center gap-[11px] px-[18px] py-3.5"
>
<span className="grid size-control-lg place-items-center rounded-full bg-accent text-ui-body-sm text-foreground/75">
{initials}
</span>
<span className="text-[14.5px] text-foreground/90">{userName}</span>
</PopoverTrigger>
{menuOpen ? (
<PopoverContent
side="top"
align="start"
className="w-[calc(316px-1.5rem)] max-w-[calc(100vw-3rem)] gap-0 p-1 data-closed:animate-none"
>
<Button
variant="ghost"
className="w-full justify-start font-normal"
aria-label={t`Settings`}
onClick={() => {
setMenuOpen(false);
openSettings("general");
}}
>
<Settings className="text-muted-foreground" strokeWidth={1.75} />
<Trans>Settings</Trans>
</Button>
<Button
variant="ghost"
className="w-full justify-start font-normal"
aria-label={t`Usage`}
onClick={() => {
setMenuOpen(false);
void rpc.usage
.summary()
.then(setUsage)
.catch(() => undefined);
openSettings("usage");
}}
>
<Gauge className="text-muted-foreground" strokeWidth={1.75} />
<Trans>Usage</Trans>
</Button>
<Button
variant="ghost"
className="w-full justify-start font-normal"
onClick={() =>
// This browser stops receiving the account's pushes once it signs out.
void disableWebPush()
.catch(() => undefined)
.then(() => authClient.signOut())
.then(() => {
clearSpaceSelection();
navigate("/");
})
}
>
<LogOut className="text-muted-foreground" strokeWidth={1.75} />
<Trans>Log out</Trans>
</Button>
</PopoverContent>
) : null}
</Popover>
<UserMenu
open={menuOpen}
onOpenChange={setMenuOpen}
initials={initials}
userName={userName}
onOpenSettings={() => openSettings("general")}
onOpenUsage={() => {
void rpc.usage
.summary()
.then(setUsage)
.catch(() => undefined);
openSettings("usage");
}}
onLogOut={() =>
// This browser stops receiving the account's pushes once it signs out.
void disableWebPush()
.catch(() => undefined)
.then(() => authClient.signOut())
.then(() => {
clearSpaceSelection();
navigate("/");
})
}
/>
</aside>
<button
@ -4301,29 +3785,11 @@ export function ShellPage() {
<div className="flex h-full flex-1 flex-col overflow-hidden">
<div className="app-drag flex items-center border-b border-sidebar-border px-3 py-[17px] md:px-[22px] lg:px-8 2xl:px-12">
<div className="flex min-w-0 items-center gap-2">
{botsSidebarCollapsed && desktopBridge() ? <WindowChrome /> : null}
<button
type="button"
aria-label={t`Open navigation`}
onClick={() => setMobileSidebarOpen(true)}
className="app-no-drag grid size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:hidden"
>
<Menu strokeWidth={1.7} className="size-icon-lg" />
</button>
<button
type="button"
data-testid="toggle-bots-sidebar"
aria-label={botsSidebarCollapsed ? t`Show bots` : t`Minimize bots`}
title={botsSidebarCollapsed ? t`Show bots` : t`Minimize bots`}
onClick={() => setBotsSidebarCollapsedPref(!botsSidebarCollapsed)}
className="app-no-drag hidden size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:grid"
>
{botsSidebarCollapsed ? (
<PanelLeftOpen strokeWidth={1.7} aria-hidden="true" className="size-icon-lg" />
) : (
<PanelLeftClose strokeWidth={1.7} aria-hidden="true" className="size-icon-lg" />
)}
</button>
<SidebarToggleButtons
collapsed={botsSidebarCollapsed}
onCollapsedChange={setBotsSidebarCollapsedPref}
onOpenNavigation={() => setMobileSidebarOpen(true)}
/>
</div>
</div>
@ -4362,30 +3828,11 @@ export function ShellPage() {
<>
<div className="app-drag flex items-center justify-between border-b border-sidebar-border px-3.5 py-2 md:px-6 md:py-3 lg:px-8 lg:py-4 2xl:px-12 2xl:py-6 bg-background/80 backdrop-blur-md">
<div className="flex min-w-0 items-center gap-2.5">
{/* Collapsed bots sidebar: this header is the leading edge for window chrome. */}
{botsSidebarCollapsed && desktopBridge() ? <WindowChrome /> : null}
<button
type="button"
aria-label={t`Open navigation`}
onClick={() => setMobileSidebarOpen(true)}
className="app-no-drag grid size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:hidden"
>
<Menu strokeWidth={1.7} className="size-icon-lg" />
</button>
<button
type="button"
data-testid="toggle-bots-sidebar"
aria-label={botsSidebarCollapsed ? t`Show bots` : t`Minimize bots`}
title={botsSidebarCollapsed ? t`Show bots` : t`Minimize bots`}
onClick={() => setBotsSidebarCollapsedPref(!botsSidebarCollapsed)}
className="app-no-drag hidden size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:grid"
>
{botsSidebarCollapsed ? (
<PanelLeftOpen strokeWidth={1.7} aria-hidden="true" className="size-icon-lg" />
) : (
<PanelLeftClose strokeWidth={1.7} aria-hidden="true" className="size-icon-lg" />
)}
</button>
<SidebarToggleButtons
collapsed={botsSidebarCollapsed}
onCollapsedChange={setBotsSidebarCollapsedPref}
onOpenNavigation={() => setMobileSidebarOpen(true)}
/>
{inGroup && projectSessionKey ? (
<button
type="button"

View File

@ -0,0 +1,85 @@
import { Trans, useLingui } from "@lingui/react/macro";
import type { Bot, Group } from "@rakazo/contracts";
import { BotAvatar, Button, GroupAvatar } from "@rakazo/ui-web";
/** Collapsible list of archived bots and groups at the foot of the sidebar. */
export function ArchivedSection({
bots,
groups,
open,
onToggle,
onRestoreBot,
onRestoreGroup,
onDeleteBot,
onDeleteGroup,
}: {
bots: Bot[];
groups: Group[];
open: boolean;
onToggle: () => void;
onRestoreBot: (bot: Bot) => void;
onRestoreGroup: (group: Group) => void;
onDeleteBot: (bot: Bot) => void;
onDeleteGroup: (group: Group) => void;
}) {
const { t } = useLingui();
return (
<div className="mt-2 border-t border-border pt-2">
<button
type="button"
aria-expanded={open}
onClick={onToggle}
className="flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-ui-body rk-sidebar-muted hover:bg-sidebar-accent"
>
<span>
<Trans>Archived</Trans>
</span>
<span>{bots.length + groups.length}</span>
</button>
{open ? (
<>
{bots.map((bot) => (
<div key={bot.id} className="flex items-center gap-2 rounded-lg px-2.5 py-2">
<BotAvatar color={bot.color} identity={bot.id} size={28} status={bot.status} />
<span className="min-w-0 flex-1 truncate text-ui-body text-foreground/75" dir="auto">
{bot.name}
</span>
<Button variant="ghost" size="xs" onClick={() => onRestoreBot(bot)}>
<Trans>Restore</Trans>
</Button>
<Button
variant="ghost"
size="xs"
className="text-destructive hover:text-destructive"
aria-label={t`Delete ${bot.name}`}
onClick={() => onDeleteBot(bot)}
>
<Trans>Delete</Trans>
</Button>
</div>
))}
{groups.map((group) => (
<div key={group.id} className="flex items-center gap-2 rounded-lg px-2.5 py-2">
<GroupAvatar members={group.members} size={28} />
<span className="min-w-0 flex-1 truncate text-ui-body text-foreground/75" dir="auto">
{group.name}
</span>
<Button variant="ghost" size="xs" onClick={() => onRestoreGroup(group)}>
<Trans>Restore</Trans>
</Button>
<Button
variant="ghost"
size="xs"
className="text-destructive hover:text-destructive"
aria-label={t`Delete ${group.name}`}
onClick={() => onDeleteGroup(group)}
>
<Trans>Delete</Trans>
</Button>
</div>
))}
</>
) : null}
</div>
);
}

View File

@ -0,0 +1,250 @@
import { Trans, useLingui } from "@lingui/react/macro";
import type { SpaceBot, SpaceGroup } from "@rakazo/contracts";
import { BotAvatar, GroupAvatar } from "@rakazo/ui-web";
import type { RefObject } from "react";
import {
needsYouChipLabel,
rosterActivitySubtitle,
rosterStatusDot,
} from "../../lib/agent-activity";
import { localizedPreview } from "../../lib/onboarding-copy";
import type { ContextMenuPosition } from "../BotContextMenu";
import { cleanRosterPreview, formatRosterTime } from "./roster-format";
import {
rosterNameClass,
rosterPreviewClass,
rosterRowClass,
rosterTimeClass,
rosterUnreadDotClass,
} from "./roster-row-style";
export type SidebarChatItem = { kind: "bot"; chat: SpaceBot } | { kind: "group"; chat: SpaceGroup };
export type BotMenuRequest = {
kind: "bot" | "group";
id: string;
position: ContextMenuPosition;
};
/** A bot in the conversations section: reorderable by drag or Alt+Arrow within its group. */
export function TeamRosterRow({
item,
groupBotIds,
draggedBotId,
onDraggedBotIdChange,
onReorder,
selected,
onOpen,
activeSpaceId,
menuAnchor,
onMenu,
}: {
item: Extract<SidebarChatItem, { kind: "bot" }>;
groupBotIds: string[];
draggedBotId: string | null;
onDraggedBotIdChange: (botId: string | null) => void;
onReorder: (sourceId: string, targetId: string, groupBotIds: string[]) => void;
selected: boolean;
onOpen: () => void;
activeSpaceId: string | undefined;
menuAnchor: RefObject<HTMLElement | null>;
onMenu: (menu: BotMenuRequest) => void;
}) {
const { t } = useLingui();
return (
<button
type="button"
draggable={item.kind === "bot"}
data-roster-bot-id={item.kind === "bot" ? item.chat.id : undefined}
aria-keyshortcuts={item.kind === "bot" ? "Alt+ArrowUp Alt+ArrowDown" : undefined}
onDragStart={(event) => {
if (item.kind !== "bot") return;
onDraggedBotIdChange(item.chat.id);
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", item.chat.id);
}}
onDragOver={(event) => {
if (item.kind === "bot" && draggedBotId && groupBotIds.includes(draggedBotId)) {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}
}}
onDrop={(event) => {
if (item.kind !== "bot" || !draggedBotId) return;
event.preventDefault();
onReorder(draggedBotId, item.chat.id, groupBotIds);
onDraggedBotIdChange(null);
}}
onDragEnd={() => onDraggedBotIdChange(null)}
onKeyDown={(event) => {
if (
item.kind !== "bot" ||
!event.altKey ||
(event.key !== "ArrowUp" && event.key !== "ArrowDown")
)
return;
const index = groupBotIds.indexOf(item.chat.id);
const target = groupBotIds[index + (event.key === "ArrowUp" ? -1 : 1)];
if (!target) return;
event.preventDefault();
onReorder(item.chat.id, target, groupBotIds);
}}
aria-label={
item.kind === "bot"
? t`${item.chat.name}, 1:1 conversation`
: t`${item.chat.name}, group conversation`
}
onClick={onOpen}
onContextMenu={(event) => {
if (item.chat.spaceId !== activeSpaceId) return;
event.preventDefault();
menuAnchor.current = event.currentTarget;
onMenu({
kind: item.kind,
id: item.chat.id,
position: { x: event.clientX, y: event.clientY },
});
}}
className={`${rosterRowClass(selected)} cursor-grab active:cursor-grabbing`}
style={{
opacity: draggedBotId === item.chat.id ? 0.55 : 1,
}}
>
<div className="relative shrink-0">
<BotAvatar
color={item.chat.color}
identity={item.chat.id}
size={40}
status={item.chat.status}
/>
<span
className={`absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full border-2 border-sidebar ${
rosterStatusDot(item.chat.status) === "idle" ? "bg-success" : "bg-warning animate-pulse"
}`}
/>
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-1.5">
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
<span
dir="auto"
data-roster-bot-name=""
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${rosterNameClass(
item.chat.unread,
)}`}
>
{item.chat.name}
</span>
{needsYouChipLabel(item.chat.status) ? (
<span className="shrink-0 rounded-full border border-warning/30 bg-warning/10 px-2 py-0.5 text-ui-label font-medium text-foreground dark:text-warning">
{needsYouChipLabel(item.chat.status)}
</span>
) : item.chat.title ? (
<span className="min-w-0 truncate rounded-full border border-border bg-muted px-2 py-0.5 text-ui-label font-medium text-muted-foreground">
{item.chat.title}
</span>
) : null}
{item.chat.unread ? (
<span className="sr-only">
<Trans> (unread)</Trans>
</span>
) : null}
</div>
<div className="flex shrink-0 items-center gap-1.5">
<span className={rosterTimeClass}>{formatRosterTime(item.chat.updatedAt)}</span>
{item.chat.unread ? <span aria-hidden="true" className={rosterUnreadDotClass} /> : null}
</div>
</div>
<div
dir="auto"
className={`mt-0.5 truncate text-ui-body leading-snug ${rosterPreviewClass(
item.chat.unread,
)}`}
>
{rosterActivitySubtitle(item.chat.status, cleanRosterPreview(item.chat.preview))}
</div>
</div>
</button>
);
}
/** A conversation in the projects section; a project's main conversation has no chat actions. */
export function ProjectRosterRow({
item,
selected,
onOpen,
activeSpaceId,
menuAnchor,
onMenu,
}: {
item: SidebarChatItem;
selected: boolean;
onOpen: () => void;
activeSpaceId: string | undefined;
menuAnchor: RefObject<HTMLElement | null>;
onMenu: (menu: BotMenuRequest) => void;
}) {
const { t } = useLingui();
return (
<button
type="button"
aria-label={
item.kind === "bot"
? t`${item.chat.name}, 1:1 conversation`
: item.chat.kind === "main"
? t`${item.chat.name}, main conversation`
: t`${item.chat.name}, group conversation`
}
onClick={onOpen}
onContextMenu={(event) => {
// A project's main conversation has no chat actions.
if (item.chat.spaceId !== activeSpaceId || item.chat.kind === "main") return;
event.preventDefault();
menuAnchor.current = event.currentTarget;
onMenu({
kind: item.kind,
id: item.chat.id,
position: { x: event.clientX, y: event.clientY },
});
}}
className={rosterRowClass(selected)}
>
{item.kind === "bot" ? (
<BotAvatar
color={item.chat.color}
identity={item.chat.id}
size={40}
status={item.chat.status}
/>
) : (
<GroupAvatar members={(item.chat as any).members} size={40} />
)}
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-1.5">
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
<span
dir="auto"
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${rosterNameClass(
item.chat.unread,
)}`}
>
{item.chat.name}
</span>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<span className={rosterTimeClass}>{formatRosterTime(item.chat.updatedAt)}</span>
{item.chat.unread ? <span aria-hidden="true" className={rosterUnreadDotClass} /> : null}
</div>
</div>
<div
dir="auto"
className={`mt-0.5 truncate text-ui-body leading-snug ${rosterPreviewClass(
item.chat.unread,
)}`}
>
{cleanRosterPreview(localizedPreview(item.chat))}
</div>
</div>
</button>
);
}

View File

@ -0,0 +1,161 @@
import { useLingui } from "@lingui/react/macro";
import type { BotSection, Space } from "@rakazo/contracts";
import { Button } from "@rakazo/ui-web";
import { ChevronDown, Folder, GripVertical, Lock, MoreHorizontal } from "lucide-react";
import type { RefObject } from "react";
import { ProjectDragGrip } from "../../components/SortableGroup";
import type { ContextMenuPosition } from "../BotContextMenu";
export type SidebarGroupHeaderGroup = {
key: string;
title: string | null;
sectionId: string | null;
spaceId: string;
spaceName: string;
canDeleteSpace: boolean;
emptySpaceId: string | undefined;
showLock: boolean;
};
export type SpaceMenuRequest = { id: string; position: ContextMenuPosition };
export type SectionMenuRequest = {
section: BotSection;
spaceId: string;
position: ContextMenuPosition;
};
/**
* Collapsible title of a sidebar group. Project groups add a drag grip, show a lock for other
* spaces and select an empty space instead of opening it.
*/
export function SidebarGroupHeader({
variant,
group,
collapsed,
activeSpaceId,
botSections,
spaces,
onOpenEmptySpace,
onToggle,
sectionMenuAnchor,
onSectionMenu,
spaceMenuAnchor,
onSpaceMenu,
}: {
variant: "conversation" | "project";
group: SidebarGroupHeaderGroup;
collapsed: boolean;
activeSpaceId: string | undefined;
botSections: BotSection[];
spaces: Space[];
onOpenEmptySpace: (spaceId: string) => void;
onToggle: (key: string) => void;
sectionMenuAnchor: RefObject<HTMLElement | null>;
onSectionMenu: (menu: SectionMenuRequest) => void;
spaceMenuAnchor: RefObject<HTMLElement | null>;
onSpaceMenu: (menu: SpaceMenuRequest) => void;
}) {
const { t } = useLingui();
const project = variant === "project";
return (
<div
className={
project ? "flex items-center pt-3 pb-0.5 relative" : "flex items-center pt-3 pb-0.5"
}
>
{project ? (
// Drag grip — positioned to the left of the title text
<ProjectDragGrip className="absolute left-1 top-1/2 -translate-y-1/2 w-6 h-6 rounded flex items-center justify-center text-muted-foreground/30 hover:text-muted-foreground/70 active:text-muted-foreground transition-colors cursor-grab active:cursor-grabbing">
<GripVertical className="size-icon-sm" strokeWidth={2} />
</ProjectDragGrip>
) : null}
<button
type="button"
className={`flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg ${project ? "pl-8" : "px-2.5"} py-1 text-ui-label font-semibold tracking-wider uppercase text-foreground/65 dark:text-muted-foreground/80 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring`}
onClick={() => {
if (group.emptySpaceId) {
onOpenEmptySpace(group.emptySpaceId);
return;
}
onToggle(group.key);
}}
onContextMenu={
group.sectionId
? (event) => {
event.preventDefault();
// Prefer section rename over delete-space when both apply;
// the dedicated space-actions button still opens the space menu.
const sections =
group.spaceId === activeSpaceId
? botSections
: (spaces.find((space) => space.id === group.spaceId)?.botSections ?? []);
const section = sections.find((item) => item.id === group.sectionId);
if (!section) return;
sectionMenuAnchor.current = event.currentTarget;
onSectionMenu({
section,
spaceId: group.spaceId,
position: { x: event.clientX, y: event.clientY },
});
}
: group.canDeleteSpace
? (event) => {
event.preventDefault();
spaceMenuAnchor.current = event.currentTarget;
onSpaceMenu({
id: group.spaceId,
position: { x: event.clientX, y: event.clientY },
});
}
: undefined
}
aria-expanded={group.emptySpaceId ? undefined : !collapsed}
aria-label={
group.emptySpaceId
? project
? t`Select ${group.title}`
: t`Open ${group.title}`
: collapsed
? t`Expand ${group.title}`
: t`Collapse ${group.title}`
}
>
<span className="flex min-w-0 items-center gap-1.5 truncate">
{project ? (
group.showLock ? (
<Lock strokeWidth={2} aria-hidden="true" className="size-icon-sm" />
) : null
) : (
<Folder strokeWidth={2} aria-hidden="true" className="size-icon-sm" />
)}
<span className="truncate">{group.title}</span>
</span>
{group.emptySpaceId ? null : (
<ChevronDown
strokeWidth={1.8}
className={`size-icon-md ${collapsed ? "-rotate-90 transition-transform" : "transition-transform"}`}
aria-hidden="true"
/>
)}
</button>
{group.canDeleteSpace ? (
<Button
variant="ghost"
size="icon-sm"
aria-label={t`Actions for ${group.spaceName}`}
onClick={(event) => {
const rect = event.currentTarget.getBoundingClientRect();
spaceMenuAnchor.current = event.currentTarget;
onSpaceMenu({
id: group.spaceId,
position: { x: rect.left, y: rect.bottom },
});
}}
>
<MoreHorizontal aria-hidden="true" className="size-icon-md" />
</Button>
) : null}
</div>
);
}

View File

@ -0,0 +1,118 @@
import { useLingui } from "@lingui/react/macro";
import type { Bot } from "@rakazo/contracts";
import { Popover, PopoverContent, PopoverTrigger, Wordmark } from "@rakazo/ui-web";
import { Bell } from "lucide-react";
import { WindowChrome } from "../WindowChrome";
import { BotCreatePicker } from "./bot-picker";
/** Window chrome, wordmark, the activity toggle and the create menu atop the sidebar. */
export function SidebarHeader({
activityMode,
onToggleActivityMode,
createMenuOpen,
onCreateMenuOpenChange,
onCloseMobileSidebar,
bots,
onCreateBot,
onOpenBot,
onCreateGroup,
onCreateSpace,
onShowGroupInfo,
onShowSpaceInfo,
}: {
activityMode: boolean;
onToggleActivityMode: () => void;
createMenuOpen: boolean;
onCreateMenuOpenChange: (open: boolean) => void;
onCloseMobileSidebar: () => void;
bots: Bot[];
onCreateBot: () => void;
onOpenBot: (id: string) => void;
onCreateGroup: () => void;
onCreateSpace: () => void;
onShowGroupInfo: () => void;
onShowSpaceInfo: () => void;
}) {
const { t } = useLingui();
return (
<div className="app-drag flex items-center justify-between px-4 lg:px-6 2xl:px-8 pb-3 pt-4">
<WindowChrome />
<div className="flex-1 px-1">
<Wordmark />
</div>
<div className="relative flex items-center gap-2.5">
<button
type="button"
aria-label={t`Activity`}
aria-pressed={activityMode}
title={t`Activity`}
data-activity-mode={activityMode ? "on" : "off"}
onClick={onToggleActivityMode}
className={`app-no-drag flex size-control-sm items-center justify-center rounded-full ${
activityMode
? "bg-primary text-primary-foreground"
: "rk-sidebar-muted hover:text-foreground/75"
}`}
>
<Bell
strokeWidth={1.8}
fill={activityMode ? "currentColor" : "none"}
aria-hidden="true"
className="size-icon-md"
/>
</button>
<Popover open={createMenuOpen} onOpenChange={onCreateMenuOpenChange}>
<PopoverTrigger
className="app-no-drag text-[21px] rk-sidebar-muted hover:text-foreground/75"
title={t`Create`}
data-testid="create-menu-trigger"
>
+
</PopoverTrigger>
{/* Unmount with the state change so the panel it opens never coexists with the menu. */}
{createMenuOpen ? (
<PopoverContent
align="end"
className="app-no-drag w-auto gap-0 overflow-hidden p-0 data-closed:animate-none"
>
<BotCreatePicker
bots={bots}
onCreateBot={() => {
onCreateMenuOpenChange(false);
onCloseMobileSidebar();
onCreateBot();
}}
onOpenBot={(id) => {
onCreateMenuOpenChange(false);
onCloseMobileSidebar();
onOpenBot(id);
}}
onCreateGroup={() => {
onCreateMenuOpenChange(false);
onCloseMobileSidebar();
onCreateGroup();
}}
onCreateSpace={() => {
onCreateMenuOpenChange(false);
onCloseMobileSidebar();
onCreateSpace();
}}
onShowGroupInfo={() => {
onCreateMenuOpenChange(false);
onCloseMobileSidebar();
onShowGroupInfo();
}}
onShowSpaceInfo={() => {
onCreateMenuOpenChange(false);
onCloseMobileSidebar();
onShowSpaceInfo();
}}
/>
</PopoverContent>
) : null}
</Popover>
</div>
</div>
);
}

View File

@ -0,0 +1,45 @@
import { useLingui } from "@lingui/react/macro";
import { Menu, PanelLeftClose, PanelLeftOpen } from "lucide-react";
import { desktopBridge } from "../../lib/desktop";
import { WindowChrome } from "../WindowChrome";
/** Leading controls of a main header: open the mobile navigation or collapse the bots sidebar. */
export function SidebarToggleButtons({
collapsed,
onCollapsedChange,
onOpenNavigation,
}: {
collapsed: boolean;
onCollapsedChange: (collapsed: boolean) => void;
onOpenNavigation: () => void;
}) {
const { t } = useLingui();
return (
<>
{/* Collapsed bots sidebar: this header is the leading edge for window chrome. */}
{collapsed && desktopBridge() ? <WindowChrome /> : null}
<button
type="button"
aria-label={t`Open navigation`}
onClick={onOpenNavigation}
className="app-no-drag grid size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:hidden"
>
<Menu strokeWidth={1.7} className="size-icon-lg" />
</button>
<button
type="button"
data-testid="toggle-bots-sidebar"
aria-label={collapsed ? t`Show bots` : t`Minimize bots`}
title={collapsed ? t`Show bots` : t`Minimize bots`}
onClick={() => onCollapsedChange(!collapsed)}
className="app-no-drag hidden size-control-lg shrink-0 place-items-center rounded-lg text-foreground/75 hover:bg-accent md:grid"
>
{collapsed ? (
<PanelLeftOpen strokeWidth={1.7} aria-hidden="true" className="size-icon-lg" />
) : (
<PanelLeftClose strokeWidth={1.7} aria-hidden="true" className="size-icon-lg" />
)}
</button>
</>
);
}

View File

@ -0,0 +1,73 @@
import { Trans, useLingui } from "@lingui/react/macro";
import { Button, Popover, PopoverContent, PopoverTrigger } from "@rakazo/ui-web";
import { Gauge, LogOut, Settings } from "lucide-react";
/** The account button at the foot of the sidebar and its menu. */
export function UserMenu({
open,
onOpenChange,
initials,
userName,
onOpenSettings,
onOpenUsage,
onLogOut,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
initials: string;
userName: string;
onOpenSettings: () => void;
onOpenUsage: () => void;
onLogOut: () => void;
}) {
const { t } = useLingui();
return (
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger
data-testid="user-menu-trigger"
className="flex items-center gap-[11px] px-[18px] py-3.5"
>
<span className="grid size-control-lg place-items-center rounded-full bg-accent text-ui-body-sm text-foreground/75">
{initials}
</span>
<span className="text-[14.5px] text-foreground/90">{userName}</span>
</PopoverTrigger>
{open ? (
<PopoverContent
side="top"
align="start"
className="w-[calc(316px-1.5rem)] max-w-[calc(100vw-3rem)] gap-0 p-1 data-closed:animate-none"
>
<Button
variant="ghost"
className="w-full justify-start font-normal"
aria-label={t`Settings`}
onClick={() => {
onOpenChange(false);
onOpenSettings();
}}
>
<Settings className="text-muted-foreground" strokeWidth={1.75} />
<Trans>Settings</Trans>
</Button>
<Button
variant="ghost"
className="w-full justify-start font-normal"
aria-label={t`Usage`}
onClick={() => {
onOpenChange(false);
onOpenUsage();
}}
>
<Gauge className="text-muted-foreground" strokeWidth={1.75} />
<Trans>Usage</Trans>
</Button>
<Button variant="ghost" className="w-full justify-start font-normal" onClick={onLogOut}>
<LogOut className="text-muted-foreground" strokeWidth={1.75} />
<Trans>Log out</Trans>
</Button>
</PopoverContent>
) : null}
</Popover>
);
}