feat(dsh): adapt to DSH 0.1.5-alpha.2
Baseline moves to DSH 0.1.5-alpha.2 only (peer floor ^0.1.5-alpha.2, CI pin, manifest engines); alpha.1 users stay on v0.19.0-alpha.0. Host-contract changes handled: - the native guide entry lost its `description` field (entries are icon+title capsules now): `TabDescriptor.description` and the six `guideDesc*` keys plus the `nativeGuideDesc` fallback are removed. - the file address grammar changed: `fileAddressFor` always returns a session-scoped address (absolute paths keep their leading `/`), `parseFileAddress` is prefix-based and strips `?`/`#`. The plugin's purity-gated mirror follows. - global main panels replaced the `conversation` root slot with the keyed `main` slot (`main.conversation`): the bottom workbench's center-column locator now accepts both keys and skips `display: contents` slot hosts. Fixes a pre-existing fill bug the alpha.2 probe made obvious: the native tab body host is a block scroller with a definite height, so tab roots that only declared `flex: 1` collapsed to content height and the side chat composer drifted off the pane bottom. The native adapter now wraps every tab body in a `height: 100%` column flex host. Also promotes the dependencies `dsh-client-ui-primitives@0.1.5-alpha.2` dropped from its manifest (anser / shiki / mdast-util-* / micromark-* / katex / @shikijs/langs) and the newly unmet `dsh-session-persistence` peer into devDependencies.
This commit is contained in:
@@ -82,7 +82,6 @@ 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.
|
||||
@@ -149,7 +148,6 @@ 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,
|
||||
@@ -172,7 +170,6 @@ 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,
|
||||
@@ -201,7 +198,6 @@ 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
|
||||
@@ -247,7 +243,6 @@ 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,
|
||||
@@ -312,7 +307,6 @@ 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
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
/** Stable selector for the DSH AppFrame conversation slot. */
|
||||
const CENTER_COLUMN_SELECTOR = '#root [data-slot="conversation"]'
|
||||
/**
|
||||
* Anchor selectors for the DSH AppFrame conversation slot host, newest first:
|
||||
*
|
||||
* - DSH 0.1.5-alpha.2 replaced the root `conversation` slot with a root-scoped
|
||||
* keyed `main` slot, so the conversation renders under `main.conversation`
|
||||
* (ConversationPanel -> `renderSlot('main.conversation')`);
|
||||
* - alpha.1 and earlier spell the same host `conversation`.
|
||||
*
|
||||
* Both keys are accepted deliberately so one build spans the alpha.1 → alpha.2
|
||||
* boundary; only one exists in any given host.
|
||||
*/
|
||||
const CENTER_COLUMN_SELECTOR = '#root [data-slot="main.conversation"], #root [data-slot="conversation"]'
|
||||
|
||||
/** Match Sidebar's existing last-resort retry cadence (issue #248). */
|
||||
export const CENTER_COLUMN_REVALIDATE_MS = 1500
|
||||
@@ -27,6 +37,22 @@ const validatedAt = new WeakMap<HTMLElement, number>()
|
||||
*/
|
||||
const documentStyleState = new WeakMap<Document, string | null>()
|
||||
|
||||
/**
|
||||
* From the matched conversation slot host, walk up through transparent slot
|
||||
* hosts to the real column: DSH slot hosts render with `display: contents`,
|
||||
* so on alpha.2 the `main.conversation` host (and any further keyed slot
|
||||
* wrappers above it) contributes no box, and the center column is the first
|
||||
* ancestor whose computed display is NOT `contents`.
|
||||
*/
|
||||
function columnOfSlotHost(host: Element): HTMLElement | null {
|
||||
const view = host.ownerDocument.defaultView
|
||||
let node = host.parentElement
|
||||
while (node !== null && (view?.getComputedStyle(node).display ?? '') === 'contents') {
|
||||
node = node.parentElement
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the AppFrame center column while keeping streaming mutations cheap.
|
||||
*
|
||||
@@ -74,7 +100,8 @@ export function resolveCenterColumn(
|
||||
}
|
||||
|
||||
const query = options.query ?? (() => doc.querySelector(CENTER_COLUMN_SELECTOR))
|
||||
const col = query()?.parentElement as HTMLElement | null | undefined
|
||||
const host = query()
|
||||
const col = host === null || host === undefined ? null : columnOfSlotHost(host)
|
||||
if (col === null || col === undefined || !col.isConnected) return undefined
|
||||
|
||||
validatedAt.set(col, now)
|
||||
|
||||
@@ -15,8 +15,11 @@
|
||||
* transitions so the reserved edge tracks the pointer.
|
||||
*/
|
||||
/* The AppFrame's center column. The Sidebar shell's locator (Sidebar.tsx
|
||||
locate/measureCenter) tags the measured column node with
|
||||
[data-dsh-center-col] — one attribute write per node lifetime. The rule
|
||||
locate/measureCenter, resolving via center-column.ts) tags the measured
|
||||
column node with [data-dsh-center-col] — one attribute write per node
|
||||
lifetime. The locator anchors on the conversation slot host
|
||||
(`main.conversation` on DSH 0.1.5-alpha.2+, `conversation` on alpha.1)
|
||||
and skips the transparent display:contents slot hosts above it. The rule
|
||||
used to anchor on a structural has()-selector over the conversation slot
|
||||
wrapper, whose match cache invalidates on every #root subtree mutation
|
||||
(the streaming chat mutates it constantly), re-evaluating the selector
|
||||
|
||||
@@ -18,13 +18,6 @@
|
||||
/** The ar dictionary (key-set-equal to zh, enforced by the type annotation in locales.ts). */
|
||||
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: 'قراءة',
|
||||
|
||||
@@ -3,13 +3,6 @@
|
||||
*/
|
||||
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',
|
||||
|
||||
@@ -10,13 +10,6 @@
|
||||
/** fr dictionary for the `betterSidebar` namespace. */
|
||||
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',
|
||||
|
||||
@@ -17,13 +17,6 @@
|
||||
/** The hi dictionary (key-set-equal to zh, enforced by the type annotation in locales.ts). */
|
||||
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: 'पठन',
|
||||
|
||||
@@ -15,13 +15,6 @@
|
||||
|
||||
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',
|
||||
|
||||
@@ -8,13 +8,6 @@
|
||||
|
||||
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',
|
||||
|
||||
@@ -17,13 +17,6 @@
|
||||
/** The ja dictionary (key-set-equal to zh, enforced by the type annotation in locales.ts). */
|
||||
export const ja: Record<string, string> = {
|
||||
files: 'ファイル',
|
||||
nativeGuideDesc: 'DSH のサイドバーで開く',
|
||||
guideDescFiles: 'ワークスペースのファイルツリーを閲覧し、クリックでプレビュー/編集',
|
||||
guideDescGit: 'Git の変更と今回のセッションのファイル変更を 1 つのタブに',
|
||||
guideDescSubagent: 'サブエージェントの構成とバックグラウンドタスクをリアルタイム表示',
|
||||
guideDescSidechat: '現在のセッションの文脈を引き継ぐ独立した追加スレッド',
|
||||
guideDescTerminal: '本物のシェル(xterm + pty)、再接続対応',
|
||||
guideDescBrowser: '埋め込みブラウザ。チャット内のリンクも引き受け可能',
|
||||
changesSessionEmpty: 'このセッションにはファイル操作がまだありません',
|
||||
changesRead: '読み取り',
|
||||
changesMdReading: '閲覧',
|
||||
|
||||
@@ -9,13 +9,6 @@
|
||||
|
||||
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: '읽기',
|
||||
|
||||
@@ -15,13 +15,6 @@
|
||||
*/
|
||||
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',
|
||||
|
||||
@@ -19,13 +19,6 @@
|
||||
/** The pl dictionary (key-set-equal to zh, enforced by the type annotation in locales.ts). */
|
||||
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,12 +1,5 @@
|
||||
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',
|
||||
|
||||
@@ -13,13 +13,6 @@
|
||||
|
||||
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,12 +1,5 @@
|
||||
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',
|
||||
|
||||
@@ -17,13 +17,6 @@
|
||||
/** The th dictionary (key-set-equal to zh, enforced by the type annotation in locales.ts). */
|
||||
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: 'อ่าน',
|
||||
|
||||
@@ -17,13 +17,6 @@
|
||||
/** The tr dictionary (key-set-equal to zh, enforced by the type annotation in locales.ts). */
|
||||
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',
|
||||
|
||||
@@ -17,13 +17,6 @@
|
||||
/** The vi dictionary (key-set-equal to zh, enforced by the type annotation in locales.ts). */
|
||||
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',
|
||||
|
||||
@@ -32,13 +32,6 @@
|
||||
/** The zh-HK dictionary (key-set-equal to zh, enforced by the type annotation in locales.ts). */
|
||||
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: '閱讀',
|
||||
|
||||
@@ -32,13 +32,6 @@
|
||||
/** The zh-HK dictionary (key-set-equal to zh, enforced by the type annotation in locales.ts). */
|
||||
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: '閱讀',
|
||||
|
||||
@@ -32,13 +32,6 @@
|
||||
/** The zh-HK dictionary (key-set-equal to zh, enforced by the type annotation in locales.ts). */
|
||||
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: '閱讀',
|
||||
|
||||
@@ -24,13 +24,6 @@
|
||||
/** The zh dictionary (also registered into the DSH locale registry under {@link LOCALE_NS}). */
|
||||
export const zh = {
|
||||
files: '文件',
|
||||
nativeGuideDesc: '在 DSH 原生右侧栏打开',
|
||||
guideDescFiles: '浏览工作区文件树,点开即预览或编辑',
|
||||
guideDescGit: 'Git 变更与本轮文件改动,双视角合一',
|
||||
guideDescSubagent: '子代理拓扑与后台任务,实时状态',
|
||||
guideDescSidechat: '继承当前会话上下文的独立追问线程',
|
||||
guideDescTerminal: '真实 shell 终端(xterm + pty),断线重连',
|
||||
guideDescBrowser: '内嵌网页浏览器,可接管聊天里的外链',
|
||||
changesSessionEmpty: '本会话还没有文件操作',
|
||||
changesRead: '读取',
|
||||
changesMdReading: '阅读',
|
||||
@@ -465,13 +458,6 @@ export const zh = {
|
||||
/** The en dictionary (key-set-equal to zh, enforced by the type annotation). */
|
||||
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',
|
||||
|
||||
@@ -43,7 +43,7 @@ interface NativeTabRegistry {
|
||||
priority?: 'extension' | 'builtin' | 'fallback'
|
||||
canOpen?: (address: string) => boolean
|
||||
title: (address: string) => string
|
||||
guide?: readonly { order: number; title: () => string; description: () => string; icon?: unknown }[]
|
||||
guide?: readonly { order: number; title: () => string; icon?: unknown }[]
|
||||
}): () => void
|
||||
}
|
||||
|
||||
@@ -63,17 +63,6 @@ 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
|
||||
@@ -192,7 +181,6 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
|
||||
guide: [{
|
||||
order: descriptor.order ?? 100,
|
||||
title: () => titleOf(descriptor),
|
||||
description: () => descriptionOf(descriptor),
|
||||
...guideIconOf(icon),
|
||||
}],
|
||||
}),
|
||||
@@ -219,7 +207,6 @@ export function registerNativeSurface(deps: NativeSurfaceDeps): () => void {
|
||||
guide: [{
|
||||
order: 10,
|
||||
title: () => t('files'),
|
||||
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.
|
||||
|
||||
@@ -281,33 +281,49 @@ export function NativeTabBody(props: NativeBodyInjected & NativeBodyFrameworkPro
|
||||
})
|
||||
useEffect(() => () => { records.drop(nativeTab.id) }, [records, nativeTab.id])
|
||||
if (descriptor === undefined) {
|
||||
return createElement(OrphanedTab, { ctx, store, scope, tab: view.tab, visible: nativeTab.visible })
|
||||
// The orphaned fallback sits in the SAME native host as a live body, so
|
||||
// it gets the same full-height box (its own root also relies on the
|
||||
// `flex: 1` contract the column host restores). The host div carries
|
||||
// `data-dsh-native-tab-host` (empty value) so the e2e lane can assert
|
||||
// the fill, mirroring `data-dsh-better-sidebar`.
|
||||
return createElement(
|
||||
'div',
|
||||
{ className: css.nativeTabHost, 'data-dsh-native-tab-host': '' },
|
||||
createElement(OrphanedTab, { ctx, store, scope, tab: view.tab, visible: nativeTab.visible }),
|
||||
)
|
||||
}
|
||||
return createElement(
|
||||
RenderBoundary,
|
||||
{ className: css.tabBoundaryError },
|
||||
createElement(descriptor.component, {
|
||||
ctx,
|
||||
store,
|
||||
scope,
|
||||
tab: view.tab,
|
||||
visible: nativeTab.visible,
|
||||
expanded: view.expanded,
|
||||
revealed: view.revealed,
|
||||
onToggleDir: (path: string) => { records.toggleExpanded(nativeTab.id, path) },
|
||||
onReferenceFile: (path: string, isDir: boolean) => { referenceInChat(ctx, sessionId, cwd, path, isDir) },
|
||||
onOpenDiff: (tab: SidebarTab) => {
|
||||
service.openTab({
|
||||
type: 'diff',
|
||||
title: tab.title,
|
||||
id: tab.id,
|
||||
...(tab.diff === undefined ? {} : { diff: tab.diff }),
|
||||
}, scope)
|
||||
},
|
||||
onSubagentJump: (childSessionId: string) => {
|
||||
service.openTab({ type: 'subagent', meta: { childSessionId } }, scope)
|
||||
},
|
||||
}),
|
||||
// The full-height host wrapper (see the `.nativeTabHost` rule in
|
||||
// sidebar.module.css for the native `.paneBody` contract); its
|
||||
// `data-dsh-native-tab-host` attribute lets the e2e lane assert the fill.
|
||||
createElement(
|
||||
'div',
|
||||
{ className: css.nativeTabHost, 'data-dsh-native-tab-host': '' },
|
||||
createElement(descriptor.component, {
|
||||
ctx,
|
||||
store,
|
||||
scope,
|
||||
tab: view.tab,
|
||||
visible: nativeTab.visible,
|
||||
expanded: view.expanded,
|
||||
revealed: view.revealed,
|
||||
onToggleDir: (path: string) => { records.toggleExpanded(nativeTab.id, path) },
|
||||
onReferenceFile: (path: string, isDir: boolean) => { referenceInChat(ctx, sessionId, cwd, path, isDir) },
|
||||
onOpenDiff: (tab: SidebarTab) => {
|
||||
service.openTab({
|
||||
type: 'diff',
|
||||
title: tab.title,
|
||||
id: tab.id,
|
||||
...(tab.diff === undefined ? {} : { diff: tab.diff }),
|
||||
}, scope)
|
||||
},
|
||||
onSubagentJump: (childSessionId: string) => {
|
||||
service.openTab({ type: 'subagent', meta: { childSessionId } }, scope)
|
||||
},
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
* tab's content identity: the same address is the same tab. Two scopes exist:
|
||||
*
|
||||
* - `dsh-resource://file/session/<sessionId>/<path>` names a file by its path
|
||||
* relative to that session's workspace root (`src/a.ts`, no leading `/`);
|
||||
* relative to that session's workspace root OR by its absolute path kept
|
||||
* absolute inside the session scope (`src/a.ts` vs `/outside/a.ts` — the
|
||||
* leading `/` survives; the host resolves it against the root it holds for
|
||||
* the session);
|
||||
* - `dsh-resource://file/absolute/<path>` names a file by its absolute path
|
||||
* with the leading `/` dropped (`absolute/home/me/x.txt`; Windows
|
||||
* `absolute/C:/x/y.txt`; a UNC path keeps an empty first segment,
|
||||
* `absolute//server/share/x.txt`). It carries no session.
|
||||
* `absolute//server/share/x.txt`). It carries no session. `fileAddressFor`
|
||||
* no longer produces this scope on DSH 0.1.5-alpha.2, but it must keep
|
||||
* parsing: legacy and third-party addresses still spell it.
|
||||
*
|
||||
* Every id and path segment is component-encoded, so a name carrying `#`, `?`
|
||||
* or a space survives the round trip; `:` stays literal so a drive letter
|
||||
@@ -18,8 +23,10 @@
|
||||
* The plugin parses these addresses itself instead of importing
|
||||
* `@deepseek-ai/dsh-util-workspace-path`: the client bundle's purity gate
|
||||
* forbids value-importing an unlisted `@deepseek-ai/*` package. This module
|
||||
* mirrors that package's implementation (`packages/util/workspace-path` in
|
||||
* DSH 0.1.5) and is pinned by tests/resource-address.spec.ts.
|
||||
* mirrors that package's implementation — `packages/util/workspace-path/src/file-address.ts`
|
||||
* and `fileAddressFor` in `packages/util/workspace-path/src/index.ts` in
|
||||
* DSH 0.1.5-alpha.2 (github.com/deepseek-ai/deepseek-harness, tag
|
||||
* `dsh-v0.1.5-alpha.2`) — and is pinned by tests/resource-address.spec.ts.
|
||||
*/
|
||||
|
||||
/** The scheme and type every file address opens with. */
|
||||
@@ -31,9 +38,9 @@ export const FILE_ADDRESS_PREFIX = 'dsh-resource://file/'
|
||||
export type FileAddress =
|
||||
| {
|
||||
readonly scope: 'session'
|
||||
/** The session whose workspace root the path is relative to. */
|
||||
/** The session whose workspace root resolves the path. */
|
||||
readonly sessionId: string
|
||||
/** Workspace-relative `/`-separated path, no leading `/`; empty for the root itself. */
|
||||
/** Absolute or workspace-relative `/`-separated path; empty for the workspace root itself. */
|
||||
readonly path: string
|
||||
}
|
||||
| {
|
||||
@@ -58,19 +65,21 @@ function isDriveSegment(segment: string | undefined): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the address of a file inside one session's workspace.
|
||||
* @param sessionId - the session whose workspace root the path is relative to.
|
||||
* @param path - workspace-relative path; backslashes normalize to `/`, a leading `./` or `/` is dropped.
|
||||
* Build the address of a file read through one session.
|
||||
* @param sessionId - the session whose workspace root resolves the path.
|
||||
* @param path - absolute or workspace-relative path; backslashes are normalized
|
||||
* to `/`, and leading `./` prefixes are dropped (a leading `/` is KEPT: an
|
||||
* absolute path stays absolute inside the session scope).
|
||||
* @returns the `dsh-resource://file/session/<sessionId>/<path>` address.
|
||||
*/
|
||||
export function sessionFileAddress(sessionId: string, path: string): string {
|
||||
const relative = path.replace(/\\/g, '/').replace(/^(?:\.\/)+/, '').replace(/^\/+/, '')
|
||||
return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(relative)}`
|
||||
const normalized = path.replace(/\\/g, '/').replace(/^(?:\.\/)+/, '')
|
||||
return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the address of a file by its absolute path.
|
||||
* @param path - absolute path; backslashes normalize to `/` and the leading `/`
|
||||
* @param path - absolute path; backslashes are normalized to `/` and the leading `/`
|
||||
* is dropped, except that a UNC path keeps one empty first segment.
|
||||
* @returns the `dsh-resource://file/absolute/<path>` address.
|
||||
*/
|
||||
@@ -82,16 +91,18 @@ export function absoluteFileAddress(path: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file address back into its parts.
|
||||
* Read a file address back into its parts without resolving `.` or `..`.
|
||||
* Query and fragment suffixes are ignored; encoded path segments are decoded.
|
||||
* @param address - a candidate address.
|
||||
* @returns the parts, or `undefined` when the string is not a
|
||||
* `dsh-resource://file/` URI in a known scope with a valid path.
|
||||
* `dsh-resource://file/` URI in a known scope with a path, or a segment is
|
||||
* not validly encoded.
|
||||
*/
|
||||
export function parseFileAddress(address: string): FileAddress | undefined {
|
||||
try {
|
||||
const url = new URL(address)
|
||||
if (url.protocol !== 'dsh-resource:' || url.host !== 'file') return undefined
|
||||
const [, scope, ...rest] = url.pathname.split('/')
|
||||
if (!address.startsWith(FILE_ADDRESS_PREFIX)) return undefined
|
||||
const end = address.search(/[?#]/)
|
||||
const [scope, ...rest] = address.slice(FILE_ADDRESS_PREFIX.length, end === -1 ? undefined : end).split('/')
|
||||
if (scope === 'session') {
|
||||
const [id, ...segments] = rest
|
||||
if (id === undefined || id === '' || segments.length === 0) return undefined
|
||||
@@ -107,8 +118,7 @@ export function parseFileAddress(address: string): FileAddress | undefined {
|
||||
}
|
||||
return undefined
|
||||
} catch {
|
||||
// `new URL` throws on a non-URL and `decodeURIComponent` on a malformed
|
||||
// escape; both mean "not a file address".
|
||||
// `decodeURIComponent` throws URIError on a malformed escape.
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -119,10 +129,12 @@ function isAbsolutePath(path: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* The address for a path as a caller holds it: a relative path, or an absolute
|
||||
* path inside the session's workspace, becomes a `session`-scoped address; an
|
||||
* absolute path outside it, or one whose workspace root is unknown, becomes an
|
||||
* `absolute`-scoped address. (Mirror of `fileAddressFor`.)
|
||||
* The address for a path as a caller holds it: ALWAYS session-scoped. A
|
||||
* relative path, or an absolute path inside the session's workspace, becomes
|
||||
* the relative spelling; an absolute path outside it, or one whose workspace
|
||||
* root is unknown, keeps its absolute path in that session's address. (Mirror
|
||||
* of `fileAddressFor` — on alpha.2 this helper no longer produces the
|
||||
* `absolute` scope, though such addresses still parse.)
|
||||
* @param sessionId - the session the path is read in.
|
||||
* @param cwd - that session's workspace root, when known.
|
||||
* @param path - absolute or workspace-relative path, either separator spelling.
|
||||
@@ -134,5 +146,5 @@ export function fileAddressFor(sessionId: string, cwd: string | undefined, path:
|
||||
const root = cwd === undefined ? '' : cwd.replace(/\\/g, '/').replace(/\/+$/, '')
|
||||
if (root !== '' && normalized === root) return sessionFileAddress(sessionId, '')
|
||||
if (root !== '' && normalized.startsWith(`${root}/`)) return sessionFileAddress(sessionId, normalized.slice(root.length + 1))
|
||||
return absoluteFileAddress(normalized)
|
||||
return sessionFileAddress(sessionId, normalized)
|
||||
}
|
||||
|
||||
@@ -162,14 +162,6 @@ 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
|
||||
@@ -526,7 +518,7 @@ export function matchUrlTarget(tabs: readonly TabDescriptor[], url: URL): TabDes
|
||||
* The plugin version this service instance reports. Keep in lockstep with
|
||||
* `package.json`'s version — `tests/service.spec.ts` asserts the pair.
|
||||
*/
|
||||
export const SIDEBAR_SERVICE_VERSION = '0.19.0-alpha.0'
|
||||
export const SIDEBAR_SERVICE_VERSION = '0.19.0-alpha.1'
|
||||
|
||||
/**
|
||||
* Monotonic capability list consumers use to gate new API usage (features
|
||||
|
||||
@@ -2004,6 +2004,24 @@ body[data-dsh-sidebar-dragging] .bottomPanel {
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
/* ── Native right-Sidebar tab-body host ──────────────────────────────────
|
||||
DSH's native tab body host (`.paneBody` in dsh-client-ui-dockkit) is a
|
||||
BLOCK scroller with a definite height, not a flex container — the host's
|
||||
own tab bodies (TextPreview, FilesBody) declare `height: 100%` on their
|
||||
root for exactly this reason. Our tab roots declare `flex: 1; min-height:
|
||||
0`, which collapses to content height in that block host (the side-chat
|
||||
composer would float mid-pane). A column flex host of `height: 100%`
|
||||
restores the `flex: 1` semantics our tab roots rely on, and gives
|
||||
third-party descriptors' components the same box inside the native pane.
|
||||
The wrapper div carries `data-dsh-native-tab-host` so the e2e lane can
|
||||
assert the fill (mirroring `data-dsh-better-sidebar`). */
|
||||
.nativeTabHost {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ── Diff tab (generic states + shell; the changes tab owns the rest in
|
||||
changes.module.css, and the diff rows themselves live in
|
||||
diff/diff.module.css) ────────────────────────────────────────────────── */
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
* the bottom workbench spans ONLY the app shell's center column ("squeezes
|
||||
* the agent output area") — it starts at the app sidebar's right edge and
|
||||
* ends at the details column's left edge. Measured directly from the
|
||||
* AppFrame's center column DOM (the parent of the [data-slot="conversation"]
|
||||
* wrapper — layout.css's center column) so the workbench tracks the column's
|
||||
* AppFrame's center column DOM (center-column.ts anchors on the conversation
|
||||
* slot host — `main.conversation` on DSH 0.1.5-alpha.2+, `conversation` on
|
||||
* alpha.1 — and walks up the transparent `display: contents` slot hosts to
|
||||
* the column; layout.css's center column) so the workbench tracks the
|
||||
* column's
|
||||
* real horizontal edges, including the animated track moves while DSH's
|
||||
* native right Sidebar opens/closes; a frame that never appears keeps the
|
||||
* initial zero-size fallback (the panel renders at 0 width until measured).
|
||||
@@ -67,15 +70,17 @@ export function useCenterColumn(
|
||||
let disposed = false
|
||||
let observer: ResizeObserver | undefined
|
||||
// Locate the AppFrame's center column. DSH 0.1.x wraps slot hosts in
|
||||
// [data-slot] containers: the conversation slot wrapper
|
||||
// ([data-slot="conversation"]) sits directly inside the center column,
|
||||
// so its parent IS that column — no hashed-class or positional
|
||||
// dependency (layout.css uses the same anchor). The shell swaps the
|
||||
// boot page for the AppFrame only AFTER boot settles, so the first
|
||||
// query may miss it. Never give up: watch #root's subtree (the swap and
|
||||
// HMR re-renders mutate it) and re-run this locator — querying once and
|
||||
// bailing would strand the panel at the zero-size fallback forever
|
||||
// (observed: a 1px sliver at the viewport's left edge).
|
||||
// [data-slot] containers: the conversation slot host (key
|
||||
// `main.conversation` on 0.1.5-alpha.2+, `conversation` on alpha.1)
|
||||
// sits inside the center column behind transparent `display: contents`
|
||||
// slot hosts, so the resolver walks up to the first ancestor that
|
||||
// renders a box — no hashed-class or positional dependency
|
||||
// (layout.css uses the same anchor). The shell swaps the boot page for
|
||||
// the AppFrame only AFTER boot settles, so the first query may miss it.
|
||||
// Never give up: watch #root's subtree (the swap and HMR re-renders
|
||||
// mutate it) and re-run this locator — querying once and bailing would
|
||||
// strand the panel at the zero-size fallback forever (observed: a 1px
|
||||
// sliver at the viewport's left edge).
|
||||
const locate = (): void => {
|
||||
if (disposed) return
|
||||
// Hot path (#403): streaming output mutates #root at token cadence.
|
||||
|
||||
Reference in New Issue
Block a user