mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
Merge 108fabb839 into 53fb175855
This commit is contained in:
commit
1f8f827ad9
28 changed files with 1557 additions and 496 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -2353,8 +2353,8 @@ export function CommentSidePanel({
|
|||
<button
|
||||
type="button"
|
||||
className="icon-btn"
|
||||
title={t('chat.cliSettingsTitle')}
|
||||
aria-label={t('chat.cliSettingsAria')}
|
||||
title={t('chat.mentionButtonTitle')}
|
||||
aria-label={t('chat.mentionButtonAria')}
|
||||
disabled
|
||||
>
|
||||
<span className="composer-tools-at" aria-hidden>
|
||||
|
|
|
|||
|
|
@ -867,6 +867,10 @@ export const ar: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter للإرسال · اذكر الهدف والمحتوى والأسلوب والتنسيق",
|
||||
'chat.cliSettingsTitle': 'إعدادات CLI والنموذج',
|
||||
'chat.cliSettingsAria': 'فتح إعدادات CLI والنموذج',
|
||||
'chat.resourcesMenuTitle': 'الموارد',
|
||||
'chat.resourcesMenuAria': 'فتح قائمة الموارد',
|
||||
'chat.mentionButtonTitle': 'منتقي الإشارات',
|
||||
'chat.mentionButtonAria': 'فتح منتقي الإشارات',
|
||||
'chat.attachTitle': 'إرفاق ملفات (أو الصق / اسحب)',
|
||||
'chat.attachAria': 'إرفاق ملفات',
|
||||
'chat.importTitle': 'استيراد المصادر (قريباً)',
|
||||
|
|
|
|||
|
|
@ -755,6 +755,10 @@ export const de: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter zum Senden · Ziel, Inhalt, Stil und Format angeben",
|
||||
'chat.cliSettingsTitle': 'CLI- & Modelleinstellungen',
|
||||
'chat.cliSettingsAria': 'CLI- und Modelleinstellungen öffnen',
|
||||
'chat.resourcesMenuTitle': 'Ressourcen',
|
||||
'chat.resourcesMenuAria': 'Ressourcenmenü öffnen',
|
||||
'chat.mentionButtonTitle': 'Erwähnungsauswahl',
|
||||
'chat.mentionButtonAria': 'Erwähnungsauswahl öffnen',
|
||||
'chat.attachTitle': 'Dateien anhängen (oder einfügen / ablegen)',
|
||||
'chat.attachAria': 'Dateien anhängen',
|
||||
'chat.importTitle': 'Quellen importieren (demnächst)',
|
||||
|
|
|
|||
|
|
@ -1471,6 +1471,10 @@ export const en: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter to send · include goals, content, style, and format",
|
||||
'chat.cliSettingsTitle': 'CLI & model settings',
|
||||
'chat.cliSettingsAria': 'Open CLI and model settings',
|
||||
'chat.resourcesMenuTitle': 'Resources',
|
||||
'chat.resourcesMenuAria': 'Open resources menu',
|
||||
'chat.mentionButtonTitle': 'Mention picker',
|
||||
'chat.mentionButtonAria': 'Open mention picker',
|
||||
'chat.attachTitle': 'Attach files (or paste / drop)',
|
||||
'chat.attachAria': 'Attach files',
|
||||
'chat.importTitle': 'Import sources (coming soon)',
|
||||
|
|
|
|||
|
|
@ -756,6 +756,10 @@ export const esES: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter para enviar · incluye objetivo, contenido, estilo y formato",
|
||||
'chat.cliSettingsTitle': 'Ajustes de CLI y modelo',
|
||||
'chat.cliSettingsAria': 'Abrir ajustes de CLI y modelo',
|
||||
'chat.resourcesMenuTitle': 'Recursos',
|
||||
'chat.resourcesMenuAria': 'Abrir menú de recursos',
|
||||
'chat.mentionButtonTitle': 'Selector de menciones',
|
||||
'chat.mentionButtonAria': 'Abrir selector de menciones',
|
||||
'chat.attachTitle': 'Adjuntar archivos (o pegar / soltar)',
|
||||
'chat.attachAria': 'Adjuntar archivos',
|
||||
'chat.importTitle': 'Importar fuentes (próximamente)',
|
||||
|
|
|
|||
|
|
@ -889,6 +889,10 @@ export const fa: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter برای ارسال · هدف، محتوا، سبک و قالب را ذکر کنید",
|
||||
'chat.cliSettingsTitle': 'تنظیمات CLI و مدل',
|
||||
'chat.cliSettingsAria': 'باز کردن تنظیمات CLI و مدل',
|
||||
'chat.resourcesMenuTitle': 'منابع',
|
||||
'chat.resourcesMenuAria': 'باز کردن منوی منابع',
|
||||
'chat.mentionButtonTitle': 'انتخابگر منشن',
|
||||
'chat.mentionButtonAria': 'باز کردن انتخابگر منشن',
|
||||
'chat.attachTitle': 'ضمیمه کردن فایلها (یا چسباندن / رها کردن)',
|
||||
'chat.attachAria': 'ضمیمه کردن فایلها',
|
||||
'chat.importTitle': 'وارد کردن منابع (به زودی)',
|
||||
|
|
|
|||
|
|
@ -1403,6 +1403,10 @@ export const fr: Dict = {
|
|||
'chat.composerHint': '⌘/Ctrl + Entrée pour envoyer · indiquez objectif, contenu, style et format',
|
||||
'chat.cliSettingsTitle': 'Paramètres CLI et modèle',
|
||||
'chat.cliSettingsAria': 'Ouvrir les paramètres CLI et modèle',
|
||||
'chat.resourcesMenuTitle': 'Ressources',
|
||||
'chat.resourcesMenuAria': 'Ouvrir le menu des ressources',
|
||||
'chat.mentionButtonTitle': 'Sélecteur de mentions',
|
||||
'chat.mentionButtonAria': 'Ouvrir le sélecteur de mentions',
|
||||
'chat.attachTitle': 'Attacher des fichiers (ou coller / déposer)',
|
||||
'chat.attachAria': 'Attacher des fichiers',
|
||||
'chat.importTitle': 'Importer des sources (bientôt disponible)',
|
||||
|
|
|
|||
|
|
@ -867,6 +867,10 @@ export const hu: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter küldéshez · add meg a célt, tartalmat, stílust és formátumot",
|
||||
'chat.cliSettingsTitle': 'CLI- és modellbeállítások',
|
||||
'chat.cliSettingsAria': 'CLI- és modellbeállítások megnyitása',
|
||||
'chat.resourcesMenuTitle': 'Erőforrások',
|
||||
'chat.resourcesMenuAria': 'Erőforrások menü megnyitása',
|
||||
'chat.mentionButtonTitle': 'Megemlítésválasztó',
|
||||
'chat.mentionButtonAria': 'Megemlítésválasztó megnyitása',
|
||||
'chat.attachTitle': 'Fájlok csatolása (vagy beillesztés / húzás)',
|
||||
'chat.attachAria': 'Fájlok csatolása',
|
||||
'chat.importTitle': 'Források importálása (hamarosan)',
|
||||
|
|
|
|||
|
|
@ -980,6 +980,10 @@ export const id: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter untuk mengirim · sertakan tujuan, konten, gaya, dan format",
|
||||
'chat.cliSettingsTitle': 'Pengaturan CLI',
|
||||
'chat.cliSettingsAria': 'Buka pengaturan CLI',
|
||||
'chat.resourcesMenuTitle': 'Sumber daya',
|
||||
'chat.resourcesMenuAria': 'Buka menu sumber daya',
|
||||
'chat.mentionButtonTitle': 'Pemilih sebutan',
|
||||
'chat.mentionButtonAria': 'Buka pemilih sebutan',
|
||||
'chat.attachTitle': 'Lampirkan',
|
||||
'chat.attachAria': 'Lampirkan file atau konteks',
|
||||
'chat.importTitle': 'Impor',
|
||||
|
|
|
|||
|
|
@ -782,6 +782,10 @@ export const it: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Invio per inviare · includi obiettivo, contenuto, stile e formato",
|
||||
'chat.cliSettingsTitle': 'Impostazioni CLI e modello',
|
||||
'chat.cliSettingsAria': 'Apri impostazioni CLI e modello',
|
||||
'chat.resourcesMenuTitle': 'Risorse',
|
||||
'chat.resourcesMenuAria': 'Apri menu risorse',
|
||||
'chat.mentionButtonTitle': 'Selettore menzioni',
|
||||
'chat.mentionButtonAria': 'Apri selettore menzioni',
|
||||
'chat.attachTitle': 'Allega file (o incolla / trascina)',
|
||||
'chat.attachAria': 'Allega file',
|
||||
'chat.importTitle': 'Importa fonti (presto disponibile)',
|
||||
|
|
|
|||
|
|
@ -754,6 +754,10 @@ export const ja: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter で送信 · 目的、内容、スタイル、形式を含める",
|
||||
'chat.cliSettingsTitle': 'CLI とモデルの設定',
|
||||
'chat.cliSettingsAria': 'CLI とモデルの設定を開く',
|
||||
'chat.resourcesMenuTitle': 'リソース',
|
||||
'chat.resourcesMenuAria': 'リソースメニューを開く',
|
||||
'chat.mentionButtonTitle': 'メンションピッカー',
|
||||
'chat.mentionButtonAria': 'メンションピッカーを開く',
|
||||
'chat.attachTitle': 'ファイルを添付(または貼り付け / ドロップ)',
|
||||
'chat.attachAria': 'ファイルを添付',
|
||||
'chat.importTitle': 'ソースをインポート(近日公開)',
|
||||
|
|
|
|||
|
|
@ -867,6 +867,10 @@ export const ko: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter로 보내기 · 목표, 내용, 스타일, 형식을 포함하세요",
|
||||
'chat.cliSettingsTitle': 'CLI 및 모델 설정',
|
||||
'chat.cliSettingsAria': 'CLI 및 모델 설정 열기',
|
||||
'chat.resourcesMenuTitle': '리소스',
|
||||
'chat.resourcesMenuAria': '리소스 메뉴 열기',
|
||||
'chat.mentionButtonTitle': '멘션 선택기',
|
||||
'chat.mentionButtonAria': '멘션 선택기 열기',
|
||||
'chat.attachTitle': '파일 첨부 (또는 붙여넣기 / 끌어놓기)',
|
||||
'chat.attachAria': '파일 첨부',
|
||||
'chat.importTitle': '소스 가져오기 (곧 지원 예정)',
|
||||
|
|
|
|||
|
|
@ -867,6 +867,10 @@ export const pl: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter, aby wysłać · podaj cel, treść, styl i format",
|
||||
'chat.cliSettingsTitle': 'Ustawienia CLI i modelu',
|
||||
'chat.cliSettingsAria': 'Otwórz ustawienia CLI i modelu',
|
||||
'chat.resourcesMenuTitle': 'Zasoby',
|
||||
'chat.resourcesMenuAria': 'Otwórz menu zasobów',
|
||||
'chat.mentionButtonTitle': 'Wybór wzmianek',
|
||||
'chat.mentionButtonAria': 'Otwórz wybór wzmianek',
|
||||
'chat.attachTitle': 'Załącz pliki (lub wklej / przeciągnij)',
|
||||
'chat.attachAria': 'Załącz pliki',
|
||||
'chat.importTitle': 'Importuj źródła (wkrótce)',
|
||||
|
|
|
|||
|
|
@ -888,6 +888,10 @@ export const ptBR: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter para enviar · inclua objetivo, conteúdo, estilo e formato",
|
||||
'chat.cliSettingsTitle': 'Configurações de CLI e modelo',
|
||||
'chat.cliSettingsAria': 'Abrir configurações de CLI e modelo',
|
||||
'chat.resourcesMenuTitle': 'Recursos',
|
||||
'chat.resourcesMenuAria': 'Abrir menu de recursos',
|
||||
'chat.mentionButtonTitle': 'Seletor de menções',
|
||||
'chat.mentionButtonAria': 'Abrir seletor de menções',
|
||||
'chat.attachTitle': 'Anexar arquivos (ou colar / arrastar)',
|
||||
'chat.attachAria': 'Anexar arquivos',
|
||||
'chat.importTitle': 'Importar fontes (em breve)',
|
||||
|
|
|
|||
|
|
@ -888,6 +888,10 @@ export const ru: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter для отправки · укажите цель, содержание, стиль и формат",
|
||||
'chat.cliSettingsTitle': 'Настройки CLI и модели',
|
||||
'chat.cliSettingsAria': 'Открыть настройки CLI и модели',
|
||||
'chat.resourcesMenuTitle': 'Ресурсы',
|
||||
'chat.resourcesMenuAria': 'Открыть меню ресурсов',
|
||||
'chat.mentionButtonTitle': 'Выбор упоминаний',
|
||||
'chat.mentionButtonAria': 'Открыть выбор упоминаний',
|
||||
'chat.attachTitle': 'Прикрепить файлы (или вставить / перетащить)',
|
||||
'chat.attachAria': 'Прикрепить файлы',
|
||||
'chat.importTitle': 'Импортировать источники (скоро)',
|
||||
|
|
|
|||
|
|
@ -822,6 +822,10 @@ export const th: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter เพื่อส่ง · ระบุเป้าหมาย เนื้อหา สไตล์ และรูปแบบ",
|
||||
'chat.cliSettingsTitle': 'ตั้งค่าโมเดลและ CLI',
|
||||
'chat.cliSettingsAria': 'ตั้งค่า',
|
||||
'chat.resourcesMenuTitle': 'ทรัพยากร',
|
||||
'chat.resourcesMenuAria': 'เปิดเมนูทรัพยากร',
|
||||
'chat.mentionButtonTitle': 'ตัวเลือกการกล่าวถึง',
|
||||
'chat.mentionButtonAria': 'เปิดตัวเลือกการกล่าวถึง',
|
||||
'chat.attachTitle': 'แนบไฟล์ (วาง / ลาก)',
|
||||
'chat.attachAria': 'แนบไฟล์',
|
||||
'chat.importTitle': 'นำเข้าจากที่อื่น (เร็วๆ นี้)',
|
||||
|
|
|
|||
|
|
@ -856,6 +856,10 @@ export const tr: Dict = {
|
|||
'chat.composerHint': "Göndermek için ⌘/Ctrl + Enter · hedef, içerik, stil ve formatı belirtin",
|
||||
'chat.cliSettingsTitle': 'CLI & model ayarları',
|
||||
'chat.cliSettingsAria': 'CLI ve model ayarlarını aç',
|
||||
'chat.resourcesMenuTitle': 'Kaynaklar',
|
||||
'chat.resourcesMenuAria': 'Kaynaklar menüsünü aç',
|
||||
'chat.mentionButtonTitle': 'Bahsetme seçici',
|
||||
'chat.mentionButtonAria': 'Bahsetme seçiciyi aç',
|
||||
'chat.attachTitle': 'Dosyaları iliştirin (veya yapıştırın / sürükleyin)',
|
||||
'chat.attachAria': 'Dosyaları iliştirin',
|
||||
'chat.importTitle': 'Kaynakları içe aktar (yakında)',
|
||||
|
|
|
|||
|
|
@ -889,6 +889,10 @@ export const uk: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter, щоб надіслати · вкажіть мету, зміст, стиль і формат",
|
||||
'chat.cliSettingsTitle': 'Налаштування CLI та моделі',
|
||||
'chat.cliSettingsAria': 'Відкрити налаштування CLI та моделі',
|
||||
'chat.resourcesMenuTitle': 'Ресурси',
|
||||
'chat.resourcesMenuAria': 'Відкрити меню ресурсів',
|
||||
'chat.mentionButtonTitle': 'Вибір згадок',
|
||||
'chat.mentionButtonAria': 'Відкрити вибір згадок',
|
||||
'chat.attachTitle': 'Прикріпити файли (або вставити / перенести)',
|
||||
'chat.attachAria': 'Прикріпити файли',
|
||||
'chat.importTitle': 'Імпортувати джерела (скоро)',
|
||||
|
|
|
|||
|
|
@ -1462,6 +1462,10 @@ export const zhCN: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter 发送 · 说清目标、内容、风格和格式",
|
||||
'chat.cliSettingsTitle': 'CLI 与模型设置',
|
||||
'chat.cliSettingsAria': '打开 CLI 与模型设置',
|
||||
'chat.resourcesMenuTitle': '资源',
|
||||
'chat.resourcesMenuAria': '打开资源菜单',
|
||||
'chat.mentionButtonTitle': '提及选择器',
|
||||
'chat.mentionButtonAria': '打开提及选择器',
|
||||
'chat.attachTitle': '附加文件(也可以粘贴/拖入)',
|
||||
'chat.attachAria': '附加文件',
|
||||
'chat.importTitle': '导入素材(即将上线)',
|
||||
|
|
|
|||
|
|
@ -1062,6 +1062,10 @@ export const zhTW: Dict = {
|
|||
'chat.composerHint': "⌘/Ctrl + Enter 傳送 · 說清目標、內容、風格和格式",
|
||||
'chat.cliSettingsTitle': 'CLI 與模型設定',
|
||||
'chat.cliSettingsAria': '開啟 CLI 與模型設定',
|
||||
'chat.resourcesMenuTitle': '資源',
|
||||
'chat.resourcesMenuAria': '開啟資源選單',
|
||||
'chat.mentionButtonTitle': '提及選擇器',
|
||||
'chat.mentionButtonAria': '開啟提及選擇器',
|
||||
'chat.attachTitle': '附加檔案(也可以貼上/拖入)',
|
||||
'chat.attachAria': '附加檔案',
|
||||
'chat.importTitle': '匯入素材(即將上線)',
|
||||
|
|
|
|||
|
|
@ -1796,6 +1796,10 @@ export interface Dict {
|
|||
'chat.composerHint': string;
|
||||
'chat.cliSettingsTitle': string;
|
||||
'chat.cliSettingsAria': string;
|
||||
'chat.resourcesMenuTitle': string;
|
||||
'chat.resourcesMenuAria': string;
|
||||
'chat.mentionButtonTitle': string;
|
||||
'chat.mentionButtonAria': string;
|
||||
'chat.attachTitle': string;
|
||||
'chat.attachAria': string;
|
||||
'chat.importTitle': string;
|
||||
|
|
|
|||
|
|
@ -1886,8 +1886,8 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
|
|||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
|
|
@ -1896,7 +1896,8 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
|
|||
width: 100%;
|
||||
}
|
||||
.composer-import-item:hover {
|
||||
background: var(--bg-subtle);
|
||||
background: color-mix(in srgb, var(--bg-panel) 72%, var(--bg-subtle));
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
.composer-import-item-enabled {
|
||||
|
|
@ -1904,7 +1905,8 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
|
|||
color: var(--text);
|
||||
}
|
||||
.composer-import-item-enabled:hover {
|
||||
background: var(--bg-subtle);
|
||||
background: color-mix(in srgb, var(--bg-panel) 72%, var(--bg-subtle));
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
.composer-import-item-label { flex: 1; }
|
||||
|
|
|
|||
|
|
@ -598,18 +598,45 @@
|
|||
/* ============================================================
|
||||
Composer — consolidated Tools popover
|
||||
------------------------------------------------------------
|
||||
The leading @ glyph now opens a single tabbed popover
|
||||
containing MCP / Import / Pet quick actions plus a shortcut
|
||||
to the full Settings dialog. Replaces three standalone row
|
||||
buttons that were overflowing in narrow chats.
|
||||
The @ glyph opens the unified mention picker. The sliders icon
|
||||
opens the tabbed resources popover for Plugins / Skills / MCP /
|
||||
Import quick actions. Keeping the two entry points distinct
|
||||
prevents the mention picker and resources menu from drifting.
|
||||
============================================================ */
|
||||
.composer-tools-wrap {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.composer-mention-trigger,
|
||||
.composer-tools-trigger {
|
||||
position: relative;
|
||||
}
|
||||
.composer-row .icon-btn.composer-tools-trigger {
|
||||
width: auto;
|
||||
min-width: 94px;
|
||||
flex: 0 0 auto;
|
||||
gap: 6px;
|
||||
padding: 0 9px;
|
||||
border: 1px solid var(--border-soft);
|
||||
background: color-mix(in srgb, var(--bg-subtle) 62%, transparent);
|
||||
}
|
||||
.composer-row .icon-btn.composer-tools-trigger:hover:not(:disabled) {
|
||||
border-color: var(--border);
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
.composer-tools-label {
|
||||
display: inline-block;
|
||||
flex: 0 0 auto;
|
||||
max-width: 6.5rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.composer-tools-at {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -624,17 +651,26 @@
|
|||
color: currentColor;
|
||||
transform: translateY(-0.5px);
|
||||
}
|
||||
.composer-tools-trigger.active {
|
||||
.composer-mention-trigger.active {
|
||||
background: var(--bg-subtle);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
.composer-tools-trigger.active {
|
||||
background: color-mix(in srgb, var(--text) 9%, var(--bg-subtle));
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
.composer-tools-menu {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 0;
|
||||
width: 320px;
|
||||
width: 420px;
|
||||
max-width: calc(100vw - 32px);
|
||||
z-index: 30;
|
||||
height: min(480px, 72vh);
|
||||
min-height: 248px;
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
|
|
@ -645,28 +681,35 @@
|
|||
}
|
||||
.composer-tools-tabs {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
min-height: 42px;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
background: var(--bg-subtle);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.composer-tools-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.composer-tools-tab {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
min-height: 30px;
|
||||
padding: 6px 6px;
|
||||
padding: 5px 10px;
|
||||
font-size: 11.5px;
|
||||
line-height: 18px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
.composer-tools-tab:hover { color: var(--text); }
|
||||
.composer-tools-tab.active {
|
||||
|
|
@ -692,18 +735,19 @@
|
|||
align-self: center;
|
||||
}
|
||||
.composer-tools-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
max-height: 360px;
|
||||
padding: 3px 0 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.composer-tools-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 2px 2px 6px;
|
||||
padding: 6px 8px 4px;
|
||||
}
|
||||
.composer-tools-segments {
|
||||
display: grid;
|
||||
|
|
@ -718,7 +762,9 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
min-height: 28px;
|
||||
min-width: 0;
|
||||
padding: 5px 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: calc(var(--radius-sm) - 1px);
|
||||
|
|
@ -727,7 +773,8 @@
|
|||
font: inherit;
|
||||
font-size: 11.5px;
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.composer-tools-segment:hover {
|
||||
color: var(--text);
|
||||
|
|
@ -740,15 +787,10 @@
|
|||
font-weight: 600;
|
||||
box-shadow: var(--shadow-xs);
|
||||
}
|
||||
.composer-tools-segment {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.composer-tools-search {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
padding: 0 9px;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-panel);
|
||||
|
|
@ -756,8 +798,8 @@
|
|||
font-size: 12px;
|
||||
}
|
||||
.composer-tools-search:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--text) 12%, transparent);
|
||||
}
|
||||
.composer-tools-empty {
|
||||
padding: 14px 10px;
|
||||
|
|
@ -769,30 +811,32 @@
|
|||
.composer-tools-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 0;
|
||||
}
|
||||
.composer-tools-section-label {
|
||||
padding: 4px 6px 2px;
|
||||
padding: 6px 8px 2px;
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.composer-tools-row {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
width: 100%;
|
||||
min-height: 32px;
|
||||
padding: 7px 10px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
.composer-tools-row:hover {
|
||||
.composer-tools-row:hover,
|
||||
.composer-tools-row[aria-selected="true"] {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text);
|
||||
}
|
||||
|
|
@ -801,9 +845,10 @@
|
|||
border-color: var(--border);
|
||||
}
|
||||
.composer-tools-row-body {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
@ -813,47 +858,29 @@
|
|||
}
|
||||
.composer-tools-row-meta {
|
||||
font-size: 10.5px;
|
||||
color: var(--text-faint);
|
||||
text-transform: lowercase;
|
||||
}
|
||||
/* Plugins panel — each row is a horizontal split: the main button
|
||||
applies the plugin, the side button opens the details modal. */
|
||||
.composer-tools-row--plugin {
|
||||
padding: 0;
|
||||
gap: 0;
|
||||
}
|
||||
.composer-tools-row--plugin .composer-tools-row-body {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 1px;
|
||||
}
|
||||
.composer-tools-row--plugin .composer-tools-row-meta {
|
||||
text-transform: none;
|
||||
white-space: normal;
|
||||
line-height: 1.3;
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.composer-tools-row-main {
|
||||
flex: 1;
|
||||
display: inline-flex;
|
||||
.composer-tools-row-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
min-width: 0;
|
||||
gap: 0;
|
||||
padding: 0 6px 0 0;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.composer-tools-row-main:hover {
|
||||
.composer-tools-row-group:hover,
|
||||
.composer-tools-row-group:has(.composer-tools-row[aria-selected="true"]) {
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
.composer-tools-row-main:disabled {
|
||||
.composer-tools-row-group .composer-tools-row {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.composer-tools-row:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
@ -861,7 +888,8 @@
|
|||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
width: 30px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
|
@ -871,18 +899,43 @@
|
|||
}
|
||||
.composer-tools-row-side:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-subtle);
|
||||
background: color-mix(in srgb, var(--bg-panel) 60%, transparent);
|
||||
}
|
||||
.composer-tools-row-side:disabled {
|
||||
color: var(--text-faint);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.42;
|
||||
}
|
||||
.composer-tools-row-pending {
|
||||
font-size: 10.5px;
|
||||
color: var(--text-faint);
|
||||
margin-left: auto;
|
||||
}
|
||||
.composer-tools-row-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.composer-tools-action-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
min-width: 42px;
|
||||
min-height: 20px;
|
||||
padding: 0 7px;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 999px;
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-muted);
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.composer-tools-row-action {
|
||||
border-top: 1px solid var(--border-soft);
|
||||
border-radius: 0;
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.composer-tools-row-action:hover { color: var(--text); }
|
||||
|
|
@ -1059,4 +1112,3 @@
|
|||
.pet-custom-sprite {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,12 @@
|
|||
color: var(--text);
|
||||
min-height: 32px;
|
||||
}
|
||||
.mention-item:hover { background: var(--bg-subtle); border-color: transparent; }
|
||||
.mention-item:hover,
|
||||
.mention-item.is-active,
|
||||
.mention-item[aria-selected="true"] {
|
||||
background: var(--bg-subtle);
|
||||
border-color: transparent;
|
||||
}
|
||||
.mention-section-label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// @vitest-environment jsdom
|
||||
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
|
|
@ -41,6 +41,22 @@ const USER_PLUGIN = {
|
|||
},
|
||||
};
|
||||
|
||||
function makePlugin(overrides: { id: string; title: string; description: string }): typeof COMMUNITY_PLUGIN {
|
||||
return {
|
||||
...COMMUNITY_PLUGIN,
|
||||
id: overrides.id,
|
||||
title: overrides.title,
|
||||
source: `bundled/${overrides.id}`,
|
||||
manifest: {
|
||||
...COMMUNITY_PLUGIN.manifest,
|
||||
name: overrides.id,
|
||||
title: overrides.title,
|
||||
description: overrides.description,
|
||||
},
|
||||
fsPath: `/plugins/${overrides.id}`,
|
||||
};
|
||||
}
|
||||
|
||||
const SKILL = {
|
||||
id: 'deck-builder',
|
||||
name: 'Deck Builder',
|
||||
|
|
@ -82,6 +98,15 @@ const MCP_SERVER = {
|
|||
command: 'slack-mcp',
|
||||
};
|
||||
|
||||
const MCP_TEMPLATE = {
|
||||
id: 'figma-context',
|
||||
label: 'Figma Context',
|
||||
description: 'Read design frames from Figma files.',
|
||||
transport: 'stdio' as const,
|
||||
category: 'design-systems' as const,
|
||||
command: 'figma-mcp',
|
||||
};
|
||||
|
||||
const APPLY_RESULT = {
|
||||
ok: true,
|
||||
query: 'Run plugin.',
|
||||
|
|
@ -116,6 +141,7 @@ let fetchMock: ReturnType<typeof vi.fn>;
|
|||
let plugins = [COMMUNITY_PLUGIN, USER_PLUGIN];
|
||||
let skills = [SKILL];
|
||||
let servers = [MCP_SERVER];
|
||||
let templates = [MCP_TEMPLATE];
|
||||
|
||||
function renderComposer(
|
||||
overrides: Partial<ComponentProps<typeof ChatComposer>> = {},
|
||||
|
|
@ -152,9 +178,10 @@ beforeEach(() => {
|
|||
plugins = [COMMUNITY_PLUGIN, USER_PLUGIN];
|
||||
skills = [SKILL];
|
||||
servers = [MCP_SERVER];
|
||||
templates = [MCP_TEMPLATE];
|
||||
fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
|
||||
if (url === '/api/mcp/servers') {
|
||||
return new Response(JSON.stringify({ servers, templates: [] }), {
|
||||
return new Response(JSON.stringify({ servers, templates }), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
});
|
||||
|
|
@ -213,6 +240,181 @@ describe('ChatComposer context pickers', () => {
|
|||
expect(screen.getByText('Search plugins, skills, MCP servers, connectors, and Design Files.')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('opens the same @ panel from the composer mention button', async () => {
|
||||
renderComposer({
|
||||
projectFiles: [
|
||||
{
|
||||
path: 'designs/landing.html',
|
||||
name: 'landing.html',
|
||||
kind: 'html',
|
||||
mime: 'text/html',
|
||||
mtime: 1,
|
||||
size: 128,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open mention picker' }));
|
||||
|
||||
expect(screen.getByTestId('mention-popover')).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: 'Plugins' })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: 'Skills' })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: 'MCP' })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: 'Connectors' })).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: 'Design files' })).toBeTruthy();
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Community Deck')).toBeTruthy());
|
||||
expect(screen.getByText('Deck Builder')).toBeTruthy();
|
||||
expect(screen.getByText('Slack MCP')).toBeTruthy();
|
||||
expect(screen.getByText('designs/landing.html')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('navigates typed @ results with arrow keys and picks the active result', async () => {
|
||||
renderComposer();
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: '@', selectionStart: 1 },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getByText('My Export')).toBeTruthy());
|
||||
const popover = screen.getByTestId('mention-popover');
|
||||
const selectedText = () =>
|
||||
popover.querySelector('[role="option"][aria-selected="true"]')?.textContent ?? '';
|
||||
|
||||
expect(selectedText()).toContain('Community Deck');
|
||||
expect(input.getAttribute('aria-activedescendant')).toBe('chat-composer-mention-option-0');
|
||||
|
||||
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
||||
expect(selectedText()).toContain('My Export');
|
||||
expect(input.getAttribute('aria-activedescendant')).toBe('chat-composer-mention-option-1');
|
||||
|
||||
fireEvent.keyDown(input, { key: 'ArrowUp' });
|
||||
expect(selectedText()).toContain('Community Deck');
|
||||
|
||||
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
|
||||
await waitFor(() => expect(input.value).toBe('@My Export '));
|
||||
expect(screen.queryByTestId('mention-popover')).toBeNull();
|
||||
});
|
||||
|
||||
it('supports keyboard selection after opening the @ picker button', async () => {
|
||||
plugins = [];
|
||||
skills = [];
|
||||
servers = [];
|
||||
renderComposer({
|
||||
projectFiles: [
|
||||
{
|
||||
path: 'designs/landing.html',
|
||||
name: 'landing.html',
|
||||
kind: 'html',
|
||||
mime: 'text/html',
|
||||
mtime: 1,
|
||||
size: 128,
|
||||
},
|
||||
],
|
||||
});
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open mention picker' }));
|
||||
|
||||
await waitFor(() => expect(input.value).toBe('@'));
|
||||
expect(screen.getByTestId('mention-popover')).toBeTruthy();
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('mention-popover')
|
||||
.querySelector('[role="option"][aria-selected="true"]')?.textContent,
|
||||
).toContain('designs/landing.html');
|
||||
|
||||
fireEvent.keyDown(input, { key: 'Tab' });
|
||||
|
||||
expect(input.value).toBe('@designs/landing.html ');
|
||||
expect(screen.getByTestId('staged-attachments').textContent).toContain('landing.html');
|
||||
expect(screen.queryByTestId('mention-popover')).toBeNull();
|
||||
});
|
||||
|
||||
it('closes the @ picker when the tools panel opens', async () => {
|
||||
renderComposer();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Open mention picker' }));
|
||||
expect(screen.getByTestId('mention-popover')).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Open resources menu'));
|
||||
|
||||
expect(screen.queryByTestId('mention-popover')).toBeNull();
|
||||
await waitFor(() => expect(screen.getByText('Community Deck')).toBeTruthy());
|
||||
});
|
||||
|
||||
it('does not hijack Enter or arrows when @ search has no results', async () => {
|
||||
plugins = [];
|
||||
skills = [];
|
||||
servers = [];
|
||||
renderComposer();
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: '@missing', selectionStart: 8 },
|
||||
});
|
||||
|
||||
expect(screen.getByText('No results for “missing”.')).toBeTruthy();
|
||||
fireEvent.keyDown(input, { key: 'ArrowDown' });
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
|
||||
expect(input.value).toBe('@missing');
|
||||
expect(screen.getByTestId('mention-popover')).toBeTruthy();
|
||||
|
||||
fireEvent.keyDown(input, { key: 'Escape' });
|
||||
expect(screen.queryByTestId('mention-popover')).toBeNull();
|
||||
});
|
||||
|
||||
it('ranks direct plugin name matches above incidental substring matches', async () => {
|
||||
plugins = [
|
||||
makePlugin({
|
||||
id: 'stone-staircase',
|
||||
title: '3D Stone Staircase Evolution Infographic',
|
||||
description: 'Transforms a flat evolutionary timeline into a realistic 3D stone staircase infographic.',
|
||||
}),
|
||||
makePlugin({
|
||||
id: 'airbnb',
|
||||
title: 'Airbnb',
|
||||
description: 'Travel marketplace. Warm coral accent, photography-driven, rounded UI.',
|
||||
}),
|
||||
makePlugin({
|
||||
id: 'airtable',
|
||||
title: 'Airtable',
|
||||
description: 'Spreadsheet-database hybrid. Colorful, friendly, structured data aesthetic.',
|
||||
}),
|
||||
makePlugin({
|
||||
id: 'dcf-valuation',
|
||||
title: 'Dcf Valuation',
|
||||
description: 'Discounted cash flow valuation and intrinsic value analysis using fair value assumptions.',
|
||||
}),
|
||||
];
|
||||
renderComposer();
|
||||
const input = screen.getByTestId('chat-composer-input') as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: '@air', selectionStart: 4 },
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Airtable')).toBeTruthy());
|
||||
const popover = screen.getByTestId('mention-popover');
|
||||
const pluginNames = Array.from(
|
||||
popover.querySelectorAll('.mention-section [role="option"] strong'),
|
||||
(node) => node.textContent,
|
||||
);
|
||||
|
||||
expect(pluginNames.slice(0, 3)).toEqual([
|
||||
'Airbnb',
|
||||
'Airtable',
|
||||
'3D Stone Staircase Evolution Infographic',
|
||||
]);
|
||||
expect(
|
||||
popover.querySelector('[role="option"][aria-selected="true"]')?.textContent,
|
||||
).toContain('Airbnb');
|
||||
});
|
||||
|
||||
it('localizes @ panel tabs and empty states in Chinese mode', async () => {
|
||||
plugins = [];
|
||||
skills = [];
|
||||
|
|
@ -462,9 +664,13 @@ describe('ChatComposer context pickers', () => {
|
|||
|
||||
it('lets the tools panel switch between Official and My plugins', async () => {
|
||||
renderComposer();
|
||||
fireEvent.click(screen.getByLabelText('Open CLI and model settings'));
|
||||
fireEvent.click(screen.getByLabelText('Open resources menu'));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Community Deck')).toBeTruthy());
|
||||
await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('Search plugins')));
|
||||
const menu = screen.getByRole('menu');
|
||||
expect(within(menu).getByRole('tab', { name: 'Plugins' })).toBeTruthy();
|
||||
expect(within(menu).getAllByText('Apply').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('My Export')).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByText('My plugins'));
|
||||
|
|
@ -477,6 +683,106 @@ describe('ChatComposer context pickers', () => {
|
|||
expect(screen.getByText('Private export workflow')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps Resources search focused and supports arrow-key selection', async () => {
|
||||
plugins = [
|
||||
COMMUNITY_PLUGIN,
|
||||
makePlugin({
|
||||
id: 'brief-writer',
|
||||
title: 'Brief Writer',
|
||||
description: 'Turns source notes into a concise creative brief.',
|
||||
}),
|
||||
];
|
||||
renderComposer();
|
||||
fireEvent.click(screen.getByLabelText('Open resources menu'));
|
||||
|
||||
const search = await screen.findByLabelText('Search plugins');
|
||||
await waitFor(() => expect(document.activeElement).toBe(search));
|
||||
await waitFor(() => expect(screen.getByText('Brief Writer')).toBeTruthy());
|
||||
expect(search.getAttribute('aria-activedescendant')).toBe('composer-tools-plugins-option-0');
|
||||
|
||||
fireEvent.keyDown(search, { key: 'ArrowDown' });
|
||||
|
||||
expect(search.getAttribute('aria-activedescendant')).toBe('composer-tools-plugins-option-1');
|
||||
expect(screen.getByText('Brief Writer').closest('[role="menuitem"]')?.getAttribute('aria-selected')).toBe('true');
|
||||
|
||||
fireEvent.keyDown(search, { key: 'Enter' });
|
||||
|
||||
await waitFor(() => expect(screen.queryByRole('menu')).toBeNull());
|
||||
});
|
||||
|
||||
it('ranks Resources search results and resets the active option as the query changes', async () => {
|
||||
plugins = [
|
||||
makePlugin({
|
||||
id: 'stone-staircase',
|
||||
title: '3D Stone Staircase Evolution Infographic',
|
||||
description: 'Transforms a flat evolutionary timeline into realistic stone stairs.',
|
||||
}),
|
||||
makePlugin({
|
||||
id: 'airbnb',
|
||||
title: 'Airbnb',
|
||||
description: 'Travel marketplace with warm coral UI.',
|
||||
}),
|
||||
makePlugin({
|
||||
id: 'airtable',
|
||||
title: 'Airtable',
|
||||
description: 'Spreadsheet-database hybrid.',
|
||||
}),
|
||||
];
|
||||
renderComposer();
|
||||
fireEvent.click(screen.getByLabelText('Open resources menu'));
|
||||
|
||||
const search = await screen.findByLabelText('Search plugins');
|
||||
fireEvent.change(search, { target: { value: 'air' } });
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Airtable')).toBeTruthy());
|
||||
const menu = screen.getByRole('menu');
|
||||
const pluginNames = within(menu)
|
||||
.getAllByRole('menuitem')
|
||||
.map((item) => item.querySelector('strong')?.textContent)
|
||||
.filter(Boolean);
|
||||
expect(pluginNames.slice(0, 3)).toEqual([
|
||||
'Airbnb',
|
||||
'Airtable',
|
||||
'3D Stone Staircase Evolution Infographic',
|
||||
]);
|
||||
|
||||
fireEvent.keyDown(search, { key: 'ArrowDown' });
|
||||
expect(search.getAttribute('aria-activedescendant')).toBe('composer-tools-plugins-option-1');
|
||||
|
||||
fireEvent.change(search, { target: { value: 'stone' } });
|
||||
|
||||
expect(search.getAttribute('aria-activedescendant')).toBe('composer-tools-plugins-option-0');
|
||||
expect(
|
||||
screen.getByText('3D Stone Staircase Evolution Infographic').closest('[role="menuitem"]')?.getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
});
|
||||
|
||||
it('keeps MCP Resources search wired to a stable result container for templates and empty results', async () => {
|
||||
servers = [];
|
||||
templates = [MCP_TEMPLATE];
|
||||
const onOpenMcpSettings = vi.fn();
|
||||
renderComposer({ onOpenMcpSettings });
|
||||
fireEvent.click(screen.getByLabelText('Open resources menu'));
|
||||
fireEvent.click(await screen.findByRole('tab', { name: 'MCP' }));
|
||||
|
||||
const search = await screen.findByLabelText('Search MCP servers and templates');
|
||||
await waitFor(() => expect(document.activeElement).toBe(search));
|
||||
expect(document.getElementById('composer-tools-mcp-results')).toBeTruthy();
|
||||
expect(search.getAttribute('aria-activedescendant')).toBe('composer-tools-mcp-option-0');
|
||||
|
||||
fireEvent.change(search, { target: { value: 'figma' } });
|
||||
|
||||
expect(screen.getByText('Figma Context')).toBeTruthy();
|
||||
expect(document.getElementById(search.getAttribute('aria-activedescendant') ?? '')).toBeTruthy();
|
||||
|
||||
fireEvent.change(search, { target: { value: 'definitely-missing' } });
|
||||
|
||||
expect(screen.getByText('No MCP results for “definitely-missing”.')).toBeTruthy();
|
||||
expect(document.getElementById(search.getAttribute('aria-activedescendant') ?? '')).toBeTruthy();
|
||||
fireEvent.keyDown(search, { key: 'Enter' });
|
||||
expect(onOpenMcpSettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('clears absolute anchors when the pet popover switches to fixed positioning', async () => {
|
||||
renderComposer({
|
||||
petConfig: {
|
||||
|
|
|
|||
|
|
@ -89,8 +89,9 @@ describe('ChatComposer Tools -> Import menu', () => {
|
|||
const onProjectMetadataChange = vi.fn();
|
||||
renderComposer({ onProjectMetadataChange });
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Open CLI and model settings'));
|
||||
fireEvent.click(screen.getByLabelText('Open resources menu'));
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Import' }));
|
||||
await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('Search imports')));
|
||||
|
||||
const folderItem = await screen.findByRole('menuitem', { name: /Link code folder/i });
|
||||
expect(folderItem).toBeTruthy();
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
// (#2929 review — boundary alignment).
|
||||
|
||||
import { StrictMode } from 'react';
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ChatComposer } from '../../src/components/ChatComposer';
|
||||
|
|
@ -95,6 +95,16 @@ function renderComposer() {
|
|||
);
|
||||
}
|
||||
|
||||
function findToolsPluginButton(title: string): HTMLButtonElement | undefined {
|
||||
return within(screen.getByRole('menu'))
|
||||
.getAllByRole('menuitem')
|
||||
.find(
|
||||
(btn): btn is HTMLButtonElement =>
|
||||
btn instanceof HTMLButtonElement &&
|
||||
btn.querySelector('strong')?.textContent?.trim() === title,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock = vi.fn(async (url: string) => {
|
||||
if (url === '/api/mcp/servers') {
|
||||
|
|
@ -177,14 +187,10 @@ describe('ChatComposer plugin clear prunes draft (#2881)', () => {
|
|||
fireEvent.click(trigger!);
|
||||
await waitFor(() => expect(screen.getByRole('menu')).toBeTruthy());
|
||||
|
||||
// Pick the plugin from inside the now-open tools popover. The
|
||||
// plugin row's main button title lives inside a <strong> child; we
|
||||
// match against that to avoid the row's description trailing text.
|
||||
const popoverPluginButton = Array.from(
|
||||
document.querySelectorAll<HTMLButtonElement>('.composer-tools-row-main'),
|
||||
).find(
|
||||
(btn) => btn.querySelector('strong')?.textContent?.trim() === 'Airbnb',
|
||||
);
|
||||
// Pick the plugin from inside the now-open tools popover. The row
|
||||
// title lives inside a <strong> child; match against that to avoid
|
||||
// the description and action text in the accessible name.
|
||||
const popoverPluginButton = findToolsPluginButton('Airbnb');
|
||||
expect(popoverPluginButton).toBeTruthy();
|
||||
fireEvent.click(popoverPluginButton!);
|
||||
|
||||
|
|
@ -698,11 +704,7 @@ describe('ChatComposer plugin clear prunes draft (#2881)', () => {
|
|||
fireEvent.click(trigger!);
|
||||
await waitFor(() => expect(screen.getByRole('menu')).toBeTruthy());
|
||||
|
||||
const popoverPluginButton = Array.from(
|
||||
document.querySelectorAll<HTMLButtonElement>('.composer-tools-row-main'),
|
||||
).find(
|
||||
(btn) => btn.querySelector('strong')?.textContent?.trim() === 'SecondPlugin',
|
||||
);
|
||||
const popoverPluginButton = findToolsPluginButton('SecondPlugin');
|
||||
expect(popoverPluginButton).toBeTruthy();
|
||||
fireEvent.click(popoverPluginButton!);
|
||||
|
||||
|
|
@ -930,11 +932,7 @@ describe('ChatComposer plugin clear prunes draft (#2881)', () => {
|
|||
fireEvent.click(trigger!);
|
||||
await waitFor(() => expect(screen.getByRole('menu')).toBeTruthy());
|
||||
|
||||
const popoverPluginButton = Array.from(
|
||||
document.querySelectorAll<HTMLButtonElement>('.composer-tools-row-main'),
|
||||
).find(
|
||||
(btn) => btn.querySelector('strong')?.textContent?.trim() === 'Airbnb',
|
||||
);
|
||||
const popoverPluginButton = findToolsPluginButton('Airbnb');
|
||||
expect(popoverPluginButton).toBeTruthy();
|
||||
fireEvent.click(popoverPluginButton!);
|
||||
|
||||
|
|
@ -1185,11 +1183,7 @@ describe('ChatComposer plugin clear prunes draft (#2881)', () => {
|
|||
fireEvent.click(trigger!);
|
||||
await waitFor(() => expect(screen.getByRole('menu')).toBeTruthy());
|
||||
|
||||
const popoverSecondPluginButton = Array.from(
|
||||
document.querySelectorAll<HTMLButtonElement>('.composer-tools-row-main'),
|
||||
).find(
|
||||
(btn) => btn.querySelector('strong')?.textContent?.trim() === 'SecondPlugin',
|
||||
);
|
||||
const popoverSecondPluginButton = findToolsPluginButton('SecondPlugin');
|
||||
expect(popoverSecondPluginButton).toBeTruthy();
|
||||
fireEvent.click(popoverSecondPluginButton!);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue