refactor(desktop): import-session entry is a sidebar nav row; browser module named as a foreign_sessions sibling

- Sidebar: the entry joins SIDEBAR_NAV (same chrome, active state, data-tour
  handle as the other rows) instead of a one-off Button below the rail;
  i18n moves to sidebar.nav['session-import'].
- Reuse common.retry / common.refresh / common.back instead of duplicating them
  under sessionImport in six locales.
- hermes_cli/foreign_session_browser.py -> foreign_sessions_browser.py so it
  sorts as a sibling of the foreign_sessions module it extends.
This commit is contained in:
Teknium
2026-09-06 03:55:29 -07:00
parent dfd0660fd7
commit fe49acb670
14 changed files with 31 additions and 46 deletions
+9 -10
View File
@@ -3,10 +3,9 @@ import { sortableKeyboardCoordinates } from '@dnd-kit/sortable'
import { useStore } from '@nanostores/react'
import type * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useLocation, useNavigate } from 'react-router'
import { useLocation } from 'react-router'
import { PlatformAvatar } from '@/app/messaging/platform-icon'
import { SESSION_IMPORT_ROUTE } from '@/app/routes'
import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { ContextMenu, ContextMenuContent, ContextMenuTrigger } from '@/components/ui/context-menu'
@@ -140,6 +139,7 @@ import {
ARTIFACTS_ROUTE,
CRON_ROUTE,
MESSAGING_ROUTE,
SESSION_IMPORT_ROUTE,
SIDEBAR_NAV_AREA,
type SidebarNavContribution,
SKILLS_ROUTE
@@ -226,6 +226,12 @@ const SIDEBAR_NAV: SidebarNavItem[] = [
icon: props => <Codicon name="watch" {...props} />,
route: CRON_ROUTE,
keybindActionId: 'nav.cron'
},
{
id: 'session-import',
label: '',
icon: props => <Codicon name="cloud-download" {...props} />,
route: SESSION_IMPORT_ROUTE
}
]
@@ -334,7 +340,6 @@ export function ChatSidebar({
const { t } = useI18n()
const s = t.sidebar
const { pathname } = useLocation()
const navigate = useNavigate()
// Contributed nav rows (plugins pairing a page with a sidebar entry) render
// below the built-ins with the same chrome; active = at their route.
const navContributions = useContributions(SIDEBAR_NAV_AREA)
@@ -1499,6 +1504,7 @@ export function ChatSidebar({
(item.id === 'messaging' && currentView === 'messaging') ||
(item.id === 'artifacts' && currentView === 'artifacts') ||
(item.id === 'cron' && currentView === 'cron') ||
(item.id === 'session-import' && currentView === 'session-import') ||
// Contributed rows light up at their own route.
(Boolean(item.route) && pathname === item.route)
@@ -1619,13 +1625,6 @@ export function ChatSidebar({
</SidebarGroupContent>
</SidebarGroup>
<div className="shrink-0 px-3 py-2">
<Button onClick={() => navigate(SESSION_IMPORT_ROUTE)} size="sm" variant="text">
<Codicon name="cloud-download" />
{t.sessionImport.action}
</Button>
</div>
{showSessionSections && (
<div className="shrink-0 px-2 pb-1 pt-1">
<SearchField
@@ -116,7 +116,7 @@ export function SessionImportView({ owner, onClose, onOpenSession }: SessionImpo
rootClassName="bg-(--ui-bg-elevated)"
titlebarActions={
<Button
aria-label={copy.refresh}
aria-label={t.common.refresh}
disabled={sessions.isFetching}
onClick={() => {
void sessions.refetch()
@@ -183,7 +183,7 @@ export function SessionImportView({ owner, onClose, onOpenSession }: SessionImpo
{sessions.isError && (
<ErrorState className="p-5" description={copy.scanHelp} title={copy.scanError}>
<Button onClick={() => void sessions.refetch()} variant="secondary">
{copy.retry}
{t.common.retry}
</Button>
</ErrorState>
)}
@@ -261,7 +261,7 @@ export function SessionImportView({ owner, onClose, onOpenSession }: SessionImpo
<div className="px-8 pb-5 pt-6">
<div className="mb-4 hidden max-[760px]:block">
<Button onClick={() => setSelected(null)} variant="text">
{copy.back}
{t.common.back}
</Button>
</div>
<h2 className="text-xl font-medium leading-7 tracking-tight">{current.title}</h2>
@@ -274,7 +274,7 @@ export function SessionImportView({ owner, onClose, onOpenSession }: SessionImpo
{preview.isError && (
<ErrorState description={copy.previewHelp} title={copy.previewError}>
<Button onClick={() => void preview.refetch()} variant="secondary">
{copy.retry}
{t.common.retry}
</Button>
</ErrorState>
)}
+2 -1
View File
@@ -162,7 +162,8 @@ export type CommandDispatchResponse =
| SendCommandDispatchResponse
| PrefillCommandDispatchResponse
export type SidebarNavId = 'artifacts' | 'command-center' | 'cron' | 'messaging' | 'new-session' | 'settings' | 'skills'
export type SidebarNavId =
'artifacts' | 'command-center' | 'cron' | 'messaging' | 'new-session' | 'session-import' | 'settings' | 'skills'
export interface SidebarNavItem {
/** Built-in view id, or a contributed row's namespaced contribution id. */
+2 -4
View File
@@ -2,7 +2,6 @@ import { defineLocale } from './define-locale'
export const ar = defineLocale({
sessionImport: {
refresh: 'تحديث الجلسات',
title: 'المتابعة من تطبيق آخر',
subtitle: 'انقل محادثة إلى Hermes وتابع من حيث توقفت.',
action: 'استيراد جلسة',
@@ -12,7 +11,6 @@ export const ar = defineLocale({
all: 'الكل',
search: 'البحث في الجلسات المحملة',
scanning: 'جارٍ البحث عن المحادثات',
retry: 'حاول مرة أخرى',
scanError: 'تعذر العثور على الجلسات',
scanHelp: 'تحقق من اتصال الخادم ثم أعد المحاولة. قد تحتاج الخوادم القديمة إلى تحديث.',
empty: 'لا توجد محادثات',
@@ -24,7 +22,6 @@ export const ar = defineLocale({
messages: 'رسائل',
choose: 'محادثة تستحق المتابعة',
chooseHelp: 'اختر جلسة لقراءة سجلها قبل نقلها إلى Hermes.',
back: 'العودة إلى الجلسات',
previewLoading: 'جارٍ فتح المعاينة',
previewError: 'المعاينة غير متاحة',
previewHelp: 'ربما تم نقل الملف الأصلي أو تغييره. حدّث القائمة وحاول مرة أخرى.',
@@ -1734,7 +1731,8 @@ export const ar = defineLocale({
chat: 'المحادثة',
settings: 'الإعدادات',
cron: 'المهام المجدولة',
agents: 'الوكلاء'
agents: 'الوكلاء',
'session-import': 'استيراد جلسة'
},
searchAria: 'البحث في الجلسات',
searchPlaceholder: 'البحث في الجلسات...',
+2 -4
View File
@@ -4,7 +4,6 @@ import type { Translations } from './types'
export const en: Translations = {
sessionImport: {
refresh: 'Refresh sessions',
title: 'Continue from another app',
subtitle: 'Bring a conversation into Hermes and pick up where you left off.',
action: 'Import session',
@@ -14,7 +13,6 @@ export const en: Translations = {
all: 'All',
search: 'Search loaded sessions',
scanning: 'Finding conversations',
retry: 'Try again',
scanError: 'Could not find sessions',
scanHelp: 'Check your backend connection, then try again. Older backends may need an update.',
empty: 'No conversations found',
@@ -26,7 +24,6 @@ export const en: Translations = {
messages: 'messages',
choose: 'A conversation worth continuing',
chooseHelp: 'Choose a session to read its history before bringing it into Hermes.',
back: 'Back to sessions',
previewLoading: 'Opening preview',
previewError: 'Preview unavailable',
previewHelp: 'The source may have moved or changed. Refresh the list and try again.',
@@ -2360,7 +2357,8 @@ export const en: Translations = {
skills: 'Capabilities',
messaging: 'Messaging',
artifacts: 'Artifacts',
cron: 'Scheduled jobs'
cron: 'Scheduled jobs',
'session-import': 'Import session'
},
searchAria: 'Search sessions',
searchPlaceholder: 'Search sessions…',
+2 -4
View File
@@ -4,7 +4,6 @@ import { defineLocale } from './define-locale'
export const ja = defineLocale({
sessionImport: {
refresh: '一覧を更新',
title: '別のアプリから続ける',
subtitle: '会話をHermesに取り込み、続きを始めましょう。',
action: 'セッションを取り込む',
@@ -14,7 +13,6 @@ export const ja = defineLocale({
all: 'すべて',
search: '読み込み済みのセッションを検索',
scanning: '会話を検索中',
retry: '再試行',
scanError: 'セッションを取得できません',
scanHelp: 'バックエンドの接続を確認して再試行してください。古いバックエンドは更新が必要な場合があります。',
empty: '会話が見つかりません',
@@ -26,7 +24,6 @@ export const ja = defineLocale({
messages: 'メッセージ',
choose: '会話の続きを始めましょう',
chooseHelp: 'セッションを選び、取り込む前に履歴を確認できます。',
back: 'セッションに戻る',
previewLoading: 'プレビューを開いています',
previewError: 'プレビューできません',
previewHelp: '元のファイルが移動または変更された可能性があります。一覧を更新してください。',
@@ -2034,7 +2031,8 @@ export const ja = defineLocale({
skills: 'スキルとツール',
messaging: 'メッセージング',
artifacts: 'アーティファクト',
cron: 'スケジュール済みジョブ'
cron: 'スケジュール済みジョブ',
'session-import': 'セッションを取り込む'
},
searchAria: 'セッションを検索',
searchPlaceholder: 'セッションを検索…',
+2 -4
View File
@@ -25,7 +25,6 @@ const RU_NOUN = (count: number | string, one: string, few: string, many: string)
export const ru = defineLocale({
sessionImport: {
refresh: 'Обновить сессии',
title: 'Продолжить из другого приложения',
subtitle: 'Перенесите разговор в Hermes и продолжите с того места, где остановились.',
action: 'Импортировать сессию',
@@ -35,7 +34,6 @@ export const ru = defineLocale({
all: 'Все',
search: 'Поиск по загруженным сессиям',
scanning: 'Поиск разговоров',
retry: 'Повторить',
scanError: 'Не удалось найти сессии',
scanHelp: 'Проверьте подключение к серверу и повторите попытку. Старому серверу может требоваться обновление.',
empty: 'Разговоров пока нет',
@@ -47,7 +45,6 @@ export const ru = defineLocale({
messages: 'сообщений',
choose: 'Разговор, который стоит продолжить',
chooseHelp: 'Выберите сессию, чтобы прочитать историю перед импортом в Hermes.',
back: 'Назад к сессиям',
previewLoading: 'Открываем просмотр',
previewError: 'Просмотр недоступен',
previewHelp: 'Исходный файл мог переместиться или измениться. Обновите список и повторите попытку.',
@@ -2396,7 +2393,8 @@ export const ru = defineLocale({
skills: 'Возможности',
messaging: 'Сообщения',
artifacts: 'Артефакты',
cron: 'Запланированные задачи'
cron: 'Запланированные задачи',
'session-import': 'Импортировать сессию'
},
searchAria: 'Поиск сеансов',
searchPlaceholder: 'Поиск сеансов…',
-3
View File
@@ -52,7 +52,6 @@ interface AuxTaskCopy {
export interface Translations {
sessionImport: {
refresh: string
title: string
subtitle: string
action: string
@@ -62,7 +61,6 @@ export interface Translations {
all: string
search: string
scanning: string
retry: string
scanError: string
scanHelp: string
empty: string
@@ -74,7 +72,6 @@ export interface Translations {
messages: string
choose: string
chooseHelp: string
back: string
previewLoading: string
previewError: string
previewHelp: string
+2 -4
View File
@@ -4,7 +4,6 @@ import { defineLocale } from './define-locale'
export const zhHant = defineLocale({
sessionImport: {
refresh: '重新整理工作階段',
title: '從其他應用程式繼續',
subtitle: '將對話匯入 Hermes,接著上次的進度繼續。',
action: '匯入工作階段',
@@ -14,7 +13,6 @@ export const zhHant = defineLocale({
all: '全部',
search: '搜尋已載入的工作階段',
scanning: '正在尋找對話',
retry: '重試',
scanError: '無法尋找工作階段',
scanHelp: '請檢查後端連線並重試。舊版後端可能需要更新。',
empty: '找不到對話',
@@ -26,7 +24,6 @@ export const zhHant = defineLocale({
messages: '則訊息',
choose: '繼續一段對話',
chooseHelp: '選擇工作階段,在匯入 Hermes 前查看歷程記錄。',
back: '返回工作階段',
previewLoading: '正在開啟預覽',
previewError: '無法預覽',
previewHelp: '來源檔案可能已移動或變更。請重新整理清單後重試。',
@@ -1959,7 +1956,8 @@ export const zhHant = defineLocale({
skills: '技能與工具',
messaging: '訊息平台',
artifacts: '成品',
cron: '排程工作'
cron: '排程工作',
'session-import': '匯入工作階段'
},
searchAria: '搜尋工作階段',
searchPlaceholder: '搜尋工作階段…',
+2 -4
View File
@@ -4,7 +4,6 @@ import type { Translations } from './types'
export const zh: Translations = {
sessionImport: {
refresh: '刷新会话',
title: '从其他应用继续',
subtitle: '将对话导入 Hermes,接着上次的进度继续。',
action: '导入会话',
@@ -14,7 +13,6 @@ export const zh: Translations = {
all: '全部',
search: '搜索已加载的会话',
scanning: '正在查找对话',
retry: '重试',
scanError: '无法查找会话',
scanHelp: '请检查后端连接并重试。旧版后端可能需要更新。',
empty: '未找到对话',
@@ -26,7 +24,6 @@ export const zh: Translations = {
messages: '条消息',
choose: '继续一段对话',
chooseHelp: '选择会话,在导入 Hermes 前查看历史记录。',
back: '返回会话列表',
previewLoading: '正在打开预览',
previewError: '预览不可用',
previewHelp: '源文件可能已移动或更改。请刷新列表后重试。',
@@ -2526,7 +2523,8 @@ export const zh: Translations = {
skills: '技能与工具',
messaging: '消息平台',
artifacts: '产物',
cron: '定时任务'
cron: '定时任务',
'session-import': '导入会话'
},
searchAria: '搜索会话',
searchPlaceholder: '搜索会话…',
@@ -5,7 +5,7 @@ from pathlib import Path
import pytest
from hermes_cli.foreign_session_browser import list_foreign_sessions
from hermes_cli.foreign_sessions_browser import list_foreign_sessions
@pytest.mark.parametrize("operation", ["resolve", "stat"])
+1 -1
View File
@@ -60,7 +60,7 @@ def test_foreign_rpc_preview_import_and_profile_isolation(tmp_path, monkeypatch)
def test_foreign_pages_confine_handles_and_failed_import_rolls_back(tmp_path, monkeypatch):
from hermes_cli import foreign_session_browser as browser
from hermes_cli import foreign_sessions_browser as browser
from hermes_state import SessionDB
monkeypatch.setattr(Path, "home", lambda: tmp_path)
+2 -2
View File
@@ -8,7 +8,7 @@ method = _registry.method
@method("session.foreign.list")
def _foreign_list(rid, params):
from hermes_cli.foreign_session_browser import list_foreign_sessions
from hermes_cli.foreign_sessions_browser import list_foreign_sessions
try:
return _ok(rid, list_foreign_sessions(params.get("source"), params.get("offset", 0), params.get("limit", 25)))
except ValueError as exc:
@@ -18,7 +18,7 @@ def _foreign_list(rid, params):
def _foreign_history_request(rid, params, importing):
from hermes_cli.foreign_session_browser import import_browser_session, preview_foreign_session
from hermes_cli.foreign_sessions_browser import import_browser_session, preview_foreign_session
try:
with _profile_db(params) as db:
if db is None: