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:
Menghuan1918
2026-09-09 22:25:59 +08:00
parent 9ebaa09f80
commit d4765819c0
26 changed files with 186 additions and 5 deletions
+7
View File
@@ -27,6 +27,7 @@
| 内置类型接管 | 插件的 `editor` 类型以 `extension` 优先级认领 `dsh-resource://file/**`(压过内置 `text` 预览),并接管内置 `files` 页面 kind(`openTab('files')` 打开插件的文件树);插件卸载/禁用时内置实现自动复位 | | 内置类型接管 | 插件的 `editor` 类型以 `extension` 优先级认领 `dsh-resource://file/**`(压过内置 `text` 预览),并接管内置 `files` 页面 kind(`openTab('files')` 打开插件的文件树);插件卸载/禁用时内置实现自动复位 |
| 终端上限 | 终端 tab 的数量上限只统计插件自己底部工作台里的终端;原生栏里的终端不计入 | | 终端上限 | 终端 tab 的数量上限只统计插件自己底部工作台里的终端;原生栏里的终端不计入 |
| 底部工作台的开合 | 落到底部工作台的打开一律展开它(新建与聚焦都算),因此 `openTab` 的落点永远可见;开合按钮注册在 DSH 会话头的 utilities 槽(`conversation.session.header.utilities`),不在插件自己的宿主里 | | 底部工作台的开合 | 落到底部工作台的打开一律展开它(新建与聚焦都算),因此 `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` 三个设置项同步删除(旧文档里的键会被忽略) | | 已移除 | 插件自绘右侧面板(含宽度拖拽 / 新会话默认宽度)与**自由窗口**(`features` 里的 `'floatWindows'` 已删除,v0.18.x 及更早版本的消费者请勿再 gate 该能力);`openByDefault` / `defaultWidthPercent` / `changesDiffFloat` 三个设置项同步删除(旧文档里的键会被忽略) |
--- ---
@@ -178,6 +179,12 @@ interface TabDescriptor {
id: string id: string
/** 标题(i18n 友好:传字符串或返回字符串的函数) */ /** 标题(i18n 友好:传字符串或返回字符串的函数) */
title: string | (() => string) title: string | (() => string)
/**
* 一行说明,渲染在标题下方(DSH 原生右侧栏的新建标签页 / guide 列表)。
* 不声明时回退到通用句「在 DSH 原生右侧栏打开」——所有插件页面就都长一样了,
* 所以请写清这一页到底做什么。函数形式在渲染时求值,跟随语言。
*/
description?: string | (() => string)
/** 图标:ReactNode 或 (size: number) => ReactNode */ /** 图标:ReactNode 或 (size: number) => ReactNode */
icon?: ReactNode | ((size: number) => ReactNode) icon?: ReactNode | ((size: number) => ReactNode)
/** + 菜单排序(升序);默认 100。内置:editor=10, git=20, subagent=30, sidechat=35, terminal=40, browser=50 */ /** + 菜单排序(升序);默认 100。内置:editor=10, git=20, subagent=30, sidechat=35, terminal=40, browser=50 */
+6
View File
@@ -82,6 +82,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
return [ return [
{ {
id: 'editor', id: 'editor',
description: () => t('guideDescFiles'),
// The single files window: an editor tab with no path IS the file // The single files window: an editor tab with no path IS the file
// explorer (empty hint + docked tree); with a path it previews/edits // explorer (empty hint + docked tree); with a path it previews/edits
// the file. Visible in the + menu in the explorer's old slot. // 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). // log, and the git status needs a fetch — both stay out of the badge).
id: 'git', id: 'git',
title: () => t('changes'), title: () => t('changes'),
description: () => t('guideDescGit'),
icon: (size: number) => <IconDiffOutline16 size={size} />, icon: (size: number) => <IconDiffOutline16 size={size} />,
order: 20, order: 20,
single: true, single: true,
@@ -170,6 +172,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
{ {
id: 'subagent', id: 'subagent',
title: () => t('subagent'), title: () => t('subagent'),
description: () => t('guideDescSubagent'),
icon: (size: number) => <IconThinkOutline16 size={size} />, icon: (size: number) => <IconThinkOutline16 size={size} />,
order: 30, order: 30,
single: true, single: true,
@@ -198,6 +201,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
{ {
id: 'sidechat', id: 'sidechat',
title: () => t('sideChat'), title: () => t('sideChat'),
description: () => t('guideDescSidechat'),
icon: (size: number) => <IconNewChatOutline16 size={size} />, icon: (size: number) => <IconNewChatOutline16 size={size} />,
order: 35, order: 35,
// Codex-style: EVERY side conversation is its own tab. A plain open // 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', id: 'terminal',
title: () => t('terminal'), title: () => t('terminal'),
description: () => t('guideDescTerminal'),
icon: (size: number) => <IconTerminalOutline16 size={size} />, icon: (size: number) => <IconTerminalOutline16 size={size} />,
order: 40, order: 40,
available: (_ctx, _scope, state) => uiTerminalCount(state) < TERMINAL_LIMIT, available: (_ctx, _scope, state) => uiTerminalCount(state) < TERMINAL_LIMIT,
@@ -307,6 +312,7 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
{ {
id: 'browser', id: 'browser',
title: () => t('browser'), title: () => t('browser'),
description: () => t('guideDescBrowser'),
icon: (size: number) => <IconGlobeOutline16 size={size} />, icon: (size: number) => <IconGlobeOutline16 size={size} />,
order: 50, order: 50,
// Declarative settings: the sandbox escape hatch, the link-takeover // Declarative settings: the sandbox escape hatch, the link-takeover
+6
View File
@@ -19,6 +19,12 @@
export const ar: Record<string, string> = { export const ar: Record<string, string> = {
files: 'الملفات', files: 'الملفات',
nativeGuideDesc: 'Open in the DSH sidebar', nativeGuideDesc: 'Open in the DSH sidebar',
guideDescFiles: 'استعرض شجرة ملفات مساحة العمل؛ انقر للمعاينة أو التحرير',
guideDescGit: 'تغييرات Git وتعديلات ملفات هذه الجلسة في تبويب واحد',
guideDescSubagent: 'بنية الوكلاء الفرعيين والمهام الخلفية مباشرةً',
guideDescSidechat: 'خيط متابعة مستقل يرث سياق هذه الجلسة',
guideDescTerminal: 'صدفة حقيقية (xterm + pty) مع إعادة الاتصال',
guideDescBrowser: 'متصفح مدمج يمكنه فتح روابط المحادثة',
changesSessionEmpty: 'لا توجد عمليات ملفات في هذه الجلسة بعد', changesSessionEmpty: 'لا توجد عمليات ملفات في هذه الجلسة بعد',
changesRead: 'قراءة', changesRead: 'قراءة',
changesMdReading: 'قراءة', changesMdReading: 'قراءة',
+6
View File
@@ -4,6 +4,12 @@
export const de: Record<string, string> = { export const de: Record<string, string> = {
files: 'Dateien', files: 'Dateien',
nativeGuideDesc: 'Open in the DSH sidebar', 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', changesSessionEmpty: 'Noch keine Dateioperationen in dieser Sitzung',
changesRead: 'Lesen', changesRead: 'Lesen',
changesMdReading: 'Lesen', changesMdReading: 'Lesen',
+6
View File
@@ -11,6 +11,12 @@
export const fr: Record<string, string> = { export const fr: Record<string, string> = {
files: 'Fichiers', files: 'Fichiers',
nativeGuideDesc: 'Open in the DSH sidebar', 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', changesSessionEmpty: 'Aucune opération de fichier dans cette session',
changesRead: 'Lecture', changesRead: 'Lecture',
changesMdReading: 'Lecture', changesMdReading: 'Lecture',
+6
View File
@@ -18,6 +18,12 @@
export const hi: Record<string, string> = { export const hi: Record<string, string> = {
files: 'फ़ाइलें', files: 'फ़ाइलें',
nativeGuideDesc: 'Open in the DSH sidebar', nativeGuideDesc: 'Open in the DSH sidebar',
guideDescFiles: 'वर्कस्पेस फ़ाइल ट्री देखें; क्लिक करने पर प्रीव्यू या संपादन खुलता है',
guideDescGit: 'Git बदलाव और इस सत्र के फ़ाइल बदलाव एक ही टैब में',
guideDescSubagent: 'सबएजेंट टोपोलॉजी और बैकग्राउंड कार्य, लाइव',
guideDescSidechat: 'इस सत्र का संदर्भ विरासत में लेने वाला स्वतंत्र फ़ॉलो-अप थ्रेड',
guideDescTerminal: 'असली शेल (xterm + pty), रीकनेक्ट सहित',
guideDescBrowser: 'अंतर्निहित ब्राउज़र, चैट के लिंक भी खोल सकता है',
changesSessionEmpty: 'इस सत्र में अभी कोई फ़ाइल ऑपरेशन नहीं', changesSessionEmpty: 'इस सत्र में अभी कोई फ़ाइल ऑपरेशन नहीं',
changesRead: 'पढ़ना', changesRead: 'पढ़ना',
changesMdReading: 'पठन', changesMdReading: 'पठन',
+6
View File
@@ -16,6 +16,12 @@
export const id: Record<string, string> = { export const id: Record<string, string> = {
files: 'Berkas', files: 'Berkas',
nativeGuideDesc: 'Open in the DSH sidebar', 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', changesSessionEmpty: 'Belum ada operasi file di sesi ini',
changesRead: 'Baca', changesRead: 'Baca',
changesMdReading: 'Baca', changesMdReading: 'Baca',
+6
View File
@@ -9,6 +9,12 @@
export const it: Record<string, string> = { export const it: Record<string, string> = {
files: 'File', files: 'File',
nativeGuideDesc: 'Open in the DSH sidebar', 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', changesSessionEmpty: 'Ancora nessuna operazione sui file in questa sessione',
changesRead: 'Lettura', changesRead: 'Lettura',
changesMdReading: 'Lettura', changesMdReading: 'Lettura',
+6
View File
@@ -18,6 +18,12 @@
export const ja: Record<string, string> = { export const ja: Record<string, string> = {
files: 'ファイル', files: 'ファイル',
nativeGuideDesc: 'DSH のサイドバーで開く', nativeGuideDesc: 'DSH のサイドバーで開く',
guideDescFiles: 'ワークスペースのファイルツリーを閲覧し、クリックでプレビュー/編集',
guideDescGit: 'Git の変更と今回のセッションのファイル変更を 1 つのタブに',
guideDescSubagent: 'サブエージェントの構成とバックグラウンドタスクをリアルタイム表示',
guideDescSidechat: '現在のセッションの文脈を引き継ぐ独立した追加スレッド',
guideDescTerminal: '本物のシェル(xterm + pty)、再接続対応',
guideDescBrowser: '埋め込みブラウザ。チャット内のリンクも引き受け可能',
changesSessionEmpty: 'このセッションにはファイル操作がまだありません', changesSessionEmpty: 'このセッションにはファイル操作がまだありません',
changesRead: '読み取り', changesRead: '読み取り',
changesMdReading: '閲覧', changesMdReading: '閲覧',
+6
View File
@@ -10,6 +10,12 @@
export const ko: Record<string, string> = { export const ko: Record<string, string> = {
files: '파일', files: '파일',
nativeGuideDesc: 'Open in the DSH sidebar', nativeGuideDesc: 'Open in the DSH sidebar',
guideDescFiles: '워크스페이스 파일 트리를 보고 클릭하면 미리보기/편집',
guideDescGit: 'Git 변경 사항과 이번 세션의 파일 변경을 한 탭에서',
guideDescSubagent: '하위 에이전트 구조와 백그라운드 작업을 실시간으로',
guideDescSidechat: '현재 세션의 컨텍스트를 이어받는 독립 후속 스레드',
guideDescTerminal: '실제 셸(xterm + pty), 재연결 지원',
guideDescBrowser: '내장 브라우저, 채팅 링크도 대신 열 수 있음',
changesSessionEmpty: '이 세션에 아직 파일 작업이 없습니다', changesSessionEmpty: '이 세션에 아직 파일 작업이 없습니다',
changesRead: '읽기', changesRead: '읽기',
changesMdReading: '읽기', changesMdReading: '읽기',
+6
View File
@@ -16,6 +16,12 @@
export const nl: Record<string, string> = { export const nl: Record<string, string> = {
files: 'Bestanden', files: 'Bestanden',
nativeGuideDesc: 'Open in the DSH sidebar', 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', changesSessionEmpty: 'Nog geen bestandsbewerkingen in deze sessie',
changesRead: 'Lezen', changesRead: 'Lezen',
changesMdReading: 'Lezen', changesMdReading: 'Lezen',
+6
View File
@@ -20,6 +20,12 @@
export const pl: Record<string, string> = { export const pl: Record<string, string> = {
files: 'Pliki', files: 'Pliki',
nativeGuideDesc: 'Open in the DSH sidebar', 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', changesSessionEmpty: 'Brak operacji na plikach w tej sesji',
changesRead: 'Odczyt', changesRead: 'Odczyt',
changesMdReading: 'Czytanie', changesMdReading: 'Czytanie',
+6
View File
@@ -1,6 +1,12 @@
export const pt: Record<string, string> = { export const pt: Record<string, string> = {
files: 'Arquivos', files: 'Arquivos',
nativeGuideDesc: 'Open in the DSH sidebar', 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', changesSessionEmpty: 'Ainda sem operações de arquivo nesta sessão',
changesRead: 'Leitura', changesRead: 'Leitura',
changesMdReading: 'Leitura', changesMdReading: 'Leitura',
+6
View File
@@ -14,6 +14,12 @@
export const ru: Record<string, string> = { export const ru: Record<string, string> = {
files: 'Файлы', files: 'Файлы',
nativeGuideDesc: 'Open in the DSH sidebar', nativeGuideDesc: 'Open in the DSH sidebar',
guideDescFiles: 'Дерево файлов рабочей области: клик открывает просмотр или редактор',
guideDescGit: 'Изменения Git и правки файлов этой сессии в одной вкладке',
guideDescSubagent: 'Топология субагентов и фоновые задачи в реальном времени',
guideDescSidechat: 'Независимый дополнительный тред с контекстом этой сессии',
guideDescTerminal: 'Настоящий shell (xterm + pty) с переподключением',
guideDescBrowser: 'Встроенный браузер, перехватывает ссылки из чата',
changesSessionEmpty: 'В этой сессии пока нет файловых операций', changesSessionEmpty: 'В этой сессии пока нет файловых операций',
changesRead: 'Чтение', changesRead: 'Чтение',
changesMdReading: 'Чтение', changesMdReading: 'Чтение',
+6
View File
@@ -1,6 +1,12 @@
export const sv: Record<string, string> = { export const sv: Record<string, string> = {
files: 'Filer', files: 'Filer',
nativeGuideDesc: 'Open in the DSH sidebar', 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', changesSessionEmpty: 'Inga filoperationer i den här sessionen ännu',
changesRead: 'Läs', changesRead: 'Läs',
changesMdReading: 'Läsning', changesMdReading: 'Läsning',
+6
View File
@@ -18,6 +18,12 @@
export const th: Record<string, string> = { export const th: Record<string, string> = {
files: 'ไฟล์', files: 'ไฟล์',
nativeGuideDesc: 'Open in the DSH sidebar', nativeGuideDesc: 'Open in the DSH sidebar',
guideDescFiles: 'เรียกดูแผนผังไฟล์ของเวิร์กสเปซ คลิกเพื่อดูตัวอย่างหรือแก้ไข',
guideDescGit: 'การเปลี่ยนแปลง Git และไฟล์ที่แก้ในเซสชันนี้ในแท็บเดียว',
guideDescSubagent: 'โครงสร้างซับเอเจนต์และงานเบื้องหลังแบบเรียลไทม์',
guideDescSidechat: 'เธรดถามต่ออิสระที่สืบทอดบริบทของเซสชันนี้',
guideDescTerminal: 'เชลล์จริง (xterm + pty) เชื่อมต่อใหม่ได้',
guideDescBrowser: 'เบราว์เซอร์ในตัว รับช่วงลิงก์จากแชทได้',
changesSessionEmpty: 'ยังไม่มีการดำเนินการไฟล์ในเซสชันนี้', changesSessionEmpty: 'ยังไม่มีการดำเนินการไฟล์ในเซสชันนี้',
changesRead: 'อ่าน', changesRead: 'อ่าน',
changesMdReading: 'อ่าน', changesMdReading: 'อ่าน',
+6
View File
@@ -18,6 +18,12 @@
export const tr: Record<string, string> = { export const tr: Record<string, string> = {
files: 'Dosyalar', files: 'Dosyalar',
nativeGuideDesc: 'Open in the DSH sidebar', 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', changesSessionEmpty: 'Bu oturumda henüz dosya işlemi yok',
changesRead: 'Okuma', changesRead: 'Okuma',
changesMdReading: 'Okuma', changesMdReading: 'Okuma',
+6
View File
@@ -18,6 +18,12 @@
export const vi: Record<string, string> = { export const vi: Record<string, string> = {
files: 'Tệp', files: 'Tệp',
nativeGuideDesc: 'Open in the DSH sidebar', 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', changesSessionEmpty: 'Chưa có thao tác tệp nào trong phiên này',
changesRead: 'Đọc', changesRead: 'Đọc',
changesMdReading: 'Đọc', changesMdReading: 'Đọc',
+6
View File
@@ -33,6 +33,12 @@
export const zhHK: Record<string, string> = { export const zhHK: Record<string, string> = {
files: '檔案', files: '檔案',
nativeGuideDesc: 'Open in the DSH sidebar', nativeGuideDesc: 'Open in the DSH sidebar',
guideDescFiles: '瀏覽工作區檔案樹,點開即可預覽或編輯',
guideDescGit: 'Git 變更與本輪檔案改動,雙視角合一',
guideDescSubagent: '子代理拓撲與背景任務,即時狀態',
guideDescSidechat: '繼承目前工作階段上下文的獨立追問執行緒',
guideDescTerminal: '真實 shell 終端(xterm + pty),斷線重連',
guideDescBrowser: '內嵌網頁瀏覽器,可接管聊天中的外鏈',
changesSessionEmpty: '本工作階段還沒有檔案操作', changesSessionEmpty: '本工作階段還沒有檔案操作',
changesRead: '讀取', changesRead: '讀取',
changesMdReading: '閱讀', changesMdReading: '閱讀',
+6
View File
@@ -33,6 +33,12 @@
export const zhMO: Record<string, string> = { export const zhMO: Record<string, string> = {
files: '檔案', files: '檔案',
nativeGuideDesc: 'Open in the DSH sidebar', nativeGuideDesc: 'Open in the DSH sidebar',
guideDescFiles: '瀏覽工作區檔案樹,點開即可預覽或編輯',
guideDescGit: 'Git 變更與本輪檔案改動,雙視角合一',
guideDescSubagent: '子代理拓撲與背景任務,即時狀態',
guideDescSidechat: '繼承目前工作階段上下文的獨立追問執行緒',
guideDescTerminal: '真實 shell 終端(xterm + pty),斷線重連',
guideDescBrowser: '內嵌網頁瀏覽器,可接管聊天中的外鏈',
changesSessionEmpty: '本工作階段還沒有檔案操作', changesSessionEmpty: '本工作階段還沒有檔案操作',
changesRead: '讀取', changesRead: '讀取',
changesMdReading: '閱讀', changesMdReading: '閱讀',
+6
View File
@@ -33,6 +33,12 @@
export const zhTW: Record<string, string> = { export const zhTW: Record<string, string> = {
files: '檔案', files: '檔案',
nativeGuideDesc: 'Open in the DSH sidebar', nativeGuideDesc: 'Open in the DSH sidebar',
guideDescFiles: '瀏覽工作區檔案樹,點開即可預覽或編輯',
guideDescGit: 'Git 變更與本輪檔案改動,雙視角合一',
guideDescSubagent: '子代理拓撲與背景任務,即時狀態',
guideDescSidechat: '繼承目前工作階段上下文的獨立追問執行緒',
guideDescTerminal: '真實 shell 終端(xterm + pty),斷線重連',
guideDescBrowser: '內嵌網頁瀏覽器,可接管聊天中的外鏈',
changesSessionEmpty: '本作業階段還沒有檔案操作', changesSessionEmpty: '本作業階段還沒有檔案操作',
changesRead: '讀取', changesRead: '讀取',
changesMdReading: '閱讀', changesMdReading: '閱讀',
+12
View File
@@ -25,6 +25,12 @@
export const zh = { export const zh = {
files: '文件', files: '文件',
nativeGuideDesc: '在 DSH 原生右侧栏打开', nativeGuideDesc: '在 DSH 原生右侧栏打开',
guideDescFiles: '浏览工作区文件树,点开即预览或编辑',
guideDescGit: 'Git 变更与本轮文件改动,双视角合一',
guideDescSubagent: '子代理拓扑与后台任务,实时状态',
guideDescSidechat: '继承当前会话上下文的独立追问线程',
guideDescTerminal: '真实 shell 终端(xterm + pty),断线重连',
guideDescBrowser: '内嵌网页浏览器,可接管聊天里的外链',
changesSessionEmpty: '本会话还没有文件操作', changesSessionEmpty: '本会话还没有文件操作',
changesRead: '读取', changesRead: '读取',
changesMdReading: '阅读', changesMdReading: '阅读',
@@ -460,6 +466,12 @@ export const zh = {
export const en: Record<keyof typeof zh, string> = { export const en: Record<keyof typeof zh, string> = {
files: 'Files', files: 'Files',
nativeGuideDesc: 'Open in the DSH sidebar', 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', changesSessionEmpty: 'No file operations in this session yet',
changesRead: 'Read', changesRead: 'Read',
changesMdReading: 'Reading', changesMdReading: 'Reading',
+13 -2
View File
@@ -63,6 +63,17 @@ function titleOf(descriptor: TabDescriptor): string {
return typeof descriptor.title === 'function' ? descriptor.title() : descriptor.title 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 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 * The native guide renders `entry.icon`, so a takeover registered without one
@@ -181,7 +192,7 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
guide: [{ guide: [{
order: descriptor.order ?? 100, order: descriptor.order ?? 100,
title: () => titleOf(descriptor), title: () => titleOf(descriptor),
description: () => t('nativeGuideDesc'), description: () => descriptionOf(descriptor),
...guideIconOf(icon), ...guideIconOf(icon),
}], }],
}), }),
@@ -208,7 +219,7 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
guide: [{ guide: [{
order: 10, order: 10,
title: () => t('files'), title: () => t('files'),
description: () => t('nativeGuideDesc'), description: () => descriptionOf(editor),
// The takeover IS the editor descriptor's page, so it carries the // The takeover IS the editor descriptor's page, so it carries the
// editor's glyph: without it the "Files" row is the only guide // editor's glyph: without it the "Files" row is the only guide
// entry with an empty icon slot. // entry with an empty icon slot.
+8
View File
@@ -162,6 +162,14 @@ export interface TabDescriptor {
/** Unique id; also the `SidebarTab.type` value (`'explorer'`, `'my-plugin:db'`). */ /** Unique id; also the `SidebarTab.type` value (`'explorer'`, `'my-plugin:db'`). */
id: string id: string
title: string | (() => 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) icon?: ReactNode | ((size: number) => ReactNode)
/** + menu sort order (ascending); default 100. */ /** + menu sort order (ascending); default 100. */
order?: number order?: number
+16
View File
@@ -44,6 +44,22 @@ describe('built-in tab registrations', () => {
expect(changes?.component).toBeDefined() 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)', () => { it('the changes tab declares no settings of its own (the diff always docks)', () => {
const { service } = setup() const { service } = setup()
expect(service.getTab('git')?.settings).toBeUndefined() expect(service.getTab('git')?.settings).toBeUndefined()
+10 -3
View File
@@ -162,8 +162,8 @@ describe('registerNativeSurface lifecycle (service-driven registration)', () =>
const store = createSidebarStore() const store = createSidebarStore()
store.setSession('s1') store.setSession('s1')
const service = createBetterSidebarService(store) const service = createBetterSidebarService(store)
service.registerTab({ id: 'terminal', title: 'Terminal', component: () => null }) service.registerTab({ id: 'terminal', title: 'Terminal', component: () => null, description: () => 'Runs a shell' })
service.registerTab({ id: 'editor', title: 'Files', component: () => null, icon: () => null }) service.registerTab({ id: 'editor', title: 'Files', component: () => null, icon: () => null, description: () => 'Browse the tree' })
const records = createNativeTabRecords() const records = createNativeTabRecords()
const registered: Array<{ id: string; kind: string; title: (address: string) => string; guide: unknown }> = [] 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() expect(filesType?.guide).toBeDefined()
// The takeover carries the editor's glyph, so the "Files" guide row is // The takeover carries the editor's glyph, so the "Files" guide row is
// not the only one with a blank icon slot. // 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() 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() dispose()
}) })