From d4765819c08579a83b2f0fbf361ba342967b41d9 Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Wed, 9 Sep 2026 22:25:59 +0800 Subject: [PATCH] feat(guide): let a tab type declare its own line in the new-tab list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- docs/external-plugin-guide.md | 7 +++++++ src/client/builtins/tabs.tsx | 6 ++++++ src/client/locales-ar.ts | 6 ++++++ src/client/locales-de.ts | 6 ++++++ src/client/locales-fr.ts | 6 ++++++ src/client/locales-hi.ts | 6 ++++++ src/client/locales-id.ts | 6 ++++++ src/client/locales-it.ts | 6 ++++++ src/client/locales-ja.ts | 6 ++++++ src/client/locales-ko.ts | 6 ++++++ src/client/locales-nl.ts | 6 ++++++ src/client/locales-pl.ts | 6 ++++++ src/client/locales-pt.ts | 6 ++++++ src/client/locales-ru.ts | 6 ++++++ src/client/locales-sv.ts | 6 ++++++ src/client/locales-th.ts | 6 ++++++ src/client/locales-tr.ts | 6 ++++++ src/client/locales-vi.ts | 6 ++++++ src/client/locales-zh-HK.ts | 6 ++++++ src/client/locales-zh-MO.ts | 6 ++++++ src/client/locales-zh-TW.ts | 6 ++++++ src/client/locales.ts | 12 ++++++++++++ src/client/native/index.ts | 15 +++++++++++++-- src/client/service.ts | 8 ++++++++ tests/builtins.spec.ts | 16 ++++++++++++++++ tests/native-surface.spec.ts | 13 ++++++++++--- 26 files changed, 186 insertions(+), 5 deletions(-) diff --git a/docs/external-plugin-guide.md b/docs/external-plugin-guide.md index 562a2ad..8a78d32 100644 --- a/docs/external-plugin-guide.md +++ b/docs/external-plugin-guide.md @@ -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 */ diff --git a/src/client/builtins/tabs.tsx b/src/client/builtins/tabs.tsx index 2bc5fc5..0982808 100644 --- a/src/client/builtins/tabs.tsx +++ b/src/client/builtins/tabs.tsx @@ -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) => , 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) => , 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) => , 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) => , 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) => , order: 50, // Declarative settings: the sandbox escape hatch, the link-takeover diff --git a/src/client/locales-ar.ts b/src/client/locales-ar.ts index e9ae36c..c13168f 100644 --- a/src/client/locales-ar.ts +++ b/src/client/locales-ar.ts @@ -19,6 +19,12 @@ export const ar: Record = { files: 'الملفات', nativeGuideDesc: 'Open in the DSH sidebar', + guideDescFiles: 'استعرض شجرة ملفات مساحة العمل؛ انقر للمعاينة أو التحرير', + guideDescGit: 'تغييرات Git وتعديلات ملفات هذه الجلسة في تبويب واحد', + guideDescSubagent: 'بنية الوكلاء الفرعيين والمهام الخلفية مباشرةً', + guideDescSidechat: 'خيط متابعة مستقل يرث سياق هذه الجلسة', + guideDescTerminal: 'صدفة حقيقية (xterm + pty) مع إعادة الاتصال', + guideDescBrowser: 'متصفح مدمج يمكنه فتح روابط المحادثة', changesSessionEmpty: 'لا توجد عمليات ملفات في هذه الجلسة بعد', changesRead: 'قراءة', changesMdReading: 'قراءة', diff --git a/src/client/locales-de.ts b/src/client/locales-de.ts index d901a5b..5012597 100644 --- a/src/client/locales-de.ts +++ b/src/client/locales-de.ts @@ -4,6 +4,12 @@ export const de: Record = { 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', diff --git a/src/client/locales-fr.ts b/src/client/locales-fr.ts index a7a0a8f..c01741d 100644 --- a/src/client/locales-fr.ts +++ b/src/client/locales-fr.ts @@ -11,6 +11,12 @@ export const fr: Record = { 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', diff --git a/src/client/locales-hi.ts b/src/client/locales-hi.ts index 7703a95..9c55809 100644 --- a/src/client/locales-hi.ts +++ b/src/client/locales-hi.ts @@ -18,6 +18,12 @@ export const hi: Record = { files: 'फ़ाइलें', nativeGuideDesc: 'Open in the DSH sidebar', + guideDescFiles: 'वर्कस्पेस फ़ाइल ट्री देखें; क्लिक करने पर प्रीव्यू या संपादन खुलता है', + guideDescGit: 'Git बदलाव और इस सत्र के फ़ाइल बदलाव एक ही टैब में', + guideDescSubagent: 'सबएजेंट टोपोलॉजी और बैकग्राउंड कार्य, लाइव', + guideDescSidechat: 'इस सत्र का संदर्भ विरासत में लेने वाला स्वतंत्र फ़ॉलो-अप थ्रेड', + guideDescTerminal: 'असली शेल (xterm + pty), रीकनेक्ट सहित', + guideDescBrowser: 'अंतर्निहित ब्राउज़र, चैट के लिंक भी खोल सकता है', changesSessionEmpty: 'इस सत्र में अभी कोई फ़ाइल ऑपरेशन नहीं', changesRead: 'पढ़ना', changesMdReading: 'पठन', diff --git a/src/client/locales-id.ts b/src/client/locales-id.ts index cefa087..88dc751 100644 --- a/src/client/locales-id.ts +++ b/src/client/locales-id.ts @@ -16,6 +16,12 @@ export const id: Record = { 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', diff --git a/src/client/locales-it.ts b/src/client/locales-it.ts index ae54f55..e47bdcf 100644 --- a/src/client/locales-it.ts +++ b/src/client/locales-it.ts @@ -9,6 +9,12 @@ export const it: Record = { 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', diff --git a/src/client/locales-ja.ts b/src/client/locales-ja.ts index b0d412d..6f03fea 100644 --- a/src/client/locales-ja.ts +++ b/src/client/locales-ja.ts @@ -18,6 +18,12 @@ export const ja: Record = { files: 'ファイル', nativeGuideDesc: 'DSH のサイドバーで開く', + guideDescFiles: 'ワークスペースのファイルツリーを閲覧し、クリックでプレビュー/編集', + guideDescGit: 'Git の変更と今回のセッションのファイル変更を 1 つのタブに', + guideDescSubagent: 'サブエージェントの構成とバックグラウンドタスクをリアルタイム表示', + guideDescSidechat: '現在のセッションの文脈を引き継ぐ独立した追加スレッド', + guideDescTerminal: '本物のシェル(xterm + pty)、再接続対応', + guideDescBrowser: '埋め込みブラウザ。チャット内のリンクも引き受け可能', changesSessionEmpty: 'このセッションにはファイル操作がまだありません', changesRead: '読み取り', changesMdReading: '閲覧', diff --git a/src/client/locales-ko.ts b/src/client/locales-ko.ts index 3654dd8..8be6feb 100644 --- a/src/client/locales-ko.ts +++ b/src/client/locales-ko.ts @@ -10,6 +10,12 @@ export const ko: Record = { files: '파일', nativeGuideDesc: 'Open in the DSH sidebar', + guideDescFiles: '워크스페이스 파일 트리를 보고 클릭하면 미리보기/편집', + guideDescGit: 'Git 변경 사항과 이번 세션의 파일 변경을 한 탭에서', + guideDescSubagent: '하위 에이전트 구조와 백그라운드 작업을 실시간으로', + guideDescSidechat: '현재 세션의 컨텍스트를 이어받는 독립 후속 스레드', + guideDescTerminal: '실제 셸(xterm + pty), 재연결 지원', + guideDescBrowser: '내장 브라우저, 채팅 링크도 대신 열 수 있음', changesSessionEmpty: '이 세션에 아직 파일 작업이 없습니다', changesRead: '읽기', changesMdReading: '읽기', diff --git a/src/client/locales-nl.ts b/src/client/locales-nl.ts index d9183e5..dad2700 100644 --- a/src/client/locales-nl.ts +++ b/src/client/locales-nl.ts @@ -16,6 +16,12 @@ export const nl: Record = { 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', diff --git a/src/client/locales-pl.ts b/src/client/locales-pl.ts index b9dbc76..77758c4 100644 --- a/src/client/locales-pl.ts +++ b/src/client/locales-pl.ts @@ -20,6 +20,12 @@ export const pl: Record = { 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', diff --git a/src/client/locales-pt.ts b/src/client/locales-pt.ts index 4b6a71e..9903ecd 100644 --- a/src/client/locales-pt.ts +++ b/src/client/locales-pt.ts @@ -1,6 +1,12 @@ export const pt: Record = { 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', diff --git a/src/client/locales-ru.ts b/src/client/locales-ru.ts index 722dacb..0e47e5d 100644 --- a/src/client/locales-ru.ts +++ b/src/client/locales-ru.ts @@ -14,6 +14,12 @@ export const ru: Record = { files: 'Файлы', nativeGuideDesc: 'Open in the DSH sidebar', + guideDescFiles: 'Дерево файлов рабочей области: клик открывает просмотр или редактор', + guideDescGit: 'Изменения Git и правки файлов этой сессии в одной вкладке', + guideDescSubagent: 'Топология субагентов и фоновые задачи в реальном времени', + guideDescSidechat: 'Независимый дополнительный тред с контекстом этой сессии', + guideDescTerminal: 'Настоящий shell (xterm + pty) с переподключением', + guideDescBrowser: 'Встроенный браузер, перехватывает ссылки из чата', changesSessionEmpty: 'В этой сессии пока нет файловых операций', changesRead: 'Чтение', changesMdReading: 'Чтение', diff --git a/src/client/locales-sv.ts b/src/client/locales-sv.ts index f70ab43..7517621 100644 --- a/src/client/locales-sv.ts +++ b/src/client/locales-sv.ts @@ -1,6 +1,12 @@ export const sv: Record = { 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', diff --git a/src/client/locales-th.ts b/src/client/locales-th.ts index d360d16..64c133b 100644 --- a/src/client/locales-th.ts +++ b/src/client/locales-th.ts @@ -18,6 +18,12 @@ export const th: Record = { files: 'ไฟล์', nativeGuideDesc: 'Open in the DSH sidebar', + guideDescFiles: 'เรียกดูแผนผังไฟล์ของเวิร์กสเปซ คลิกเพื่อดูตัวอย่างหรือแก้ไข', + guideDescGit: 'การเปลี่ยนแปลง Git และไฟล์ที่แก้ในเซสชันนี้ในแท็บเดียว', + guideDescSubagent: 'โครงสร้างซับเอเจนต์และงานเบื้องหลังแบบเรียลไทม์', + guideDescSidechat: 'เธรดถามต่ออิสระที่สืบทอดบริบทของเซสชันนี้', + guideDescTerminal: 'เชลล์จริง (xterm + pty) เชื่อมต่อใหม่ได้', + guideDescBrowser: 'เบราว์เซอร์ในตัว รับช่วงลิงก์จากแชทได้', changesSessionEmpty: 'ยังไม่มีการดำเนินการไฟล์ในเซสชันนี้', changesRead: 'อ่าน', changesMdReading: 'อ่าน', diff --git a/src/client/locales-tr.ts b/src/client/locales-tr.ts index 50da4a8..27c9fb9 100644 --- a/src/client/locales-tr.ts +++ b/src/client/locales-tr.ts @@ -18,6 +18,12 @@ export const tr: Record = { 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', diff --git a/src/client/locales-vi.ts b/src/client/locales-vi.ts index 3b3fc28..9b020b9 100644 --- a/src/client/locales-vi.ts +++ b/src/client/locales-vi.ts @@ -18,6 +18,12 @@ export const vi: Record = { 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', diff --git a/src/client/locales-zh-HK.ts b/src/client/locales-zh-HK.ts index dcd9e19..ca4b64b 100644 --- a/src/client/locales-zh-HK.ts +++ b/src/client/locales-zh-HK.ts @@ -33,6 +33,12 @@ export const zhHK: Record = { files: '檔案', nativeGuideDesc: 'Open in the DSH sidebar', + guideDescFiles: '瀏覽工作區檔案樹,點開即可預覽或編輯', + guideDescGit: 'Git 變更與本輪檔案改動,雙視角合一', + guideDescSubagent: '子代理拓撲與背景任務,即時狀態', + guideDescSidechat: '繼承目前工作階段上下文的獨立追問執行緒', + guideDescTerminal: '真實 shell 終端(xterm + pty),斷線重連', + guideDescBrowser: '內嵌網頁瀏覽器,可接管聊天中的外鏈', changesSessionEmpty: '本工作階段還沒有檔案操作', changesRead: '讀取', changesMdReading: '閱讀', diff --git a/src/client/locales-zh-MO.ts b/src/client/locales-zh-MO.ts index 5b60c3f..7252c5f 100644 --- a/src/client/locales-zh-MO.ts +++ b/src/client/locales-zh-MO.ts @@ -33,6 +33,12 @@ export const zhMO: Record = { files: '檔案', nativeGuideDesc: 'Open in the DSH sidebar', + guideDescFiles: '瀏覽工作區檔案樹,點開即可預覽或編輯', + guideDescGit: 'Git 變更與本輪檔案改動,雙視角合一', + guideDescSubagent: '子代理拓撲與背景任務,即時狀態', + guideDescSidechat: '繼承目前工作階段上下文的獨立追問執行緒', + guideDescTerminal: '真實 shell 終端(xterm + pty),斷線重連', + guideDescBrowser: '內嵌網頁瀏覽器,可接管聊天中的外鏈', changesSessionEmpty: '本工作階段還沒有檔案操作', changesRead: '讀取', changesMdReading: '閱讀', diff --git a/src/client/locales-zh-TW.ts b/src/client/locales-zh-TW.ts index 4dbf024..7d37179 100644 --- a/src/client/locales-zh-TW.ts +++ b/src/client/locales-zh-TW.ts @@ -33,6 +33,12 @@ export const zhTW: Record = { files: '檔案', nativeGuideDesc: 'Open in the DSH sidebar', + guideDescFiles: '瀏覽工作區檔案樹,點開即可預覽或編輯', + guideDescGit: 'Git 變更與本輪檔案改動,雙視角合一', + guideDescSubagent: '子代理拓撲與背景任務,即時狀態', + guideDescSidechat: '繼承目前工作階段上下文的獨立追問執行緒', + guideDescTerminal: '真實 shell 終端(xterm + pty),斷線重連', + guideDescBrowser: '內嵌網頁瀏覽器,可接管聊天中的外鏈', changesSessionEmpty: '本作業階段還沒有檔案操作', changesRead: '讀取', changesMdReading: '閱讀', diff --git a/src/client/locales.ts b/src/client/locales.ts index 7fab631..b3f64ac 100644 --- a/src/client/locales.ts +++ b/src/client/locales.ts @@ -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 = { 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', diff --git a/src/client/native/index.ts b/src/client/native/index.ts index 8d2e394..52979cf 100644 --- a/src/client/native/index.ts +++ b/src/client/native/index.ts @@ -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. diff --git a/src/client/service.ts b/src/client/service.ts index f948402..ff981d8 100644 --- a/src/client/service.ts +++ b/src/client/service.ts @@ -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 diff --git a/tests/builtins.spec.ts b/tests/builtins.spec.ts index ab47fa5..36ed48c 100644 --- a/tests/builtins.spec.ts +++ b/tests/builtins.spec.ts @@ -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() diff --git a/tests/native-surface.spec.ts b/tests/native-surface.spec.ts index b045756..e7e87bd 100644 --- a/tests/native-surface.spec.ts +++ b/tests/native-surface.spec.ts @@ -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() })