feat(guide): let a tab type declare its own line in the new-tab list
The native new-tab list rendered one generic line ("opens in the DSH
sidebar") under every plugin entry, so six plugin rows read identically.
`TabDescriptor.description` now carries the real purpose (string or
locale-following thunk; absent falls back to the generic line), and the
six visible builtins declare theirs across all 20 dictionaries.
- service.ts: `TabDescriptor.description` + docs (guide §4.1, §0 row).
- native/: the guide entry's description follows the descriptor; the
`files` takeover reuses the editor's line.
- builtins: files / changes / tasks / side chat / terminal / browser.
- tests: builtins assert every visible type declares a distinct line;
the lifecycle test asserts descriptor lines (and the takeover reuse).
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
| 内置类型接管 | 插件的 `editor` 类型以 `extension` 优先级认领 `dsh-resource://file/**`(压过内置 `text` 预览),并接管内置 `files` 页面 kind(`openTab('files')` 打开插件的文件树);插件卸载/禁用时内置实现自动复位 |
|
||||
| 终端上限 | 终端 tab 的数量上限只统计插件自己底部工作台里的终端;原生栏里的终端不计入 |
|
||||
| 底部工作台的开合 | 落到底部工作台的打开一律展开它(新建与聚焦都算),因此 `openTab` 的落点永远可见;开合按钮注册在 DSH 会话头的 utilities 槽(`conversation.session.header.utilities`),不在插件自己的宿主里 |
|
||||
| 新建标签页列表 | 每个 tab 类型在原生 guide 里占一行:标题取 `title`,说明取 `description`(未声明则回退通用句),图标取 `icon`;`hidden: true` 的类型不占行。插件的 `editor` 类型不再单独占行(它认领的文件资源由 `files` 接管页承载同一视图) |
|
||||
| 已移除 | 插件自绘右侧面板(含宽度拖拽 / 新会话默认宽度)与**自由窗口**(`features` 里的 `'floatWindows'` 已删除,v0.18.x 及更早版本的消费者请勿再 gate 该能力);`openByDefault` / `defaultWidthPercent` / `changesDiffFloat` 三个设置项同步删除(旧文档里的键会被忽略) |
|
||||
|
||||
---
|
||||
@@ -178,6 +179,12 @@ interface TabDescriptor {
|
||||
id: string
|
||||
/** 标题(i18n 友好:传字符串或返回字符串的函数) */
|
||||
title: string | (() => string)
|
||||
/**
|
||||
* 一行说明,渲染在标题下方(DSH 原生右侧栏的新建标签页 / guide 列表)。
|
||||
* 不声明时回退到通用句「在 DSH 原生右侧栏打开」——所有插件页面就都长一样了,
|
||||
* 所以请写清这一页到底做什么。函数形式在渲染时求值,跟随语言。
|
||||
*/
|
||||
description?: string | (() => string)
|
||||
/** 图标:ReactNode 或 (size: number) => ReactNode */
|
||||
icon?: ReactNode | ((size: number) => ReactNode)
|
||||
/** + 菜单排序(升序);默认 100。内置:editor=10, git=20, subagent=30, sidechat=35, terminal=40, browser=50 */
|
||||
|
||||
@@ -82,6 +82,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
||||
return [
|
||||
{
|
||||
id: 'editor',
|
||||
description: () => t('guideDescFiles'),
|
||||
// The single files window: an editor tab with no path IS the file
|
||||
// explorer (empty hint + docked tree); with a path it previews/edits
|
||||
// the file. Visible in the + menu in the explorer's old slot.
|
||||
@@ -148,6 +149,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
||||
// log, and the git status needs a fetch — both stay out of the badge).
|
||||
id: 'git',
|
||||
title: () => t('changes'),
|
||||
description: () => t('guideDescGit'),
|
||||
icon: (size: number) => <IconDiffOutline16 size={size} />,
|
||||
order: 20,
|
||||
single: true,
|
||||
@@ -170,6 +172,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
||||
{
|
||||
id: 'subagent',
|
||||
title: () => t('subagent'),
|
||||
description: () => t('guideDescSubagent'),
|
||||
icon: (size: number) => <IconThinkOutline16 size={size} />,
|
||||
order: 30,
|
||||
single: true,
|
||||
@@ -198,6 +201,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
||||
{
|
||||
id: 'sidechat',
|
||||
title: () => t('sideChat'),
|
||||
description: () => t('guideDescSidechat'),
|
||||
icon: (size: number) => <IconNewChatOutline16 size={size} />,
|
||||
order: 35,
|
||||
// Codex-style: EVERY side conversation is its own tab. A plain open
|
||||
@@ -243,6 +247,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
||||
{
|
||||
id: 'terminal',
|
||||
title: () => t('terminal'),
|
||||
description: () => t('guideDescTerminal'),
|
||||
icon: (size: number) => <IconTerminalOutline16 size={size} />,
|
||||
order: 40,
|
||||
available: (_ctx, _scope, state) => uiTerminalCount(state) < TERMINAL_LIMIT,
|
||||
@@ -307,6 +312,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
|
||||
{
|
||||
id: 'browser',
|
||||
title: () => t('browser'),
|
||||
description: () => t('guideDescBrowser'),
|
||||
icon: (size: number) => <IconGlobeOutline16 size={size} />,
|
||||
order: 50,
|
||||
// Declarative settings: the sandbox escape hatch, the link-takeover
|
||||
|
||||
@@ -19,6 +19,12 @@
|
||||
export const ar: Record<string, string> = {
|
||||
files: 'الملفات',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'استعرض شجرة ملفات مساحة العمل؛ انقر للمعاينة أو التحرير',
|
||||
guideDescGit: 'تغييرات Git وتعديلات ملفات هذه الجلسة في تبويب واحد',
|
||||
guideDescSubagent: 'بنية الوكلاء الفرعيين والمهام الخلفية مباشرةً',
|
||||
guideDescSidechat: 'خيط متابعة مستقل يرث سياق هذه الجلسة',
|
||||
guideDescTerminal: 'صدفة حقيقية (xterm + pty) مع إعادة الاتصال',
|
||||
guideDescBrowser: 'متصفح مدمج يمكنه فتح روابط المحادثة',
|
||||
changesSessionEmpty: 'لا توجد عمليات ملفات في هذه الجلسة بعد',
|
||||
changesRead: 'قراءة',
|
||||
changesMdReading: 'قراءة',
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
export const de: Record<string, string> = {
|
||||
files: 'Dateien',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Dateibaum des Arbeitsbereichs durchsuchen; Klick öffnet Vorschau oder Editor',
|
||||
guideDescGit: 'Git-Änderungen und Dateiänderungen dieser Sitzung in einem Tab',
|
||||
guideDescSubagent: 'Subagent-Topologie und Hintergrundaufgaben, live',
|
||||
guideDescSidechat: 'Ein unabhängiger Folge-Thread mit dem Kontext dieser Sitzung',
|
||||
guideDescTerminal: 'Eine echte Shell (xterm + pty) mit Wiederverbindung',
|
||||
guideDescBrowser: 'Eingebetteter Browser, der Chat-Links übernehmen kann',
|
||||
changesSessionEmpty: 'Noch keine Dateioperationen in dieser Sitzung',
|
||||
changesRead: 'Lesen',
|
||||
changesMdReading: 'Lesen',
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
export const fr: Record<string, string> = {
|
||||
files: 'Fichiers',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Parcourir l\'arborescence du workspace ; un clic ouvre l\'aperçu ou l\'édition',
|
||||
guideDescGit: 'Modifications Git et éditions de cette session dans un seul onglet',
|
||||
guideDescSubagent: 'Topologie des sous-agents et tâches d\'arrière-plan, en direct',
|
||||
guideDescSidechat: 'Un fil de suivi indépendant héritant du contexte de cette session',
|
||||
guideDescTerminal: 'Un vrai shell (xterm + pty) avec reconnexion',
|
||||
guideDescBrowser: 'Navigateur intégré, qui peut reprendre les liens du chat',
|
||||
changesSessionEmpty: 'Aucune opération de fichier dans cette session',
|
||||
changesRead: 'Lecture',
|
||||
changesMdReading: 'Lecture',
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
export const hi: Record<string, string> = {
|
||||
files: 'फ़ाइलें',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'वर्कस्पेस फ़ाइल ट्री देखें; क्लिक करने पर प्रीव्यू या संपादन खुलता है',
|
||||
guideDescGit: 'Git बदलाव और इस सत्र के फ़ाइल बदलाव एक ही टैब में',
|
||||
guideDescSubagent: 'सबएजेंट टोपोलॉजी और बैकग्राउंड कार्य, लाइव',
|
||||
guideDescSidechat: 'इस सत्र का संदर्भ विरासत में लेने वाला स्वतंत्र फ़ॉलो-अप थ्रेड',
|
||||
guideDescTerminal: 'असली शेल (xterm + pty), रीकनेक्ट सहित',
|
||||
guideDescBrowser: 'अंतर्निहित ब्राउज़र, चैट के लिंक भी खोल सकता है',
|
||||
changesSessionEmpty: 'इस सत्र में अभी कोई फ़ाइल ऑपरेशन नहीं',
|
||||
changesRead: 'पढ़ना',
|
||||
changesMdReading: 'पठन',
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
export const id: Record<string, string> = {
|
||||
files: 'Berkas',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Telusuri pohon berkas workspace; klik untuk pratinjau atau edit',
|
||||
guideDescGit: 'Perubahan Git dan perubahan berkas sesi ini dalam satu tab',
|
||||
guideDescSubagent: 'Topologi subagent dan tugas latar belakang, langsung',
|
||||
guideDescSidechat: 'Utas lanjutan independen yang mewarisi konteks sesi ini',
|
||||
guideDescTerminal: 'Shell sungguhan (xterm + pty), bisa menyambung ulang',
|
||||
guideDescBrowser: 'Peramban tertanam, bisa mengambil alih tautan chat',
|
||||
changesSessionEmpty: 'Belum ada operasi file di sesi ini',
|
||||
changesRead: 'Baca',
|
||||
changesMdReading: 'Baca',
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
export const it: Record<string, string> = {
|
||||
files: 'File',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Sfoglia l\'albero del workspace; un clic apre anteprima o editor',
|
||||
guideDescGit: 'Modifiche Git e modifiche ai file di questa sessione in un solo tab',
|
||||
guideDescSubagent: 'Topologia dei subagent e attività in background, in tempo reale',
|
||||
guideDescSidechat: 'Un thread di approfondimento indipendente che eredita il contesto della sessione',
|
||||
guideDescTerminal: 'Una vera shell (xterm + pty) con riconnessione',
|
||||
guideDescBrowser: 'Browser integrato, può aprire i link della chat',
|
||||
changesSessionEmpty: 'Ancora nessuna operazione sui file in questa sessione',
|
||||
changesRead: 'Lettura',
|
||||
changesMdReading: 'Lettura',
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
export const ja: Record<string, string> = {
|
||||
files: 'ファイル',
|
||||
nativeGuideDesc: 'DSH のサイドバーで開く',
|
||||
guideDescFiles: 'ワークスペースのファイルツリーを閲覧し、クリックでプレビュー/編集',
|
||||
guideDescGit: 'Git の変更と今回のセッションのファイル変更を 1 つのタブに',
|
||||
guideDescSubagent: 'サブエージェントの構成とバックグラウンドタスクをリアルタイム表示',
|
||||
guideDescSidechat: '現在のセッションの文脈を引き継ぐ独立した追加スレッド',
|
||||
guideDescTerminal: '本物のシェル(xterm + pty)、再接続対応',
|
||||
guideDescBrowser: '埋め込みブラウザ。チャット内のリンクも引き受け可能',
|
||||
changesSessionEmpty: 'このセッションにはファイル操作がまだありません',
|
||||
changesRead: '読み取り',
|
||||
changesMdReading: '閲覧',
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
export const ko: Record<string, string> = {
|
||||
files: '파일',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: '워크스페이스 파일 트리를 보고 클릭하면 미리보기/편집',
|
||||
guideDescGit: 'Git 변경 사항과 이번 세션의 파일 변경을 한 탭에서',
|
||||
guideDescSubagent: '하위 에이전트 구조와 백그라운드 작업을 실시간으로',
|
||||
guideDescSidechat: '현재 세션의 컨텍스트를 이어받는 독립 후속 스레드',
|
||||
guideDescTerminal: '실제 셸(xterm + pty), 재연결 지원',
|
||||
guideDescBrowser: '내장 브라우저, 채팅 링크도 대신 열 수 있음',
|
||||
changesSessionEmpty: '이 세션에 아직 파일 작업이 없습니다',
|
||||
changesRead: '읽기',
|
||||
changesMdReading: '읽기',
|
||||
|
||||
@@ -16,6 +16,12 @@
|
||||
export const nl: Record<string, string> = {
|
||||
files: 'Bestanden',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Blader door de bestandsboom van de workspace; klik opent voorbeeld of editor',
|
||||
guideDescGit: 'Git-wijzigingen en bestandswijzigingen van deze sessie in één tabblad',
|
||||
guideDescSubagent: 'Subagent-topologie en achtergrondtaken, live',
|
||||
guideDescSidechat: 'Een onafhankelijke vervolgthread met de context van deze sessie',
|
||||
guideDescTerminal: 'Een echte shell (xterm + pty) met herverbinding',
|
||||
guideDescBrowser: 'Ingebouwde browser die chatlinks kan overnemen',
|
||||
changesSessionEmpty: 'Nog geen bestandsbewerkingen in deze sessie',
|
||||
changesRead: 'Lezen',
|
||||
changesMdReading: 'Lezen',
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
export const pl: Record<string, string> = {
|
||||
files: 'Pliki',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Przeglądaj drzewo plików obszaru roboczego; klik otwiera podgląd lub edycję',
|
||||
guideDescGit: 'Zmiany Git i zmiany plików z tej sesji w jednej karcie',
|
||||
guideDescSubagent: 'Topologia subagentów i zadania w tle na żywo',
|
||||
guideDescSidechat: 'Niezależny wątek uzupełniający dziedziczący kontekst tej sesji',
|
||||
guideDescTerminal: 'Prawdziwy shell (xterm + pty) z ponownym łączeniem',
|
||||
guideDescBrowser: 'Wbudowana przeglądarka, może przejmować linki z czatu',
|
||||
changesSessionEmpty: 'Brak operacji na plikach w tej sesji',
|
||||
changesRead: 'Odczyt',
|
||||
changesMdReading: 'Czytanie',
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
export const pt: Record<string, string> = {
|
||||
files: 'Arquivos',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Navegue pela árvore do workspace; um clique abre a pré-visualização ou a edição',
|
||||
guideDescGit: 'Alterações do Git e edições desta sessão em um único separador',
|
||||
guideDescSubagent: 'Topologia de subagentes e tarefas em segundo plano, ao vivo',
|
||||
guideDescSidechat: 'Uma conversa paralela independente que herda o contexto desta sessão',
|
||||
guideDescTerminal: 'Um shell real (xterm + pty) com reconexão',
|
||||
guideDescBrowser: 'Navegador incorporado, que pode abrir links do chat',
|
||||
changesSessionEmpty: 'Ainda sem operações de arquivo nesta sessão',
|
||||
changesRead: 'Leitura',
|
||||
changesMdReading: 'Leitura',
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
export const ru: Record<string, string> = {
|
||||
files: 'Файлы',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Дерево файлов рабочей области: клик открывает просмотр или редактор',
|
||||
guideDescGit: 'Изменения Git и правки файлов этой сессии в одной вкладке',
|
||||
guideDescSubagent: 'Топология субагентов и фоновые задачи в реальном времени',
|
||||
guideDescSidechat: 'Независимый дополнительный тред с контекстом этой сессии',
|
||||
guideDescTerminal: 'Настоящий shell (xterm + pty) с переподключением',
|
||||
guideDescBrowser: 'Встроенный браузер, перехватывает ссылки из чата',
|
||||
changesSessionEmpty: 'В этой сессии пока нет файловых операций',
|
||||
changesRead: 'Чтение',
|
||||
changesMdReading: 'Чтение',
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
export const sv: Record<string, string> = {
|
||||
files: 'Filer',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Bläddra i arbetsytans filträd; klick öppnar förhandsvisning eller redigering',
|
||||
guideDescGit: 'Git-ändringar och sessionens filändringar i en flik',
|
||||
guideDescSubagent: 'Subagenttopologi och bakgrundsjobb, live',
|
||||
guideDescSidechat: 'En oberoende uppföljningstråd som ärver sessionens kontext',
|
||||
guideDescTerminal: 'Ett riktigt skal (xterm + pty) med återanslutning',
|
||||
guideDescBrowser: 'Inbäddad webbläsare som kan ta över chattlänkar',
|
||||
changesSessionEmpty: 'Inga filoperationer i den här sessionen ännu',
|
||||
changesRead: 'Läs',
|
||||
changesMdReading: 'Läsning',
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
export const th: Record<string, string> = {
|
||||
files: 'ไฟล์',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'เรียกดูแผนผังไฟล์ของเวิร์กสเปซ คลิกเพื่อดูตัวอย่างหรือแก้ไข',
|
||||
guideDescGit: 'การเปลี่ยนแปลง Git และไฟล์ที่แก้ในเซสชันนี้ในแท็บเดียว',
|
||||
guideDescSubagent: 'โครงสร้างซับเอเจนต์และงานเบื้องหลังแบบเรียลไทม์',
|
||||
guideDescSidechat: 'เธรดถามต่ออิสระที่สืบทอดบริบทของเซสชันนี้',
|
||||
guideDescTerminal: 'เชลล์จริง (xterm + pty) เชื่อมต่อใหม่ได้',
|
||||
guideDescBrowser: 'เบราว์เซอร์ในตัว รับช่วงลิงก์จากแชทได้',
|
||||
changesSessionEmpty: 'ยังไม่มีการดำเนินการไฟล์ในเซสชันนี้',
|
||||
changesRead: 'อ่าน',
|
||||
changesMdReading: 'อ่าน',
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
export const tr: Record<string, string> = {
|
||||
files: 'Dosyalar',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Çalışma alanı dosya ağacına göz atın; tıklayınca önizleme veya düzenleme açılır',
|
||||
guideDescGit: 'Git değişiklikleri ve bu oturumun dosya değişiklikleri tek sekmede',
|
||||
guideDescSubagent: 'Alt ajan topolojisi ve arka plan görevleri, canlı',
|
||||
guideDescSidechat: 'Bu oturumun bağlamını devralan bağımsız bir yan konu',
|
||||
guideDescTerminal: 'Gerçek bir shell (xterm + pty), yeniden bağlanma destekli',
|
||||
guideDescBrowser: 'Sohbet bağlantılarını devralabilen gömülü tarayıcı',
|
||||
changesSessionEmpty: 'Bu oturumda henüz dosya işlemi yok',
|
||||
changesRead: 'Okuma',
|
||||
changesMdReading: 'Okuma',
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
export const vi: Record<string, string> = {
|
||||
files: 'Tệp',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Duyệt cây tệp của workspace; bấm để xem trước hoặc chỉnh sửa',
|
||||
guideDescGit: 'Thay đổi Git và thay đổi tệp của phiên này trong một tab',
|
||||
guideDescSubagent: 'Sơ đồ subagent và tác vụ nền, theo thời gian thực',
|
||||
guideDescSidechat: 'Luồng hỏi tiếp độc lập kế thừa ngữ cảnh của phiên này',
|
||||
guideDescTerminal: 'Shell thật (xterm + pty), tự kết nối lại',
|
||||
guideDescBrowser: 'Trình duyệt nhúng, có thể mở link trong chat',
|
||||
changesSessionEmpty: 'Chưa có thao tác tệp nào trong phiên này',
|
||||
changesRead: 'Đọc',
|
||||
changesMdReading: 'Đọc',
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
export const zhHK: Record<string, string> = {
|
||||
files: '檔案',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: '瀏覽工作區檔案樹,點開即可預覽或編輯',
|
||||
guideDescGit: 'Git 變更與本輪檔案改動,雙視角合一',
|
||||
guideDescSubagent: '子代理拓撲與背景任務,即時狀態',
|
||||
guideDescSidechat: '繼承目前工作階段上下文的獨立追問執行緒',
|
||||
guideDescTerminal: '真實 shell 終端(xterm + pty),斷線重連',
|
||||
guideDescBrowser: '內嵌網頁瀏覽器,可接管聊天中的外鏈',
|
||||
changesSessionEmpty: '本工作階段還沒有檔案操作',
|
||||
changesRead: '讀取',
|
||||
changesMdReading: '閱讀',
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
export const zhMO: Record<string, string> = {
|
||||
files: '檔案',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: '瀏覽工作區檔案樹,點開即可預覽或編輯',
|
||||
guideDescGit: 'Git 變更與本輪檔案改動,雙視角合一',
|
||||
guideDescSubagent: '子代理拓撲與背景任務,即時狀態',
|
||||
guideDescSidechat: '繼承目前工作階段上下文的獨立追問執行緒',
|
||||
guideDescTerminal: '真實 shell 終端(xterm + pty),斷線重連',
|
||||
guideDescBrowser: '內嵌網頁瀏覽器,可接管聊天中的外鏈',
|
||||
changesSessionEmpty: '本工作階段還沒有檔案操作',
|
||||
changesRead: '讀取',
|
||||
changesMdReading: '閱讀',
|
||||
|
||||
@@ -33,6 +33,12 @@
|
||||
export const zhTW: Record<string, string> = {
|
||||
files: '檔案',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: '瀏覽工作區檔案樹,點開即可預覽或編輯',
|
||||
guideDescGit: 'Git 變更與本輪檔案改動,雙視角合一',
|
||||
guideDescSubagent: '子代理拓撲與背景任務,即時狀態',
|
||||
guideDescSidechat: '繼承目前工作階段上下文的獨立追問執行緒',
|
||||
guideDescTerminal: '真實 shell 終端(xterm + pty),斷線重連',
|
||||
guideDescBrowser: '內嵌網頁瀏覽器,可接管聊天中的外鏈',
|
||||
changesSessionEmpty: '本作業階段還沒有檔案操作',
|
||||
changesRead: '讀取',
|
||||
changesMdReading: '閱讀',
|
||||
|
||||
@@ -25,6 +25,12 @@
|
||||
export const zh = {
|
||||
files: '文件',
|
||||
nativeGuideDesc: '在 DSH 原生右侧栏打开',
|
||||
guideDescFiles: '浏览工作区文件树,点开即预览或编辑',
|
||||
guideDescGit: 'Git 变更与本轮文件改动,双视角合一',
|
||||
guideDescSubagent: '子代理拓扑与后台任务,实时状态',
|
||||
guideDescSidechat: '继承当前会话上下文的独立追问线程',
|
||||
guideDescTerminal: '真实 shell 终端(xterm + pty),断线重连',
|
||||
guideDescBrowser: '内嵌网页浏览器,可接管聊天里的外链',
|
||||
changesSessionEmpty: '本会话还没有文件操作',
|
||||
changesRead: '读取',
|
||||
changesMdReading: '阅读',
|
||||
@@ -460,6 +466,12 @@ export const zh = {
|
||||
export const en: Record<keyof typeof zh, string> = {
|
||||
files: 'Files',
|
||||
nativeGuideDesc: 'Open in the DSH sidebar',
|
||||
guideDescFiles: 'Browse the workspace tree; click a file to preview or edit',
|
||||
guideDescGit: 'Git changes and this session\'s file edits, in one tab',
|
||||
guideDescSubagent: 'Subagent topology and background jobs, live',
|
||||
guideDescSidechat: 'An independent follow-up thread inheriting this session\'s context',
|
||||
guideDescTerminal: 'A real shell (xterm + pty) with reconnect',
|
||||
guideDescBrowser: 'An embedded browser that can take over chat links',
|
||||
changesSessionEmpty: 'No file operations in this session yet',
|
||||
changesRead: 'Read',
|
||||
changesMdReading: 'Reading',
|
||||
|
||||
@@ -63,6 +63,17 @@ function titleOf(descriptor: TabDescriptor): string {
|
||||
return typeof descriptor.title === 'function' ? descriptor.title() : descriptor.title
|
||||
}
|
||||
|
||||
/**
|
||||
* The descriptor's guide line, evaluated fresh for the current locale. The
|
||||
* host's new-tab list renders one line per entry; without a declared
|
||||
* description every plugin page would read the same generic sentence.
|
||||
*/
|
||||
function descriptionOf(descriptor: TabDescriptor | undefined): string {
|
||||
const description = descriptor?.description
|
||||
if (description === undefined) return t('nativeGuideDesc')
|
||||
return typeof description === 'function' ? description() : description
|
||||
}
|
||||
|
||||
/**
|
||||
* The guide row's glyph for a descriptor icon (nothing when it has none).
|
||||
* The native guide renders `entry.icon`, so a takeover registered without one
|
||||
@@ -181,7 +192,7 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
|
||||
guide: [{
|
||||
order: descriptor.order ?? 100,
|
||||
title: () => titleOf(descriptor),
|
||||
description: () => t('nativeGuideDesc'),
|
||||
description: () => descriptionOf(descriptor),
|
||||
...guideIconOf(icon),
|
||||
}],
|
||||
}),
|
||||
@@ -208,7 +219,7 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
|
||||
guide: [{
|
||||
order: 10,
|
||||
title: () => t('files'),
|
||||
description: () => t('nativeGuideDesc'),
|
||||
description: () => descriptionOf(editor),
|
||||
// The takeover IS the editor descriptor's page, so it carries the
|
||||
// editor's glyph: without it the "Files" row is the only guide
|
||||
// entry with an empty icon slot.
|
||||
|
||||
@@ -162,6 +162,14 @@ export interface TabDescriptor {
|
||||
/** Unique id; also the `SidebarTab.type` value (`'explorer'`, `'my-plugin:db'`). */
|
||||
id: string
|
||||
title: string | (() => string)
|
||||
/**
|
||||
* One-line description of what this tab shows, rendered under the title in
|
||||
* the host's new-tab list (DSH's native right Sidebar guide page). Without
|
||||
* it the entry falls back to a generic "opens in the DSH sidebar" line, so
|
||||
* every plugin page reads the same — declare the real purpose instead.
|
||||
* Evaluated at render time, so a function follows the active locale.
|
||||
*/
|
||||
description?: string | (() => string)
|
||||
icon?: ReactNode | ((size: number) => ReactNode)
|
||||
/** + menu sort order (ascending); default 100. */
|
||||
order?: number
|
||||
|
||||
@@ -44,6 +44,22 @@ describe('built-in tab registrations', () => {
|
||||
expect(changes?.component).toBeDefined()
|
||||
})
|
||||
|
||||
it('every visible tab declares a real guide line (no generic fallback)', () => {
|
||||
// The native new-tab list renders `description()` under the title; a tab
|
||||
// without one reads the same generic sentence as every other plugin page.
|
||||
const { service } = setup()
|
||||
const visible = service.getTabs().filter(descriptor => descriptor.hidden !== true)
|
||||
expect(visible.length).toBeGreaterThan(0)
|
||||
for (const descriptor of visible) {
|
||||
expect(descriptor.description, `${descriptor.id} must declare a description`).toBeDefined()
|
||||
const line = typeof descriptor.description === 'function' ? descriptor.description() : descriptor.description
|
||||
expect(line, `${descriptor.id} description must be non-empty`).toBeTruthy()
|
||||
}
|
||||
const lines = visible.map(descriptor =>
|
||||
typeof descriptor.description === 'function' ? descriptor.description() : descriptor.description)
|
||||
expect(new Set(lines).size, 'descriptions must differ per tab').toBe(visible.length)
|
||||
})
|
||||
|
||||
it('the changes tab declares no settings of its own (the diff always docks)', () => {
|
||||
const { service } = setup()
|
||||
expect(service.getTab('git')?.settings).toBeUndefined()
|
||||
|
||||
@@ -162,8 +162,8 @@ describe('registerNativeSurface lifecycle (service-driven registration)', () =>
|
||||
const store = createSidebarStore()
|
||||
store.setSession('s1')
|
||||
const service = createBetterSidebarService(store)
|
||||
service.registerTab({ id: 'terminal', title: 'Terminal', component: () => null })
|
||||
service.registerTab({ id: 'editor', title: 'Files', component: () => null, icon: () => null })
|
||||
service.registerTab({ id: 'terminal', title: 'Terminal', component: () => null, description: () => 'Runs a shell' })
|
||||
service.registerTab({ id: 'editor', title: 'Files', component: () => null, icon: () => null, description: () => 'Browse the tree' })
|
||||
const records = createNativeTabRecords()
|
||||
|
||||
const registered: Array<{ id: string; kind: string; title: (address: string) => string; guide: unknown }> = []
|
||||
@@ -225,8 +225,15 @@ describe('registerNativeSurface lifecycle (service-driven registration)', () =>
|
||||
expect(filesType?.guide).toBeDefined()
|
||||
// The takeover carries the editor's glyph, so the "Files" guide row is
|
||||
// not the only one with a blank icon slot.
|
||||
const filesGuide = filesType?.guide as Array<{ icon?: unknown }> | undefined
|
||||
const filesGuide = filesType?.guide as Array<{ icon?: unknown; description?: () => string }> | undefined
|
||||
expect(filesGuide?.[0]?.icon).toBeDefined()
|
||||
// The guide line is the descriptor's OWN description (the takeover reuses
|
||||
// the editor's), and a descriptor that declares none falls back to the
|
||||
// generic line — otherwise every plugin page would read identically.
|
||||
expect(filesGuide?.[0]?.description?.()).toBe('Browse the tree')
|
||||
const terminalGuide = registered.find(entry => entry.kind === 'terminal')?.guide as
|
||||
Array<{ description?: () => string }> | undefined
|
||||
expect(terminalGuide?.[0]?.description?.()).toBe('Runs a shell')
|
||||
|
||||
dispose()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user