feat: adapt to DSH 0.1.7-rc.1

Move the support line from 0.1.7-alpha.1 to 0.1.7-rc.1 (peer floor
`^0.1.7-rc.1`). 0.21.0-alpha.1 never reached npm, and once the code-card
labels below are fixed the same build crashes on alpha.1 (its DiffBlock
still reads the `labels.files` upstream deleted), so declaring only the
host we verify against is the honest floor — and from rc.1 on the host
enforces peers at boot anyway: a row whose `@deepseek-ai/dsh-*` peers do
not satisfy the runtime is silently disabled.

Dependencies: 28 devDependency pins and 14 peers move to 0.1.7-rc.1.
rc.1 also raised the vendored runtime (cordis 4.0.4, schemastery 3.18.4,
cordis-plugin-group 1.0.4, cordis-plugin-include 1.0.9) and publishes DSH
cross-references as exact versions, so bumping schemastery/cordis alone
left four unmet transitive peers — the three vendor packages are hoisted
into devDependencies, which is what makes `pnpm peers check` clean again.

One compile break: rc.1 added `CodeToolbarLabels` (codeLabel / wrapLabel /
unwrapLabel, all required), made DiffBlockLabels / ReadBlockLabels extend
it and deleted `DiffBlockLabels.files`. Fixing SideChatView then leaving
markdown on the legacy banner would render two different code-card styles
inside one side-chat view, so the plugin adopts the card everywhere:
`markdownTextProps()` now passes `labels.code.toolbarLabels` with the
host's own wording (codeBlock.title|wrap|unwrap, added to all 20
dictionaries; `sideChatBlockFiles` retires with the deleted footer).

Adopting the card surfaced a silent regression the suite caught: the new
toolbar shows `codeLabel` for languages the highlighter does not know, and
mermaid has no shiki grammar, so the info string no longer reaches the DOM
and the banner-text probe in mermaid.tsx matched nothing — mermaid fences
stopped being swapped for diagrams. Detection now matches the FENCE BODY
from the source that produced the render.

`tests/market-manifest.spec.ts` pins every DSH peer as a floating caret
range: an exact pin (the devDependency style) or a comparator without a
prerelease would fail rc.1's preflight and silently stop the plugin from
loading, and `pnpm peers check` cannot see that — it validates the
resolved dev tree, not the range shape the host reads.
This commit is contained in:
Menghuan1918
2026-09-23 22:24:56 +08:00
parent 2d2d78f3f2
commit bd11bad65c
36 changed files with 773 additions and 610 deletions
+1 -1
View File
@@ -175,7 +175,7 @@ jobs:
- name: Install dsh CLI (pinned)
env:
NODE_OPTIONS: --max-old-space-size=4096
run: npm install -g @deepseek-ai/dsh@0.1.7-alpha.1
run: npm install -g @deepseek-ai/dsh@0.1.7-rc.1
# lib/ is gitignored; the tarball is produced from this build.
- name: Build and pack the npm tarball
+2 -2
View File
@@ -1,10 +1,10 @@
{
"id": "dsh-external/dsh-better-sidebar",
"version": "0.21.0-alpha.1",
"version": "0.21.0-rc.1",
"main": "./lib/index.js",
"description": "更好用的右侧边栏——文件树、编辑器、文件变动、任务和侧边对话都收在这一栏里,每个会话互不干扰。暴露服务供其他插件注册侧边栏页面与文件预览器",
"engines": {
"dsh": ">=0.1.7-alpha.1"
"dsh": ">=0.1.7-rc.1"
},
"contributes": {
"tools": [],
+47 -44
View File
@@ -1,6 +1,6 @@
{
"name": "dsh-better-sidebar",
"version": "0.21.0-alpha.1",
"version": "0.21.0-rc.1",
"description": "Better Sidebar for DSH — file tree, editor, changes, tasks and side chat in one column, separate for every conversation. Also exposes a service other plugins use to register sidebar tabs and file viewers.",
"type": "module",
"repository": {
@@ -93,21 +93,21 @@
},
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.3",
"@deepseek-ai/dsh-agent": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-client-locale": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-client-ui-settings": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-client-ui-sidebar-right": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-host-webserver": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-invariants": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-settings": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-subagent": "^0.1.7-alpha.1",
"@deepseek-ai/dsh-tools": "^0.1.7-alpha.1",
"@deepseek-ai/cordis": "^4.0.4",
"@deepseek-ai/dsh-agent": "^0.1.7-rc.1",
"@deepseek-ai/dsh-client-locale": "^0.1.7-rc.1",
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.7-rc.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-rc.1",
"@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.1",
"@deepseek-ai/dsh-client-ui-sidebar-right": "^0.1.7-rc.1",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.1",
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.1",
"@deepseek-ai/dsh-invariants": "^0.1.7-rc.1",
"@deepseek-ai/dsh-llm": "^0.1.7-rc.1",
"@deepseek-ai/dsh-session": "^0.1.7-rc.1",
"@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
"@deepseek-ai/dsh-subagent": "^0.1.7-rc.1",
"@deepseek-ai/dsh-tools": "^0.1.7-rc.1",
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
@@ -142,7 +142,7 @@
"@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.7.1",
"@codemirror/view": "^6.43.8",
"@deepseek-ai/schemastery": "^3.18.3",
"@deepseek-ai/schemastery": "^3.18.4",
"@lezer/highlight": "^1.2.3",
"clsx": "^2.1.1",
"dompurify": "^3.4.14",
@@ -154,33 +154,36 @@
},
"devDependencies": {
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-agent": "0.1.7-alpha.1",
"@deepseek-ai/dsh-agent-loop": "0.1.7-alpha.1",
"@deepseek-ai/dsh-attachment": "0.1.7-alpha.1",
"@deepseek-ai/dsh-client-locale": "0.1.7-alpha.1",
"@deepseek-ai/dsh-client-store": "0.1.7-alpha.1",
"@deepseek-ai/dsh-client-ui-conversation": "0.1.7-alpha.1",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.7-alpha.1",
"@deepseek-ai/dsh-client-ui-settings": "0.1.7-alpha.1",
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.7-alpha.1",
"@deepseek-ai/dsh-client-ui-slots": "0.1.7-alpha.1",
"@deepseek-ai/dsh-host-webserver": "0.1.7-alpha.1",
"@deepseek-ai/dsh-invariants": "0.1.7-alpha.1",
"@deepseek-ai/dsh-llm": "0.1.7-alpha.1",
"@deepseek-ai/dsh-ptc-runtime": "0.1.7-alpha.1",
"@deepseek-ai/dsh-sandbox": "0.1.7-alpha.1",
"@deepseek-ai/dsh-sandbox-policy": "0.1.7-alpha.1",
"@deepseek-ai/dsh-scope": "0.1.7-alpha.1",
"@deepseek-ai/dsh-session": "0.1.7-alpha.1",
"@deepseek-ai/dsh-session-persistence": "0.1.7-alpha.1",
"@deepseek-ai/dsh-session-projection": "0.1.7-alpha.1",
"@deepseek-ai/dsh-settings": "0.1.7-alpha.1",
"@deepseek-ai/dsh-subagent": "0.1.7-alpha.1",
"@deepseek-ai/dsh-system-prompt": "0.1.7-alpha.1",
"@deepseek-ai/dsh-tools": "0.1.7-alpha.1",
"@deepseek-ai/dsh-user-approval": "0.1.7-alpha.1",
"@deepseek-ai/dsh-util-time": "0.1.7-alpha.1",
"@deepseek-ai/dsh-util-workspace-path": "0.1.7-alpha.1",
"@deepseek-ai/cordis": "4.0.4",
"@deepseek-ai/cordis-plugin-group": "1.0.4",
"@deepseek-ai/cordis-plugin-include": "1.0.9",
"@deepseek-ai/dsh-agent": "0.1.7-rc.1",
"@deepseek-ai/dsh-agent-loop": "0.1.7-rc.1",
"@deepseek-ai/dsh-attachment": "0.1.7-rc.1",
"@deepseek-ai/dsh-client-locale": "0.1.7-rc.1",
"@deepseek-ai/dsh-client-store": "0.1.7-rc.1",
"@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.1",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.1",
"@deepseek-ai/dsh-client-ui-settings": "0.1.7-rc.1",
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.7-rc.1",
"@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.1",
"@deepseek-ai/dsh-host-webserver": "0.1.7-rc.1",
"@deepseek-ai/dsh-invariants": "0.1.7-rc.1",
"@deepseek-ai/dsh-llm": "0.1.7-rc.1",
"@deepseek-ai/dsh-ptc-runtime": "0.1.7-rc.1",
"@deepseek-ai/dsh-sandbox": "0.1.7-rc.1",
"@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.1",
"@deepseek-ai/dsh-scope": "0.1.7-rc.1",
"@deepseek-ai/dsh-session": "0.1.7-rc.1",
"@deepseek-ai/dsh-session-persistence": "0.1.7-rc.1",
"@deepseek-ai/dsh-session-projection": "0.1.7-rc.1",
"@deepseek-ai/dsh-settings": "0.1.7-rc.1",
"@deepseek-ai/dsh-subagent": "0.1.7-rc.1",
"@deepseek-ai/dsh-system-prompt": "0.1.7-rc.1",
"@deepseek-ai/dsh-tools": "0.1.7-rc.1",
"@deepseek-ai/dsh-user-approval": "0.1.7-rc.1",
"@deepseek-ai/dsh-util-time": "0.1.7-rc.1",
"@deepseek-ai/dsh-util-workspace-path": "0.1.7-rc.1",
"@eslint/js": "^10.0.1",
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
"@playwright/test": "^1.62.1",
+525 -510
View File
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -42,9 +42,11 @@ e2e_require_cmd() {
# 缺省值(`dsh`)由调用方从环境变量取好传入。
#
# 回退的 npx 版本是显式钉死的:不钉就会解析 npm 的 `latest` dist-tag,而
# 0.1.7 线目前只有预发布(`latest` 仍是 0.1.5-rc.2),冒烟会静静地挂到一
# 个插件已不支持的宿主上。钉版必须与 package.json 的 peer 下限同步。
DSH_NPX_SPEC="${DSH_NPX_SPEC:-@deepseek-ai/dsh@0.1.7-alpha.1}"
# 0.1.7 线目前只有预发布(`latest` 仍是 0.1.5-rc.3),冒烟会静静地挂到一
# 个插件已不支持的宿主上。**必须写精确版本,不能写 `@alpha`**:0.1.7-rc.1
# 上线时 `alpha` 指的是 0.1.7-alpha.2,rc.1 走的是 `next`。钉版必须与
# package.json 的 peer 下限同步。
DSH_NPX_SPEC="${DSH_NPX_SPEC:-@deepseek-ai/dsh@0.1.7-rc.1}"
e2e_resolve_dsh_cmd() {
local explicit="${DSH_CMD}"
if ! command -v "$DSH_CMD" >/dev/null 2>&1; then
@@ -66,7 +68,7 @@ e2e_resolve_dsh_cmd() {
# 己在挂什么),只告警。
e2e_check_dsh_version() {
local explicit="$1" actual expected
expected="${DSH_EXPECT_VERSION:-0.1.7-alpha.1}"
expected="${DSH_EXPECT_VERSION:-0.1.7-rc.1}"
# `dsh --version` 冷启动要走 npx,给足预算;拿不到版本就只告警。
actual="$($DSH_CMD --version 2>/dev/null | tr -d '[:space:]' | head -c 64 || true)"
case "$actual" in
+3 -3
View File
@@ -19,7 +19,7 @@ import { useLayoutEffect, useMemo, useRef } from 'react'
import { createElement, type ReactNode } from 'react'
import DOMPurify from 'dompurify'
import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
import { markdownTextProps } from './markdown-labels.tsx'
import { markdownTextProps, type MarkdownCopyLabels } from './markdown-labels.tsx'
import { resolveLocalMediaDest, rewriteLocalImageUrls } from './markdown-images.ts'
import {
analyzeHtmlSegment,
@@ -160,7 +160,7 @@ interface MarkdownSegmentProps {
text: string
hasMermaid: boolean
media: MarkdownHtmlMedia
codeLabels: { copyLabel: string; copiedLabel: string }
codeLabels: MarkdownCopyLabels
}
/**
@@ -218,7 +218,7 @@ type PreparedSegment =
interface MarkdownDocumentProps {
info: AnalyzedMarkdownHtml
media: MarkdownHtmlMedia
codeLabels: { copyLabel: string; copiedLabel: string }
codeLabels: MarkdownCopyLabels
}
/**
+16 -2
View File
@@ -121,6 +121,11 @@ interface ThreadCache {
interface RowLabels {
copyLabel: string
copiedLabel: string
/** Code-card chrome (DSH 0.1.7-rc.1's fence toolbar): the markdown rows read
* these three straight off this object, the diff / read cards mix them in. */
codeLabel: string
wrapLabel: string
unwrapLabel: string
thinkLabel: string
injectionLabel: string
terminal: TerminalBlockLabels
@@ -345,9 +350,18 @@ export function SideChatView(props: {
expand: (hidden: number) => t('sideChatBlockExpand', { hidden }),
expandAria: (hidden: number) => t('sideChatBlockExpandAria', { hidden }),
}
// DSH 0.1.7-rc.1 widened the diff / read cards into code cards and DELETED
// `DiffBlockLabels.files`, so the old "N files" footer is gone and these
// three strings are required by both cards.
const codeCard = {
codeLabel: t('codeBlockTitle'),
wrapLabel: t('codeBlockWrap'),
unwrapLabel: t('codeBlockUnwrap'),
}
return {
copyLabel: t('copy'),
copiedLabel: t('copied'),
...codeCard,
thinkLabel: t('sideChatThink'),
injectionLabel: t('sideChatInjection'),
terminal: {
@@ -362,8 +376,8 @@ export function SideChatView(props: {
done: t('sideChatBlockDone'),
noOutput: t('sideChatBlockNoOutput'),
},
diff: { ...shared, files: (count: number) => t('sideChatBlockFiles', { count }) },
read: { ...shared, window: (shown: number, total: number) => t('sideChatBlockWindow', { shown, total }) },
diff: { ...shared, ...codeCard },
read: { ...shared, ...codeCard, window: (shown: number, total: number) => t('sideChatBlockWindow', { shown, total }) },
}
}, [])
+7 -1
View File
@@ -363,7 +363,13 @@ export function TextEditor(props: FileViewerProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
[scope.sessionId, scope.cwd, path],
)
const codeLabels = { copyLabel: t('copy'), copiedLabel: t('copied') }
const codeLabels = {
copyLabel: t('copy'),
copiedLabel: t('copied'),
codeLabel: t('codeBlockTitle'),
wrapLabel: t('codeBlockWrap'),
unwrapLabel: t('codeBlockUnwrap'),
}
/**
* Selection popup for the markdown preview: a mouse-up inside the preview
+7 -1
View File
@@ -108,7 +108,13 @@ function PaneToggle(props: { on: boolean; label: string; title?: string; onClick
* lifted out); the plain path stays byte-for-byte for documents without
* any. */
function MdReadingView(props: { text: string }) {
const codeLabels = { copyLabel: t('copy'), copiedLabel: t('copied') }
const codeLabels = {
copyLabel: t('copy'),
copiedLabel: t('copied'),
codeLabel: t('codeBlockTitle'),
wrapLabel: t('codeBlockWrap'),
unwrapLabel: t('codeBlockUnwrap'),
}
const hasMermaid = splitMermaidBlocks(props.text).some((block) => block.kind === 'mermaid')
return (
<div className={css.paneBody}>
+3 -1
View File
@@ -361,7 +361,9 @@ export const ar: Record<string, string> = {
sideChatBlockFailed: 'فشل',
sideChatBlockDone: 'اكتمل',
sideChatBlockNoOutput: '(لا يوجد إخراج)',
sideChatBlockFiles: '{count} ملفات',
codeBlockTitle: 'كتلة التعليمات البرمجية',
codeBlockWrap: 'التفاف الأسطر',
codeBlockUnwrap: 'بدون التفاف للأسطر',
sideChatBlockWindow: '{shown} من {total} سطرًا',
sideChatConnDisconnected: 'انقطع الاتصال',
sideChatConnReconnect: 'إعادة الاتصال',
+3 -1
View File
@@ -346,7 +346,9 @@ export const de: Record<string, string> = {
sideChatBlockFailed: 'Fehlgeschlagen',
sideChatBlockDone: 'Fertig',
sideChatBlockNoOutput: '(keine Ausgabe)',
sideChatBlockFiles: '{count} Dateien',
codeBlockTitle: 'Codeblock',
codeBlockWrap: 'Zeilen umbrechen',
codeBlockUnwrap: 'Zeilen nicht umbrechen',
sideChatBlockWindow: '{shown} von {total} Zeilen',
sideChatConnDisconnected: 'Verbindung verloren',
sideChatConnReconnect: 'Neu verbinden',
+3 -1
View File
@@ -353,7 +353,9 @@ export const fr: Record<string, string> = {
sideChatBlockFailed: 'Échec',
sideChatBlockDone: 'Terminé',
sideChatBlockNoOutput: '(aucune sortie)',
sideChatBlockFiles: '{count} fichiers',
codeBlockTitle: 'Bloc de code',
codeBlockWrap: 'Retour à la ligne',
codeBlockUnwrap: 'Ne pas retourner à la ligne',
sideChatBlockWindow: '{shown} sur {total} lignes',
sideChatConnDisconnected: 'Connexion perdue',
sideChatConnReconnect: 'Reconnecter',
+3 -1
View File
@@ -360,7 +360,9 @@ export const hi: Record<string, string> = {
sideChatBlockFailed: 'विफल',
sideChatBlockDone: 'पूर्ण',
sideChatBlockNoOutput: '(कोई आउटपुट नहीं)',
sideChatBlockFiles: '{count} फ़ाइलें',
codeBlockTitle: 'कोड ब्लॉक',
codeBlockWrap: 'पंक्तियाँ लपेटें',
codeBlockUnwrap: 'पंक्तियाँ न लपेटें',
sideChatBlockWindow: 'कुल {total} में से {shown} पंक्तियाँ',
sideChatConnDisconnected: 'कनेक्शन टूट गया',
sideChatConnReconnect: 'फिर से कनेक्ट करें',
+3 -1
View File
@@ -358,7 +358,9 @@ export const id: Record<string, string> = {
sideChatBlockFailed: 'Gagal',
sideChatBlockDone: 'Selesai',
sideChatBlockNoOutput: '(tanpa output)',
sideChatBlockFiles: '{count} file',
codeBlockTitle: 'Blok kode',
codeBlockWrap: 'Bungkus baris',
codeBlockUnwrap: 'Jangan bungkus baris',
sideChatBlockWindow: '{shown} dari {total} baris',
sideChatConnDisconnected: 'Koneksi terputus',
sideChatConnReconnect: 'Sambungkan ulang',
+3 -1
View File
@@ -351,7 +351,9 @@ export const it: Record<string, string> = {
sideChatBlockFailed: 'Non riuscito',
sideChatBlockDone: 'Completato',
sideChatBlockNoOutput: '(nessun output)',
sideChatBlockFiles: '{count} file',
codeBlockTitle: 'Blocco di codice',
codeBlockWrap: 'A capo automatico',
codeBlockUnwrap: 'Nessun a capo',
sideChatBlockWindow: '{shown} di {total} righe',
sideChatConnDisconnected: 'Connessione persa',
sideChatConnReconnect: 'Riconnetti',
+3 -1
View File
@@ -360,7 +360,9 @@ export const ja: Record<string, string> = {
sideChatBlockFailed: '失敗',
sideChatBlockDone: '完了',
sideChatBlockNoOutput: '(出力なし)',
sideChatBlockFiles: '{count} 個のファイル',
codeBlockTitle: 'コードブロック',
codeBlockWrap: '折り返しを有効にする',
codeBlockUnwrap: '折り返しを無効にする',
sideChatBlockWindow: '全 {total} 行中 {shown} 行を表示',
sideChatConnDisconnected: '接続が切断されました。クリックで再接続',
sideChatConnReconnect: '再接続',
+3 -1
View File
@@ -352,7 +352,9 @@ export const ko: Record<string, string> = {
sideChatBlockFailed: '실패',
sideChatBlockDone: '완료',
sideChatBlockNoOutput: '(출력 없음)',
sideChatBlockFiles: '파일 {count}개',
codeBlockTitle: '코드 블록',
codeBlockWrap: '줄 바꿈',
codeBlockUnwrap: '줄 바꿈 해제',
sideChatBlockWindow: '전체 {total}줄 중 {shown}줄 표시',
sideChatConnDisconnected: '연결이 끊겼습니다',
sideChatConnReconnect: '다시 연결',
+3 -1
View File
@@ -358,7 +358,9 @@ export const nl: Record<string, string> = {
sideChatBlockFailed: 'Mislukt',
sideChatBlockDone: 'Voltooid',
sideChatBlockNoOutput: '(geen uitvoer)',
sideChatBlockFiles: '{count} bestanden',
codeBlockTitle: 'Codeblok',
codeBlockWrap: 'Regels afbreken',
codeBlockUnwrap: 'Regels niet afbreken',
sideChatBlockWindow: '{shown} van {total} regels',
sideChatConnDisconnected: 'Verbinding verbroken',
sideChatConnReconnect: 'Opnieuw verbinden',
+3 -1
View File
@@ -362,7 +362,9 @@ export const pl: Record<string, string> = {
sideChatBlockFailed: 'Niepowodzenie',
sideChatBlockDone: 'Zakończono',
sideChatBlockNoOutput: '(brak wyniku)',
sideChatBlockFiles: 'Pliki: {count}',
codeBlockTitle: 'Blok kodu',
codeBlockWrap: 'Zawijanie wierszy',
codeBlockUnwrap: 'Bez zawijania wierszy',
sideChatBlockWindow: '{shown} z {total} wierszy',
sideChatConnDisconnected: 'Połączenie utracone',
sideChatConnReconnect: 'Połącz ponownie',
+3 -1
View File
@@ -343,7 +343,9 @@ export const pt: Record<string, string> = {
sideChatBlockFailed: 'Falhou',
sideChatBlockDone: 'Concluído',
sideChatBlockNoOutput: '(sem saída)',
sideChatBlockFiles: '{count} arquivos',
codeBlockTitle: 'Bloco de código',
codeBlockWrap: 'Quebrar linhas',
codeBlockUnwrap: 'Não quebrar linhas',
sideChatBlockWindow: '{shown} de {total} linhas',
sideChatConnDisconnected: 'Conexão perdida',
sideChatConnReconnect: 'Reconectar',
+3 -1
View File
@@ -358,7 +358,9 @@ export const ru: Record<string, string> = {
sideChatBlockFailed: 'Ошибка',
sideChatBlockDone: 'Готово',
sideChatBlockNoOutput: '(нет вывода)',
sideChatBlockFiles: 'Файлов: {count}',
codeBlockTitle: 'Блок кода',
codeBlockWrap: 'Переносить строки',
codeBlockUnwrap: 'Не переносить строки',
sideChatBlockWindow: '{shown} из {total} строк',
sideChatConnDisconnected: 'Соединение потеряно',
sideChatConnReconnect: 'Переподключиться',
+3 -1
View File
@@ -343,7 +343,9 @@ export const sv: Record<string, string> = {
sideChatBlockFailed: 'Misslyckades',
sideChatBlockDone: 'Klar',
sideChatBlockNoOutput: '(ingen utdata)',
sideChatBlockFiles: '{count} filer',
codeBlockTitle: 'Kodblock',
codeBlockWrap: 'Radbrytning',
codeBlockUnwrap: 'Ingen radbrytning',
sideChatBlockWindow: '{shown} av {total} rader',
sideChatConnDisconnected: 'Anslutningen bröts',
sideChatConnReconnect: 'Anslut igen',
+3 -1
View File
@@ -360,7 +360,9 @@ export const th: Record<string, string> = {
sideChatBlockFailed: 'ล้มเหลว',
sideChatBlockDone: 'เสร็จสิ้น',
sideChatBlockNoOutput: '(ไม่มีเอาต์พุต)',
sideChatBlockFiles: '{count} ไฟล์',
codeBlockTitle: 'บล็อกโค้ด',
codeBlockWrap: 'ตัดบรรทัดอัตโนมัติ',
codeBlockUnwrap: 'ไม่ตัดบรรทัด',
sideChatBlockWindow: '{shown} จาก {total} บรรทัด',
sideChatConnDisconnected: 'การเชื่อมต่อขาดหาย',
sideChatConnReconnect: 'เชื่อมต่อใหม่',
+3 -1
View File
@@ -360,7 +360,9 @@ export const tr: Record<string, string> = {
sideChatBlockFailed: 'Başarısız',
sideChatBlockDone: 'Tamamlandı',
sideChatBlockNoOutput: '(çıktı yok)',
sideChatBlockFiles: '{count} dosya',
codeBlockTitle: 'Kod bloğu',
codeBlockWrap: 'Satırları kaydır',
codeBlockUnwrap: 'Satırları kaydırma',
sideChatBlockWindow: '{total} satırdan {shown} satır',
sideChatConnDisconnected: 'Bağlantı koptu',
sideChatConnReconnect: 'Yeniden bağlan',
+3 -1
View File
@@ -360,7 +360,9 @@ export const vi: Record<string, string> = {
sideChatBlockFailed: 'Thất bại',
sideChatBlockDone: 'Hoàn tất',
sideChatBlockNoOutput: '(không có đầu ra)',
sideChatBlockFiles: '{count} tệp',
codeBlockTitle: 'Khối mã',
codeBlockWrap: 'Ngắt dòng',
codeBlockUnwrap: 'Không ngắt dòng',
sideChatBlockWindow: '{shown} trong {total} dòng',
sideChatConnDisconnected: 'Mất kết nối',
sideChatConnReconnect: 'Kết nối lại',
+3 -1
View File
@@ -375,7 +375,9 @@ export const zhHK: Record<string, string> = {
sideChatBlockFailed: '失敗',
sideChatBlockDone: '完成',
sideChatBlockNoOutput: '(無輸出)',
sideChatBlockFiles: '{count} 個檔案',
codeBlockTitle: '程式碼區塊',
codeBlockWrap: '自動換行',
codeBlockUnwrap: '取消自動換行',
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
sideChatConnDisconnected: '連線已中斷',
sideChatConnReconnect: '重新連線',
+3 -1
View File
@@ -375,7 +375,9 @@ export const zhMO: Record<string, string> = {
sideChatBlockFailed: '失敗',
sideChatBlockDone: '完成',
sideChatBlockNoOutput: '(無輸出)',
sideChatBlockFiles: '{count} 個檔案',
codeBlockTitle: '程式碼區塊',
codeBlockWrap: '自動換行',
codeBlockUnwrap: '取消自動換行',
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
sideChatConnDisconnected: '連線已中斷',
sideChatConnReconnect: '重新連線',
+3 -1
View File
@@ -375,7 +375,9 @@ export const zhTW: Record<string, string> = {
sideChatBlockFailed: '失敗',
sideChatBlockDone: '完成',
sideChatBlockNoOutput: '(無輸出)',
sideChatBlockFiles: '{count} 個檔案',
codeBlockTitle: '程式碼區塊',
codeBlockWrap: '自動換行',
codeBlockUnwrap: '取消自動換行',
sideChatBlockWindow: '共 {total} 行 · 顯示 {shown} 行',
sideChatConnDisconnected: '連線已中斷',
sideChatConnReconnect: '重新連線',
+6 -2
View File
@@ -372,7 +372,9 @@ export const zh = {
sideChatBlockFailed: '失败',
sideChatBlockDone: '完成',
sideChatBlockNoOutput: '(无输出)',
sideChatBlockFiles: '{count} 个文件',
codeBlockTitle: '代码块',
codeBlockWrap: '自动换行',
codeBlockUnwrap: '取消自动换行',
sideChatBlockWindow: '共 {total} 行 · 显示 {shown} 行',
sideChatConnDisconnected: '连接已断开,点击立即重连',
sideChatConnReconnect: '重新连接',
@@ -805,7 +807,9 @@ export const en: Record<keyof typeof zh, string> = {
sideChatBlockFailed: 'Failed',
sideChatBlockDone: 'Done',
sideChatBlockNoOutput: '(no output)',
sideChatBlockFiles: '{count} files',
codeBlockTitle: 'Code block',
codeBlockWrap: 'Wrap lines',
codeBlockUnwrap: 'Do not wrap lines',
sideChatBlockWindow: '{shown} of {total} lines',
sideChatConnDisconnected: 'Disconnected, reconnect now',
sideChatConnReconnect: 'Reconnect',
+26 -4
View File
@@ -1,5 +1,5 @@
/**
* Copy-button / chrome labels for DSH's shared `MarkdownText` (DSH
* Chrome labels for DSH's shared `MarkdownText` (DSH
* 0.1.2-alpha contract): the renderer takes a REQUIRED nested `labels` prop
* — `labels.code.copyLabel` / `labels.code.copiedLabel` for the fence copy
* buttons plus a screen-reader-only `labels.footnotes` heading — and the
@@ -8,15 +8,29 @@
* dictionary's localized pair through here (re-evaluated per render).
* `footnotes` is left empty (the heading is sr-only; give it a real string
* only if a locale key ever earns its place in all 19 dictionaries).
*
* DSH 0.1.7-rc.1 added `labels.code.toolbarLabels` (language name + wrap
* toggle + copy). Supplying it is what swaps the legacy info-string banner for
* the host's code card, and the three strings are REQUIRED once supplied, so
* every site that builds the flat pair has to carry them too — the code card
* must look the same in the transcript, the diff card and the editor preview as
* it does in DSH's own chat.
*/
import type { ComponentProps } from 'react'
import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
/** The flat copy-button pair the plugin threads through its own props (e.g.
* MermaidMarkdownProps.codeLabels — the chunk contract stays put). */
/** The code-card chrome the plugin threads through its own props (e.g.
* MermaidMarkdownProps.codeLabels — the chunk contract carries the same
* fields). */
export interface MarkdownCopyLabels {
copyLabel: string
copiedLabel: string
/** Code-card title for an absent or unsupported language. */
codeLabel: string
/** Code-card action that enables wrapping. */
wrapLabel: string
/** Code-card action that preserves source columns with horizontal scrolling. */
unwrapLabel: string
}
/** MarkdownText props carrying the nested chrome labels. */
@@ -24,7 +38,15 @@ export function markdownTextProps(text: string, labels: MarkdownCopyLabels): Com
return {
text,
labels: {
code: { copyLabel: labels.copyLabel, copiedLabel: labels.copiedLabel },
code: {
copyLabel: labels.copyLabel,
copiedLabel: labels.copiedLabel,
toolbarLabels: {
codeLabel: labels.codeLabel,
wrapLabel: labels.wrapLabel,
unwrapLabel: labels.unwrapLabel,
},
},
footnotes: '',
},
}
+3 -1
View File
@@ -9,6 +9,8 @@
* tests/mermaid-blocks.spec.ts).
*/
import type { MarkdownCopyLabels } from './markdown-labels.tsx'
/** One fenced mermaid diagram lifted out of the markdown source. */
export interface MermaidBlock {
kind: 'mermaid'
@@ -28,7 +30,7 @@ export type MdBlock = MarkdownBlock | MermaidBlock
export interface MermaidMarkdownProps {
/** The full markdown source (rendered in a single MarkdownText pass). */
text: string
codeLabels: { copyLabel: string; copiedLabel: string }
codeLabels: MarkdownCopyLabels
}
/** CommonMark opening fence: 0-3 spaces indent + a run of 3+ backticks or tildes. */
+40 -13
View File
@@ -34,6 +34,7 @@ import { markdownTextProps } from './markdown-labels.tsx'
import { t } from './locales.ts'
import { sanitizeSvg } from './mermaid-sanitize.ts'
import type { MermaidMarkdownProps } from './mermaid-blocks.ts'
import { splitMermaidBlocks } from './mermaid-blocks.ts'
import css from './sidebar.module.css'
/** Monotonic id seed: every render call gets a fresh, document-unique id. */
@@ -315,20 +316,45 @@ interface MermaidMount {
}
/**
* True when a rendered CodeBlock is a mermaid fence. The DSH CodeBlock has
* two bodies: the shiki path carries the `language-*` class on generated
* <code> elements, while the plain path (which is always the one mermaid
* takes — no shiki grammar) only shows the language in the banner
* infostring (first element of the banner row; CSS-module classes are
* hashed, so that match is structural).
* Fence bodies of every mermaid fence in the source, normalized for matching
* against the rendered block.
* @param text - the markdown source the same component renders.
* @returns the trailing-whitespace-normalized diagram sources.
*/
function isMermaidBlock(block: HTMLElement): boolean {
function mermaidFenceBodies(text: string): Set<string> {
const bodies = new Set<string>()
for (const block of splitMermaidBlocks(text)) {
if (block.kind === 'mermaid') bodies.add(block.code.trimEnd())
}
return bodies
}
/** The block's fence body as rendered: CodeBlock trims one trailing newline. */
function blockBody(block: HTMLElement): string {
return (block.querySelector('code')?.textContent ?? '').trimEnd()
}
/**
* True when a rendered CodeBlock is a mermaid fence.
*
* The match is by FENCE BODY, not by the header: DSH ≤ 0.1.7-alpha.1 rendered a
* banner whose info string said `mermaid`, but rc.1's code card shows
* `labels.codeLabel` for any language the highlighter does not know, and
* mermaid has no shiki grammar — so once the card is adopted the language word
* is nowhere in the DOM. The body is, in both card variants, and the component
* already has the source that produced it.
*
* The `language-mermaid` class stays as the first probe: the highlighted arm
* would carry it if a grammar ever appears for the fence.
* @param block - one rendered `.md-code-block`.
* @param bodies - {@link mermaidFenceBodies} of the source being rendered.
* @returns whether this block holds a mermaid diagram.
*/
function isMermaidBlock(block: HTMLElement, bodies: ReadonlySet<string>): boolean {
const code = block.querySelector('code')
if (code !== null && [...code.classList].some(c => c.startsWith('language-mermaid'))) return true
const infostring = block.firstElementChild?.firstElementChild?.firstElementChild
return infostring !== null
&& infostring !== undefined
&& (infostring.textContent ?? '').trim() === 'mermaid'
if (code === null) return false
if ([...code.classList].some(c => c.startsWith('language-mermaid'))) return true
return bodies.has(blockBody(block))
}
/**
@@ -350,10 +376,11 @@ export function MermaidMarkdown({ text, codeLabels }: MermaidMarkdownProps): Rea
if (container === null) return
const mounts = mountsRef.current
const seen = new Set<HTMLElement>()
const bodies = mermaidFenceBodies(text)
for (const block of container.querySelectorAll<HTMLElement>('.md-code-block')) {
const mount = mounts.get(block)
const isMermaid = isMermaidBlock(block)
const isMermaid = isMermaidBlock(block, bodies)
if (!isMermaid) {
// A previously swapped block that is no longer a mermaid fence:
// restore the CodeBlock children React still manages, so the plain
+1 -1
View File
@@ -633,7 +633,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.21.0-alpha.1'
export const SIDEBAR_SERVICE_VERSION = '0.21.0-rc.1'
/**
* Monotonic capability list consumers use to gate new API usage (features
+3 -1
View File
@@ -23,7 +23,9 @@ const media: MarkdownHtmlMedia = {
path: '/ws/docs/README.md',
origin: 'http://gui.origin',
}
const codeLabels = { copyLabel: 'Copy', copiedLabel: 'Copied' }
const codeLabels = {
copyLabel: 'Copy', copiedLabel: 'Copied', codeLabel: 'Code block', wrapLabel: 'Wrap lines', unwrapLabel: 'Do not wrap lines',
}
async function renderDocument(text: string): Promise<{ container: HTMLElement; root: Root }> {
const container = document.createElement('div')
+20
View File
@@ -114,4 +114,24 @@ describe('DSH community-market manifest compatibility', () => {
it('declares an exact SemVer version (no range or tag; prerelease allowed for the alpha track)', () => {
expect(pkg.version).toMatch(EXACT_SEMVER)
})
it('keeps every DSH peer a floating range the host runtime can satisfy', () => {
// DSH 0.1.7-rc.1 added a startup compatibility preflight
// (packages/boot/app-boot/src/plugin-compatibility.ts): for every peer
// named `@deepseek-ai/dsh` or `@deepseek-ai/dsh-*`, the host evaluates
// `semver.satisfies(runtimeVersion, range, { includePrerelease: true })`
// and DISABLES the whole profile row when it fails — one stderr line in a
// log nobody reads, no dialog, and `peerDependenciesMeta.optional` grants
// no exemption. An exact pin (the style devDependencies use) or a
// comparator carrying no prerelease (`^0.1.7`) would therefore make the
// plugin silently stop loading, so the published ranges must stay carets
// on the baseline tuple. `pnpm peers check` cannot see this: it validates
// the resolved dev tree, not the range shape a host will read.
const peers = pkg.peerDependencies ?? {}
const dshPeers = Object.keys(peers).filter(name => name === '@deepseek-ai/dsh' || name.startsWith('@deepseek-ai/dsh-'))
expect(dshPeers.length).toBeGreaterThan(10)
for (const name of dshPeers) {
expect(peers[name], name).toMatch(/^\^0\.1\.7-[a-z0-9.]+$/)
}
})
})
+3 -1
View File
@@ -27,7 +27,9 @@ vi.mock('mermaid', () => ({
import mermaid from 'mermaid'
import { MermaidMarkdown } from '../src/client/mermaid.tsx'
const codeLabels = { copyLabel: 'Copy', copiedLabel: 'Copied' }
const codeLabels = {
copyLabel: 'Copy', copiedLabel: 'Copied', codeLabel: 'Code block', wrapLabel: 'Wrap lines', unwrapLabel: 'Do not wrap lines',
}
async function renderMarkdown(text: string): Promise<{ container: HTMLElement; root: Root }> {
const container = document.createElement('div')