Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b79269b2c | ||
|
|
cfc9d3c884 | ||
|
|
4e60dde18d | ||
|
|
a94c43343d | ||
|
|
48e16c73c7 | ||
|
|
856d744139 | ||
|
|
e4dcd1118e | ||
|
|
f86c1d82e6 | ||
|
|
0e91e99f49 | ||
|
|
c319fb106e |
@@ -1,15 +0,0 @@
|
||||
# EditorConfig — https://editorconfig.org
|
||||
# 仓库统一编辑器基线;缩进等风格细节以各文件的既有约定为准。
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Makefile 的 recipe 必须用真实 tab(GNU Make 硬性要求)。
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
@@ -1,101 +0,0 @@
|
||||
name: 📋 问题报告 / Issue Report
|
||||
description: 报告 Bug、功能请求或建议(中英双语)| Report a bug, feature request or suggestion (bilingual)
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 🙏 感谢反馈 / Thanks for reporting!
|
||||
请尽量完整填写以下信息,能显著加快定位与处理速度。
|
||||
Please fill out the following fields as completely as possible.
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 提交前确认 / Pre-submit checklist
|
||||
description: 请逐项确认 | Please confirm each item
|
||||
options:
|
||||
- label: 我已搜索现有 issue,确认没有**完全相同**的条目(只有完全一致才算重复)| I searched existing issues and confirmed there is no **exact duplicate** (only exactly identical issues count as duplicates)
|
||||
required: true
|
||||
- label: 我已确认使用最新版本 dsh-apes-sidebar / I confirmed I am on the latest dsh-apes-sidebar version
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: environment-type
|
||||
attributes:
|
||||
label: 运行环境 / Environment
|
||||
description: 你使用的是哪种运行方式?| Which runtime are you using?
|
||||
multiple: false
|
||||
options:
|
||||
- DSH Web(浏览器访问)/ DSH Web (browser access)
|
||||
- Desktop 实现(如 deepseek-harness-desktop)/ Desktop build (e.g. deepseek-harness-desktop)
|
||||
- 其他 / 不确定 (Other / Not sure)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: dsh-web-version
|
||||
attributes:
|
||||
label: DSH Web 版本 + 浏览器 / DSH Web version + Browser
|
||||
description: |
|
||||
例:DSH 0.1.1-rc.2 + Chrome 151(macOS)。
|
||||
Example: DSH 0.1.1-rc.2 + Chrome 151 (macOS).
|
||||
placeholder: "DSH 版本 / version + 浏览器 / browser(如 0.1.1-rc.2 + Chrome 151)"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: desktop-repo
|
||||
attributes:
|
||||
label: Desktop 实现仓库 / Desktop repo(源头 / upstream)
|
||||
description: |
|
||||
若使用 Desktop 实现,请附上源头仓库链接,方便对照宿主行为。
|
||||
If using a Desktop build, please link the upstream Desktop repo for host-behavior reference.
|
||||
placeholder: "https://github.com/anywhere-labs/deepseek-harness-desktop"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: plugin-version
|
||||
attributes:
|
||||
label: 插件版本 / Plugin version
|
||||
description: dsh-apes-sidebar 的版本号 | The dsh-apes-sidebar version
|
||||
placeholder: "如 0.13.1(`dsh plugin --profile web add dsh-apes-sidebar@latest` 安装的版本)"
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
id: category
|
||||
attributes:
|
||||
label: 类别 / Category
|
||||
description: 请选择本 issue 的类型 | Please choose the type of this issue
|
||||
multiple: false
|
||||
options:
|
||||
- 🐛 Bug(功能异常 / Something is broken)
|
||||
- ✨ 功能请求(新功能 / Feature request)
|
||||
- 💡 建议(体验改进 / Suggestion & improvement)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 描述 / Description
|
||||
description: |
|
||||
请详细描述:现象、期望行为、(Bug 必填)复现步骤、截图/日志等。
|
||||
Describe in detail: what happened, expected behavior, reproduction steps (required for bugs), screenshots/logs, etc.
|
||||
placeholder: |
|
||||
现象 / What happened:
|
||||
期望行为 / Expected behavior:
|
||||
复现步骤(Bug 必填)/ Steps to reproduce (required for bugs):
|
||||
截图或日志 / Screenshots or logs:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 补充信息 / Additional context
|
||||
description: 其他有助于定位的信息(可选)| Anything else that may help (optional)
|
||||
placeholder: "操作系统 / OS、配置文件、报错堆栈、临时 workaround 等 / OS, config files, error stack, temporary workarounds, etc."
|
||||
validations:
|
||||
required: false
|
||||
@@ -1,211 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
# Cancel a superseded PR run (it is on a stale commit); never cancel a
|
||||
# push run — it is already producing the post-merge signal. The mount job
|
||||
# boots a real DSH + Chromium, so the savings matter there most.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
# Tests run git log against this repository and need the full history.
|
||||
fetch-depth: 0
|
||||
# pnpm version is pinned via packageManager in package.json; the action
|
||||
# reads it from there (specifying both makes action-setup fail on
|
||||
# purpose with ERR_PNPM_BAD_PM_VERSION).
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
# The plugin's git revert/cherry-pick spawn git without setting an
|
||||
# identity (deliberate, see tests/smoke.spec.ts) and rely on ambient
|
||||
# config; the runner has none by default.
|
||||
- name: Configure git identity for tests
|
||||
run: |
|
||||
git config --global user.name "dsh-apes-sidebar-ci"
|
||||
git config --global user.email "ci@dsh.invalid"
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
# The consumer-facing declaration surface guard: a browser-only plugin
|
||||
# (no @types/node, skipLibCheck: false) must type-check against the
|
||||
# shipped lib/types. Runs AFTER build (it reads the built output).
|
||||
- name: Check consumer type surface
|
||||
run: pnpm check:consumer-types
|
||||
|
||||
# Windows parity lane for the pure-logic suite. The codebase carries real
|
||||
# win32 branches (shell parsing, WSL path projection, the PowerShell
|
||||
# installer) that mock-injection tests alone cannot validate — this lane
|
||||
# runs the same typecheck / test / build / consumer-type-surface steps on
|
||||
# a real Windows runtime. The plugin-mount lane (real DSH + Playwright
|
||||
# Chromium) stays ubuntu-only and is deliberately NOT mirrored here.
|
||||
ci-windows:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 30
|
||||
defaults:
|
||||
run:
|
||||
# The runner ships Git Bash: multi-line steps and
|
||||
# scripts/check-consumer-types.sh work verbatim.
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
# Tests run git log against this repository and need the full history.
|
||||
fetch-depth: 0
|
||||
# pnpm version is pinned via packageManager in package.json; the action
|
||||
# reads it from there (specifying both makes action-setup fail on
|
||||
# purpose with ERR_PNPM_BAD_PM_VERSION).
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
# Same rationale as the ci job: smoke tests spawn git without an
|
||||
# identity and rely on ambient config, which the runner lacks.
|
||||
- name: Configure git identity for tests
|
||||
run: |
|
||||
git config --global user.name "dsh-apes-sidebar-ci"
|
||||
git config --global user.email "ci@dsh.invalid"
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
# `test:windows` runs the whole suite in ONE fork (`--maxWorkers=1`),
|
||||
# not `pnpm test`'s unbounded pool. Two distinct Windows-only failures
|
||||
# motivated it, both recorded in docs/plans/2026-09-10-dsh-0.1.5-rc.2-adaptation.md:
|
||||
#
|
||||
# 1. Six red runs were real 5000 ms vitest timeouts in specs that
|
||||
# really start a process (tests/agent-pty:71/:106,
|
||||
# tests/install-powershell:105 — a cold powershell.exe start
|
||||
# measured 12.1 s, tests/git.spec.ts:85 at 9.6 s). The suite now
|
||||
# declares its own budgets (global testTimeout 15 s, 30 s in the
|
||||
# two heaviest describes), and serial execution stops the
|
||||
# process-spawning specs starving each other on a 2-core runner.
|
||||
# 2. Four runs died at teardown with `[vitest-pool]: Worker forks
|
||||
# emitted error / Worker exited unexpectedly` while reporting
|
||||
# N-1 of N files passed and zero FAIL lines — a worker recycling
|
||||
# race, not a test failure. It survived --maxWorkers=2, so the
|
||||
# lane now recycles nothing at all.
|
||||
#
|
||||
# No retries: they would hide a real regression instead of removing the
|
||||
# race. The cost is wall-clock (the Windows step runs several minutes
|
||||
# against a 30-minute job budget), not signal.
|
||||
- name: Test
|
||||
run: pnpm test:windows
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
- name: Check consumer type surface
|
||||
run: pnpm check:consumer-types
|
||||
|
||||
# Pack the plugin as an npm tarball, mount it into a REAL DSH instance
|
||||
# through the official `dsh plugin --profile web add` channel, and render
|
||||
# the DSH web UI headlessly (Playwright Chromium) to prove the plugin
|
||||
# mounts without crashing the shell. Drives `pnpm test:mount`, which boots
|
||||
# a keyless `dsh web` against a scratch profile and sweeps every built-in
|
||||
# sidebar tab. No API key needed — the whole lane is deterministic.
|
||||
plugin-mount:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
# CI runs must never report to the production telemetry endpoint baked
|
||||
# into the DSH composition (same guard DSH's own CI uses).
|
||||
env:
|
||||
DSH_TELEMETRY_DISABLED: '1'
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# pnpm version is pinned via packageManager in package.json; the action
|
||||
# reads it from there (specifying both makes action-setup fail on
|
||||
# purpose with ERR_PNPM_BAD_PM_VERSION).
|
||||
- uses: pnpm/action-setup@v6
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Pin the DSH CLI the lane mounts into; the profile's bundle stack
|
||||
# (dsh-base / dsh-web-app) resolves from this installation. Keep in
|
||||
# step with the plugin's @deepseek-ai/* peer range when bumping.
|
||||
# Write an EXACT version, never a dist-tag: when 0.1.7-rc.1 shipped, the
|
||||
# npm `alpha` tag still meant 0.1.7-alpha.2, rc.1 went out on `next`, and
|
||||
# `latest` was still the 0.1.5 line — `@alpha` would mount a host this
|
||||
# plugin no longer supports. The 0.2.0 line repeats the pattern: its
|
||||
# first candidate is `next` while `latest` is still 0.1.7-rc.2.
|
||||
# The lanes speak the slash-only RPC dialect
|
||||
# (tests/e2e/host-protocol.ts).
|
||||
#
|
||||
# The larger heap is the fix for one of the two ways this step used to
|
||||
# die (runs 34490912649 and 34363627184 on the alpha line): the pinned
|
||||
# version's TRANSITIVES are declared as floating `^` ranges, so while
|
||||
# upstream publishes a prerelease in stages (0.1.5-rc.2 went out
|
||||
# 14:43-14:57 on 2026-09-10) npm resolved a mixed peer graph —
|
||||
# 3062 ERESOLVE lines, then "Ineffective mark-compacts near heap limit"
|
||||
# (exit 134). The other failure mode was a hard ETARGET on a sibling
|
||||
# package that appeared minutes later; pinning a version that is fully
|
||||
# published (not mid-flight) is the fix for that one.
|
||||
#
|
||||
# Peer resolution stays ON: `--legacy-peer-deps` was tried in the first
|
||||
# cut of this fix and made the install broken rather than robust — it
|
||||
# skips exactly the peers a global install has to supply, and
|
||||
# @deepseek-ai/dsh-app-boot requires `@deepseek-ai/cordis-plugin-group`
|
||||
# (a peer, not a dependency) at boot, so the CLI died with
|
||||
# ERR_MODULE_NOT_FOUND before the smoke could even start. Verified
|
||||
# locally that the plain install resolves it.
|
||||
- name: Install dsh CLI (pinned)
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
run: npm install -g @deepseek-ai/dsh@0.2.0-rc.1
|
||||
|
||||
# lib/ is gitignored; the tarball is produced from this build.
|
||||
- name: Build and pack the npm tarball
|
||||
run: pnpm build && pnpm pack
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Mount + headless-render smoke
|
||||
run: pnpm test:mount
|
||||
|
||||
# Aggregate double-mount regression: a fixture aggregate bundle mounts
|
||||
# dsh-apes-sidebar under its own entry id BEFORE the plugin's bundle
|
||||
# row; the plugin must back off (no duplicate /sidebar/api, boot OK).
|
||||
# Uses the pinned DSH CLI installed above.
|
||||
- name: Aggregate double-mount regression
|
||||
env:
|
||||
DSH_CMD: dsh
|
||||
run: pnpm test:mount:aggregate
|
||||
|
||||
- name: Upload Playwright report on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: |
|
||||
playwright-report/
|
||||
test-results/
|
||||
if-no-files-found: ignore
|
||||
@@ -1,116 +0,0 @@
|
||||
name: Release
|
||||
|
||||
# Publishes `dsh-apes-sidebar` to npm via npm Trusted Publishing (OIDC) —
|
||||
# no NPM_TOKEN secret required, provenance is attached to the tarball.
|
||||
#
|
||||
# Trigger: a GitHub Release is published (tag vX.Y.Z). The tag MUST match
|
||||
# package.json `version`; the workflow verifies this before publishing.
|
||||
#
|
||||
# Dist-tag policy: a PRERELEASE version (e.g. v0.18.0-alpha.0 — the DSH
|
||||
# 0.1.2-alpha adapter track) publishes under the npm `alpha` dist-tag so the
|
||||
# `latest` install target keeps pointing at the last stable cut (which
|
||||
# supports the stable DSH line); stable versions publish as `latest`.
|
||||
#
|
||||
# One-time npm-side setup (manual, on npmjs.com):
|
||||
# 1. package `dsh-apes-sidebar` -> Settings -> Publishing access
|
||||
# -> Trusted Publishers -> "Add Trusted Publisher".
|
||||
# 2. Provider: GitHub Actions
|
||||
# Organization or user: omdsh-dev
|
||||
# Repository: DSH-better-sidebar
|
||||
# Workflow filename: release.yml
|
||||
# Environment: (blank)
|
||||
# 3. Save. Do NOT add an NPM_TOKEN secret — Trusted Publishing replaces it.
|
||||
# Canonical reference: https://docs.npmjs.com/trusted-publishers
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Pack and verify everything, but do not publish
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # mint the OIDC token npm Trusted Publishing exchanges
|
||||
|
||||
concurrency:
|
||||
group: npm-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: build · verify · publish
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
# Tests run git log against this repository and need full history.
|
||||
fetch-depth: 0
|
||||
|
||||
# pnpm version is pinned via packageManager in package.json; the action
|
||||
# reads it from there (specifying both makes action-setup fail on
|
||||
# purpose with ERR_PNPM_BAD_PM_VERSION).
|
||||
- uses: pnpm/action-setup@v6
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
registry-url: https://registry.npmjs.org
|
||||
cache: pnpm
|
||||
|
||||
# The plugin's git revert/cherry-pick spawn git without an identity
|
||||
# (see tests/smoke.spec.ts); the runner has none by default.
|
||||
- name: Configure git identity for tests
|
||||
run: |
|
||||
git config --global user.name "dsh-apes-sidebar-ci"
|
||||
git config --global user.email "ci@dsh.invalid"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Verify release tag matches package.json version
|
||||
if: github.event_name == 'release'
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
expected="v$(node -p "require('./package.json').version")"
|
||||
echo "package.json version: ${expected#v}"
|
||||
echo "release tag: ${RELEASE_TAG}"
|
||||
if [ "${RELEASE_TAG}" != "${expected}" ]; then
|
||||
echo "::error::Release tag ${RELEASE_TAG} does not match package.json version ${expected#v} (expected ${expected})."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
# The consumer-facing declaration surface guard — mirrors the ci.yml
|
||||
# verification order (typecheck → lint → test → build →
|
||||
# check:consumer-types) so a release is gated on the same checks a PR
|
||||
# runs.
|
||||
- name: Check consumer type surface
|
||||
run: pnpm check:consumer-types
|
||||
|
||||
- name: Dry-run publish (validation only)
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true'
|
||||
run: pnpm publish --dry-run --no-git-checks
|
||||
|
||||
- name: Publish to npm
|
||||
if: github.event_name == 'release' || github.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
TAG=$(node -p "require('./package.json').version.includes('-') ? 'alpha' : 'latest'")
|
||||
echo "publishing to npm dist-tag: ${TAG}"
|
||||
pnpm publish --provenance --access public --no-git-checks --tag "$TAG"
|
||||
+22
-147
@@ -1,156 +1,31 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
# Build output
|
||||
lib/
|
||||
dist/
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
# Dependencies
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
.output
|
||||
|
||||
# Gatsby files
|
||||
# Logs & caches
|
||||
*.log
|
||||
.vite/
|
||||
.vitest/
|
||||
coverage/
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
# vuepress v2.x temp directory
|
||||
.temp
|
||||
|
||||
# Sveltekit cache directory
|
||||
.svelte-kit/
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Firebase cache directory
|
||||
.firebase/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
# Local env
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# pnpm
|
||||
.pnpm-store
|
||||
|
||||
# yarn v3
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# Vite files
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.vite/
|
||||
lib/
|
||||
|
||||
# plugin-registry install staging (assembled by scripts/package-registry.mjs)
|
||||
registry/
|
||||
.npmrc
|
||||
|
||||
# stray npm cache scratch dir
|
||||
.npm-cache-tmp/
|
||||
|
||||
# Playwright headless-render lane (tests/e2e) artifacts
|
||||
playwright-report/
|
||||
test-results/
|
||||
pnpm-debug.log*
|
||||
|
||||
# Unrelated work that landed in this directory (not part of apes-sidebar)
|
||||
maxkb-integration/
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# apes-sidebar build config
|
||||
# Allow node-pty (a transitive runtime dep of dsh-better-sidebar's terminal
|
||||
# panel) to run its native build script. Phase 0 never exercises the terminal
|
||||
# panel, but pnpm's ignored-builds gate hard-fails the preinstall dep check
|
||||
# otherwise, which blocks `pnpm build`.
|
||||
onlyBuiltDependencies=node-pty
|
||||
-382
@@ -1,382 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
> 本文档收录 dsh-apes-sidebar 的完整发布历史(最新版摘要见 [README](README.md);同步发布于 [GitHub Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases))。
|
||||
|
||||
### v0.24.1
|
||||
|
||||
> 🎁 **本版包含从 v0.22.1 起的全部内容**(v0.23.0 的文件页 / 文件变动页 UI/UX 重构与性能打磨、v0.24.0 的 DSH 0.2.0-rc.1 支持线前移,以及本节的修复)。
|
||||
>
|
||||
> 🐞 **修复版**:文件树「一操作就整体刷新」的两处根因。① 原生承载面(`src/client/native/index.ts` 的 `sync()`)把 `files` 接管项当成多余注册——它按 KIND 记账、永远不在 `wanted` 里,于是**每次会话状态写入**都被 dispose + 重建;替换 slot 注册等于替换宿主的 slot entry,**整个插件 tab body 被卸载重挂**(展开/收起、切 tab、拖底部工作台都会触发)→ 文件树丢失层缓存、滚动位置与目录 watcher,并重新 `fs.trees([root, ...expanded])` 重列整棵可见树。② 目录实时刷新(`FileTree.tsx` 的 `retryDir`)**先把该层缓存删掉**再请求,行被占位替换后重建(构建 / 格式化 / 模型跑 bash 时整层闪空)。
|
||||
|
||||
- 🧪 判别性证据:真实挂载 lane 新增 `tests/e2e/zz-expand-refresh.e2e.ts`(展开只请求该层、tab body 存活、行数不归零;回退任一修复即红),配套两个 jsdom 单测(回退后 `registered` 6 → 12、`disposed` 全是 `dsh-apes-sidebar:files`;另一条断言重列期间旧行仍在屏)
|
||||
- 📉 修复后实测(40 子项目录):展开 `fs.trees` payload 由 `[root, big]`(空缓存特征)变为 **`[big]`**,新开 WebSocket 1 → **0**,被删行 45 → **0**,外部写入由「删 40 建 40」变为 **只增 1 行**
|
||||
- 🔧 `src/fs-watch.ts` / `use-dir-watch.ts` / `TreePanel.tsx` / `tab-adapter.tsx` 零改动(`fs.watch` 新建自身不投递事件,已实测证伪)
|
||||
|
||||
### v0.24.0
|
||||
|
||||
> 📦 **支持线前移**:仅适配 DSH **0.2.0-rc.1+**(peer 下限 `^0.2.0-rc.1`,CI 钉 `@deepseek-ai/dsh@0.2.0-rc.1`)。**0.1.7 线(含 npm `latest` 的 0.1.7-rc.2)请固定 v0.22.1(0.1.7 线最后发布的版本)**——宿主 0.2.0 的启动兼容性预检按 `semver.satisfies(宿主版本, peer范围, {includePrerelease:true})` 判定,caret 范围跨 minor 不成立(实测 `^0.1.7-rc.1` 对 `0.2.0-rc.1` 为 **false**,整行会被静默禁用),因此本版不保留任何 0.1.7 兼容分支。
|
||||
|
||||
- 📦 **基线抬到 `0.2.0-rc.1`**:14 条 DSH peer + 27 个 `@deepseek-ai/*` devDependencies 同步钉版;`dsh.plugin.json` 的 `engines.dsh` 随之前移;挂载 lane(`scripts/e2e-common.sh`)与 CI 钉版同步。
|
||||
- 🔍 **适配前的实测结论(纯增量,故无运行时改动)**:本插件消费的 19 个宿主包里**零个值导出被删除**;类型面仅 `dsh-client-ui-primitives`(`DisclosureRow`/`TextShimmer`/`Tooltip` 新增可选 prop、overlay 顶部内距)、`dsh-session`(新增 `ToolCallRecovery`)、`dsh-api-remotes`(新增 product-analytics remote)有变化;会话格式仍 **v4**(无迁移包)、`SUBAGENT_DESCRIPTOR_VERSION` 仍 **3**、`dsh/lib/bin.js`(`dsh plugin` / `dsh web` / 启动行 / `--version`)与 `dsh-client-modules` 运行时**逐字节相同**、web profile 的客户端 inject 列表相同、`ui-primitives` 的裸 import 集合相同(无需新增测试期依赖)。
|
||||
- 🧩 **宿主 web profile 组成变化与本插件无关但已核对**:新增 desktop-only 的 `product-analytics` / `desktop-product-telemetry`(web profile 上 `disabled`);原先就 `disabled` 的 `time-context` / `schedule` / `ui-schedule` 三行移入可选的 `dsh-experimental-schedule-bundle`。「宿主 tab 只有 terminal、browser 缺席」的挂载断言不受影响。
|
||||
- 🧪 **测试与规则**:`tests/market-manifest.spec.ts` 的 peer 形状断言改为钉当前基线 tuple,并把「caret 跨 minor 必失效」写进注释(这条与 §3.4 第 12 条里「includePrerelease 更宽松」的经验不冲突:宽松只发生在同一 minor 内)。
|
||||
- ⚠️ **唯一残留**:可选集成 `@huanlin/dsh-plugin-better-locale`(未被任何源码/测试 import,仅结构化运行时探测)的 peer 钉在 `^0.1.0-rc.8`,其最新 0.4.3 同样钉 `^0.1.5-rc.2`——两者在 0.2.0 上都会被宿主预检禁用,故 `pnpm peers check` 会报它的 5 条 unmet peer。这是上游未适配 0.2.0 的结果,与本插件自身 14 条 peer 无关。
|
||||
### v0.23.0
|
||||
|
||||
> 🧭 **开发线版本(从未发布到 npm,全部内容随 v0.24.1 一并发布)**:文件页与文件变动页的整体 UI/UX 重构,加上一轮性能与交互打磨。文件页新增 **Ctrl/Cmd 与 Shift 多选**(VS Code 语义)、批量操作条、**Git 变更着色**(与 VS Code 同色的新增/修改/删除/冲突,目录带汇总点)、**新建文件夹**、拖拽上传体验重做;**多选可右键「压缩并下载」**,打包在宿主侧成为带进度的任务(`archive.build` / `archive.status`,逐条目回调,4 并发 + 5 分钟 TTL + sessionId 绑定)。「打开方式」改为**双源并存**:宿主探测到的本机关联应用(资源管理器 / VS Code / Cursor / Zed / 自定义编辑器,含 SSH 远端)与插件自研目标,可用设置开关 `openWithPluginTargets` 强制并存;右键菜单收敛层级(一级只留默认动作,应用列表进子菜单)。文件变动页**重构为层级树**,Git 视角(工作区 / 暂存 / 历史)与 Agent 视角(会话内改动)各自保留,目录行可暂存/取消暂存。交互与性能:`/sidebar/api/fs.tree` 实测 **22.8ms → 3.8ms**(10k 条目,readdir 一次取回 + 只为 cap 建行 + 小写比较排序 + 1.5s 目录级缓存,由写操作与 fs-watch 失效),新增批量路由 `fs.trees`(挂载/刷新的 N+1 请求变 1 个),右键菜单打开不再重列目录、行不再随 git 轮询重渲染。修掉「在侧边打开」没有效果(原来只写未展开的底部工作台,现按承载面分流到宿主 `openResource(preferNewPane)` 真正分栏)。
|
||||
|
||||
- ⚠️ **行为变更(安全相关)**:**删除工作区路径检测**——插件 fs 路由(tree/read/write/rename/remove/mkdir/媒体/HTML/upload/archive)不再做包含性检查,可读写宿主用户能访问的任意路径(仅受 OS 权限约束),`workspaceFence` 设置与围栏提示一并移除。
|
||||
- 🧩 抽出可复用的全局组件与共享 store(`IconButton` / `Chip` / `SectionHeader` / `Notice` / `ConfirmDialog` / `StatusBadge` + 共享 git 状态 store),皮肤契约(`tests/theme.spec.ts`)继续守护;ZIP 打包器自研(无第三方依赖)并修掉非 latin1 文件名的下载头 500(RFC 5987)。
|
||||
- 🐞 同时修掉:关闭文件后返回文件树时展开状态被重置(原生承载面的展开集合原先按 tab 记录,现改为**每会话状态**的投影)。
|
||||
|
||||
### v0.22.1
|
||||
|
||||
> 📦 **正式版**(npm `latest`):仅支持 **DSH 0.1.7-rc.1+**(peer 下限 `^0.1.7-rc.1`,CI 钉 `@deepseek-ai/dsh@0.1.7-rc.1`)——**支持线不变**,0.21.1 / 0.22.0 的用户直接升级即可;**DSH 0.1.6-alpha.2 及更早请继续固定 v0.19.1**。这是一次补丁版:两个缺陷在真机上可稳定复现,而当时的单元测试**全绿**。
|
||||
|
||||
- 🐛 **`files` 接管 id 被孤儿化 → `native register files error: … already registered` 刷屏 + 文件树落到宿主空态(需刷新页面才恢复)**(社区 issue #770 / #771 / #766)。根因链由官方桌面壳日志逐字确认(`desktop.frontdesk.log` 2026-09-27 15:51:39.338):客户端条目替换(插件市场更新 / Plugins 页禁用→启用 / HMR 重打)期间,插件 teardown 逐个注销内置描述符 → `service.notify()`(同步内联)驱动 `sync()` 在**已经 inactive** 的插件上下文上运行;清理循环把**不是描述符**的 `files` 接管释放后又**在同一轮里重建**它——`tabs.register` 建在**宿主**上下文上(宿主上下文仍活着)因此照样取走 id,紧随的 `ctx.slots.inject` 建在插件上下文上抛 `cannot create effect on inactive context`,`live.set(FILES_KIND, …)` 永不执行,disposer 丢失 → 该 id 在**整个页面生命周期内不可再注册**。修复(社区 PR #777,@yanzhaohui1999):① 清理循环**跳过 `FILES_KIND`**——接管的寿命只由编辑器类型的 `wantsFiles/hasFiles` 开关与 seat disposer 决定;② **任何在宿主取走 id 之后失败的注册都回滚释放**该 type 与已建好的槽位,失败只留一个「下次通知可重试」的状态,绝不留下无处释放的 id;③ teardown / 清理循环逐个安全释放(一个释放抛错不再中断其余)。
|
||||
- 🐛 **macOS 桌面版窗口拖拽「拖一次就失效」并且双击标题栏缩放也失效**(issue #772)。插件宿主是直挂 `body` 的子元素,宿主的 `html[data-platform=darwin] body > :not(#root) { -webkit-app-region: no-drag }`(选择器含 id,只有 `!important` 能压过)命中它,而 app-region **无视 `pointer-events`**——铺满视口的面板层把下面每条 `[data-window-drag]` 拖拽带一起抵消。修复(社区 PR #773 合并;随后 #786 补齐):`[data-dsh-apes-sidebar]`、`[data-dsh-panel-host]`(`> *` 保持 `no-drag`)与放大视图 `.mermaidModal` 用**中性值** `-webkit-app-region: initial !important` 退出计算——`initial` 的计算值 `none` **不**扣减拖拽区,而字面量 `none` 不是中性值(它计算成 `no-drag`);交互弹层(`.selectionPopup`、`FloatingWindow`、`AnchoredPopover`)**不**加 `initial`,否则按下会变成拖窗(重演 #103/#111)。
|
||||
- ✅ **守护**:`tests/native-registration.spec.ts` 把两条不变量钉在注册表的**事件日志**上(在未修复代码上 4/4 红,且「失败后仍能重新注册」不再是非判别断言);槽位失败判据改为 `name::key`,让 `registerSlots` 的**部分回滚**释放路径真正被执行;新增**部署级回归门** `tests/e2e/native-reload.e2e.ts`(`utimesSync` 已安装的 `lib/client.js` → 页面内条目替换 → 断言无 `native register … error`、无诊断条、`files` 接管重新可用;npm 0.22.0 上连续 3 次运行全红、修复版连续 3 次全绿(该用例只有 1 个 test,重复跑三次))。拖拽契约由 `tests/panel-host-css.spec.ts` 在 Linux 的 `pnpm test` 里钉形状(无 macOS runner),并由挂载 lane 的**真实级联探针**按宿主规则读计算值(面板宿主/放大视图在未修复产物上都判红)。
|
||||
- 📐 **基线不变**:`@deepseek-ai/dsh-*` 仍钉 `0.1.7-rc.1`,peer 下限 `^0.1.7-rc.1`;无公共 API / 契约 / 词典 / chunk 改动。事故记录与「为什么否决 #766 的吸收方案」见 [docs/plans/2026-09-28-native-files-takeover-reload-leak.md](./docs/plans/2026-09-28-native-files-takeover-reload-leak.md)。
|
||||
- 🧪 **验证**:`pnpm test` 122 files / 1293 passed / 9 skipped;`pnpm test:mount` 与 `DSH_CMD=… pnpm test:mount:aggregate` 绿;打包产物里 `app-region:initial!important` ×2、`no-drag` ×2。
|
||||
|
||||
### v0.22.0
|
||||
|
||||
> 📦 **正式版**(npm `latest`):仅支持 **DSH 0.1.7-rc.1+**(peer 下限 `^0.1.7-rc.1`,CI 钉 `@deepseek-ai/dsh@0.1.7-rc.1`)——本版没有动支持线,0.21.1 的用户直接升级即可。**DSH 0.1.6-alpha.2 及更早仍请固定 v0.19.1**。主内容是把任务管理页从「子代理拓扑」重做成**工作流图**,并在随后几轮里按真机反馈打磨;期间 DSH 0.1.7 删掉了 Agent Teams 的 Remote 方法,团队与后台任务两个数据面随之改写。
|
||||
|
||||
- 🧩 **任务管理页 = 工作流图(全新主显示模式)**:会话树渲染为分层节点 + 贝塞尔连线的画布——拖拽平移、滚轮缩放到光标、内容包围盒居中适配(双击背景重新适配)、右下角控制条(图/树切换 + 折叠开关 + 缩放 + 适配)。**经典缩进树保留**(键盘可导航),两种模式共享同一个视图模型,折叠状态与团队富化不会视觉漂移;默认视图走设置里的偏好。
|
||||
- 🃏 **双段式节点卡**:上段 = 类型徽章(主代理 / 子代理 / 成员 / 工作流 / 已完成聚合)+ 相位徽标 + 名称 + 元信息;下段小条 = 状态点 + 状态词 + **主 Agent 同款合并活动行**(并发工具按类别归并 + 计数 + 最新在跑那条的细节,措辞直接取宿主 `chat` 命名空间的词条)+ 已完成节点的折叠 chevron。运行中小条从左到右**完整扫过**(1.5s,令牌渐变,`prefers-reduced-motion` 下关闭)。卡片 8px 圆角、只用上段极淡底色表达层级、当前会话用加粗 accent 描边。
|
||||
- 🔀 **工作流 run 入图**:宿主侧从 `tool-workflow/*` 会话事件折叠出 run(与官方 workflow-run 面板同一批事件),run 挂在发起代理下、成员 agent 重新挂到 run 节点下并按相位分框、同色相位徽标;catalog 里没有的成员用 run 自身数据合成占位节点,所以跑完的 run 仍能看到它的成员。
|
||||
- 🗂 **折叠分两组,各自说清自己是什么**:`✓ N 已完成`(已完成 + 出错,失败会单独报 `出错 N`)与 `N 个待命`(跑完一轮、随时可被叫起来的 teammate)是**两行**;**手动 chevron 永远有效**(不设阈值),**自动聚合**只在「待命成员 ≥3」时收空闲成员——1~2 个待命成员就是这个团队的工作集,而且它们的卡上会挂任务行。聚合卡名字行写「前两个名字 + `+N`」,点聚合全部展开。
|
||||
- 🪟 **两个常驻浮动窗**(抽出可复用的 `FloatingWindow`):后台任务输出与共享任务详情/编辑都在其中——可拖拽、可四边拉伸、内容区自滚动、只靠关闭按钮或 Escape 结束时消失(外部点击、失焦、锚点离屏都不关)。任务窗把窗口余量交给描述区(编辑态交给多行输入框),拉大窗口是给内容更多空间而不是留白;动作行固定在 footer,长描述不会把按钮推出视野。
|
||||
- 👥 **Agent Teams 任务板(实验层)**:团队成员富化到对应节点上、常驻任务条列出成员与共享任务;任务状态机跟随宿主(待办 → 认领 → 进行中 → 完成 → 重开)+ 改派 / 编辑 / 两击删除,CAS 过期修订单独提示(别人改过 → 已刷新)。**成员活动由 `subagents.live` 的 running 叠加**,不是在客户端猜。
|
||||
- 🔄 **后台任务改读宿主客户端服务**:0.1.7 的 web profile 挂载客户端 `ctx.jobs`(推送 roster + 非消费输出流 + kill),插件因此删掉自建的 `jobs.list` / `jobs.output` / `jobs.kill` 三条路由与事件回放镜像,也彻底不碰模型 `job_output` 游标(宿主把这层约束变成了能力)。输出改在常驻浮动窗里流式显示并尾随,代理数 ≥ 8 时抽屉自动折叠。
|
||||
- 🛠 **DSH 0.1.7 的两处数据面重写**:① 团队读——0.1.6 的 `agentTeams.remoteView` 三个 Remote 方法被上游删除,插件改为读 Lead Session 的 **`agentTeam` Session projection**(与 `subagentCatalog` 同源、推送式),删掉 `teams.view` 路由与 5 秒轮询;写路径保留两条路由(`createTask` / `updateTask`),拒绝从「返回联合」变为「抛 `TeamError`」,`TEAM_TASK_STALE_REVISION` 映射成 409 `team-conflict`。② 后台任务——见上一条。**修掉的真实故障**:0.1.7 上团队条从来不渲染(路由报 `remoteView is not a function`,页面静默无提示)。
|
||||
- 🐛 **真机抓到的四个缺陷**(单测都绿的):
|
||||
- 逐节点折叠按钮点了没反应——它被**自动**折叠的守卫卡住了(`isAutoFoldable` 要求非成员且是叶子),而按钮是**手动**触发,现在两种触发各有各的守卫;
|
||||
- 「待命」的卡片**从来不画**折叠按钮(chevron 的渲染条件写死成「已完成/出错」),现在「不在执行」即提供;
|
||||
- 认领任务后标签变「阻塞」——`ready` 的语义只是「待办且无未完成 blocker 可认领」,对所有非 pending 任务都是 false,现在收敛成唯一规则 `taskBlocked/taskStatusLabel/taskTone/taskDotState`;
|
||||
- 任务窗「完成」在队列中的任务上必失败——宿主状态机要求先认领,动作行现在跟随状态。
|
||||
- 🎨 **窄屏与手机设置**:卡片/行距/字号按原生右侧栏的窄宽重新定档(内容包围盒居中、缩放夹取、平移边界、相位框配色、连线 stub 加粗、卡片高度预算重算);设置页新增**手机**分组——窄屏(≤768px)时新任务页不再自动弹出、任务管理页默认走树状图。
|
||||
- 📐 **基线**:`@deepseek-ai/dsh-*` 钉版与支持线**不变**(0.1.7-rc.1,peer `^0.1.7-rc.1`)。新增词条若干(含 `teamTaskClaim` / `tasksFoldIdle` / `taskWindowCreate` / `tasksStateProvisioning` 等)×20 份词典;设计文档见 [docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md](docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md)、[2026-09-27-tasks-graph-polish.md](docs/plans/2026-09-27-tasks-graph-polish.md)、[2026-09-27-floating-window-mobile-settings.md](docs/plans/2026-09-27-floating-window-mobile-settings.md)、[2026-09-27-agent-teams-dsh-0.1.7-adaptation.md](docs/plans/2026-09-27-agent-teams-dsh-0.1.7-adaptation.md)(含实施偏差与已知限制)。
|
||||
|
||||
### v0.20.0(开发线,**从未发布**)
|
||||
|
||||
> 🚫 **这一版没有发布到 npm**(npm 上 `latest` 仍是 v0.19.1):它原本是 0.1.6-alpha.2 线的正式版,开发完成后被 0.21 线直接取代,所以下面这些变更实际随 **v0.21.1** 一起发布。原本的定位:仅支持 **DSH 0.1.6-alpha.2+**(peer 下限 `^0.1.6-alpha.2`,CI 钉 `@deepseek-ai/dsh@0.1.6-alpha.2`)。**这是一次破坏性变更版**:0.1.5-rc.* 用户请固定 `dsh-apes-sidebar@0.19.1`。适配记录见 [docs/plans/2026-09-21-dsh-0.1.6-alpha.2-adaptation.md](docs/plans/2026-09-21-dsh-0.1.6-alpha.2-adaptation.md)。
|
||||
|
||||
- 🖥️ **终端交给 DSH 内置**:DSH 0.1.6 自带右列终端(`ui-sidebar-terminal`),所以插件删掉了整套自研终端——`pty-manager` / `agent-pty` / `pty-deps` / 8 个 `terminal_*` 工具 / xterm 视图与字体链接 / 跨会话固定终端,以及 `node-pty` 依赖、它的 `allowBuilds` 放行与安装脚本的 `-Repair` 模式。**注意**:模型因而失去跨调用持久的终端(只剩一次性 `bash` / `pwsh`);上游 `@deepseek-ai/dsh-tool-terminal` 未被内置 bundle 挂载,需要时请自行在 profile 里启用。
|
||||
- 🌐 **浏览器视图让给内置**:插件删掉 `BrowserView` / 沙箱状态条 / 嵌入性探测(含宿主 `browser.probe` 路由)与两个只配置自家 iframe 的设置项;**保留**聊天与界面的外链接管(按协议分流是宿主没有的能力),目标改为 DSH 内置的 `browser` tab 类型。
|
||||
- 🧩 **产物行交回宿主**:DSH 0.1.6 把 `conversation.chat.turnTail` 从 chain 改成 list(上游有意让多个插件**追加**而非互相替换),插件原来的接管只能与内置产物卡重复,因此整体删除;点击行为不变(内置走宿主 `openFile` → 本插件编辑器)。代价是失去轮尾那一个「在文件树中显示」入口(树内右键仍在)。
|
||||
- 🐛 **修复两个静默破坏**:`SidebarRightGuideEntry.id` 变成必填后,缺 id 会让**整个原生承载面静默空掉**(抛在 `ctx.inject` 回调里被吞);`turnTail` 改 list 后原注册直接抛错、产物行失效。两处都已修好并加测试守护,同时补了注册失败的上报口。
|
||||
- 🎨 **空白面板卡片对齐内置卡风格**:`.paneCard` 改用 DSH 指南胶囊那套配方(0.5px l4 描边 / 24px 圆角 / layer-1 填充 / 56px 下限 / 14×20 内边距),并加测试守护其几何、令牌化配色,以及不复制上游那套**未定义**的 `--dsw-alias-bg-l1/-l2` 拼写。
|
||||
- 📐 **基线**:`@deepseek-ai/dsh-*` 全部钉 `0.1.6-alpha.2`,`dsh-code-runtime` 整包消失已移除,`diff` / `simple-icons` 作为 primitives 的新裸 import 提升进 devDependencies;`ui-primitives` 本版三处破坏(`IconSendOutline16` 下线、`TerminalBlockLabels.noExitCode` 新增、`ConnectionIndicator.reconnectLabel` 移除)同步适配。
|
||||
|
||||
### v0.19.1
|
||||
|
||||
> 📌 **正式版**(npm `latest`,无 prerelease 后缀):钉版推进到 **DSH 0.1.5-rc.2**(npm `next`),**peer 下限仍是 `^0.1.5-rc.1`**——rc.2 的上游 delta 里没有任何触及本插件的面(零 `packages/api|host|session|agent` 变更,真实代码改动只有消息反馈弹窗、产物卡片 CSS 与 `CodeFileIcon` 的 SVG 数据拆分),因此 rc.1 用户无需升级 DSH 即可用本版。DSH 0.1.5-alpha.2 用户继续用 **v0.19.0-alpha.1**;0.1.2-rc.1 稳定线继续用 **v0.18.1**。
|
||||
|
||||
- 🎯 **适配 DSH 0.1.5-rc.2**:devDependency 钉版、CI 挂载车道与 `SIDEBAR_SERVICE_VERSION` 同步到 rc.2;插件侧**零代码改动**(上游 delta 未触及本插件,逐文件核对见 [docs/plans/2026-09-10-dsh-0.1.5-rc.2-adaptation.md](docs/plans/2026-09-10-dsh-0.1.5-rc.2-adaptation.md)——300 个变更文件里绝大多数只是各包 `package.json` 的版本号单行)。
|
||||
- 🎨 **文件图标(#611)与内置 Tab 图标改为彩色**(#531 + #594 合并,实现按 rc.2 重做):
|
||||
- **文件/文件夹图标走 DSH 官方图形**:回退链末端是宿主 `ui-primitives` 的 `FileTypeIcon`(48 类官方全彩代码/配置图形 + markdown/图片/PDF/Office/视频/文件夹的类目色板),插件**不再自带扩展名表,也不再需要图标懒加载 chunk**——`#429` 那份 563 条彩色数据与 `lib/client-file-icons.js`(255 kB)连同 `fileIconTheme` 设置开关一并删除,彩色就是唯一形态(核心 bundle 因此只增 10 kB)。
|
||||
- **新增对外 `registerFileIcon` API**(能力 `'fileIcons'`):按扩展名(`exts`)、精确文件名(`names`)、目录名(`folderNames`)注册自己的图标,优先级降序、同级按注册序;**注意**:宿主分类器覆盖任意路径,所以注册 `exts: []` 的 catch-all 会接管所有未具体命中的行。
|
||||
- **内置 tab 图标彩色**:文件 / 文件变动 / 任务管理 / 侧边对话 / 终端 / 浏览器 六个类型与 diff 视图的 glyph 换成彩色版本(颜色全部来自 `--dsw-alias-*` 令牌,皮肤照旧全覆盖)。
|
||||
- **原生右侧栏的 tab 芯片也带图标**:宿主的 tab 定义没有 icon 字段,但 `sidebar.right.pane.tab.title` 槽就是芯片内容——插件在该槽渲染「图标 + 标题」(编辑器 tab 带路径时显示该文件的图标),glyph 为 `aria-hidden`,芯片可访问名不变。
|
||||
- 🛠 **CI 修复一:Windows lane 的真实超时**。`ci-windows` 在 2026-09-09/10 窗口内红了 8 次,其中 6 次是真起进程的用例撞上 vitest 默认 5000ms:`tests/agent-pty.spec.ts`(真起 PowerShell + ConPTY)与 `tests/install-powershell.spec.ts`(冷启 `powershell.exe` 实测 12.1s)现在各自声明 30s 预算,且 `vitest.config.ts` 的全局 `testTimeout` 从默认 5000ms 提到 **15s**(第一次真实 Windows 跑又在**第三个**文件上翻车:`tests/git.spec.ts:85` 耗时 9607ms——三个文件同一个根因,逐文件加超时是打地鼠);`waitForTranscript` 的内层轮询预算从 5000ms 降到 15s,**内层预算必须小于外层**(原先是同一个 5000ms,结构性必然超时)。`ci-windows` 的 `Test` 改跑 `pnpm test:windows`(`--maxWorkers=2`),在 2 核 runner 上不再让真起进程的 spec 互相抢占。
|
||||
- 🛠 **CI 修复二:挂载车道的 npm 安装**。`plugin-mount` 的 `npm install -g @deepseek-ai/dsh@<ver>` 曾 4 次失败(2 次 `ETARGET`、2 次 `JavaScript heap out of memory` exit 134):钉版自身的传递依赖是浮动 `^` 范围,上游**分阶段发布**预发布版时(rc.2 于 09-10 的 14:43–14:57 逐包上线)npm 会组出 rc.1/rc.2 混合 peer 图(3062 条 ERESOLVE)。现在钉一个**已完整发布**的 rc.2(修 ETARGET)并加 `NODE_OPTIONS=--max-old-space-size=4096`(修 OOM)。中途试过 `--legacy-peer-deps`,被真实 CI 否掉:它跳过的正是全局安装必须提供的 peer,`dsh-app-boot` 在 boot 时 require 的 `@deepseek-ai/cordis-plugin-group` 是 peer 而非 dependency,加了它 CLI 直接 `ERR_MODULE_NOT_FOUND`。
|
||||
|
||||
### v0.19.0
|
||||
|
||||
> 📌 **正式版**(npm `latest`,无 prerelease 后缀):本版仅支持 **DSH 0.1.5-rc.1+**(peer 下限 `^0.1.5-rc.1`,CI 钉 `@deepseek-ai/dsh@0.1.5-rc.1`)。DSH 0.1.5-alpha.2 用户请继续用 **v0.19.0-alpha.1**(npm `alpha` 标签仍指向它);0.1.2-rc.1 稳定线继续用 **v0.18.1**。
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 📝 **新建标签页列表恢复可选说明**(#613):DSH 0.1.5-rc.1 让 `SidebarRightGuideEntry.description` 回归(可选),插件随之恢复 `TabDescriptor.description`,六个内置类型各写回一条说明(原生指南的 文件 / 文件变动 / 任务管理 / 侧边对话 / 终端 / 浏览器 六行都带上它),`guideDesc*` 词条回到 20 份词典。**宿主的原生指南只在列出的条目 ≤ 4 条时渲染说明**(更长的列表是整列丢弃,不是截断),而插件默认贡献 6 个 guide 条目——因此默认组合下说明不渲染,只有在插件设置页关掉足够多的 tab 类型、把 guide 压到 ≤ 4 条时才会出现。未声明说明的条目仍是「图标 + 标题」单行(插件不补通用兜底句)。
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 无。rc.1 相对 alpha.2 的 delta 很小(373 个变更文件,绝大多数是上游各包版本号、原生右侧栏预览 UI 打磨与测试快照),除上述说明字段回归(并附带上游 `files` 类型改用自己的彩色文件夹图标作指南字形)外,没有需要插件适配的变更。
|
||||
|
||||
**🧰 CI 与内部**
|
||||
|
||||
- 基线推进到 **DSH 0.1.5-rc.1+**(#613):peer 下限、devDependency 钉版、CI 挂载车道与 `dsh.plugin.json` 的 `engines.dsh` 同步(rc.1 在 npm 上同时是 `latest` 与 `next`)。
|
||||
- 明确未变、不必再核:全局主面板模型(`main` 槽 / `sidebar.panellist` / `ctx.layout` / 根级 `rightbar` + `rightbar.session`,插件仍不接入)、文件地址语法(`packages/util/workspace-path` 只动了版本号)、原生 tab 体宿主契约(`.paneBody` 仍是有确定高度的块级滚动容器)、core / agent / session / subagent 宿主 API 与 `ui-primitives` 导出面(仅 CodeBlock 渲染变化)。
|
||||
- `@deepseek-ai/dsh-client-ui-primitives@0.1.5-rc.1` 仍不声明 `dependencies` 而 bundle 仍裸 import `anser` / `shiki` / `@shikijs/langs/*` / `mdast-util-*` / `micromark-*` / `katex`——上一版提升进 devDependencies 的那组包因此保留,不得回退。
|
||||
- 真机验证(DSH 0.1.5-rc.1 + 插件 0.19.0):门禁 `typecheck` / `lint` / `check:consumer-types` 全绿,单测 **124 files · 1296 passed · 9 skipped**,`pnpm peers check` 干净;挂载冒烟对真实 rc.1 **7 passed**(含 tab 体填充断言,以及新增的「指南 ≤4 条时说明才渲染」断言);本地 3080 实测:guide 六行仍是「图标 + 标题」(6 > 4,说明按上游规则不渲染;上游把胶囊 `min-height` 从 48px 调到 56px),文件树点击 `AGENTS.md` 落到插件编辑器(CodeMirror 就绪),sidechat 输入框贴底(宿主盒 962px == 面板体 962px,composer 底边距 8px),底部工作台与中心列左右边完全重合,`pageerror` 0(控制台仅有第三方 `dsh-tauri-worktree` 的 `/api/dsh-worktree/attach` 500,与本次改动无关)。
|
||||
|
||||
### v0.19.0-alpha.1
|
||||
|
||||
> 🧪 **alpha 通道**(npm dist-tag `alpha`,安装 `dsh-apes-sidebar@latest`):本版仅支持 **DSH 0.1.5-alpha.2+**(peer 下限 `^0.1.5-alpha.2`,CI 钉 `@deepseek-ai/dsh@0.1.5-alpha.2`)。0.1.5-alpha.1 请继续用 **v0.19.0-alpha.0**;0.1.2-rc.1 稳定线用 **v0.18.1**(npm `latest`)。
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 🪟 **每个 tab 体都填满面板**(#609):原生右侧栏的 tab 体宿主是「有确定高度的块级滚动容器」而非 flex 容器,此前插件各 tab 的根只写 `flex: 1`,在块容器里塌成内容高度——侧边对话的输入框因此贴不到面板底(转录一长就被推出可视区)。现在 native 适配层统一给每个 tab 体包一层 `height: 100%` 的列 flex 宿主,插件全部 tab(含第三方 `registerTab` 注册的 descriptor)恢复与底部工作台一致的填满语义。
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🧭 **跟随 DSH 0.1.5-alpha.2 的文件地址语法**:`fileAddressFor` 一律产出 session 作用域地址、绝对路径保留前导 `/`;`parseFileAddress` 改为前缀解析并忽略 `?`/`#` 后缀。
|
||||
- 🪟 **底部工作台的中心列定位跟随 alpha.2 全局面板改动**:`conversation` 槽改为根级 `main` keyed 槽下的 `main.conversation`,定位器改认新 key 并跳过 `display: contents` 槽宿主(alpha.1 的旧 key 仍兼容)。
|
||||
|
||||
**🧰 CI 与内部**
|
||||
|
||||
- 基线推进到 DSH 0.1.5-alpha.2(#609):peer 下限、22 个 devDependency 钉版、CI 挂载车道与 `dsh.plugin.json` engines 同步;`pnpm peers check` 干净(按 §3-9 补提 `dsh-session-persistence` 传递 peer)。
|
||||
- **移除 `TabDescriptor.description`**:alpha.2 的原生指南条目不再渲染第二行(改为「图标+标题」胶囊),该字段与 6 个 `guideDesc*` 词条(20 份词典)一并下线;新建标签页的默认页改由注册表选(恰好 1 个指南条目则直接打开它)。
|
||||
|
||||
### v0.19.0-alpha.0
|
||||
|
||||
> 🧪 **alpha 通道**(npm dist-tag `alpha`,安装 `dsh-apes-sidebar@latest`):本版仅支持 **DSH 0.1.5-alpha.1+**(peer 下限 `^0.1.5-alpha.1`,CI 钉 `@deepseek-ai/dsh@0.1.5-alpha.1`)。0.1.2-rc.1 稳定线请继续用 **v0.18.1**(npm `latest`)。
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 🖥️ **接入 DSH 原生右侧栏**(#604):右列改为 DSH 自己的右侧栏——插件的 7 个 tab 类型全部注册成原生 tab 类型 + 原生 tab 体;聊天里的文件打开统一走 `ctx.sidebarRight.openResource(dsh-resource://file/…)`;`editor` 类型以 `extension` 优先级认领文件资源(压过内置文本预览)并接管内置「文件」页 kind(注销即复位);跨会话打开在目标会话未上屏时排队重放。
|
||||
- 🧩 **退役插件自绘右侧面板与自由窗口**(#605):右列归 DSH 后,插件只保留底部工作台(单分栏树、随会话持久化),开合按钮注册进 DSH 会话头 utilities 槽;浮窗 API(`floats` / `floatTab` / `dockFloat` / `raiseFloat` / 右键「移动到自由窗口」)、`features` 里的 `'floatWindows'`,以及 `openByDefault` / `defaultWidthPercent` / `changesDiffFloat` 三个设置项一并删除(旧持久化文档里的 `floats` 字段被忽略,不影响加载)。
|
||||
- 🔗 **适配 DSH 0.1.5 宿主契约**(#603):`assistant/chunk` 事件删除 → 实时增量改由 `agent/assistant-stream` 帧折叠(侧边对话转录的 `live` 字段);`sessionPersistence.inspect` 删除 → 冷会话读取改走 `open(id,'read')`;会话头 `version` 用 `SESSION_FORMAT_VERSION`。
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 侧边对话转录 / `jobs.output` 回放 / fork 继承等 8 处会话事件读取跟随 0.1.5 契约;自定义种子补齐 fork 标记对,避免继承父会话未领取的 inbox 输入。
|
||||
|
||||
**🧰 CI 与内部**
|
||||
|
||||
- 真机挂载冒烟门禁钉 0.1.5-alpha.1,e2e 增加「展开原生栏 → 经引导页逐个打开插件 tab 类型」的巡检;typecheck / lint / 单测 / 挂载车道全绿。
|
||||
|
||||
### v0.19.0(未发布)
|
||||
|
||||
- 🎨 **可选彩色图标主题**:设置页「文件 → 文件图标」可在**内置单色**与**彩色品牌图标**之间切换——彩色主题含 563 条规则(218 扩展名 + 197 精确文件名 + 148 目录名,如 `.tsx → React`、`package.json → npm`、`node_modules` 着色),数据在**懒加载 chunk**(`lib/client-file-icons.js`)里,只有选中时才下载,默认关闭时启动零开销。数据源自 [#429](https://github.com/omdsh-dev/DSH-better-sidebar/pull/429)(@fenter)。
|
||||
- 🎨 **文件图标多样化 + 对外图标注册 API([file-icons.tsx](./src/client/file-icons.tsx))**:文件树与编辑器文件 tab 不再是单一 `VscFile`——markdown / 图片媒体 / PDF / JSON / 40+ 代码扩展 / 配置 / 数据库 / lock / 压缩包各有专属 glyph(VSCodicons 单色 `currentColor`,遵循皮肤契约),未知扩展回退通用图标。`ctx.betterSidebar` 新增 `registerFileIcon`(`features` 含 `'fileIcons'`):按扩展名 / 精确文件名(`names`)/ 目录名(`folderNames`)注册自定义图标(彩色 ReactNode 亦可,颜色责任在注册方),`exts: []` 为全局默认(只兜内置 glyph 没认领的扩展,不吞掉内置多样性),保留扩展名 `'folder'` / `'folder-open'` 可换目录行图标;`fileIcon` / `folderIcon` 为权威解析器(完整回退链 + 逐工厂崩溃隔离),注册/注销即时生效。接入示例见[外部插件指南 §7](./docs/external-plugin-guide.md)。
|
||||
|
||||
### v0.18.1
|
||||
|
||||
> 📌 **正式版**(npm `latest`):DSH 基线不变(**0.1.2-rc.1+**,peer 下限 `^0.1.2-rc.1`)——本版是 v0.18.0 之后的增量发布:变更面板预览能力增强、文件树可写,以及五项修复。
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 📄 **变更面板操作预览增强**(#499):`.md` 阅读模式(含 mermaid 渲染)、`.html` 与 `.pdf` 内嵌渲染预览;diff 语法高亮扩展到 mjs/cjs/mts/cts、CSS/SCSS/Less、HTML/XML/SVG/Vue、GraphQL、JSONC/JSON5;新增**密钥脱敏**层(预览默认开启,面板头部可切换)
|
||||
- 🗂️ **文件树重命名 / 删除**(#550):行内重命名 + 确认式删除,右键菜单减重与子菜单视口钳制
|
||||
- 🧩 **插件目录名与 shell 预设文案词典化**(#535):跟随宿主语言
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🔀 **git diff 折叠上下文真实展开**(#576,修复 #577):折叠行此前显示「n 行…点击展开」却点不动(`-U3` 裁剪使 gap 段没有行文本);现按需经 `git.show` 拉取两侧完整内容切片填充,带加载 / 失败降级三态与请求去重;顺带修复该路由的 `rev:path` 寻址(此前恒返空)
|
||||
- 💬 **侧边对话种子不再继承父会话未领取的 inbox 消息**(#562):补 fork 标记对,消除「上下文很长时侧边对话先把之前的 User 消息发出去」的幽灵消息
|
||||
- 🖼️ **Markdown 分栏渲染器内的本地图片**(#569):改写为可访问 URL,不再 404
|
||||
|
||||
**🧰 CI 与内部**
|
||||
|
||||
- ESLint flat config 接入 CI 与 Makefile(#536)、Makefile 命令面规范化(#526)、e2e 脚本加固(#527)、共享组件测试工具收敛样板(#524)
|
||||
- 重构:Sidebar.tsx 按关注点拆分(#542)、四处轮询习语收敛到 `use-polling`(#541)、删除 rc.7 宿主的 `__DSH_MODULES__` 回退路径(#540)、One Dark/Light 语法色板单源化(#534)、重复实现收敛与死代码清理(#525)
|
||||
|
||||
### v0.18.0
|
||||
|
||||
> 📌 **正式版**(npm `latest`):本版仅支持 **DSH 0.1.2-rc.1+**(peer 下限 `^0.1.2-rc.1`);不再支持 0.1.0-rc.8 ~ 0.1.1-rc.2——DSH stable 用户请固定安装 `dsh-apes-sidebar@0.17.1`,停留在 0.1.2-alpha.x 的宿主继续用 `dsh-apes-sidebar@latest`(v0.18.0-alpha.0)。
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 🌿 **「文件变动」统一 tab**(#475):Git 视角(真 diff / 历史 / 暂存·提交·还原 / worktree·子仓库选择)与本轮文件视角(模型读/写/编辑实时追踪)双视角合一;统一 diff 渲染(改蓝配对 + 行内字符级高亮 + 语法着色 + 上下文折叠)、底部可拖拽预览面板、一键展开独立 diff tab
|
||||
- 💬 **侧边对话渲染升级**(#486):主对话级 Blocks 结构、turn 用量尾标、断线重连横幅
|
||||
- ⚙️ **工作区路径围栏开关**(#458):新增 `workspaceFence` 声明式设置键,工作区外路径 403 时错误面一键关闭并指引
|
||||
|
||||
**⚡ 性能**
|
||||
|
||||
- 🚀 **核心包 -45%**(#489):19 个非中英词典 chunk 懒加载、渲染稳定化(转录行复用 / tree Sets / 批量 drag)、启动与轮询开销削减(单次 settings fetch、每 tick 单 git 进程);新增 perf 测量 lane;顺带修复底栏拖拽把面板宽度泄漏进宿主布局、原生左栏突跳的 bug
|
||||
- 📉 会话列重复 DOM 查询削减(#456)
|
||||
|
||||
**🐛 修复(节选)**
|
||||
|
||||
- ✏️ 编辑器 / Markdown:SSH 远程链接客户端打开(#522)、预览/编辑切换保持阅读位置(#467)、预览隐藏 YAML frontmatter(#394)、TOC 外点关闭与层级修复(#461)、@-引用保留 basename(#417)
|
||||
- 💻 终端 / 平台:Windows 自定义 shell 解析(#503)、resize 失败容错(#428)、字体解析兜底等宽(#366)、WSL 会话 Linux 绝对路径(#455)、Windows Explorer reveal 保留选中(#508)
|
||||
- 🗂️ 布局 / 状态 / 文件树 / 对话:桌面 shell 布局与侧卡片共存(#398)、窄屏自动激活不强制抽屉(#373)、自由窗口 id 恢复冲突(#385)、窗口聚焦自动刷新文件树(#469)、引用提示固定行尾(#509)、划选弹窗关闭与草稿插入锚定(#427)、折叠态开关组对齐(#361)、旧引擎滚动跳变兼容(#448)、旧版 Git worktree 列表(#454)
|
||||
|
||||
**🧰 CI 与内部**
|
||||
|
||||
- Windows CI 车道(#520)、Makefile 命令面规范化(#526)、e2e 脚本加固 + 聚合双挂载回归(#527)、共享组件测试工具收敛样板(#524)、重复实现收敛与死代码清理(#525)
|
||||
|
||||
**🌐 生态收录**
|
||||
|
||||
- 新收录 10+ 插件:dsh-apes-sidebar-icons(#441)、dsh-sidenote(#451,原 dsh-sidechat #470)、dsh-github-workbench(#410)、dsh-bilingual-reader(#379)、dsh-server-deck(#413)、dsh-md-export(#405)、dsh-code-nav(#404)、dsh-suhuang-scroll(#392)、dsh-better-overleaf(#370)等(均含 18+ 语言 i18n 补齐)
|
||||
|
||||
<details>
|
||||
<summary><b>历史版本(v0.12.0 – v0.15.2)</b></summary>
|
||||
|
||||
### v0.19.0-alpha.0(0.1.2-alpha.5 适配,未发布,内容并入 v0.18.0)
|
||||
|
||||
> 🧪 **alpha 通道**:本版仅支持 **DSH 0.1.2-alpha.x**(peer 下限 `^0.1.2-alpha.5`,npm dist-tag `alpha`)。该版本号当时未单独发布,内容已并入 **v0.18.0** 正式版;v0.19.0-alpha.0 这个号后来被 0.1.5 适配线复用(见上)。
|
||||
|
||||
- 🔗 **适配 DSH 0.1.2-alpha.5(npm 已发布,`alpha` dist-tag)**:CI 挂载门禁钉版、`dsh.plugin.json` engines 下限与 `@deepseek-ai/*` peer / devDependencies 基线升至 0.1.2-alpha.5(真机挂载冒烟 14/14 验证)。`dsh-client-locale` 上游停在 0.1.2-alpha.3 未发新版,其 peer 下限 / devDep 保持并天然兼容 alpha.5 运行时(`pnpm peers check` 零失配,无需新增提升传递 peer)。代码适配了 alpha.4 的兼容性标记改动——`Session.events` 属性移除,迁移到按需读 API `snapshotEvents()`(sidechat 转录 live 读、fork 继承、`jobs.output` 回放、subagent 活跃度共 8 处),新建线程 meta 中宿主已删的 `seedLength` 一并移除;alpha.4 其余变化(双向 `send_message`、自定义模型发现复用 Profile 请求头、`SessionSeq`/`SessionLogOffset` 强类型)与 alpha.5(升级启动修复)经核实不触及本插件其余表面。
|
||||
|
||||
### v0.18.1-alpha.0
|
||||
|
||||
> 🧪 **alpha 通道**:本版仅支持 **DSH 0.1.2-alpha.x**(peer 下限 `^0.1.2-alpha.3`,npm dist-tag `alpha`,安装 `dsh-apes-sidebar@latest`)。该版本号未单独发布,内容已并入 **v0.18.0** 正式版。
|
||||
|
||||
- 🔗 **适配 DSH 0.1.2-alpha.3(npm 已发布,`alpha` dist-tag)**:CI 挂载门禁钉版、`dsh.plugin.json` engines 下限与 `@deepseek-ai/*` peer / devDependencies 基线升至 0.1.2-alpha.3(真机挂载冒烟 14/14 验证)。逐点核查了 alpha.2 → alpha.3 全部 117 个 commit:插件依赖的宿主契约(token 鉴权、斜杠 RPC、`MarkdownText` labels、`sidechat.events` 所依赖的事件流与持久化 API、`SettingsNamespaceInput`、`SUBAGENT_DESCRIPTOR_VERSION`(仍为 3)、`dsh-client-store`、profile 加载器、node-pty 钉版)均无变化,无需代码适配;alpha.3 的破坏性改动(`BeginSubmissionInput.mode` 必填、subagent 错误码改名 `attachment-invalid`、SQLite 持久化后端移除、投影 change feed 收紧为 identity-gated)经核实均不触及本插件。
|
||||
|
||||
### v0.18.0-alpha.0
|
||||
|
||||
> 🧪 **alpha 通道**:本版仅支持 **DSH 0.1.2-alpha.x**(peer 下限 `^0.1.2-alpha.2`,npm dist-tag `alpha`,安装 `dsh-apes-sidebar@latest`);不再支持 0.1.0-rc.8 ~ 0.1.1-rc.2——stable DSH 用户请用 v0.17.1(npm `latest`)。
|
||||
|
||||
- 🔗 **适配 DSH 0.1.2-alpha.2(npm 已发布,`alpha` dist-tag)**:CI 挂载门禁钉版与 `@deepseek-ai/*` devDependencies 基线升至 0.1.2-alpha.2(真机挂载冒烟 14/14 验证)。适配点:`dsh-settings` 移除运行时导出 `settingsNamespace`(命名空间改为编译期校验,宿主侧直接传常量);`dsh-subagent` 描述符版本 2→3(由宿主包盖章,测试断言跟随 `SUBAGENT_DESCRIPTOR_VERSION` 常量);`SessionEvent.ignorable` 恢复与 Remote 网关 `RemoteError` 封装经核实对本插件无破坏。
|
||||
- 🐛 **修复 DSH 0.1.2-alpha.1+ 上侧边对话转录空白**:转录轮询此前仍走 alpha.1 已移除的 `ctx.connection.api`(错误被静默吞掉,tab 永远渲染空转录),现改由插件自有 `sidechat.events` 路由供给(活线程读内存事件日志、冷线程读会话持久化,`afterSeq` 增量拉取,[sidechat-routes.ts](./src/sidechat-routes.ts))。
|
||||
- 🧹 **删除对 0.1.1-rc.x 及更早的兼容层**:e2e 宿主 RPC 从点分/斜杠双方言收敛为斜杠单方言(token URL 必选,[host-protocol.ts](./tests/e2e/host-protocol.ts));`MarkdownText` labels 收敛为嵌套单形状([markdown-labels.tsx](./src/client/markdown-labels.tsx),不再双 prop 名);peerDependencies / devDependencies / `dsh.client.inject` / chunk externals 白名单四处同步清除已消亡的 `@deepseek-ai/dsh-client-runtime`。
|
||||
|
||||
### v0.17.1
|
||||
|
||||
- 🔗 **DSH 0.1.2-alpha.1 适配(双版本兼容)**:DSH 0.1.2-alpha.1 的 Remote gateway、一次性 token 浏览器鉴权与 `MarkdownText` labels 契约变更已全量适配,插件在 0.1.0-rc.8 ~ 0.1.1-rc.2 与 0.1.2-alpha.1 上一致工作(后者经 GitHub tag 源码构建的真机挂载冒烟 14/14 验证;alpha.1 至今未发布 npm,CI 钉版已在 v0.18.0-alpha.0 升至 npm 发布的 0.1.2-alpha.2)。要点:`MarkdownText` 四个渲染点统一改走双形状 labels helper([markdown-labels.tsx](./src/client/markdown-labels.tsx)),修复 alpha.1 上 markdown/mermaid 预览的 `reading 'code'` 崩溃;e2e 冒烟双协议化(token URL 换 cookie、`/api` 斜杠端点 + 按参数名包装 args,[tests/e2e/host-protocol.ts](./tests/e2e/host-protocol.ts));移除已在 0.1.2-alpha.1 消亡的 `@deepseek-ai/dsh-client-runtime` peer
|
||||
|
||||
### v0.16.1
|
||||
|
||||
自 v0.16.0 以来的全部更改:
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🧊 **Git 面板卡死 + 重启死循环**([#376](https://github.com/omdsh-dev/DSH-better-sidebar/pull/376),修复 [#369](https://github.com/omdsh-dev/DSH-better-sidebar/issues/369)):开启「源代码管理」面板可能整页冻结、重启后自动恢复冻结状态且无法退出——三层无上限操作叠加所致,现已全部设界:**① status 截断**——`git status --untracked-files=all` 响应上限 2000 条(超限置 `truncated`,面板显示截断提示,对齐 `fs.read` 截断语义;worktree 变更计数同步有界),海量未跟踪文件不再冻结浏览器主线程;**② 仓库发现限界**——cwd 非 Git 仓库(如家目录)时不再对每个可见子目录串行无界探测:探测超时 30s→5s、子目录探测上限 200 个、并发请求共享同一次扫描并按 60s TTL 缓存,家目录不再引发 `git rev-parse` 进程风暴;**③ 重置逃生通道**——带 `?dsh-sidebar-reset` 打开页面即丢弃持久化布局(含共享宽度)从默认布局启动,即使原页面已卡死也能自救,移除参数后恢复持久化;`statusTruncated` 文案同步全部 19 个词典
|
||||
|
||||
### v0.16.0
|
||||
|
||||
自 v0.15.2 以来的全部更改:
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 🪟 **自由窗口**([#354](https://github.com/omdsh-dev/DSH-better-sidebar/pull/354)):把标签栏的任意 tab(内置或插件注册)**拖到主会话区域**——会话列出现虚线提示浮层,松开即成为悬浮窗口(默认 390×780,手机竖屏比例,创建时按视口钳制后居中于松点);窗口支持头部拖动移动、右下角 SE 缩放(≥320×200)、点击任意处置顶、头部右键「回到侧边栏 / 关闭」、X 走 `closeTab` 正常关闭生命周期(释放终端等);拖到侧边栏 pane 上时该 pane 高亮、松开即**停靠**合并回该 pane;`floats` 随会话持久化(刷新原样恢复,宽容 sanitize + 几何钳入视口);服务语义:`features` 新增 `'floatWindows'`——`openTab` 的 dedupe/id 聚焦命中浮动 tab = **置顶窗口**(不重复开、不展开面板),`closeTab` / `activateTab` 对浮窗正常关窗 / 置顶并照常触发回调,浮窗内 tab `visible` 恒 true,agent 终端 reconcile 覆盖浮窗;tab 内容复用常规渲染、插件 tab 与 pane 完全同契约;附带文件二级页面 8px 网格间距规整([设计文档](docs/plans/2026-08-23-free-window-design.md))
|
||||
- 📂 **模型主动打开(`sidebar_open` 工具)**([#353](https://github.com/omdsh-dev/DSH-better-sidebar/pull/353)):侧边栏新增全局设置 `agentOpenTools`(**默认关闭**),开启后向模型注入**一个**工具——模型可在调用方会话的侧边栏打开本地**文件**(editor tab,按 path 去重)、**文件夹**(全窗树窗口,以该目录为根,`meta.dir`)与 **HTTP(S) 网页**(browser tab,URL 预填);关闭设置即注销工具并清空未投递队列,已打开 tab 保留;非激活会话的打开排队、下次可见时重放(`/sidebar/ws/agent-opens` 推送,同一 trust fence);无新增公共 API、不改变 `BetterSidebarService`([设计文档](docs/plans/2026-08-23-agent-open-tools-design.md))
|
||||
- 📝 **Markdown README 级内嵌 HTML + 目录大纲(TOC)**([#360](https://github.com/omdsh-dev/DSH-better-sidebar/pull/360)):Markdown 预览现在真实渲染**块级内嵌 HTML**——徽章墙 `<div align=center>`、`<details>` 折叠块内嵌 markdown、表格单元格 `<br/>`/`<sub>`/`<img>`、`<video>`/`<picture>` 全部经 DOMPurify 白名单消毒(`<script>` 等活性内容剥除、`<a>` 强制 `_blank rel=noopener`),本地媒体 src 重写为会话媒体路由;≥3 标题出现浮动**目录大纲**按钮,点击平滑滚动并自动展开折叠 `<details>`,HTML 段内标题同样收录;渲染器仍是宿主 `MarkdownText`(shiki / KaTeX / GFM 保留),纯 markdown(零 HTML)文档走原路径零回归([设计文档](docs/plans/2026-08-24-markdown-html-toc-design.md))
|
||||
- 🌏 **第三语言覆盖(19 语言)**([#339](https://github.com/omdsh-dev/DSH-better-sidebar/pull/339)):接入可选 peer `@huanlin/dsh-plugin-better-locale`——ja / de / fr / pt / ko / ar / hi / id / tr / vi / th / ru / it / nl / sv / pl / zh-HK / zh-TW / zh-MO 全量词典(每种约 340 keys);覆盖**借用 DSH 英文槽位**(DSH active=en 时生效,zh 下完全惰性、界面不混语言);19 语言词典同时注册进 better-locale,外部 `ctx.locale.lookup('betterSidebar', key)` 调用者同样可拿覆盖文本;未安装时 `ctx.get('betterLocale')` 为 undefined、整段 no-op,zh/en 行为不变
|
||||
- 🌿 **Git 多仓库选择 + linked worktree 变更发现**([#326](https://github.com/omdsh-dev/DSH-better-sidebar/pull/326) [#285](https://github.com/omdsh-dev/DSH-better-sidebar/pull/285)):会话 cwd 是工作区容器(非 Git 仓库)时自动发现直接子仓库并显示**仓库选择器**——status / 分支 / 历史 / diff / 暂存 / 提交 / 还原 / cherry-pick / 文件打开全部按所选仓库线程化;linked worktree 的变更发现与按工作树操作(含延迟分页响应的事务一致性),并拒绝过期 / 可修剪的 worktree 命令目标、对单库存取失败降级
|
||||
- 🖥️ **浏览器本地回环允许清单**([#365](https://github.com/omdsh-dev/DSH-better-sidebar/pull/365)):新增侧边卡设置 `browserAllowedLoopback`(逗号分隔 host 或 host:port;裸 host 匹配任意端口、带有端口精确匹配)——显式信任的本地开发服务器(如 Vite)可导航,并额外获得 iframe `allow-same-origin` 令牌(模块 / HMR / fetch 管线需要真实 origin,否则白屏);页面相对 GUI 与其他站点仍是跨源;服务端 `browser.probe` 镜像同一允许清单,本地服务器不再被误拒
|
||||
- 📝 **编辑器 Vue + 28 种 legacy 语言语法高亮**([#202](https://github.com/omdsh-dev/DSH-better-sidebar/pull/202)):`.vue` 映射 `@codemirror/lang-vue`(template / script / style 按 `lang` 属性分派、`<style lang="scss">` 预处理器);零新依赖用 legacy-modes 补齐 scss/sass/less/stylus/ruby/lua/perl/r/dart/scala/groovy/powershell/diff/protobuf/cmake/pug/tcl/haskell/clojure/erlang/julia/pascal/vb/vhdl/stex/objectivecpp;语言工厂抛错降级纯文本(console.warn),不再炸编辑器;`.v` / `.m` 跨语言歧义故意不映射
|
||||
- 🔄 **编辑器预览刷新三件套**([#215](https://github.com/omdsh-dev/DSH-better-sidebar/pull/215) [#228](https://github.com/omdsh-dev/DSH-better-sidebar/pull/228),修复 [#167](https://github.com/omdsh-dev/DSH-better-sidebar/issues/167)):文本预览新增**手动刷新**按钮;编辑保存后切回预览自动重载(dirty 时抑制,草稿不丢);预览模式下保存成功边沿自动重载;移除自动轮询与 `fs.stat` 版本端点(后台 API 零流量)
|
||||
- 🖼️ **Markdown 本地 / 相对图片**([#292](https://github.com/omdsh-dev/DSH-better-sidebar/pull/292)):``、`/cwd/img.png` 与引用式 `[id]: url` 目标重写为 `/sidebar/file` 媒体 URL(会话 cwd 边界不变)——预览不再只显示 alt 文本
|
||||
- ➕ **推荐插件目录新增 ego-browser**([#340](https://github.com/omdsh-dev/DSH-better-sidebar/pull/340)):`@dsh-external/ego-browser` Agent 浏览器 Tab(会话侧边栏自动注册本机浏览器页,无 better-sidebar 时回退浮动浮窗);描述词典 19 语言补全([#371](https://github.com/omdsh-dev/DSH-better-sidebar/pull/371))
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🛒 **DSH 市场受管安装兼容**([#338](https://github.com/omdsh-dev/DSH-better-sidebar/pull/338)):移除 `peerDependencies` 里的公开版 `cordis`(市场预览硬拒依赖字段出现 `cordis`,optional 无效)——npm 包满足 [dsh-community-market 安装规范](https://github.com/anywhere-labs/deepseek-harness-desktop/blob/master/dsh-community-market/docs/install-and-uninstall.zh.md),dshfind / 1024Store 目录里的条目重新获得 `repository_backlink` 验证目标,可直接从 Desktop 市场受管安装
|
||||
- 🔤 **类型基底迁移到 `@deepseek-ai/cordis`**([#338](https://github.com/omdsh-dev/DSH-better-sidebar/pull/338)):`Context` = 真实 vendored cordis Context 与结构化服务面的**交集**,`ctx.betterSidebar` 类型合并改挂 `@deepseek-ai/cordis`,公开版 cordis 不再被依赖。**消费者迁移**:`import type { Context } from 'cordis'` 改为 `import type { Context } from '@deepseek-ai/cordis'`(`import type {} from 'dsh-apes-sidebar'` 的类型合并方式不变);未使用该导入的插件无影响
|
||||
- 🧩 **插件树内 `ctx.betterSidebar` 读取全面修复**([#357](https://github.com/omdsh-dev/DSH-better-sidebar/pull/357),修复 [#356](https://github.com/omdsh-dev/DSH-better-sidebar/issues/356)):npm 安装的 DSH 0.1.1-rc.x(web bundle)下侧边栏页面每次加载即崩(`cannot get property "betterSidebar" without inject`)——26 处内部直读 `ctx.betterSidebar` 改走 `ctx.get('betterSidebar')`(root reflect store 解析,不受 fiber 链影响);外部消费者 `inject: ['betterSidebar'] + ctx.betterSidebar` 契约不变
|
||||
- 🔐 **文件 API 会话工作区边界**([#345](https://github.com/omdsh-dev/DSH-better-sidebar/pull/345),修复 [#328](https://github.com/omdsh-dev/DSH-better-sidebar/issues/328)):`fs.tree / fs.read / fs.write` 的 workspace 越界访问修复;媒体、HTML 预览与上传统一 real-path 符号链接校验;新增绝对路径 / 符号链接 / 上传 / 嵌套 Git 会话回归测试
|
||||
- 🪟 **面板宿主层级与视口裁剪**([#330](https://github.com/omdsh-dev/DSH-better-sidebar/pull/330) [#278](https://github.com/omdsh-dev/DSH-better-sidebar/pull/278),修复 [#277](https://github.com/omdsh-dev/DSH-better-sidebar/issues/277)):面板宿主层 z-index 40→25——低于 DSH cordis 动态插件面板 30,工作台不再遮挡 cordis 清单 / 审批面(AppFrame 20 之上、100+ 浮层之下);宿主 `overflow: hidden` 裁剪视口边缘,收起的面板不再把文档撑出双向滚动(实测 `scrollWidth` 2289→1672 / `scrollHeight` 1280→1032,任意皮肤)
|
||||
- 📐 **布局推挤加固**([#310](https://github.com/omdsh-dev/DSH-better-sidebar/pull/310) [#130](https://github.com/omdsh-dev/DSH-better-sidebar/pull/130) [#180](https://github.com/omdsh-dev/DSH-better-sidebar/pull/180)):对话列补 `min-height: 0` + `overflow: hidden` + `overflow-wrap: anywhere`(长不可断 URL / OAuth 链接不再把 composer 与左侧设置按钮挤出视口);layout-push effect 拆「仅设置 + 仅卸载移除」并按 `panelOpen` 门控宽度 push——右栏关闭时拖底部高度不再挤压对话区、松手瞬间不再整页右铺再回弹;`useLayoutEffect` 消除跨 paint 全宽闪帧;松手 flush 最终帧 + `centerRect.right` 同步提交;底部高度按 `viewportHeight - PANEL_MIN` 封顶;拖拽手柄拖动中不再高亮
|
||||
- 📱 **移动端无会话状态说明 + 1px 溢出修复**([#254](https://github.com/omdsh-dev/DSH-better-sidebar/pull/254)):无会话时开关改用 `aria-disabled` 保持不可执行语义、同时允许触摸 / 键盘聚焦显示「选择一个会话以使用侧边栏」提示;panel 改 `border-box`——移动端 `100vw` 含左边框,不再产生 1px 横向溢出
|
||||
- 📏 **侧边栏宽度跨会话共享**([#36](https://github.com/omdsh-dev/DSH-better-sidebar/pull/36)):面板宽度是布局偏好而非会话内容——「最后一次拖拽胜出」写入全局 `dsh-sidebar:v1:width`,缓存会话切换与新建会话即时跟随;无全局键(首次运行 / 旧会话)时行为逐字节不变
|
||||
- 🧹 **会话删除立即关闭该会话终端**([#130](https://github.com/omdsh-dev/DSH-better-sidebar/pull/130)):新增 `PtyManager.closeSession()` + 订阅 DSH `session/disposed`——删除会话不再等 30s 重连宽限到期(agent 终端由 agent 生命周期管理,不受影响)
|
||||
- 🔍 **文件名搜索跳过噪声目录**([#342](https://github.com/omdsh-dev/DSH-better-sidebar/pull/342)):`node_modules` / `.pnpm-store` / `.yarn` / `.turbo` / `.next` / `dist` / `build` / `coverage` 等黑名单(小写不敏感,`.git` 仍跳)——超大依赖树不再耗尽 10 万访问预算提前 `truncated`,`docs/` 等后序目录里的真实文件能搜到;不引入 `.gitignore` 语义,保持「文件名查找」
|
||||
- 📝 **mermaid 全局错误渲染抑制**([#341](https://github.com/omdsh-dev/DSH-better-sidebar/pull/341)):开启 `suppressErrorRendering`——非法图表不再把大错误 SVG 注入 `document.body`;组件级错误回退与源码展示保留
|
||||
- 🖥️ **终端 Nerd Font 图标字体回退**([#190](https://github.com/omdsh-dev/DSH-better-sidebar/pull/190)):starship / powerlevel10k 提示符的补充平面 PUA 图标(Nerd Fonts v3 Material 图标集)不再显示豆腐块——`withIconFontFallbacks()` 为胜出的基础字体追加 Nerd Font 图标族(插入首个通用族之前、按族名去重、过滤 CSS 全局关键字、不列彩色 emoji 字体)
|
||||
- 🌐 **HTML 预览 UTF-8 声明**([#193](https://github.com/omdsh-dev/DSH-better-sidebar/pull/193),修复 [#170](https://github.com/omdsh-dev/DSH-better-sidebar/issues/170)):`/sidebar/html` 响应带 `charset=utf-8`(无 `<meta charset>` 的中文片段不再乱码),保留原始文件字节
|
||||
- 🧪 **trust-fence Origin 改按 hostname 比较**([#182](https://github.com/omdsh-dev/DSH-better-sidebar/pull/182)):Edge 151 把非默认端口 loopback 页面的 Origin 序列化为无端口形式——`http://127.0.0.1` 对 `Host: 127.0.0.1:3080` 不再 403(对齐 DSH 官方网关栅栏);不同 hostname / opaque null origin 仍拒绝
|
||||
- 🪟 **「在文件夹中显示」改为资源管理器揭示**([#94](https://github.com/omdsh-dev/DSH-better-sidebar/pull/94)):不再把目录当文件开进编辑器(`"..." is a directory`)——`revealInExplorer` 切到资源管理器 tab、面板折叠时自动展开、展开父目录并高亮滚动到本轮产出文件;产物行数据改读引擎 Turn deliverable(与 ui-deliverables 同源)
|
||||
- 🖱️ **面板拖动布局闪烁**([#180](https://github.com/omdsh-dev/DSH-better-sidebar/pull/180)):右侧栏关闭时拖底部高度不再左移挤压对话区;松手瞬间不再整体右铺再回弹
|
||||
- 🖥️ **PowerShell 安装脚本修复**([#47](https://github.com/omdsh-dev/DSH-better-sidebar/pull/47)):远程入口统一为「下载脚本 → 移除 UTF-8 BOM → 内存执行」,`-Version` / `-DryRun` 参数在 Windows PowerShell 5.1 下恢复生效(BOM 解析不再吃掉首行 `param(...)`);安装前校验 `pnpm --version`(主版本 <10 时明确报错并以退出码 1 结束,不再写一半 profile)
|
||||
- 🔄 **浏览器嵌入探测 GET 兜底**([#69](https://github.com/omdsh-dev/DSH-better-sidebar/pull/69)):HEAD 响应同时缺 CSP 与 X-Frame-Options 时回退 GET 重试一次——阿里云百炼等只在 GET 回头发嵌入策略的站点不再显示误导性「拒绝连接请求」,而是正确显示「该站点拒绝嵌入」面板 + 「在浏览器中打开」
|
||||
- 🔧 **git 源安装修复 `unrun` devDependency**([#336](https://github.com/omdsh-dev/DSH-better-sidebar/pull/336)):tsdown 0.22 经 `unrun` 加载配置而 pnpm 11 不自动装 peer——git-hosted 安装的 `prepare` 不再报 `Failed to import module "unrun"`(npm tarball 不受影响)
|
||||
- 🍃 **`ctx.effect` 严格化顺手修了 4 处**:拦截注册失败时 effect 体返回 `undefined` 改为 no-op disposer(vendored cordis 的 effect 契约要求返回 disposer,返回 `undefined` 属非法形状)
|
||||
|
||||
### v0.15.2
|
||||
|
||||
自 v0.15.1 以来的全部更改:
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 🗂️ **文件树「在应用中打开」子菜单**([#334](https://github.com/omdsh-dev/DSH-better-sidebar/pull/334)):文件树右键菜单新增「在应用中打开 >」子菜单——内置打开方式(资源管理器显示/选中、VS Code、Cursor、Zed),每行右侧图钉可固定为右键菜单顶层直达项(再点取消);配置可选 SSH host 后 VSCode 系条目改用 `vscode-remote/ssh-remote+<host>/<path>` 协议打开,本地专用条目自动隐藏;支持自定义编辑器(名称 + URL 模板 `{path}` + 是否 VSCode 系,配置入口在 Files 卡片齿轮弹窗)。打开动作经新宿主路由 `POST /sidebar/api/open.external`(argv 数组 spawn,无 shell 注入)([设计文档](docs/plans/2026-08-22-open-with-menu-design.md))
|
||||
- 📑 **Tab 右键菜单**([#331](https://github.com/omdsh-dev/DSH-better-sidebar/pull/331)):页签右键提供「关闭 / 关闭其他页签 / 关闭左侧页签 / 关闭右侧页签」,作用范围为当前 pane(标签组),无可关对象时置灰;仅打开菜单、不切换激活页签;批量关闭逐条走既有 `onClose` 路径,生命周期完整
|
||||
- 📄 **Diff 文件默认折叠**([#270](https://github.com/omdsh-dev/DSH-better-sidebar/pull/270)):改动文件头部改为可访问的展开/折叠控件;识别出的源文件默认展开,测试 / 文档 / 生成文件 / lockfile 与未知类型默认折叠;保留现有 500 行上限
|
||||
- 📖 **README 更新**:特性巡礼改为表格展示(每行两张图,节省空间);社区补全微信群 / QQ 群二维码([#325](https://github.com/omdsh-dev/DSH-better-sidebar/pull/325),QQ 群 577011007)
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🪟 **空分栏清理**([#268](https://github.com/omdsh-dev/DSH-better-sidebar/pull/268)):持久化的 split pane 在临时 diff tab 被清理后遗留全尺寸空分栏——`sanitizeState` 现在同时修剪空的 split leaf,并修复修剪后的失效激活 pane 指针;整个工作台为空时保留唯一空 pane
|
||||
- 🖥️ **Windows 下隐藏 Git 子进程窗口**([#301](https://github.com/omdsh-dev/DSH-better-sidebar/pull/301),关闭 [#124](https://github.com/omdsh-dev/DSH-better-sidebar/issues/124)):`runGit()` 统一加 `windowsHide: true`,仓库状态轮询与操作不再闪现控制台窗口(其他平台行为不变)
|
||||
- 📁 **未跟踪文件夹内文件差异**([#242](https://github.com/omdsh-dev/DSH-better-sidebar/pull/242)):`git status` 从 `--untracked-files=normal` 切换为 `--untracked-files=all`——新文件夹内每个文件独立成行、可正常加载差异(修正 `fs.read` 报 "is a directory",与 VSCode 默认行为一致)
|
||||
- ⚡ **开关/拖拽每帧 React 重渲染消除**(关闭 [#315](https://github.com/omdsh-dev/DSH-better-sidebar/issues/315)):centerRect 改 ref + 底栏 DOM 直写(零 React 渲染);TabContent memo(显式比较器);新增 frame-batcher 对 Divider/dock 拖拽按帧合并;拖拽期跳过无意义 locate。4x CPU 节流 A/B:开关 >17ms 帧 collapse 19→6 / expand 24→4~6,p95 21ms→15ms;拖拽不变(非回归)
|
||||
|
||||
### v0.15.1
|
||||
|
||||
自 v0.15.0 以来的全部更改:
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 💬 **侧边对话 Codex 风格转录重构**([#314](https://github.com/omdsh-dev/DSH-better-sidebar/pull/314)):转录改为**折叠行**——工具调用 / 思考 / 上下文注入统一为安静的单行 chrome(chevron + 标签 + 单行参数摘要,展开为 hairline 缩进正文,无卡片无填充),流式标签与创建 shimmer(shimmer = 生成中)、失败工具 danger、`prefers-reduced-motion` 停帧;**首条问题不再被边界提示吞掉**——上下文注入与首问拆分交付(边界 + 快照经 `agent.inject` 排队、问题唤醒驱动),转录把注入映射为可折叠注入行、真实用户消息(**含首问**)渲染为用户气泡,旧线程的首问同样拆分为独立气泡
|
||||
- 📖 **README 重写**:功能导览(逐特性实机截图)、用户视角 DSH 兼容徽章、简化安装流程(`add` → `approve-builds` → `add`、node-pty 安全构建、粘贴到 DSH 安装提示)、插件生态 28+ 与分类折叠展示
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🖥️ **终端跨会话切换保活**([#323](https://github.com/omdsh-dev/DSH-better-sidebar/pull/323)):切到其他会话不再被当作瞬时掉线——客户端卸载时发送 `park` 控制帧,主机跳过 30s 重连宽限倒计时;切回会话(`open()` 取消 parked)或显式关闭恢复正常生命周期;agent 终端保持无限期存活
|
||||
- 📂 **文件树上传遮罩不再拦截 Tab 拖拽**([#317](https://github.com/omdsh-dev/DSH-better-sidebar/pull/317)):拖拽 Tab(重排 / 跨 pane split)经过资源管理器时不再弹上传遮罩、不吞事件——统一按 `dataTransfer.types` 含 `Files` 门控(与面板宿主 shield 一致),Tab 正常落下;OS 文件拖拽行为不变
|
||||
- 💬 **子代理自动展开去抖**([#314](https://github.com/omdsh-dev/DSH-better-sidebar/pull/314)):Side Chat 线程创建不再误弹任务页——0→N 触发 500ms 重臂并对实时快照按原基线重评估,标题过滤器识别线程后才放行;真实子代理依然自动激活任务页(宽屏展开侧边栏,窄屏只准备 Tab、不强制展开抽屉)
|
||||
|
||||
### v0.15.0
|
||||
|
||||
自 v0.14.0 以来的全部更改:
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 💬 **侧边对话(beta) Tab**([#286](https://github.com/omdsh-dev/DSH-better-sidebar/pull/286)):Codex 风格的侧边线程,**每个对话一个独立 Tab**——子会话继承主会话完整上下文(已完成回合 + 未回答消息 + 进行中回合的 assistant 输出与工具调用,以「interrupted」冻结标记诚实继承);同组合创建(同 preset / provider / model)复用前缀输入缓存;线程对主会话列表不可见、零子代理目录噪音;线程内可持续追问(重启后自动冷恢复);一键「保存为新会话」提升为顶层会话([设计文档](docs/plans/2026-08-20-sidechat-tab-design.md))
|
||||
- 📤 **文件窗口上传**([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)):头部「上传文件 / 上传文件夹」按钮 + 拖放上传(拖到树区 = 工作区根,目录行 = 进该目录,文件行 = 进其所在目录,对齐 VSCode);上传时全屏模糊进度弹层(文件级进度 + 取消 / Esc);上传中按钮禁用、成功后文件树自动刷新
|
||||
- 🧩 **桌面兼容四选项**([#284](https://github.com/omdsh-dev/DSH-better-sidebar/pull/284)):位置兼容模式改为**主行下拉**——**自动检测**(默认,保守:仅使用标准的 Window Controls Overlay 几何,32/36px 等各壳差异自动跟随、最大化/还原实时更新,网页环境零修改)/ **DSH官方Web**(显式零适配)/ **壳兼容方案**(内置预设,手动启用;只收录 issue/PR 中出现过且 100+ star 的壳,命中环境带「已检测」提示)/ **自定义方案**(自定义 CSS + 下移距离)。旧版本已有兼容配置的用户自动落到自定义方案;交互控件统一退出桌面拖拽区(`no-drag`);底栏推挤锚点复合选择器双保险(`[data-pane]` 与 `:has(> [data-slot])`)
|
||||
- 🎛️ **设置页 UI/UX 现代化**([#300](https://github.com/omdsh-dev/DSH-better-sidebar/pull/300)):侧边卡片二级设置入口改为卡片底部「功能设置」设置条(替代右下角隐形齿轮,可发现性提升);协调双色启用态(brand 激活强调 + success 绿勾选徽标);全部颜色仍为 `--dsw-alias-*` 令牌派生,皮肤体系自动跟随
|
||||
- ➕ **推荐插件目录新增**:`dsh-docs-panel` 全局文档面板([#230](https://github.com/omdsh-dev/DSH-better-sidebar/pull/230))、`dsh-flowglass`([#261](https://github.com/omdsh-dev/DSH-better-sidebar/pull/261))、`dsh-git-forge` 与 `dsh-ssh-tunnel`([#204](https://github.com/omdsh-dev/DSH-better-sidebar/pull/204))、`dsh-turn-review`([#102](https://github.com/omdsh-dev/DSH-better-sidebar/pull/102))
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- ⚡ **子代理页实时预览批量接口**([#298](https://github.com/omdsh-dev/DSH-better-sidebar/pull/298)):旧实现每个 running 子代理独立轮询 `subagents.history`,host 侧每次触发全量子代理枚举形成 O(N²) 放大、多子代理并发时页面卡顿——改为单个批量接口 `subagents.live`(一次枚举整棵子代理树)+ 客户端单轮询、单在途请求;展示逻辑与文案不变
|
||||
- 🖱️ **拖拽中断 / 快速释放不再回滚**([#249](https://github.com/omdsh-dev/DSH-better-sidebar/pull/249),关闭 [#247](https://github.com/omdsh-dev/DSH-better-sidebar/issues/247) [#248](https://github.com/omdsh-dev/DSH-better-sidebar/issues/248)):中断 / 快速释放提交最后已知位置;HMR 后中心列重定位兜底(修复热更新后底栏空白)
|
||||
- 📐 **推挤变量挂载期持续有效**([#259](https://github.com/omdsh-dev/DSH-better-sidebar/pull/259),修复 [#258](https://github.com/omdsh-dev/DSH-better-sidebar/issues/258)):拖拽松手后底边栏不再闪全宽
|
||||
- 🔧 **适配 DSH 0.1.1-rc.1 / rc.2(@next)**([#297](https://github.com/omdsh-dev/DSH-better-sidebar/pull/297) [#305](https://github.com/omdsh-dev/DSH-better-sidebar/pull/305)):无代码逻辑改动
|
||||
- 🔒 **上传链路安全加固**([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)):`relativePath` 空段 / 绝对路径显式拒绝;临时文件唯一命名(并发上传互不干扰、崩溃不阻塞);写流错误监听(磁盘失败不崩溃进程);客户端错误码与服务端统一、413 本地化
|
||||
- 🔐 **文件 API workspace 边界加固**([#328](https://github.com/omdsh-dev/DSH-better-sidebar/issues/328)):`fs.tree/read/write`、媒体、HTML 预览和上传统一按真实路径限制在会话 workspace 内,拒绝越界绝对路径与外链符号链接
|
||||
|
||||
### v0.14.0
|
||||
|
||||
> ⚠️ 本版起需要 DSH ≥ 0.1.0-rc.8。自 v0.13.1 以来的全部更改:
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 🖼️ **统一面板宿主注入重构**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):面板/开关簇迁入 `[data-dsh-panel-host]` 固定含块层(`fixed inset-0 z-40`),免疫桌面套壳中间层 transform 对 fixed 含块的劫持;挂载自检(页面级 transform → `data-dsh-panel-host-degraded` 降级同步,按未修正几何判定、祖先变换消失才退出);推挤锚点改 `#root [data-dsh-frame] > [data-pane="conversation"]` + `#root` calc 宽度防桌面壳加性溢出;chunk 激活重验证(HEAD+ETag 保留未变 chunk,5s 超时兜底 fail-open);`visualViewport` 键盘 inset + `env(safe-area-inset-*)` 移动端适配
|
||||
- 📂 **文件打开方式默认独立**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):`editorExplorer` 默认从「合并」改为「独立」——新会话树点击 / 打开文件按路径**新开**文件 tab,无路径窗口即纯资源管理器;合并模式保留为可选手动开启
|
||||
- 🖥️ **终端 shell / shellArgs 设置页可配**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):终端卡齿轮二级页面新增「Shell 路径」「Shell 参数」两行配置(此前只能通过 `cordis.patch.yml` 配置)——设置页写入后对**之后打开的** UI 终端与模型终端(`terminal_create`)即时生效;留空保持 yaml → `$SHELL` / 登录 shell / `powershell.exe` 的既有解析顺序
|
||||
- 🏷️ **设置页版本徽标**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):侧边卡片设置页顶部新增 `DSH-better-sidebar v0.14.0` 身份徽标(版本与服务实例同步,由测试守护)
|
||||
- 🔍 **添加插件目录搜索 / 分组 / 独立滚动**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):为插件生态增长做准备——目录列表顶部加实时搜索(按名称 / id / 描述过滤),条目支持可选 `category` 分组渲染,列表独立滚动(弹窗不再随条目数无限增长)
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🧩 **rc.8 模块系统迁移**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):rc.8 不再暴露 `window.__DSH_MODULES__` 页面全局(改由 `ctx.modules` 服务提供),懒加载 chunk 的外部依赖解析全面失效——client 注入 `modules` 服务 + 插件自有全局共享给 chunk 副本(终端 / 编辑器 / Mermaid 恢复正常按需加载)
|
||||
- 🧩 **chunk 重验证屏障健壮性**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):HEAD 重验证加 5s 超时兜底(路由挂起时 fail-open 重取,屏障不再可能无限期阻塞懒加载);`resetChunks` 清挂起的重验证屏障
|
||||
- 🖱️ **拖拽健壮性**([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)):快速释放(浏览器合并 / 丢失 pointermove 突发)时提交最后已知拖动位置而非回退;`pointercancel` / 捕获丢失中断同样保留拖动结果;提交后立即重测中心列(消除底栏宽度中间帧抖动);HMR 重激活后中心列重定位兜底(`<html>` 样式观察 + 底栏打开重测),修复热更新后底栏空白 / 输入框位移
|
||||
|
||||
### v0.13.1
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 📊 **Markdown 预览安全渲染 Mermaid 图表**([#164](https://github.com/omdsh-dev/DSH-better-sidebar/pull/164)):预览的 md 含 mermaid fence 时按需下发 `client-mermaid.js` chunk(~7MB,无 mermaid 文件零加载);纵深防御渲染——`securityLevel: 'strict'` + `htmlLabels: false`(节点文字走真实 SVG `<text>`)+ SVG 注入前二次清洗(删 `foreignObject`/`script`/外来 HTML 元素、剥 `@*`/`on*`/`href` 属性);点击图表在弹窗中放大(滚轮以鼠标为中心缩放、拖拽平移、工具栏与快捷键),深浅色跟随重渲、解析失败回退原码
|
||||
- 🖥️ **终端 shell 与 shellArgs 可配置**([#125](https://github.com/omdsh-dev/DSH-better-sidebar/pull/125)):`cordis.patch.yml` 的 `better-sidebar.config` 可指定 `shell` / `shellArgs`(`shellArgs` 非空时完全替换默认参数;未配置维持自动解析 `$SHELL` / 登录 shell / `powershell.exe` 原行为),UI 终端与 agent 终端(`terminal_create`)同时生效;终端 tab 标题改用 shell 名(bash / zsh / powershell),内部标识改 UUID,同 shell 可开多个终端
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🔗 **聚合双挂载自动退让**([#200](https://github.com/omdsh-dev/DSH-better-sidebar/pull/200)):聚合包(如 dsh-web-ui-all)以独立条目 id 挂载同包时,`cordis.patch.yml` 的守卫表达式自动禁用自身 `better-sidebar` 行,不再重复注册 `/sidebar/api` 导致 `duplicate prefix route` 整个插件树启动失败(`dsh web` 崩溃);独立安装行为不变
|
||||
- 🔧 **适配 DSH 0.1.0-rc.7**([#207](https://github.com/omdsh-dev/DSH-better-sidebar/pull/207),修复 [#206](https://github.com/omdsh-dev/DSH-better-sidebar/issues/206)):修复 DSH 主框架升至 rc.7 后选模型 / 发消息报 `agent-presets: refusing to compose an unscoped context` 的问题
|
||||
|
||||
### v0.13.0
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 📁 **文件窗口与资源管理器二合一**([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)):新 `editorExplorer` 设置(编辑器卡齿轮)——文件 tab 增加路径输入框头部 + 可开关的右侧停靠文件树(每 tab 记忆展开/宽度,左缘拖拽调宽 160~480px,全局文件名搜索走 host `fs.search` 路由,预算封顶并跳过 `.git` / 符号链接目录);独立模式(默认)树点击 / 输入框 Enter **按路径新开**文件 tab,合并模式**原地切换**当前 tab;新会话默认 seed 空文件窗口(`Files`)替代 explorer tab,无路径窗口在独立模式为纯资源管理器、合并模式为带 chrome 的空文件窗口;树右键提供「在新 Tab 中打开」「在侧边打开」(split)
|
||||
- 🎛️ **声明式设置 select 行**([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)):设置项新增 `type: 'select'`(`options` 支持 value/title/desc/icon,`multi` 多选存数组);带图标的选项渲染大图标选项卡、收起态同样显示图标;`editorExplorer` 改为图标化下拉(合并 / 独立);能力清单新增 `settingSelect`
|
||||
- 🔀 **与 dsh-web-ui 家族右侧面板互斥**([#181](https://github.com/omdsh-dev/DSH-better-sidebar/pull/181)):读取 `aionui-panel` 设置命名空间的提供方选择——当选择「使用 aionui-panel」时,整个 better-sidebar(右侧栏 / 底部面板 / 浮动入口 / 各类接管)不再挂载;选择 DSH-better-sidebar(或未安装 aionui)时正常。设置页保存后实时生效(settings-document 推送),无需刷新
|
||||
|
||||
### v0.12.3
|
||||
|
||||
**✨ 新功能**
|
||||
|
||||
- 🎨 **皮肤兼容(令牌驱动)**:全面消费 DSH 设计令牌,与 dsh-web-ui 皮肤中心 10 款皮肤兼容,换肤自动跟随;终端/编辑器表面在透明/半透明玻璃值下回退不透明底色,文字不叠在皮肤背景上([#110](https://github.com/omdsh-dev/DSH-better-sidebar/pull/110),修复 #106 #105 #90 #60,附带 #52 #57 #92)
|
||||
- 🗂️ **统一路径处理**:UNC 路径 / 软链接分类(目录软链接可展开、失效链接标红)、HTML 路由平台守卫([#134](https://github.com/omdsh-dev/DSH-better-sidebar/pull/134),#65 #67 #43 #79 #115)
|
||||
- 🖥️ **终端 shell 可配置**:设置项自定义 shell,Windows 自动探测 pwsh([#95](https://github.com/omdsh-dev/DSH-better-sidebar/pull/95))
|
||||
- 📝 **编辑器新增语言**:C# / Kotlin / Swift 语法高亮([#120](https://github.com/omdsh-dev/DSH-better-sidebar/pull/120))
|
||||
- 🧭 **设置页导航图标**:设置页导航图标与布局优化([#114](https://github.com/omdsh-dev/DSH-better-sidebar/pull/114))
|
||||
- ➕ **推荐插件目录新增**:`dsh-git-remotes`——Git 远程 Tab(分支/上游/ahead-behind、fetch 可 prune、ff-only pull、确认后才 push,不替换内置暂存/提交)([#91](https://github.com/omdsh-dev/DSH-better-sidebar/pull/91));`dsh-video-preview`——视频内联预览(.mp4/.webm/.mov/.mkv/.avi 等,自带 /video 宿主路由支持 HTTP Range 206 拖进度条,不受 20MB mediaLimit 限制)([#126](https://github.com/omdsh-dev/DSH-better-sidebar/pull/126))
|
||||
|
||||
**🐛 修复**
|
||||
|
||||
- 🔧 **xterm 依赖迁移**:弃用的 xterm 迁移至 `@xterm/xterm`(Closes [#122](https://github.com/omdsh-dev/DSH-better-sidebar/issues/122),[#128](https://github.com/omdsh-dev/DSH-better-sidebar/pull/128))
|
||||
- 📝 **Markdown 编辑器**:选区转对话弹窗恢复可用([#24](https://github.com/omdsh-dev/DSH-better-sidebar/pull/24))
|
||||
- 🖼️ **Markdown 预览支持本地/相对路径图片**:预览 `.md` 时把指向本地文件的图片目标(相对/绝对路径、引用式 `[id]: url`)重写为 `/sidebar/file` 媒体 URL 并显示(此前仅绝对 http(s) 图片能渲染,相对路径只显示 alt 文本)
|
||||
- 🐛 **node-pty 加载失败不再拖垮 server**([#140](https://github.com/omdsh-dev/DSH-better-sidebar/issues/140)):宿主半改为懒加载 node-pty,缺失时插件照常挂载,终端以修复提示横幅(可复制命令 + 重试按钮)呈现,agent 终端工具自动跳过
|
||||
- 🧪 测试工程:单元测试拆分(#141)+ smoke 偶发失败修复
|
||||
|
||||
</details>
|
||||
-379
@@ -1,379 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
> Full release history of dsh-apes-sidebar (see the [README](README_EN.md) for the latest release; also mirrored on [GitHub Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases)).
|
||||
|
||||
### v0.24.1
|
||||
|
||||
> 🎁 **This release carries everything since v0.22.1** (the Files/Changes UI-UX rebuild and performance work of v0.23.0, the DSH 0.2.0-rc.1 support-line move of v0.24.0, and the fixes below).
|
||||
>
|
||||
> 🐞 **Fix release** for the file tree rebuilding itself on every interaction. (1) The native surface (`sync()` in `src/client/native/index.ts`) treated the `files` takeover as a surplus registration — it is keyed by KIND and never appears in `wanted`, so **every session-state write** disposed and re-created it. Replacing a slot registration replaces the host's slot entry, which **unmounts and remounts the whole plugin tab body** (folder toggles, tab switches, bottom-workbench drags all trigger it): the explorer lost its level cache, scroll position and directory watcher, then re-listed the entire visible tree via `fs.trees([root, ...expanded])`. (2) The live-refresh path (`retryDir` in `FileTree.tsx`) **deleted a level from the cache** before refetching, so its rows were replaced by a placeholder and rebuilt (a build, a formatter or the model's own bash made entire folders blink).
|
||||
|
||||
- 🧪 Discriminating evidence: a new real-host lane `tests/e2e/zz-expand-refresh.e2e.ts` (a toggle asks only for the toggled level, the tab body survives, rows never drop to zero; reverting either fix turns it red) plus two jsdom guards (reverting the surface fix: `registered` 6 → 12 with every `disposed` entry being `dsh-apes-sidebar:files`; the other asserts stale rows stay on screen during a re-list)
|
||||
- 📉 Measured after the fix (40-child directory): the toggle's `fs.trees` payload goes from `[root, big]` (the empty-cache signature) to **`[big]`**, new WebSockets 1 → **0**, removed rows 45 → **0**, and an external write goes from "remove 40, add 40" to **one added row**
|
||||
- 🔧 `src/fs-watch.ts` / `use-dir-watch.ts` / `TreePanel.tsx` / `tab-adapter.tsx` untouched (`fs.watch` does not emit on creation — measured and ruled out)
|
||||
|
||||
### v0.24.0
|
||||
|
||||
> 📦 **Support line moved**: DSH **0.2.0-rc.1+** only (peer floor `^0.2.0-rc.1`, CI pins `@deepseek-ai/dsh@0.2.0-rc.1`). **The 0.1.7 line (including npm `latest` = 0.1.7-rc.2) should pin v0.22.1 (the last published release on the 0.1.7 line)** — the 0.2.0 host's startup compatibility preflight evaluates `semver.satisfies(hostVersion, peerRange, {includePrerelease:true})`, and a caret never spans a minor bump (measured: `^0.1.7-rc.1` vs `0.2.0-rc.1` is **false**, so the whole row is disabled silently). This version therefore keeps no 0.1.7 compatibility branch.
|
||||
|
||||
- 📦 **Baseline lifted to `0.2.0-rc.1`**: 14 DSH peers and 27 `@deepseek-ai/*` devDependencies move together; `dsh.plugin.json`'s `engines.dsh` follows; the mount lane (`scripts/e2e-common.sh`) and CI pins follow.
|
||||
- 🔍 **Measured before adapting (purely additive, hence no runtime change)**: across the 19 host packages this plugin consumes, **zero value exports were removed**; the only type-surface changes are `dsh-client-ui-primitives` (optional props on `DisclosureRow` / `TextShimmer` / `Tooltip`, overlay top inset), `dsh-session` (new `ToolCallRecovery`) and `dsh-api-remotes` (a new product-analytics remote); session format stays **v4** (no migration package), `SUBAGENT_DESCRIPTOR_VERSION` stays **3**, `dsh/lib/bin.js` (`dsh plugin` / `dsh web` / the ready line / `--version`) and the `dsh-client-modules` runtime are **byte-identical**, the web profile's client inject list is unchanged, and `ui-primitives`' bare imports are identical (no new test-time dependency).
|
||||
- 🧩 **Host web-profile composition changes, checked though unrelated**: desktop-only `product-analytics` / `desktop-product-telemetry` rows are added (disabled on the web profile); the already-disabled `time-context` / `schedule` / `ui-schedule` rows move into the optional `dsh-experimental-schedule-bundle`. The mount assertion "the host owns exactly one tab entry (terminal) and browser is absent" is unaffected.
|
||||
- 🧪 **Tests and rules**: the peer-shape assertion in `tests/market-manifest.spec.ts` now pins the current baseline tuple and records the "a caret never spans a minor bump" lesson (compatible with §3.4 item 12's "includePrerelease is more permissive" note — that permissiveness only holds inside one minor line).
|
||||
- ⚠️ **Only residue**: the optional `@huanlin/dsh-plugin-better-locale` integration (imported by no source file or test; detected structurally at runtime) pins `^0.1.0-rc.8`, and even its newest 0.4.3 pins `^0.1.5-rc.2` — both fail the 0.2.0 preflight, so `pnpm peers check` reports its 5 unmet peers. That is upstream not having adapted to 0.2.0, unrelated to this plugin's own 14 peers.
|
||||
### v0.23.0
|
||||
|
||||
> 🧭 **Development-line version (never published to npm; its content ships with v0.24.1)**: the Files and Changes pages rebuilt end to end, plus a round of performance and interaction polish. The Files page gains **Ctrl/Cmd and Shift multi-select** (VS Code semantics), a batch action bar, **Git change coloring** (VS Code's add/modify/delete/conflict tones, directories get a summary dot), **new-folder creation**, reworked drag-and-drop upload, and **"Zip and download" on a multi-selection** — archiving became a host-side task with progress (`archive.build` / `archive.status`, per-entry callbacks, 4 concurrent builds, 5-minute TTL, sessionId-bound). "Open with" now keeps **both sources side by side**: the local associated apps the host detects (file manager / VS Code / Cursor / Zed / custom editors, SSH remotes included) plus the plugin's own targets, with a setting (`openWithPluginTargets`) to force coexistence; the context menu was restructured (only default actions at level one, the app list moves into a submenu). The Changes page is **rebuilt as a hierarchy tree** with both the Git view (working tree / staged / history) and the Agent view (per-session edits), and directory rows stage/unstage. Interaction and performance: `/sidebar/api/fs.tree` measured **22.8ms → 3.8ms** (10k entries: one `readdir`, rows built only up to the cap, lowercase-comparison sort, 1.5s directory-level cache invalidated by writes and `fs-watch`), a new batch route `fs.trees` (mount/refresh goes from N+1 requests to one), opening the context menu no longer re-lists directories, and rows no longer re-render with every git poll. Also fixed: "Open to the Side" did nothing (it only wrote into the collapsed bottom workbench; it now routes by surface to the host's `openResource(preferNewPane)` and really splits).
|
||||
|
||||
- ⚠️ **Behavior change (security-relevant)**: the **workspace path fence was removed** — the plugin's fs routes (tree/read/write/rename/remove/mkdir/media/HTML/upload/archive) no longer enforce containment and can read or write any path the host user can reach (OS permissions are the only limit); the `workspaceFence` setting and its notice row are gone.
|
||||
- 🧩 Reusable global components and shared stores were extracted (`IconButton` / `Chip` / `SectionHeader` / `Notice` / `ConfirmDialog` / `StatusBadge` plus a shared git-status store) under the skin contract (`tests/theme.spec.ts`); the ZIP writer is our own (no third-party dependency) and a non-latin1 download name no longer 500s (RFC 5987).
|
||||
- 🐞 Also fixed: the tree's expansion was reset when a file preview was closed and the tree came back (the native surface kept the expansion set per tab; it is now projected from **per-session state**).
|
||||
|
||||
### v0.22.1
|
||||
|
||||
> 📦 **Stable release** (npm `latest`): DSH **0.1.7-rc.1+** only (peer floor `^0.1.7-rc.1`, CI pins `@deepseek-ai/dsh@0.1.7-rc.1`) — the support line is **unchanged**, so 0.21.1 / 0.22.0 users can upgrade straight away; **hosts on DSH 0.1.6-alpha.2 or earlier still pin v0.19.1**. A patch release: both defects reproduced reliably on a real host while the unit suite stayed **green**.
|
||||
|
||||
- 🐛 **The `files` takeover's tab-type id could be orphaned → `native register files error: … already registered` spam plus the Files window falling back to the host's empty state until a page refresh** (community issues #770 / #771 / #766). The chain was confirmed verbatim from the desktop shell's own log (`desktop.frontdesk.log`, 2026-09-27 15:51:39.338): during an in-page client entry replacement (plugin-market update, Plugins page disable→enable, HMR rebundle) the plugin's teardown unregisters each built-in descriptor one by one → `service.notify()` (synchronous, inline) drives `sync()` on an **already inactive** plugin context; the drop loop released the `files` takeover — which is **not a descriptor** — and re-created it **in the same pass**: `tabs.register` lives on the **host** context (still alive) so it took the id anyway, while the `ctx.slots.inject` right after it threw `cannot create effect on inactive context`, `live.set(FILES_KIND, …)` never ran and the disposer was lost — the id became **unregistrable for the rest of the page's life**. Fix (community PR #777, @yanzhaohui1999): ① the drop loop **skips `FILES_KIND`** — the takeover lives and dies by the editor type's `wantsFiles/hasFiles` switch and the seat disposer; ② **any registration that fails after the host took the id releases that type and the slots already installed**, leaving only a failure the next notification retries; ③ teardown / drop loops release entry by entry, so one throwing release no longer aborts the rest.
|
||||
- 🐛 **macOS desktop: window drag worked once and then stopped, and the double-click-title zoom was gone** (issue #772). The plugin host is a direct `body` child, so the shell's `html[data-platform=darwin] body > :not(#root) { -webkit-app-region: no-drag }` (an id-bearing selector — only `!important` wins) applied to it, and app-region **ignores `pointer-events`** — the viewport-sized panel layer cancelled every `[data-window-drag]` strip beneath it. Fix (community PR #773 merged; #786 completed it): `[data-dsh-apes-sidebar]`, `[data-dsh-panel-host]` (with `> *` staying `no-drag`) and the zoom modal `.mermaidModal` opt out with the **neutral** `-webkit-app-region: initial !important` — `initial` computes to `none`, which does **not** subtract from the drag region, whereas the literal keyword `none` is not neutral (it computes to `no-drag`). Interactive popups (`.selectionPopup`, `FloatingWindow`, `AnchoredPopover`) deliberately do **not** get `initial`, or a press would start a window drag instead (the #103/#111 regression).
|
||||
- ✅ **Guards**: `tests/native-registration.spec.ts` pins both invariants to the registry's **event log** (4/4 red on the unfixed code, and "a failed registration can still be retried" is no longer a non-discriminating assertion); slot failures are keyed by `name::key` so `registerSlots`' partial-release path is actually exercised; a **deployment-level regression gate** `tests/e2e/native-reload.e2e.ts` was added (`utimesSync` the installed `lib/client.js` → in-page entry replacement → assert no `native register … error`, no diagnostic strip, the Files takeover usable again; red on 3 consecutive runs against npm 0.22.0, green on 3 consecutive runs of the fixed build (it is ONE case, run repeatedly)). The drag contract is shape-pinned by `tests/panel-host-css.spec.ts` on Linux `pnpm test` (there is no macOS runner) and value-pinned by a real cascade probe in the mount lane that reads computed styles against the shell's own rules (both the panel host and the zoom modal red the unfixed artifact).
|
||||
- 📐 **Baseline unchanged**: `@deepseek-ai/dsh-*` still pinned to `0.1.7-rc.1`, peer floor `^0.1.7-rc.1`; no public API, contract, dictionary or chunk changes. Incident write-up (and why #766's "absorb" approach was rejected): [docs/plans/2026-09-28-native-files-takeover-reload-leak.md](./docs/plans/2026-09-28-native-files-takeover-reload-leak.md).
|
||||
- 🧪 **Verification**: `pnpm test` 122 files / 1293 passed / 9 skipped; `pnpm test:mount` and `DSH_CMD=… pnpm test:mount:aggregate` green; the packed bundle carries `app-region:initial!important` twice and `no-drag` twice.
|
||||
|
||||
### v0.22.0
|
||||
|
||||
> 📦 **Stable release** (npm `latest`): DSH **0.1.7-rc.1+** only (peer floor `^0.1.7-rc.1`, CI pins `@deepseek-ai/dsh@0.1.7-rc.1`) — the support line is unchanged, so v0.21.1 users can upgrade straight away. **Hosts on DSH 0.1.6-alpha.2 or earlier still pin v0.19.1.** The headline is the Tasks page rebuilt from a "subagent topology" into a **workflow graph**, then polished over several rounds of real-host feedback; along the way DSH 0.1.7 deleted the Agent Teams Remote methods, so both the team and background-job data planes were rewritten.
|
||||
|
||||
- 🧩 **The Tasks page is now a workflow graph (the primary view)**: the session tree renders as layered nodes joined by bezier edges — drag to pan, wheel-zoom to the cursor, fit to the content box (double-click the background to refit), and a control cluster bottom-right (graph/tree toggle + fold switch + zoom + fit). The classic **indented tree is kept** (keyboard-navigable); both modes share one view model, so fold state and team enrichment never drift apart, and the default view is a setting.
|
||||
- 🃏 **Two-segment node cards**: the top segment is the kind badge (main agent / subagent / teammate / workflow / completed aggregate) + phase badge + name + meta line; the bottom bar is the state dot + state word + **the same merged activity line the main agent shows** (concurrent tools grouped by category, counted, with the running call's detail — the wording comes straight from the host's `chat` namespace) + a fold chevron on finished nodes. A running bar is swept left-to-right across its **whole** width (1.5 s, token gradient, disabled under `prefers-reduced-motion`). Cards are 8px-rounded, hierarchy is carried by a faint top-segment tint only, and the session you are on wears a heavier accent border.
|
||||
- 🔀 **Workflow runs enter the graph**: the host folds `tool-workflow/*` session events into runs (the same events the official workflow-run panel folds); a run hangs under its origin agent, its member agents are re-parented below it and boxed per phase with matching phase badges. Members with no catalog row are synthesized from the run's own data, so a finished run still shows who took part.
|
||||
- 🗂 **Folding is split into two groups that say what they hold**: `✓ N completed` (finished work plus failures, with failures called out as `N failed`) and `N idle` (teammates that finished a turn and can be called back at any moment) are **two separate rows**. The **manual chevron always works** (no threshold); the **automatic** rule only sweeps idle members once there are 3 or more of them — one or two waiting members are the team's working set, and their cards are where a task line shows up. An aggregate's name line reads "first two names + `+N`", and clicking it expands everything.
|
||||
- 🪟 **Two persistent floating windows** (extracted into a reusable `FloatingWindow`): background-job output, and the shared task's detail/edit surface — draggable, resizable from every edge, with a self-scrolling body, dismissed only by its close button or Escape (an outside click, a blur or the anchor leaving the viewport never closes them). The task window hands its spare height to the description (or the multi-line editor), so enlarging it gives the content more room rather than leaving a gap, and the action row is pinned to the footer so a long description cannot push it out of sight.
|
||||
- 👥 **Agent Teams board (experimental layer)**: team members enrich their matching nodes and an always-visible strip lists the roster and shared tasks; the task state machine follows the host (pending → claim → in progress → complete → reopen) with reassign / edit / two-step delete, and a stale CAS revision gets its own notice ("changed elsewhere — refreshed"). **Member activity is overlaid from `subagents.live`'s `running` flag** rather than guessed on the client.
|
||||
- 🔄 **Background jobs now read the host's client service**: DSH 0.1.7's web profile mounts a client-side `ctx.jobs` (a pushed roster plus a non-consuming output stream and `kill`), so the plugin deleted its own `jobs.list` / `jobs.output` / `jobs.kill` routes and the event-replay mirror — and never touches the model's `job_output` cursor at all (the host turned that constraint into a capability). Output streams in a persistent floating window with tail-follow, and the drawer auto-collapses once a tree has 8 or more agents.
|
||||
- 🛠 **Two data planes rewritten for DSH 0.1.7**: ① team reads — upstream deleted the three `agentTeams.remoteView`-style Remote methods, so the plugin reads the Lead Session's **`agentTeam` Session projection** instead (the same snapshot `subagentCatalog` comes from, push-based), deleting the `teams.view` route and its 5-second poll; the write path keeps two routes (`createTask` / `updateTask`) whose rejections moved from a result union to a thrown `TeamError`, with `TEAM_TASK_STALE_REVISION` mapped to a 409 `team-conflict`. ② background jobs — see above. **The bug this fixed in the wild**: on 0.1.7 the team strip never rendered at all (the route answered `remoteView is not a function`, and the page degraded silently).
|
||||
- 🐛 **Four defects caught on a real host** (every one of them green in unit tests): the per-node fold button did nothing because it answered to the **automatic** rule's guards (non-member + leaf) while the button is a **manual** trigger — the two triggers now have their own guards; an **idle** card never drew a fold chevron at all (its render condition was hard-coded to done/error) — "not executing" now qualifies; claiming a task flipped its tag to **blocked** because `ready` only means "queued with every blocker done, clear to claim" and is false for every non-pending task — the rule is now the single `taskBlocked` / `taskStatusLabel` / `taskTone` / `taskDotState` pair; and "complete" on a queued task always failed (the host state machine demands a claim first), so the action row now follows the state.
|
||||
- 🎨 **Narrow panes and mobile settings**: type scale, row heights and spacing were re-tuned for the native right sidebar's narrow width (content-box centering, zoom clamping, pan bounds, phase-frame colours, thicker edge stubs, a recomputed card height budget); the settings page gained a **Mobile** group — on a narrow viewport (≤768px) the Tasks page no longer auto-opens and it defaults to the tree view.
|
||||
- 📐 **Baseline**: the DSH pin and support line are **unchanged** (0.1.7-rc.1, peer `^0.1.7-rc.1`). New copy keys (including `teamTaskClaim` / `tasksFoldIdle` / `taskWindowCreate` / `tasksStateProvisioning`) across all 20 dictionaries; design docs: [docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md](docs/plans/2026-09-14-tasks-graph-workflow-teams-design.md), [2026-09-27-tasks-graph-polish.md](docs/plans/2026-09-27-tasks-graph-polish.md), [2026-09-27-floating-window-mobile-settings.md](docs/plans/2026-09-27-floating-window-mobile-settings.md), [2026-09-27-agent-teams-dsh-0.1.7-adaptation.md](docs/plans/2026-09-27-agent-teams-dsh-0.1.7-adaptation.md) (implementation deviations and known limits included).
|
||||
|
||||
### v0.20.0 (dev line, **never published**)
|
||||
|
||||
> 🚫 **This version never reached npm** (npm `latest` is still v0.19.1): it was meant to be the stable release on the 0.1.6-alpha.2 line, but the 0.21 line replaced it before release, so everything below actually shipped in **v0.21.1**. Its original positioning: supports **DSH 0.1.6-alpha.2+** only (peer floor `^0.1.6-alpha.2`, CI pins `@deepseek-ai/dsh@0.1.6-alpha.2`). **This is a breaking-change release**: hosts on 0.1.5-rc.* should pin `dsh-apes-sidebar@0.19.1`. See [docs/plans/2026-09-21-dsh-0.1.6-alpha.2-adaptation.md](docs/plans/2026-09-21-dsh-0.1.6-alpha.2-adaptation.md) for the adaptation record.
|
||||
|
||||
- 🖥️ **Terminal handed to DSH's built-in**: DSH 0.1.6 ships its own right-column terminal (`ui-sidebar-terminal`), so the plugin deleted its whole terminal stack — `pty-manager` / `agent-pty` / `pty-deps` / the 8 `terminal_*` tools / the xterm view and font-linking / cross-session terminal pinning — along with the `node-pty` dependency, its `allowBuilds` grant and the installer's `-Repair` mode. **Note**: the model therefore loses a cross-call persistent terminal (only one-shot `bash` / `pwsh` remain); the upstream `@deepseek-ai/dsh-tool-terminal` is not mounted by any shipped bundle, so enable it yourself in the profile when you need it.
|
||||
- 🌐 **Browser view handed to the built-in**: the plugin deleted `BrowserView` / the sandbox status bar / the embeddability probe (including the host's `browser.probe` route) and the two settings that only configured its own iframe; it **keeps** the external-link takeover in the chat and UI (protocol-based routing is the capability the host does not have), now targeting DSH's built-in `browser` tab type.
|
||||
- 🧩 **The produced-files row goes back to the host**: DSH 0.1.6 turned `conversation.chat.turnTail` from a chain into a list (upstream deliberately lets several plugins **append** rather than replace each other), so the plugin's takeover could only duplicate the built-in deliverables card and was removed wholesale; click behaviour is unchanged (the built-in goes through the host's `openFile` → this plugin's editor). The cost is the one "reveal in the file tree" entry at the turn tail (the in-tree context menu remains).
|
||||
- 🐛 **Two silent breakages fixed**: once `SidebarRightGuideEntry.id` became required, a missing id left the **whole native surface silently empty** (the throw happens inside an `ctx.inject` callback and is swallowed); once `turnTail` became a list the old registration threw outright and the row died. Both are fixed and guarded by tests, with a registration-failure reporting hook added alongside.
|
||||
- 🎨 **The empty-pane card now matches the built-in card recipe**: `.paneCard` adopts DSH's guide-capsule recipe (0.5px l4 border / 24px radius / layer-1 fill / 56px minimum / 14×20 padding), with tests guarding its geometry, its tokenized colours, and that it does not copy upstream's **undefined** `--dsw-alias-bg-l1/-l2` spellings.
|
||||
- 📐 **Baseline**: every `@deepseek-ai/dsh-*` package is pinned to `0.1.6-alpha.2`, the vanished `dsh-code-runtime` package is removed, `diff` / `simple-icons` are hoisted into devDependencies as primitives' new bare imports, and `ui-primitives`' three breakages this release (`IconSendOutline16` gone, `TerminalBlockLabels.noExitCode` added, `ConnectionIndicator.reconnectLabel` removed) are adapted.
|
||||
|
||||
### v0.19.1
|
||||
|
||||
> 📌 **Stable release** (npm `latest`, no prerelease suffix): the pinned baseline moves to **DSH 0.1.5-rc.2** (npm `next`) while the **peer floor stays `^0.1.5-rc.1`** — nothing in the rc.2 upstream delta touches this plugin (zero changes under `packages/api|host|session|agent`; the only real code edits are the message-feedback dialog, deliverables card CSS and the `CodeFileIcon` SVG data split), so rc.1 hosts need no DSH upgrade to run this version. Hosts on DSH 0.1.5-alpha.2 stay on **v0.19.0-alpha.1**; the 0.1.2-rc.1 stable line keeps using **v0.18.1**.
|
||||
|
||||
- 🎯 **Adapted to DSH 0.1.5-rc.2**: devDependency pins, the CI mount lane and `SIDEBAR_SERVICE_VERSION` all move to rc.2; there is **no plugin-side code change** (the upstream delta does not touch this plugin — file-by-file audit in [docs/plans/2026-09-10-dsh-0.1.5-rc.2-adaptation.md](docs/plans/2026-09-10-dsh-0.1.5-rc.2-adaptation.md): the vast majority of the 300 changed files are single-line `version` bumps).
|
||||
- 🎨 **File icons (#611) and the built-in tab glyphs are now colored** (#531 + #594 merged, implementation redone for rc.2):
|
||||
- **Files and folders use DSH's own artwork**: the fallback link is the host's `FileTypeIcon` from `ui-primitives` (48 full-color official code/config glyphs plus the category-colored sheets for markdown, images, PDF, Office, video and folders), so the plugin **ships no extension table and needs no icon chunk** — #429's 563-entry color dataset and `lib/client-file-icons.js` (255 kB) are gone, along with the `fileIconTheme` switch. Color is simply the behaviour now, and the core bundle grew by 10 kB.
|
||||
- **New public `registerFileIcon` API** (capability `'fileIcons'`): register your own glyphs by extension (`exts`), exact file name (`names`) or directory name (`folderNames`), ranked by priority then registration order. **Caveat**: the host classifier covers every path, so a catch-all (`exts: []`) claims every row the specific rules miss.
|
||||
- **Colored built-in tab glyphs**: Files / Changes / Tasks / Side chat / Terminal / Browser and the diff view all get a colored glyph, painted from `--dsw-alias-*` tokens so every skin still applies.
|
||||
- **The native right Sidebar's tab chips carry a glyph too**: the host's tab definition has no icon field, but the `sidebar.right.pane.tab.title` slot *is* the chip's content — the plugin renders `[glyph][title]` there (an editor tab with a path shows that file's icon). The glyph is `aria-hidden`, so the chip's accessible name is unchanged.
|
||||
- 🛠 **CI fix 1: real Windows-lane timeouts**. `ci-windows` went red eight times in the 2026-09-09/10 window; six of those were process-spawning cases hitting vitest's 5000 ms default: `tests/agent-pty.spec.ts` (real PowerShell + ConPTY per terminal) and `tests/install-powershell.spec.ts` (a cold `powershell.exe` start measured 12.1 s) now declare a 30 s budget each, vitest.config.ts raises the global `testTimeout` from its 5000 ms default to **15 s** (the first real Windows run then lost a *third* file to the same default — `tests/git.spec.ts:85` at 9607 ms; three files, one cause, so per-file patches were whack-a-mole), and `waitForTranscript`'s inner poll budget dropped from 5000 ms to 15 s — **the inner budget must sit below the outer one** (they used to be the same 5000 ms, a structurally guaranteed timeout). `ci-windows`'s `Test` step now runs `pnpm test:windows` (`--maxWorkers=2`) so the process-spawning specs stop starving each other on a 2-core runner.
|
||||
- 🛠 **CI fix 2: the mount lane's npm install**. `plugin-mount`'s `npm install -g @deepseek-ai/dsh@<ver>` failed four times (two `ETARGET`, two `JavaScript heap out of memory` / exit 134): the pinned version declares its transitives as floating `^` ranges, so while upstream publishes a prerelease in stages (rc.2 appeared package by package between 14:43 and 14:57 UTC on 09-10) npm resolved a mixed rc.1/rc.2 peer graph (3062 ERESOLVE lines). The lane now pins an **already-complete** rc.2 (fixes ETARGET) and passes `NODE_OPTIONS=--max-old-space-size=4096` (fixes the OOM). `--legacy-peer-deps` was tried on the way and rejected by real CI: it skips exactly the peers a global install must supply, and `@deepseek-ai/cordis-plugin-group` — required at boot by `dsh-app-boot` — is a peer rather than a dependency, so the CLI died with `ERR_MODULE_NOT_FOUND`.
|
||||
|
||||
### v0.19.0
|
||||
|
||||
> 📌 **Stable release** (npm `latest`, no prerelease suffix): supports **DSH 0.1.5-rc.1+** only (peer floor `^0.1.5-rc.1`, CI pins `@deepseek-ai/dsh@0.1.5-rc.1`). Hosts on DSH 0.1.5-alpha.2 should stay on **v0.19.0-alpha.1** (the npm `alpha` tag still points there); the 0.1.2-rc.1 stable line keeps using **v0.18.1**.
|
||||
|
||||
**✨ New**
|
||||
|
||||
- 📝 **The new-tab list gets an optional description back**: DSH 0.1.5-rc.1 restored `SidebarRightGuideEntry.description` (optional), so the plugin restores `TabDescriptor.description` — each of the six built-in types declares a line again (all six guide rows — Files / Changes / Tasks / Side chat / Terminal / Browser — carry one), and the `guideDesc*` keys return to all 20 dictionaries. **The host's native guide renders descriptions only while the listed entries are ≤ 4** (a longer list drops every description rather than truncating), and the plugin contributes six guide entries by default — so in the default composition no description is rendered; they appear once the reader disables enough tab types (per-type switches live on the plugin's settings page) to bring the guide down to four entries or fewer. An entry that declares none stays a single icon + title line (the plugin ships no generic fallback sentence). (#613)
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- None. The rc.1 delta over alpha.2 is small (373 changed files, overwhelmingly upstream package version bumps, native-sidebar preview UI polish, and test snapshots); apart from the restored description field (plus upstream's `files` type now using its own coloured folder glyph as the guide glyph) nothing needed adapting.
|
||||
|
||||
**🧰 CI & internals**
|
||||
|
||||
- Baseline moved to **DSH 0.1.5-rc.1+** (#613): peer floor, devDependency pins, the CI mount lane, and `dsh.plugin.json`'s `engines.dsh` all follow (rc.1 is both the `latest` and `next` dist-tag on npm).
|
||||
- Explicitly unchanged, no need to re-check: the global main-panel model (the `main` slot / `sidebar.panellist` / `ctx.layout` / root-scoped `rightbar` + `rightbar.session`, still not adopted), the file address grammar (`packages/util/workspace-path` only had its version bumped), the native tab-body host contract (`.paneBody` is still a block scroller with a definite height), the core / agent / session / subagent host APIs, and the `ui-primitives` export surface (only CodeBlock rendering changed).
|
||||
- `@deepseek-ai/dsh-client-ui-primitives@0.1.5-rc.1` still declares no `dependencies` while its bundle still bare-imports `anser` / `shiki` / `@shikijs/langs/*` / `mdast-util-*` / `micromark-*` / `katex` — the third-party devDependencies hoisted in the previous release therefore stay, and must not be reverted.
|
||||
- Real-host verification (DSH 0.1.5-rc.1 + plugin 0.19.0): `typecheck` / `lint` / `check:consumer-types` green; unit suite **124 files · 1296 passed · 9 skipped**; `pnpm peers check` clean; the mount smoke lane against real rc.1 **7 passed** (including the tab-body fill assertion and the new "descriptions render only at ≤4 guide entries" assertion); on the local 3080 instance the guide still shows six icon+title capsules (6 > 4, so upstream renders no descriptions — upstream also raised the capsule `min-height` from 48px to 56px), clicking `AGENTS.md` in the file tree lands in the plugin editor (CodeMirror mounted), the side-chat composer sits on the pane floor (host box 962px == pane body 962px, composer 8px off the bottom), the bottom workbench's edges coincide with the centre column, and `pageerror` is 0 (the only console noise is the third-party `dsh-tauri-worktree` `/api/dsh-worktree/attach` 500, unrelated to this change).
|
||||
|
||||
### v0.19.0-alpha.1
|
||||
|
||||
> 🧪 **alpha channel** (npm dist-tag `alpha`, install `dsh-apes-sidebar@latest`): supports **DSH 0.1.5-alpha.2+** only (peer floor `^0.1.5-alpha.2`, CI pins `@deepseek-ai/dsh@0.1.5-alpha.2`). Stay on **v0.19.0-alpha.0** for 0.1.5-alpha.1; the 0.1.2-rc.1 stable line keeps using **v0.18.1** (npm `latest`).
|
||||
|
||||
**✨ New**
|
||||
|
||||
- 🪟 **Every tab body now fills its pane** (#609): the native sidebar's tab-body host is a block scroller with a definite height, not a flex container, and the plugin's tab roots declared only `flex: 1` — so they collapsed to content height and the side-chat composer sat right after the transcript instead of at the pane bottom (a long transcript pushed it out of view). The native adapter now wraps every tab body in a `height: 100%` column flex host, restoring the same fill semantics the bottom workbench has for all plugin tabs, third-party `registerTab` descriptors included.
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🧭 **File address grammar follows DSH 0.1.5-alpha.2**: `fileAddressFor` always produces a session-scoped address and absolute paths keep their leading `/`; `parseFileAddress` is prefix-based and ignores `?`/`#` suffixes.
|
||||
- 🪟 **Center-column anchor follows alpha.2's global panels**: the `conversation` slot became `main.conversation` under the root-scoped keyed `main` slot, so the locator now resolves the new key and skips `display: contents` slot hosts (alpha.1's old key still works).
|
||||
|
||||
**🧰 CI & internals**
|
||||
|
||||
- Baseline moved to DSH 0.1.5-alpha.2 (#609): peer floor, 22 devDependency pins, the CI mount lane, and `dsh.plugin.json` engines all follow; `pnpm peers check` is clean (hoisted the `dsh-session-persistence` transitive peer per §3-9).
|
||||
- **`TabDescriptor.description` removed**: alpha.2's native guide entries no longer render a second line (they are icon + title capsules), so the field and the six `guideDesc*` keys (20 dictionaries) are gone; a new pane's default page is now selected from the registry (exactly one guide entry opens that page directly).
|
||||
|
||||
### v0.19.0-alpha.0
|
||||
|
||||
> 🧪 **alpha channel** (npm dist-tag `alpha`, install `dsh-apes-sidebar@latest`): supports **DSH 0.1.5-alpha.1+** only (peer floor `^0.1.5-alpha.1`, CI pins `@deepseek-ai/dsh@0.1.5-alpha.1`). The 0.1.2-rc.1 stable line keeps using **v0.18.1** (npm `latest`).
|
||||
|
||||
**✨ New**
|
||||
|
||||
- 🖥️ **DSH's native right sidebar** (#604): the right column is now DSH's own sidebar — all seven plugin tab types register as native tab types with native tab bodies; every file open from the chat goes through `ctx.sidebarRight.openResource(dsh-resource://file/…)`; the `editor` type claims file resources at `extension` priority (outranking the built-in text preview) and takes over the built-in Files page kind (restored on unregister); cross-session opens queue until the target session is on screen.
|
||||
- 🧩 **Own right panel and free windows retired** (#605): with the right column handed back to DSH, the plugin keeps only its bottom workbench (single split tree, per-session persistence) and registers its toggle into DSH's session-header utilities slot; the float API (`floats` / `floatTab` / `dockFloat` / `raiseFloat` / the "Move to Free Window" menu entry), the `'floatWindows'` feature string, and the `openByDefault` / `defaultWidthPercent` / `changesDiffFloat` settings are gone (a persisted `floats` field is ignored, so old documents still load).
|
||||
- 🔗 **DSH 0.1.5 host contracts** (#603): the `assistant/chunk` event is gone — live deltas now fold from `agent/assistant-stream` frames (the side chat transcript's `live` field); `sessionPersistence.inspect` is gone — cold reads go through `open(id,'read')`; session headers use `SESSION_FORMAT_VERSION`.
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- Eight session-event read sites follow the 0.1.5 contracts (side-chat transcript, `jobs.output` replay, fork inheritance); custom seeds now carry the fork marker pair, so they no longer inherit the parent's unclaimed inbox input.
|
||||
|
||||
**🧰 CI & internals**
|
||||
|
||||
- The real-host mount smoke lane pins 0.1.5-alpha.1 and sweeps the native sidebar's guide page to open every plugin tab type; typecheck / lint / unit tests / mount lane all green.
|
||||
|
||||
### v0.18.1
|
||||
|
||||
> 📌 **Stable release** (npm `latest`): the DSH baseline is unchanged (**0.1.2-rc.1+**, peer floor `^0.1.2-rc.1`) — this is the incremental cut after v0.18.0: richer op previews in the changes panel, a writable file tree, and five fixes.
|
||||
|
||||
**✨ Features**
|
||||
|
||||
- 📄 **Op-preview upgrades in the changes panel** (#499): markdown reading mode with mermaid fences, plus inline `.html` and `.pdf` render previews; diff syntax highlighting extended to mjs/cjs/mts/cts, CSS/SCSS/Less, HTML/XML/SVG/Vue, GraphQL, JSONC/JSON5; new **secret redaction** layer (on by default for previews, toggleable in the pane header)
|
||||
- 🗂️ **File-tree rename / delete** (#550): inline rename plus confirmed delete, slimmer context menus and viewport-clamped submenus
|
||||
- 🧩 **Plugin catalog names and shell preset text localized** (#535): they now follow the host language
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🔀 **Git diff gap folds really expand** (#576, fixes #577): the fold row promised "n lines… click to expand" but did nothing (`-U3` leaves gap segments without row text); the hidden rows now load on demand through `git.show`, sliced from both sides' contents, with loading / failure states and request deduplication — plus a fix to that route's `rev:path` addressing (it always returned empty before)
|
||||
- 💬 **Side-chat seeds no longer inherit the parent's unclaimed inbox messages** (#562): the seed carries the fork markers, so the "side chat sends earlier User messages first on long contexts" ghost message is gone
|
||||
- 🖼️ **Local images in the split markdown renderer** (#569): rewritten to reachable URLs instead of 404ing
|
||||
|
||||
**🧰 CI and internals**
|
||||
|
||||
- ESLint flat config wired into CI and the Makefile (#536), Makefile command surface (#526), hardened e2e scripts (#527), shared component-test utilities (#524)
|
||||
- Refactors: Sidebar.tsx split by concern (#542), four polling idioms converged onto `use-polling` (#541), rc.7 `__DSH_MODULES__` fallback removed (#540), One Dark/Light syntax palettes single-sourced (#534), duplicated implementations converged and dead code removed (#525)
|
||||
|
||||
### v0.18.0
|
||||
|
||||
> 📌 **Stable release** (npm `latest`): this release targets **DSH 0.1.2-rc.1+ only** (peer floor `^0.1.2-rc.1`); 0.1.0-rc.8 ~ 0.1.1-rc.2 are not supported — stable-DSH users should stay pinned to `dsh-apes-sidebar@0.17.1`, hosts on 0.1.2-alpha.x keep `dsh-apes-sidebar@latest` (v0.18.0-alpha.0).
|
||||
|
||||
All changes since v0.17.1 (the two intermediate version numbers v0.18.1-alpha.0 / v0.19.0-alpha.0 were never published; their content is folded into this release):
|
||||
|
||||
**🔗 Host track graduation**
|
||||
|
||||
- **Onto the DSH 0.1.2 line and graduating to stable**: v0.18.0-alpha.0 dropped the 0.1.1-rc.x compatibility layer and fixed blank Side Chat transcripts on alpha.1+ (#472); the baseline then climbed through alpha.3 (#497) and alpha.5 (#516, `Session.events` → `snapshotEvents()`); the chat file-open funnel moved to `remote.session.openWorkspacePath` (#494) and the "Show in folder" reveal scroll was scoped to the tree body (#453). This release pins the baseline to **DSH 0.1.2-rc.1** (zero source delta vs alpha.5 — a pure version bump; `dsh-client-locale` resumed publishing, everything aligned to rc.1; real-host mount smoke 14/14 in CI)
|
||||
|
||||
**✨ Features**
|
||||
|
||||
- 🌿 **Unified "Changes" tab** (#475): one tab, two lenses — Git (real diff / history / stage·commit·revert / worktree & child-repo selection) and This Session (live tracking of every file the model reads / writes / edits); a unified diff renderer (mod pairing + intra-line character highlights + syntax coloring + context folding), a draggable bottom preview pane, and one-click expansion into a dedicated diff tab
|
||||
- 💬 **Side Chat rendering upgrade** (#486): main-conversation-grade Blocks structure, per-turn usage tails, reconnect banner
|
||||
- ⚙️ **Workspace path fence toggle** (#458): a new `workspaceFence` declarative settings key with a one-click off affordance and guidance on 403 error surfaces
|
||||
|
||||
**⚡ Performance**
|
||||
|
||||
- 🚀 **Core bundle -45%** (#489): 19 non-zh/en dictionary chunks lazy-loaded, render stabilization (transcript row reuse / tree Sets / batched drags), startup & polling cost cuts (single settings fetch, one git process per tick); a new perf measurement lane; also fixes the bottom-pane drag leaking panel width into the host layout and jumping the native left sidebar
|
||||
- 📉 Fewer repeated center-column DOM queries (#456)
|
||||
|
||||
**🐛 Fixes (selection)**
|
||||
|
||||
- ✏️ Editor / Markdown: SSH remote editor links open on the client (#522), reading position kept across preview/edit switches (#467), YAML frontmatter hidden in preview (#394), TOC dismiss-on-outside-click + popover z-index (#461), file basename kept in @-reference links (#417)
|
||||
- 💻 Terminal / platform: Windows custom shell executables resolve (#503), transient resize failures contained (#428), monospace fallback for unresolvable fonts (#366), Linux absolute paths in WSL workspaces (#455), Windows Explorer reveal selection preserved (#508)
|
||||
- 🗂️ Layout / state / file tree / chat: desktop shell layout coexists with the side card (#398), no forced mobile drawer on auto-activation (#373), restored free-window id conflicts (#385), tree auto-refresh on window focus (#469), reference affordance pinned to row tail (#509), selection-popup dismissal + caret-anchored draft insert (#427), collapsed toggle cluster aligned (#361), old-engine scroll-jump compat (#448), worktree listing on older Git (#454)
|
||||
|
||||
**🧰 CI & internals**
|
||||
|
||||
- Windows CI lane (#520), Makefile command surface (#526), e2e script hardening + aggregate double-mount regression (#527), shared component test utils deduplicating boilerplate (#524), duplicate-implementation convergence + dead code removal (#525)
|
||||
|
||||
**🌐 Ecosystem**
|
||||
|
||||
- 10+ new curated plugins: dsh-apes-sidebar-icons (#441), dsh-sidenote (#451, formerly dsh-sidechat #470), dsh-github-workbench (#410), dsh-bilingual-reader (#379), dsh-server-deck (#413), dsh-md-export (#405), dsh-code-nav (#404), dsh-suhuang-scroll (#392), dsh-better-overleaf (#370), and more (each with 18+ language i18n coverage)
|
||||
|
||||
### v0.19.0-alpha.0 (0.1.2-alpha.5 adaptation, unpublished, folded into v0.18.0)
|
||||
|
||||
> 🧪 **Alpha track**: this release targeted **DSH 0.1.2-alpha.x only** (peer floor `^0.1.2-alpha.5`, npm dist-tag `alpha`). This number was never published separately — its content shipped in the **v0.18.0** stable release, and the same number was later reused for the 0.1.5 adaptation line (see above).
|
||||
|
||||
- 🔗 **Adapted to DSH 0.1.2-alpha.5 (published to npm, `alpha` dist-tag)**: the CI mount gate's pin, the `dsh.plugin.json` engines floor, and the `@deepseek-ai/*` peer / devDependencies baseline moved up to 0.1.2-alpha.5 (verified by a real-host mount smoke 14/14). `dsh-client-locale` has no alpha.5 upstream (newest is 0.1.2-alpha.3), so its peer floor / devDep pin trail the baseline and already accept the alpha.5 runtime (`pnpm peers check` reports zero mismatches — no new transitive peers to hoist). The code adapts to alpha.4's compatibility-flagged change — the `Session.events` property was removed, migrated to the on-demand `snapshotEvents()` API (8 sites: sidechat transcript live reads, fork inheritance, `jobs.output` replay, subagent activity), and the `seedLength` meta field the host dropped was removed from thread creation; alpha.4's remaining changes (bidirectional `send_message`, custom-model discovery reusing Profile headers, `SessionSeq`/`SessionLogOffset` strong typing) and alpha.5's upgrade-startup fix were verified not to touch any other plugin surface.
|
||||
|
||||
### v0.18.1-alpha.0
|
||||
|
||||
> 🧪 **Alpha track**: this release targets **DSH 0.1.2-alpha.x only** (peer floor `^0.1.2-alpha.3`, npm dist-tag `alpha`, install `dsh-apes-sidebar@latest`). This version number was never published separately; its content is folded into the **v0.18.0** stable release.
|
||||
|
||||
- 🔗 **Adapted to DSH 0.1.2-alpha.3 (published to npm, `alpha` dist-tag)**: the CI mount gate's pin, the `dsh.plugin.json` engines floor, and the `@deepseek-ai/*` peer / devDependencies baseline moved up to 0.1.2-alpha.3 (verified by a real-host mount smoke 14/14). All 117 commits between alpha.2 and alpha.3 were audited point by point: every host contract this plugin relies on (token auth, slash RPC, `MarkdownText` labels, the event-stream and persistence APIs behind `sidechat.events`, `SettingsNamespaceInput`, `SUBAGENT_DESCRIPTOR_VERSION` (still 3), `dsh-client-store`, the profile loader, the node-pty pin) is unchanged, so no code adaptation was needed; alpha.3's breaking changes (the required `BeginSubmissionInput.mode`, the renamed `attachment-invalid` subagent error, the removed SQLite persistence backend, the identity-gated projection change feed) were all verified not to touch this plugin.
|
||||
|
||||
### v0.18.0-alpha.0
|
||||
|
||||
> 🧪 **Alpha track**: this release targets **DSH 0.1.2-alpha.x only** (peer floor `^0.1.2-alpha.2`, npm dist-tag `alpha`, install `dsh-apes-sidebar@latest`); 0.1.0-rc.8 ~ 0.1.1-rc.2 are no longer supported — stable-DSH users should stay on v0.17.1 (npm `latest`).
|
||||
|
||||
- 🔗 **Adapted to DSH 0.1.2-alpha.2 (published to npm, `alpha` dist-tag)**: the CI mount gate's pin and the `@deepseek-ai/*` devDependencies baseline moved up to it (verified by a real-host mount smoke 14/14). Adaptation points: `dsh-settings` dropped the runtime `settingsNamespace` export (namespaces are now validated at compile time — the host passes the constant directly); the `dsh-subagent` descriptor version went 2→3 (stamped by the host package; the test assertion follows the `SUBAGENT_DESCRIPTOR_VERSION` constant); the restored `SessionEvent.ignorable` and the Remote gateway's unified `RemoteError` wrapping were verified to not affect this plugin.
|
||||
- 🐛 **Fixed blank Side Chat transcripts on DSH 0.1.2-alpha.1+**: transcript polling still called the `ctx.connection.api` face removed in alpha.1 (the error was silently swallowed, so the tab rendered an empty transcript forever). Transcripts now come from the plugin's own `sidechat.events` route (live threads read the in-memory event log, cold threads read session persistence, with `afterSeq` delta pulls, [sidechat-routes.ts](./src/sidechat-routes.ts)).
|
||||
- 🧹 **Dropped the pre-alpha (0.1.1-rc.x and older) compatibility layer**: the e2e host RPC collapsed from the dot/slash dual dialect to slash-only (token URL required, [host-protocol.ts](./tests/e2e/host-protocol.ts)); `MarkdownText` labels collapsed to the nested single shape ([markdown-labels.tsx](./src/client/markdown-labels.tsx), no more dual prop names); peerDependencies / devDependencies / `dsh.client.inject` / the chunk externals allowlist all dropped the defunct `@deepseek-ai/dsh-client-runtime` (four synchronized spots).
|
||||
|
||||
### v0.17.1
|
||||
|
||||
- 🔗 **DSH 0.1.2-alpha.1 adaptation (dual-version compatible)**: fully adapted to DSH 0.1.2-alpha.1's Remote gateway, one-time-token browser authentication, and the `MarkdownText` labels contract change — the plugin works identically on 0.1.0-rc.8 ~ 0.1.1-rc.2 and 0.1.2-alpha.1 (the latter verified by a real-host mount smoke 14/14 against a source build of the GitHub tag; alpha.1 was never published to npm, and the CI pin moved to the npm-published 0.1.2-alpha.2 in v0.18.0-alpha.0). Highlights: all four `MarkdownText` render sites now go through the dual-shape labels helper ([markdown-labels.tsx](./src/client/markdown-labels.tsx)), fixing the `reading 'code'` crash in markdown/mermaid previews on alpha.1; the e2e mount smoke speaks both wire dialects (token-URL cookie exchange, slash `/api` endpoints with parameter-named args, [tests/e2e/host-protocol.ts](./tests/e2e/host-protocol.ts)); dropped the `@deepseek-ai/dsh-client-runtime` peer removed upstream in 0.1.2-alpha.1
|
||||
|
||||
### v0.16.1
|
||||
|
||||
All changes since v0.16.0:
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🧊 **Git panel freeze + restart loop** ([#376](https://github.com/omdsh-dev/DSH-better-sidebar/pull/376), fixes [#369](https://github.com/omdsh-dev/DSH-better-sidebar/issues/369)): opening the Source Control panel could freeze the whole page, and the frozen layout restored itself after every reload with no way out — three unbounded layers compounding, now all bounded: **① status truncation** — the `git status --untracked-files=all` response is capped at 2,000 entries (capped results carry `truncated` and the panel shows a notice, mirroring `fs.read`'s truncation semantics; worktree change counts inherit the bound), so a huge untracked set can no longer freeze the browser main thread; **② bounded repository discovery** — a non-repository cwd (e.g. the home directory) no longer probes every visible child directory serially without limit: probe timeout 30s→5s, at most 200 probed directories, concurrent requests share one in-flight scan plus a 60s TTL cache, ending the `git rev-parse` spawn storm under `~`; **③ reset escape hatch** — loading with `?dsh-sidebar-reset` drops the persisted layout (shared width included) and starts from the default, breaking the loop even when the page is already hung; persisting resumes once the param is gone; `statusTruncated` copy synced across all 19 dictionaries
|
||||
|
||||
### v0.16.0
|
||||
|
||||
All changes since v0.15.2:
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 🪟 **Free windows** ([#354](https://github.com/omdsh-dev/DSH-better-sidebar/pull/354)): drag any tab (built-in or plugin-registered) out of the tab bar **onto the main conversation area** — a dashed drop hint marks the landing zone and release turns it into a floating window (default 390×780, phone-portrait ratio, clamped to the viewport and centered at the drop point). Windows can be moved by dragging the header, resized from the SE corner (≥320×200), raised to the top by clicking anywhere, sent back with the header context menu ("Back to sidebar" / "Close"), and closed with X through the normal `closeTab` lifecycle (pty release etc.). Dragging over a sidebar pane highlights it and releasing **docks** the window back into that pane. `floats` persist per session (restored as-is after refresh, lenient sanitize + geometry clamping). Service semantics: `features` gains `'floatWindows'` — `openTab`'s dedupe/id focus on a floating tab **raises its window** (no duplicate open, no panel expansion), `closeTab` / `activateTab` work on floating tabs and still fire their lifecycle callbacks, `visible` is always true inside a float, and agent-terminal reconcile covers floating windows. Tab content reuses the regular render path; plugin tabs are fully supported. Also ships the 8px-grid spacing cleanup of file sub-pages ([design](docs/plans/2026-08-23-free-window-design.md))
|
||||
- 📂 **Model-driven sidebar opens (`sidebar_open` tool)** ([#353](https://github.com/omdsh-dev/DSH-better-sidebar/pull/353)): a new global setting `agentOpenTools` (**off by default**) injects **one** tool letting the model open local **files** (editor tab, deduped by path), **folders** (full-window tree rooted there, `meta.dir`) and **HTTP(S) pages** (browser tab, URL prefilled) in the caller's sidebar. Turning the setting off unregisters the tool and clears the undelivered queue; already-open tabs stay. Opens targeting non-active sessions queue and replay when visible (`/sidebar/ws/agent-opens` push, same trust fence). No new public API and no new `/sidebar/api` routes ([design](docs/plans/2026-08-23-agent-open-tools-design.md))
|
||||
- 📝 **README-level inline HTML + table of contents (TOC) in Markdown previews** ([#360](https://github.com/omdsh-dev/DSH-better-sidebar/pull/360)): previews now genuinely render **block-level inline HTML** — badge walls `<div align=center>`, `<details>` blocks nesting markdown, `<br/>`/`<sub>`/`<img>` inside table cells, `<video>`/`<picture>` — all sanitized through a DOMPurify allowlist (`<script>` and other active content stripped, `<a>` forced `_blank rel=noopener`), with local media `src` rewritten to the session media route. With ≥3 headings a floating **TOC** button appears: smooth scroll, auto-expanding folded `<details>` ancestors, collecting headings from HTML segments too. The renderer is still the host `MarkdownText` (shiki / KaTeX / GFM preserved); pure-markdown documents take the original path with zero regression ([design](docs/plans/2026-08-24-markdown-html-toc-design.md))
|
||||
- 🌏 **Third-language coverage (19 languages)** ([#339](https://github.com/omdsh-dev/DSH-better-sidebar/pull/339)): optional peer `@huanlin/dsh-plugin-better-locale` adds full dictionaries for ja / de / fr / pt / ko / ar / hi / id / tr / vi / th / ru / it / nl / sv / pl / zh-HK / zh-TW / zh-MO (~340 keys each). Coverage **borrows the DSH English slot**: it is active only while DSH's active locale is `en` and is completely inert under `zh` (no mixed-language UI). The 19 dictionaries are also registered into better-locale so external `ctx.locale.lookup('betterSidebar', key)` callers get the override text. Without better-locale installed `ctx.get('betterLocale')` is undefined and the whole block is a no-op — zh/en behavior is unchanged
|
||||
- 🌿 **Multi-repository Git selection + linked-worktree discovery** ([#326](https://github.com/omdsh-dev/DSH-better-sidebar/pull/326) [#285](https://github.com/omdsh-dev/DSH-better-sidebar/pull/285)): when the session cwd is a workspace container rather than a Git repository, the Git panel discovers direct child repositories and shows a **repository selector** — status / branch / history / diff / stage / commit / revert / cherry-pick / file opening are all threaded through the selected repository. Linked-worktree change discovery and per-worktree git operations are transactionally bound to the selected checkout (including delayed pagination responses), stale/prunable worktree targets are rejected, and individual inventory failures degrade safely
|
||||
- 🖥️ **Browser loopback allowlist** ([#365](https://github.com/omdsh-dev/DSH-better-sidebar/pull/365)): a new side-card setting `browserAllowedLoopback` (comma-separated host or host:port entries; bare hosts match every port, host:port matches exactly) lets the sidebar browser navigate to explicitly trusted local addresses. Allowlisted loopback pages additionally get the `allow-same-origin` iframe sandbox token — local dev servers (Vite etc.) need a real origin for their module/HMR/fetch pipeline and would otherwise render blank. The page stays cross-origin to the GUI and to every other site; the server-side `browser.probe` route mirrors the same allowlist
|
||||
- 📝 **Vue + 28 legacy languages in the editor** ([#202](https://github.com/omdsh-dev/DSH-better-sidebar/pull/202)): `.vue` maps to `@codemirror/lang-vue` (template / script / style dispatch on the `lang` attribute, `<style lang="scss">` preprocessors); zero new dependencies bring scss/sass/less/stylus/ruby/lua/perl/r/dart/scala/groovy/powershell/diff/protobuf/cmake/pug/tcl/haskell/clojure/erlang/julia/pascal/vb/vhdl/stex/objectivecpp via legacy-modes. A throwing language factory degrades to plain text (console.warn) instead of breaking the editor; ambiguous `.v` / `.m` extensions stay unmapped on purpose
|
||||
- 🔄 **Editor preview refresh trio** ([#215](https://github.com/omdsh-dev/DSH-better-sidebar/pull/215) [#228](https://github.com/omdsh-dev/DSH-better-sidebar/pull/228), fixes [#167](https://github.com/omdsh-dev/DSH-better-sidebar/issues/167)): a **manual refresh** button for text previews; switching back to preview after an edit-save auto-reloads (suppressed while dirty so drafts survive); a successful save while in preview mode reloads on the saved edge. Automatic polling and the `fs.stat` version endpoint are gone (zero background API traffic)
|
||||
- 🖼️ **Local / relative images in Markdown** ([#292](https://github.com/omdsh-dev/DSH-better-sidebar/pull/292)): ``, `/cwd/img.png` and reference-style `[id]: url` destinations are rewritten to `/sidebar/file` media URLs (still bounded by the session cwd) — previews no longer show just the alt text
|
||||
- ➕ **New recommended-catalog entry: ego-browser** ([#340](https://github.com/omdsh-dev/DSH-better-sidebar/pull/340)): `@dsh-external/ego-browser` Agent browser tab (registers a sidebar page automatically when better-sidebar is present, falls back to a floating bubble otherwise); description dictionary completed across 19 languages ([#371](https://github.com/omdsh-dev/DSH-better-sidebar/pull/371))
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🛒 **DSH marketplace managed-install compatibility** ([#338](https://github.com/omdsh-dev/DSH-better-sidebar/pull/338)): the public `cordis` entry was removed from `peerDependencies` (the market preview hard-rejects `cordis` in any dependency field — optional does not help), so the npm package satisfies the [dsh-community-market install rules](https://github.com/anywhere-labs/deepseek-harness-desktop/blob/master/dsh-community-market/docs/install-and-uninstall.zh.md) — catalog sources (dshfind / 1024Store) can re-issue the `repository_backlink` verified target and the plugin becomes installable through the Desktop market
|
||||
- 🔤 **Type base migrated to `@deepseek-ai/cordis`** ([#338](https://github.com/omdsh-dev/DSH-better-sidebar/pull/338)): the declaration surface (`src/context-types.ts`) no longer depends on or restates the public `cordis` — `Context` is now an **intersection** of the real vendored cordis Context with the structural service faces, and the `ctx.betterSidebar` type merge lives on `@deepseek-ai/cordis`. **Consumer migration**: change `import type { Context } from 'cordis'` to `import type { Context } from '@deepseek-ai/cordis'` (the `import type {} from 'dsh-apes-sidebar'` merge path is unchanged); plugins that never used that import are unaffected
|
||||
- 🧩 **All internal `ctx.betterSidebar` reads fixed** ([#357](https://github.com/omdsh-dev/DSH-better-sidebar/pull/357), fixes [#356](https://github.com/omdsh-dev/DSH-better-sidebar/issues/356)): on npm-installed DSH 0.1.1-rc.x (web bundle) the sidebar died on every page load (`cannot get property "betterSidebar" without inject`) — the 26 internal direct reads now go through `ctx.get('betterSidebar')` (root reflect-store resolution, immune to the fiber chain); external consumers keep `inject: ['betterSidebar'] + ctx.betterSidebar`
|
||||
- 🔐 **Session-workspace boundary for file APIs** ([#345](https://github.com/omdsh-dev/DSH-better-sidebar/pull/345), fixes [#328](https://github.com/omdsh-dev/DSH-better-sidebar/issues/328)): fixed `fs.tree / fs.read / fs.write` workspace escapes; media, HTML preview and uploads share real-path symlink validation; regression tests for absolute paths, symlinks, uploads and nested Git sessions
|
||||
- 🪟 **Panel-host layering and viewport clipping** ([#330](https://github.com/omdsh-dev/DSH-better-sidebar/pull/330) [#278](https://github.com/omdsh-dev/DSH-better-sidebar/pull/278), fixes [#277](https://github.com/omdsh-dev/DSH-better-sidebar/issues/277)): panel host z-index 40→25 — below the DSH cordis dynamic-plugin layer (30), so the workbench no longer covers cordis inventory/approval surfaces (still above AppFrame's 20 and below the 100+ float stack); `overflow: hidden` on the host clips at the viewport edge, so collapsed panels no longer stretch the document into bidirectional scrolling (measured `scrollWidth` 2289→1672 / `scrollHeight` 1280→1032, any skin)
|
||||
- 📐 **Layout-push hardening** ([#310](https://github.com/omdsh-dev/DSH-better-sidebar/pull/310) [#130](https://github.com/omdsh-dev/DSH-better-sidebar/pull/130) [#180](https://github.com/omdsh-dev/DSH-better-sidebar/pull/180)): conversation column gets `min-height: 0` + `overflow: hidden` + `overflow-wrap: anywhere` (long unbreakable URLs / OAuth links no longer push the composer and left-rail Settings out of the viewport); the layout-push effect is split into "set only" + "remove only on unmount" and the width push is gated on `panelOpen` — dragging the bottom height with the right panel closed no longer squeezes the conversation, and release no longer flashes full-width; `useLayoutEffect` removes cross-paint full-width frames; all three drags flush the final frame and sync `centerRect.right` before committing; bottom height caps at `viewportHeight - PANEL_MIN`; drag handles no longer highlight mid-drag
|
||||
- 📱 **Mobile: unavailable-sidebar clarity + 1px overflow** ([#254](https://github.com/omdsh-dev/DSH-better-sidebar/pull/254)): with no session the toggle uses `aria-disabled` — keeping the non-executable semantics while allowing touch/keyboard focus so the "select a session to use the sidebar" tooltip is reachable; the panel uses `border-box` so mobile `100vw` includes the left border and no longer causes 1px horizontal overflow
|
||||
- 📏 **Side-card width shared across sessions** ([#36](https://github.com/omdsh-dev/DSH-better-sidebar/pull/36)): panel width is a layout preference, not session content — "last drag wins" writes the global `dsh-sidebar:v1:width` key that fresh and cached session switches adopt; without a global key (first run / pre-existing sessions) behavior is byte-for-byte unchanged
|
||||
- 🧹 **Terminals close immediately on session delete** ([#130](https://github.com/omdsh-dev/DSH-better-sidebar/pull/130)): new `PtyManager.closeSession()` subscribing to DSH's `session/disposed` — deleted sessions no longer wait for the 30s reconnect grace to release their terminals (agent terminals are agent-lifecycle-owned and untouched)
|
||||
- 🔍 **Filename search skips noise directories** ([#342](https://github.com/omdsh-dev/DSH-better-sidebar/pull/342)): `node_modules` / `.pnpm-store` / `.yarn` / `.turbo` / `.next` / `dist` / `build` / `coverage` etc. (case-insensitive, `.git` still skipped) — huge dependency trees no longer burn the 100k visit budget and return `truncated` early, so real files in later directories (`docs/`) are found; no `.gitignore` semantics, still filename lookup
|
||||
- 📝 **Mermaid global error rendering suppressed** ([#341](https://github.com/omdsh-dev/DSH-better-sidebar/pull/341)): `suppressErrorRendering` enabled — invalid diagrams no longer inject a large error SVG into `document.body`; the component-level error fallback and source display remain
|
||||
- 🖥️ **Terminal Nerd Font icon fallback** ([#190](https://github.com/omdsh-dev/DSH-better-sidebar/pull/190)): supplementary-plane PUA icons in starship / powerlevel10k prompts (Nerd Fonts v3 Material icon set) no longer render as tofu — `withIconFontFallbacks()` appends Nerd Font icon families to the winning base font (before the first generic family, deduped by family name, CSS global keywords filtered, no color-emoji fonts)
|
||||
- 🌐 **HTML preview declares UTF-8** ([#193](https://github.com/omdsh-dev/DSH-better-sidebar/pull/193), fixes [#170](https://github.com/omdsh-dev/DSH-better-sidebar/issues/170)): `/sidebar/html` responses carry `charset=utf-8` (Chinese fragments without `<meta charset>` no longer mojibake); original file bytes preserved
|
||||
- 🧪 **trust-fence Origin compared by hostname** ([#182](https://github.com/omdsh-dev/DSH-better-sidebar/pull/182)): some Chromium builds (Edge 151) serialize the Origin of non-default-port loopback pages without the port — `http://127.0.0.1` against `Host: 127.0.0.1:3080` no longer 403s (mirrors DSH's official gateway fence); different hostnames and opaque null origins are still rejected
|
||||
- 🪟 **"Show in folder" now reveals in the explorer** ([#94](https://github.com/omdsh-dev/DSH-better-sidebar/pull/94)): the folder-reveal gesture no longer opens the directory as a file in the editor (`"..." is a directory`) — `revealInExplorer` switches to the explorer tab, auto-expands a collapsed panel, expands the parent folder and highlights/scrolls to the produced file; the produced-files selector now reads the engine's Turn deliverables (same source as ui-deliverables)
|
||||
- 🖱️ **Panel-drag layout flash fixed** ([#180](https://github.com/omdsh-dev/DSH-better-sidebar/pull/180)): dragging the bottom height with the right panel closed no longer shifts the conversation left; release no longer flashes full-width before snapping back
|
||||
- 🖥️ **PowerShell installer fixes** ([#47](https://github.com/omdsh-dev/DSH-better-sidebar/pull/47)): the remote entry is now "download the script → strip the UTF-8 BOM → run it in memory", so `-Version` / `-DryRun` work again under Windows PowerShell 5.1 (the BOM no longer eats the leading `param(...)`); `pnpm --version` is checked before touching the profile (major <10 fails with a clear error and exit code 1)
|
||||
- 🔄 **Browser embed probing GET fallback** ([#69](https://github.com/omdsh-dev/DSH-better-sidebar/pull/69)): when a HEAD response omits both `Content-Security-Policy` and `X-Frame-Options`, probe retries once with GET — sites like Alibaba Bailian that only advertise their embed policy on GET no longer show a misleading "refused to connect"; they get the friendly "site refuses embedding" panel with an "open in browser" button
|
||||
- 🔧 **git-source installs fixed via `unrun` devDependency** ([#336](https://github.com/omdsh-dev/DSH-better-sidebar/pull/336)): tsdown 0.22 loads its config through `unrun` and pnpm 11 does not auto-install peer deps — git-hosted `prepare` no longer fails with `Failed to import module "unrun"` (npm tarballs unaffected)
|
||||
- 🍃 **Strict `ctx.effect` also fixed 4 latent sites**: interception / IME-guard effect bodies returned `undefined` on failure instead of a disposer (invalid shape under the vendored cordis effect contract) — now a no-op disposer
|
||||
|
||||
<details>
|
||||
<summary><b>Older releases (v0.12.0 – v0.15.2)</b></summary>
|
||||
|
||||
### v0.15.2
|
||||
|
||||
All changes since v0.15.1:
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 🗂️ **"Open in app" submenu in the file tree** ([#334](https://github.com/omdsh-dev/DSH-better-sidebar/pull/334)): the file-tree context menu gains an "Open in app >" submenu — built-in openers (reveal/select in file manager, VS Code, Cursor, Zed), each with a pin button that promotes it to a top-level context-menu item (click again to unpin); with an optional SSH host configured, VSCode-family entries switch to the `vscode-remote/ssh-remote+<host>/<path>` protocol and local-only entries hide automatically; custom editors supported (name + URL template with `{path}` + "VSCode-family" flag, configured in the Files card gear popup). Open actions go through the new host route `POST /sidebar/api/open.external` (argv-array spawn, no shell injection) ([design](docs/plans/2026-08-22-open-with-menu-design.md))
|
||||
- 📑 **Tab context menu** ([#331](https://github.com/omdsh-dev/DSH-better-sidebar/pull/331)): right-clicking a tab offers **Close / Close others / Close to the left / Close to the right**, scoped to the current pane (tab group); items grey out when there is nothing to close; the menu only opens — it never switches the active tab; bulk closes go through the existing per-tab `onClose` path so lifecycle callbacks, pty release and agent-terminal shutdown all stay intact
|
||||
- 📄 **Diff files collapsed by default** ([#270](https://github.com/omdsh-dev/DSH-better-sidebar/pull/270)): each changed-file header is now an accessible expand/collapse control; recognized source files expand by default while tests, docs, generated files, lockfiles and unknown types stay collapsed; the existing 500-row cap for expanded content is preserved
|
||||
- 📖 **README update**: feature tour converted to a table (two screenshots per row, saves space); community section now shows the WeChat group and QQ group QR codes ([#325](https://github.com/omdsh-dev/DSH-better-sidebar/pull/325), QQ group 577011007)
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🪟 **Prune empty restored panes** ([#268](https://github.com/omdsh-dev/DSH-better-sidebar/pull/268)): persisted split panes left empty when ephemeral diff tabs were dropped no longer survive as full-size blanks — `sanitizeState` now also prunes the empty split leaf and repairs the stale active-pane pointer; a fully empty workbench keeps its single empty pane
|
||||
- 🖥️ **Hide spawned git windows on Windows** ([#301](https://github.com/omdsh-dev/DSH-better-sidebar/pull/301), fixes [#124](https://github.com/omdsh-dev/DSH-better-sidebar/issues/124)): the shared `runGit()` spawn options now set `windowsHide: true`, so repo-status polling and git actions no longer flash console windows on Windows (no behavior change on other platforms)
|
||||
- 📁 **Untracked files inside new folders** ([#242](https://github.com/omdsh-dev/DSH-better-sidebar/pull/242)): `git status` switched from `--untracked-files=normal` to `--untracked-files=all` — each file inside a new folder now shows as its own row and its diff can be loaded (no more `fs.read` "is a directory" error), matching VSCode's default behavior
|
||||
- ⚡ **Per-frame React re-renders eliminated for toggles/drags** (closes [#315](https://github.com/omdsh-dev/DSH-better-sidebar/issues/315)): `centerRect` moved to a ref with direct DOM writes to the bottom bar (zero React renders); `TabContent` memoized with an explicit comparator; a new frame-batcher coalesces Divider/dock drags per frame; meaningless locate passes skipped during drags. 4x CPU-throttled A/B: >17ms frames on toggle collapse 19→6 / expand 24→4~6, p95 21ms→15ms; drags unchanged (non-regression)
|
||||
|
||||
### v0.15.1
|
||||
|
||||
All changes since v0.15.0:
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 💬 **Codex-style transcript rework for Side Chat** ([#314](https://github.com/omdsh-dev/DSH-better-sidebar/pull/314)): transcript rows became **collapsible** — tool calls, thinking and context injections share one quiet single-line chrome (chevron + label + one-line argument summary) that expands into an indented body on a hairline thread, no cards or fills; streaming labels and a creating hero shimmer (shimmer = generating), failed tools go danger, `prefers-reduced-motion` stills every loop; **the first question is no longer swallowed by the boundary prompt** — context injection and first contact are delivered as separate events (boundary + parked snapshot ride `agent.inject`, the question wakes the driver), so the transcript maps injections onto a collapsible injection row while genuine user messages — the first one included — render as user bubbles; legacy threads' first message is split out into its own bubble too
|
||||
- 📖 **README rewrite**: feature tour (real UI screenshots per feature), user-facing DSH compatibility badges, simplified install (`add` → `approve-builds` → `add`, node-pty-safe build, paste-to-DSH install prompt), 28+ plugin ecosystem with per-category collapsed listings
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🖥️ **Terminal pty stays alive across conversation switches** ([#323](https://github.com/omdsh-dev/DSH-better-sidebar/pull/323)): switching sessions is no longer treated as a transient drop — the client sends a `park` control frame on unmount and the host skips the 30s reconnect-grace countdown; switching back (`open()` cancels parked) or explicitly closing the tab resumes the normal lifecycle; agent terminals keep their indefinite lifetime
|
||||
- 📂 **File-tree upload overlay no longer intercepts Tab drags** ([#317](https://github.com/omdsh-dev/DSH-better-sidebar/pull/317)): dragging tabs (reorder / cross-pane split) across the explorer no longer shows the upload overlay or swallows the event — gated on `dataTransfer.types` containing `Files` (consistent with the panel-host shield), so tabs land normally; OS file drags behave as before
|
||||
- 💬 **Subagent auto-open debounced** ([#314](https://github.com/omdsh-dev/DSH-better-sidebar/pull/314)): Side Chat thread creation no longer pops the task page — the 0→N trigger rearms for 500ms and re-evaluates the original baseline against the live snapshot, by which time the title filter recognizes the thread; genuine subagents still auto-activate the Tasks page (wide viewports expand the sidebar, while narrow viewports prepare the tab without forcing the drawer open)
|
||||
|
||||
### v0.15.0
|
||||
|
||||
All changes since v0.14.0:
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 💬 **Side Chat (beta) tab** ([#286](https://github.com/omdsh-dev/DSH-better-sidebar/pull/286)): Codex-style side threads, **one independent tab per conversation** — the child inherits the parent's full context (completed turns + pending messages + the in-progress turn's assistant output and tool activity, honestly frozen with an "interrupted" marker); created with an identical composition (same preset / provider / model) so the first request reuses the parent's input prefix cache; threads stay invisible in the main session list with zero subagent-catalog noise; follow-ups survive DSH restarts (auto cold-resume); one-click "Save as new session" promotes the thread to a top-level session ([design](docs/plans/2026-08-20-sidechat-tab-design.md))
|
||||
- 📤 **Upload into the files window** ([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)): header "upload file / upload folder" buttons plus drag-drop (drop on the tree body = workspace root, on a directory row = that directory, on a file row = its parent directory, VSCode semantics); full-window blurred progress overlay while uploading (per-file progress + cancel / Esc); buttons disabled while busy, tree refreshes after the upload settles
|
||||
- 🧩 **Desktop compatibility in four options** ([#284](https://github.com/omdsh-dev/DSH-better-sidebar/pull/284)): "Position compatibility mode" is now a main-row dropdown — **Auto-detect** (default, conservative: only the standard Window Controls Overlay geometry contributes; real 32/36px caption-overlay heights per shell, live on maximize/restore; zero modification on plain web) / **DSH official web** (explicitly no adaptation) / **Shell preset** (built-in, opt-in; only shells that appeared in this repo's issues/PRs with 100+ stars, "detected" badge when the environment matches) / **Custom** (free-form CSS + shift distance). Documents that already carried compatibility values migrate to the custom scheme; interactive chrome opts out of desktop drag regions (`no-drag`); the bottom-push anchor is a composite selector (`[data-pane]` and `:has(> [data-slot])`)
|
||||
- 🎛️ **Settings page UI/UX modernization** ([#300](https://github.com/omdsh-dev/DSH-better-sidebar/pull/300)): the side-card secondary-settings entry is now a full-width "Feature settings" strip at the card bottom (replacing the invisible corner gear — much easier to discover); coordinated two-tone enabled state (brand activation accent + success-green check badge); every color is still `--dsw-alias-*` token-derived so skins follow automatically
|
||||
- ➕ **New entries in the recommended-plugin catalog**: `dsh-docs-panel` (global docs, [#230](https://github.com/omdsh-dev/DSH-better-sidebar/pull/230)), `dsh-flowglass` ([#261](https://github.com/omdsh-dev/DSH-better-sidebar/pull/261)), `dsh-git-forge` and `dsh-ssh-tunnel` ([#204](https://github.com/omdsh-dev/DSH-better-sidebar/pull/204)), `dsh-turn-review` ([#102](https://github.com/omdsh-dev/DSH-better-sidebar/pull/102))
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- ⚡ **Batched live preview for the subagent page** ([#298](https://github.com/omdsh-dev/DSH-better-sidebar/pull/298)): the old implementation polled `subagents.history` per running subagent, each poll triggering a full host-side enumeration — an O(N²) amplification that stalled the page with many concurrent subagents; now a single batch route `subagents.live` (one enumeration of the whole tree) plus one client poller with a single in-flight request; display logic and copy unchanged
|
||||
- 🖱️ **Interrupted / fast-release drags no longer roll back** ([#249](https://github.com/omdsh-dev/DSH-better-sidebar/pull/249), closes [#247](https://github.com/omdsh-dev/DSH-better-sidebar/issues/247) [#248](https://github.com/omdsh-dev/DSH-better-sidebar/issues/248)): interrupted or fast-released drags commit the last known position; HMR re-activation re-locates the center column (fixes the blank bottom panel after a hot reload)
|
||||
- 📐 **Push variables stay effective while mounted** ([#259](https://github.com/omdsh-dev/DSH-better-sidebar/pull/259), fixes [#258](https://github.com/omdsh-dev/DSH-better-sidebar/issues/258)): the bottom panel no longer flashes full-width after a drag is released
|
||||
- 🔐 **Workspace boundary hardening for file APIs** ([#328](https://github.com/omdsh-dev/DSH-better-sidebar/issues/328)): `fs.tree/read/write`, media, HTML preview, and uploads now share real-path containment checks that reject outside absolute paths and symlink escapes
|
||||
- 🔧 **Adapted to DSH 0.1.1-rc.1 / rc.2 (@next)** ([#297](https://github.com/omdsh-dev/DSH-better-sidebar/pull/297) [#305](https://github.com/omdsh-dev/DSH-better-sidebar/pull/305)): no code changes needed
|
||||
- 🔒 **Upload-chain hardening** ([#239](https://github.com/omdsh-dev/DSH-better-sidebar/pull/239)): empty and absolute `relativePath` segments are refused outright; uniquely named temp files (concurrent uploads stay independent, crashed processes never block later uploads); write-stream error listeners (a failing disk can no longer crash the host); client error codes unified with the wire (`too-large`), 413s localized
|
||||
|
||||
### v0.14.0
|
||||
|
||||
> ⚠️ This release requires DSH ≥ 0.1.0-rc.8. All changes since v0.13.1:
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 🖼️ **Unified panel-host injection refactor** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): panels/toggle clusters moved into a `[data-dsh-panel-host]` fixed containing block (`fixed inset-0 z-40`), immune to desktop-shell intermediate transforms hijacking `fixed`; mount self-check (page-level transform → `data-dsh-panel-host-degraded` degraded sync, judged on uncorrected geometry, exits only when the ancestor transform is gone); push anchor switched to `#root [data-dsh-frame] > [data-pane="conversation"]` + `#root` calc width against desktop-shell additive overflow; chunk revalidation on activation (HEAD+ETag keeps unchanged chunks, 5s timeout fails open); `visualViewport` keyboard inset + `env(safe-area-inset-*)` mobile adaptation
|
||||
- 📂 **Separate file windows by default** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): `editorExplorer` now defaults to **separate** — tree clicks / file opens create a new tab per path and the path-less window is a pure file manager; merged mode stays available as an opt-in
|
||||
- 🖥️ **Terminal shell / shellArgs configurable from the settings page** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): the terminal card's gear popup gains "Shell path" and "Shell arguments" rows (previously yaml-only via `cordis.patch.yml`) — saved values take effect immediately for terminals opened afterwards (UI terminals and model `terminal_create` alike); empty keeps the existing yaml → `$SHELL` / login shell / `powershell.exe` resolution order
|
||||
- 🏷️ **Version badge on the settings page** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): the side-card settings section now opens with a `DSH-better-sidebar v0.14.0` identity badge (version synced with the service instance, test-guarded)
|
||||
- 🔍 **Add-plugin catalog: search / grouping / independent scroll** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): built for a growing plugin ecosystem — a live search box (filters by name / id / description), optional `category` grouping for entries, and an independently scrolling list (the modal no longer grows unbounded with catalog size)
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🧩 **rc.8 module-system migration** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): rc.8 no longer exposes the `window.__DSH_MODULES__` page global (it moved to the `ctx.modules` service), which broke every lazy chunk's externals resolution — the client now injects the `modules` service and shares it with chunk-bundle copies through a plugin-owned global (terminal / editor / Mermaid on-demand loading restored)
|
||||
- 🧩 **Chunk revalidation barrier hardening** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): HEAD revalidation gains a 5s timeout (fails open on a stuck route so the barrier can never wedge lazy loads); `resetChunks` clears a pending revalidation barrier
|
||||
- 🖱️ **Drag robustness** ([#232](https://github.com/omdsh-dev/DSH-better-sidebar/pull/232)): fast releases (browsers merge/lose pointermove bursts) commit the last known dragged position instead of rolling back; `pointercancel` / lost-capture interruptions keep the drag result too; the center column is re-measured right after commit (no mid-frame bottom-panel width jump); HMR re-activation re-locates the center column via an `<html>` style observer plus a retry when the bottom panel opens (fixes the blank bottom panel / shifted input bar after a hot reload)
|
||||
|
||||
### v0.13.1
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 📊 **Safe Mermaid rendering in the Markdown preview** ([#164](https://github.com/omdsh-dev/DSH-better-sidebar/pull/164)): when a previewed md file contains mermaid fences, a `client-mermaid.js` chunk (~7MB) is served on demand (zero load without mermaid); defense-in-depth rendering — `securityLevel: 'strict'` + `htmlLabels: false` (node labels use real SVG `<text>`) + a second sanitize pass before SVG injection (foreignObject/script/foreign HTML elements removed, `@*`/`on*`/`href` attributes stripped); click a diagram to zoom in a modal overlay (wheel zoom centered on the cursor, drag pan, toolbar & shortcuts), re-renders with light/dark theme, falls back to the raw code block on parse failure
|
||||
- 🖥️ **Configurable terminal shell & shellArgs** ([#125](https://github.com/omdsh-dev/DSH-better-sidebar/pull/125)): `cordis.patch.yml` `better-sidebar.config` can set `shell` / `shellArgs` (a non-empty `shellArgs` fully replaces the defaults; unset keeps the previous auto-resolution of `$SHELL` / login shell / `powershell.exe`), applied to both UI terminals and agent terminals (`terminal_create`); terminal tab titles now show the shell name (bash/zsh/powershell) and internal tab ids use UUIDs so the same shell can open multiple terminals
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🔗 **Aggregate double-mount auto-yield** ([#200](https://github.com/omdsh-dev/DSH-better-sidebar/pull/200)): when an aggregate package (e.g. dsh-web-ui-all) mounts the same package under its own entry id, the guard expression in `cordis.patch.yml` disables the plugin's own `better-sidebar` row so `/sidebar/api` is no longer registered twice (`duplicate prefix route` crashing the whole plugin tree / `dsh web`); standalone installs behave as before
|
||||
- 🔧 **Adapted to DSH 0.1.0-rc.7** ([#207](https://github.com/omdsh-dev/DSH-better-sidebar/pull/207), fixes [#206](https://github.com/omdsh-dev/DSH-better-sidebar/issues/206)): fixes the `agent-presets: refusing to compose an unscoped context` error when picking a model / sending a message after DSH moved to rc.7
|
||||
|
||||
### v0.13.0
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 📁 **Files window merged with the explorer** ([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)): new `editorExplorer` setting (editor card gear) — file tabs gain a path-input header plus a toggleable right-docked file tree (per-tab open/width memory, drag-resize 160–480px from the left edge, global filename search via the host `fs.search` route with a hard budget, skipping `.git` and symlink dirs); in separate mode (default) tree clicks / Enter in the path input open each file **in its own new tab**, merged mode switches the current tab **in place**; fresh sessions seed an empty Files window instead of the explorer tab, and a path-less window is a bare file manager in separate mode / a chrome'd empty file window in merged mode; the tree context menu offers "Open in new tab" and "Open to the side" (split)
|
||||
- 🎛️ **Select rows for declarative settings** ([#151](https://github.com/omdsh-dev/DSH-better-sidebar/pull/151)): settings rows gain `type: 'select'` (`options` with value/title/desc/icon, `multi` stores the picked values as an array); options with icons render big-icon option cards and keep the icon in the closed anchor; `editorExplorer` became an iconed select (merged vs separate); the capability list gained `settingSelect`
|
||||
- 🔀 **Mutual exclusion with the dsh-web-ui family right panel** ([#181](https://github.com/omdsh-dev/DSH-better-sidebar/pull/181)): reads the `aionui-panel` settings namespace's provider choice — when "Use aionui-panel" is selected, the whole better-sidebar (right sidebar / bottom panel / floating entry / all takeovers) does not mount; with DSH-better-sidebar (or no aionui installed) it behaves as before. Takes effect live after a settings save (settings-document push), no reload needed
|
||||
|
||||
### v0.12.3
|
||||
|
||||
**✨ New features**
|
||||
|
||||
- 🎨 **Skin compatibility (token-driven)**: fully consumes DSH design tokens and follows the dsh-web-ui skin center's 10 skins automatically; terminal/editor surfaces fall back to opaque backgrounds under transparent/translucent-glass token values so text never scrolls over the skin art ([#110](https://github.com/omdsh-dev/DSH-better-sidebar/pull/110), fixes #106 #105 #90 #60, also #52 #57 #92)
|
||||
- 🗂️ **Unified path handling**: UNC / symlink classification (directory symlinks expandable, broken links highlighted) + HTML-route platform guards ([#134](https://github.com/omdsh-dev/DSH-better-sidebar/pull/134), #65 #67 #43 #79 #115)
|
||||
- 🖥️ **Configurable terminal shell**: custom shell setting with Windows pwsh auto-probe ([#95](https://github.com/omdsh-dev/DSH-better-sidebar/pull/95))
|
||||
- 📝 **Editor languages**: C# / Kotlin / Swift syntax highlighting ([#120](https://github.com/omdsh-dev/DSH-better-sidebar/pull/120))
|
||||
- 🧭 **Settings nav icon**: settings-page navigation icon and layout polish ([#114](https://github.com/omdsh-dev/DSH-better-sidebar/pull/114))
|
||||
- ➕ **Recommended-plugin catalog**: added `dsh-git-remotes` — Git Remotes tab (branches/upstream/ahead-behind, fetch with prune, ff-only pull, confirm-before-push; does not replace the built-in stage/commit tab) ([#91](https://github.com/omdsh-dev/DSH-better-sidebar/pull/91)); and `dsh-video-preview` — inline video preview (.mp4/.webm/.mov/.mkv/.avi etc.) backed by a /video host route with HTTP Range (206) scrubbing, not capped by the 20MB mediaLimit ([#126](https://github.com/omdsh-dev/DSH-better-sidebar/pull/126))
|
||||
|
||||
**🐛 Fixes**
|
||||
|
||||
- 🔧 **xterm migration**: deprecated xterm dependency migrated to `@xterm/xterm` (Closes [#122](https://github.com/omdsh-dev/DSH-better-sidebar/issues/122), [#128](https://github.com/omdsh-dev/DSH-better-sidebar/pull/128))
|
||||
- 📝 **Markdown editor**: selection-to-conversation popup restored ([#24](https://github.com/omdsh-dev/DSH-better-sidebar/pull/24))
|
||||
- 🖼️ **Markdown preview renders local/relative images**: image destinations that point at local files (relative/absolute paths, reference-style `[id]: url`) are rewritten to `/sidebar/file` media URLs and displayed in the preview (previously only absolute http(s) images rendered; relative paths showed just the alt text)
|
||||
- 🐛 **node-pty load failure no longer crashes the server** ([#140](https://github.com/omdsh-dev/DSH-better-sidebar/issues/140)): the host half now lazy-loads node-pty — when it is missing the plugin still mounts, the terminal shows a repair banner (copyable command + Retry button), and agent terminal tools are skipped
|
||||
- 🧪 Test engineering: unit spec split (#141) + flaky smoke cleanup fix
|
||||
|
||||
</details>
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 dsh-external
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,44 +0,0 @@
|
||||
# dsh-apes-sidebar 命令面薄封装:目标仅转发 package.json scripts(唯一事实源),
|
||||
# 新增/改名 script 不需要动这里;本文件只补目标发现(help)与 CI 门禁聚合
|
||||
# (check / mount / mount-aggregate / registry)。
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
help: ## 列出所有可用目标
|
||||
@awk -F':.*## ' '/^[a-zA-Z0-9_-]+:.*## / {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
install: ## 安装依赖(pnpm install)
|
||||
pnpm install
|
||||
|
||||
build: ## 构建(清 lib/ → tsc → tsdown)
|
||||
pnpm build
|
||||
|
||||
typecheck: ## 类型检查(tsc --noEmit)
|
||||
pnpm typecheck
|
||||
|
||||
lint: ## 代码检查(eslint .,flat config 最小规则集)
|
||||
pnpm lint
|
||||
|
||||
test: ## 单元测试(vitest run)
|
||||
pnpm test
|
||||
|
||||
check: ## 聚合校验门禁:typecheck → lint → build → test → check:consumer-types(对齐 CI)
|
||||
pnpm typecheck && pnpm lint && pnpm build && pnpm test && pnpm check:consumer-types
|
||||
|
||||
clean: ## 清理构建产物与测试报告(lib/、*.tgz、playwright-report/、test-results/)
|
||||
rm -rf lib playwright-report test-results
|
||||
rm -f *.tgz
|
||||
|
||||
pack: ## 打 npm tarball(pnpm pack)
|
||||
pnpm pack
|
||||
|
||||
mount: ## 真机挂载冒烟:build + pack → Playwright Chromium → pnpm test:mount
|
||||
pnpm build && pnpm pack && pnpm exec playwright install chromium && pnpm test:mount
|
||||
|
||||
mount-aggregate: ## 聚合双挂载回归:build + pack → pnpm test:mount:aggregate
|
||||
pnpm build && pnpm pack && pnpm test:mount:aggregate
|
||||
|
||||
registry: ## 组装 plugin-registry 暂存(registry/,不入库):build → node scripts/package-registry.mjs
|
||||
pnpm build && node scripts/package-registry.mjs
|
||||
|
||||
.PHONY: help install build typecheck lint test check clean pack mount mount-aggregate registry
|
||||
@@ -1,437 +0,0 @@
|
||||
# dsh-apes-sidebar
|
||||
|
||||
> [!NOTE]
|
||||
> **Fork notice** — 本项目是 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 的 fork,
|
||||
> 基线 **upstream v0.24.1**(commit `a2751cf`,MIT)。
|
||||
>
|
||||
> - **本 fork 的增量**:`src/client/apes/` 下的 3 个信息层 tab(`apes:agents` 智能体 / `apes:apps` 应用 /
|
||||
> `apes:profile` 画像),按 DSH 标准 `TabDescriptor` 接口注册,自动获得上游原生适配层的全部待遇
|
||||
> (进原生 guide、tab 芯片图标、开合与分栏)。
|
||||
> - **宿主版本要求(与上游 v0.24.1 一致)**:DSH **0.2.0-rc.1+**(peer `^0.2.0-rc.1`)。装在更旧的宿主上会被
|
||||
> 启动兼容性预检判定失败、**整行静默禁用**(既不报错也不加载)。
|
||||
> - **自 v0.19.1 起的上游变化**:终端 / 浏览器交给宿主内置(DSH 0.1.6 起自带右列终端与浏览器)、工作区路径围栏取消
|
||||
> (文件读写按进程权限)、右侧栏面板走 DSH 原生右列。
|
||||
> - 版本线独立于上游:`1.3.0`(fork)。以下文档主体沿用上游 v0.24.1 原文,包名已替换为本 fork 名。
|
||||
> - **📚 项目百科**(需求分析 → 系统架构 → 功能说明 → 部署指南 → 运维手册 → 故障排查 → 开发与测试):
|
||||
> <https://gitea.apescale.com/figmar/apes-sidebar/wiki> 源文件在仓库 `docs/wiki/`,
|
||||
> 改完用 `bash scripts/push-wiki.sh` 一条命令发布。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **v0.19.0 起接入 DSH 原生侧边栏**:右列就是 DSH 自己的右侧栏,插件把每个 tab 类型注册为原生 tab(不再自绘右侧面板),只保留自绘的底部工作台与开放给所有插件的 `ctx.betterSidebar` 服务。
|
||||
>
|
||||
> **v0.24.1 起要求 DSH `0.2.0-rc.1+`**(peer 下限 `^0.2.0-rc.1`)。0.2.0 对本插件所用的全部宿主 API 是**纯增量**(零导出删除、会话格式仍 v4、CLI 与客户端运行时未变),所以这一版没有运行时兼容分支,只把支持线整体前移。**DSH 0.1.7 线的用户请固定 `dsh-apes-sidebar@0.22.1`——caret 范围跨 minor 不成立,`^0.1.7-rc.1` 在 0.2.0 宿主上会被启动预检静默禁用**;按 DSH 版本选插件版本的对照表见[安装](#-安装)。
|
||||
|
||||
<!-- Hero -->
|
||||
<div align="center">
|
||||
<b style="font-size: 1.15em;">一个服务化的侧边栏框架,一套开箱即用的完整工作台</b><br /><br />
|
||||
<a href="https://www.npmjs.com/package/dsh-apes-sidebar"><img alt="npm version" src="https://img.shields.io/npm/v/dsh-apes-sidebar" /></a>
|
||||
<a href="https://www.npmjs.com/package/dsh-apes-sidebar"><img alt="npm downloads" src="https://img.shields.io/npm/dm/dsh-apes-sidebar" /></a>
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/omdsh-dev/DSH-better-sidebar/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/omdsh-dev/DSH-better-sidebar" /></a>
|
||||
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
|
||||
<a href="https://dshfind.com/zh/plugins/omdsh-dev/DSH-better-sidebar?ref=badge"><img alt="dshfind" src="https://dshfind.com/api/badge/omdsh-dev/DSH-better-sidebar?lang=zh" /></a><br /><br />
|
||||
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本(v0.24.1):0.2.0-rc.1+" src="https://img.shields.io/badge/DSH-0.2.0--rc.1%2B-4d6bfe" /></a>
|
||||
<a href="https://github.com/topics/dsh-apes-sidebar"><img alt="插件生态:GitHub topic dsh-apes-sidebar" src="https://img.shields.io/badge/%E6%8F%92%E4%BB%B6%E7%94%9F%E6%80%81-topic%20dsh--better--sidebar-4d6bfe" /></a><br /><br />
|
||||
<img alt="文件管理" src="https://img.shields.io/badge/-文件管理-4d6bfe" /> <img alt="编辑预览" src="https://img.shields.io/badge/-编辑预览-4d6bfe" /> <img alt="底部工作台" src="https://img.shields.io/badge/-底部工作台-4d6bfe" /> <img alt="文件变动" src="https://img.shields.io/badge/-文件变动-4d6bfe" /> <img alt="后台任务" src="https://img.shields.io/badge/-后台任务-4d6bfe" /> <img alt="侧边对话" src="https://img.shields.io/badge/-侧边对话-4d6bfe" /> <img alt="插件接入" src="https://img.shields.io/badge/-插件接入-4d6bfe" /><br /><br />
|
||||
<b>右侧栏 + 底部面板双工作台</b>,并把 <code>ctx.betterSidebar</code> 服务开放给所有插件——<br />
|
||||
通过 <code>registerTab</code> / <code>registerFileViewer</code> 注册新的侧边栏页面与文件预览器。
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
🌏 <a href="./README.md"><b>中文</b></a> · <a href="./README_EN.md">English</a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img alt="dsh-apes-sidebar 工作台截图" src="https://github.com/user-attachments/assets/991c4b70-d45a-461f-a8c8-0a28b4218e60" />
|
||||
<video src="https://github.com/user-attachments/assets/23187822-047e-45cc-b480-fe997bd55b86" muted autoplay loop playsinline controls width="100%"></video>
|
||||
</div>
|
||||
|
||||
## 📑 目录
|
||||
|
||||
- [✨ 功能一览](#-功能一览)
|
||||
- [🚀 安装](#-安装)
|
||||
- [🖼️ 特性巡礼](#-特性巡礼)
|
||||
- [💬 社区](#-社区)
|
||||
- [🆕 最近更新](#-最近更新)
|
||||
- [⌨️ 快捷键](#-快捷键)
|
||||
- [🔌 服务化扩展](#-服务化扩展)
|
||||
- [🛠️ 开发与构建](#-开发与构建)
|
||||
- [🔐 安全](#-安全) · [⚠️ 已知限制](#-已知限制) · [🖥️ 平台支持](#-平台支持)
|
||||
- [🌐 插件生态](#-插件生态) · [🤝 参与贡献](#-参与贡献) · [👥 贡献者](#-贡献者) · [🔗 友情链接](#-友情链接)
|
||||
|
||||
## ✨ 功能一览
|
||||
|
||||
相比 DSH 官方侧边栏,本插件补上的关键能力:
|
||||
|
||||
- **🖥️ 可编辑的代码编辑器**:官方文档预览是**只读**的 → 插件保留**可编辑**的 CodeMirror 编辑器(保存、语法高亮、预览切换);Markdown / HTML 也走插件自有渲染(Mermaid 图表安全渲染 + 点击放大、README 级内嵌 HTML、浮动目录大纲、HTML 沙箱预览)
|
||||
- **🗂️ 增强文件树**:接管内置「文件」页——懒加载目录树、**展开的目录实时 watch 自动刷新**、软链接识别、全局文件名搜索、拖拽上传、悬浮 `@文件` 一键引用进输入框;**Ctrl/Cmd 多选 + Shift 连选**(批量复制路径 / 批量删除)、**Git 变更着色 + 状态字母**(VS Code 同款)、**新建文件夹**、**多选右键「压缩并打包下载」**(服务端流式打 ZIP,无第三方依赖);右键「打开方式」= **DSH 自带 open-in-app**(宿主探测到的本机关联应用 + 文件管理器显示)**+ 插件自研打开方式**(资源管理器 / VS Code / Cursor / Zed / 自定义编辑器 URL 模板、SSH 远端、固定到菜单)两者并存
|
||||
- **🌿 文件变动**(官方侧栏没有 Git 面板):Git 视角(暂存 / 提交 / 历史 / 工作树与子仓库)+ 本轮 AI 改动视角双合一,统一 diff 渲染(行内字符级高亮、语法着色、敏感内容脱敏);两视角共用一套 28px 行、单一空态/错误通道与吸底提交条
|
||||
- **🧩 任务管理**(官方没有):子代理拓扑实时预览 + 后台任务清单(退出码 / 实时输出 / 强制终止)
|
||||
- **💬 侧边对话**(官方没有,beta):Codex 风格侧边线程——继承主会话完整上下文独立运行,可持续追问,一键提升为顶层会话
|
||||
- **🖥️ 底部工作台**(官方没有):右列交给 DSH 原生右侧栏,插件另加自绘底部工作台(拖拽分栏 / 按会话持久化),可与原生栏同时展开
|
||||
- **📂 模型打开侧边栏(可选)**:`sidebar_open` 工具让模型主动在侧边栏打开文件 / 文件夹 / 网页
|
||||
- **🔌 服务化扩展**:`ctx.betterSidebar` 向所有插件开放(`registerTab` / `registerFileViewer`),内置 5 tab + 3 viewer 走同一套 API,已有 **28+ 生态插件**(见「🌐 插件生态」)
|
||||
- **⚡ 按需加载**:启动只拉 ~325KB 核心,编辑器 / Mermaid / 第三语言词典按需加载 · **🌏 多语言**跟随 DSH · **🔁 会话隔离**按会话持久化布局
|
||||
|
||||
## 🚀 安装
|
||||
|
||||
**前置**:已装好 DSH(`dsh web` 能正常运行),Node.js ≥ 20、pnpm ≥ 10。
|
||||
|
||||
**支持的 DSH 版本**:
|
||||
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本(v0.24.0):0.2.0-rc.1+" src="https://img.shields.io/badge/DSH-0.2.0--rc.1%2B-4d6bfe" /></a>
|
||||
|
||||
> 📌 **通道与支持线**:`v0.24.1` 适配 DSH **0.2.0-rc.1+**(0.2.0 首个候选版走 npm `next` 通道,`latest` 仍是 0.1.7-rc.2)。**装 DSH 请写精确版本号**:`npm i -g @deepseek-ai/dsh@0.2.0-rc.1`。**0.1.7 线的用户请固定 `dsh-apes-sidebar@0.22.1`**:0.2.0 是宿主 minor 变更,`^0.1.7-rc.1` 这类 caret 范围在 0.2.0 上会被宿主的启动兼容性预检判定失败、整行静默禁用。
|
||||
|
||||
> 🧭 **按你的 DSH 版本选插件版本**:
|
||||
>
|
||||
> | 你的 DSH 版本 | 安装命令 | 版本 / peer 声明 |
|
||||
> | --- | --- | --- |
|
||||
> | **0.2.0-rc.1+**(含之后的 0.2.0 正式版) | `dsh plugin --profile web add dsh-apes-sidebar@latest` | **0.24.1**,`^0.2.0-rc.1` |
|
||||
> | **0.1.7-rc.1 ~ 0.1.7-rc.2**(含 0.1.7 正式版;npm `latest` 目前仍是 0.1.7-rc.2) | `dsh plugin --profile web add dsh-apes-sidebar@0.22.1` | **0.22.1**,`^0.1.7-rc.1` |
|
||||
> | 0.1.7-alpha.1 / 0.1.7-alpha.2 | **没有可装版本**——先把 DSH 升到 rc.1,再跑上一行:<br>`npm i -g @deepseek-ai/dsh@0.1.7-rc.1` | — |
|
||||
> | 0.1.6-alpha.2 及更早、`0.1.5-rc.*`(含 npm `latest` 的 0.1.5-rc.3) | `dsh plugin --profile web add dsh-apes-sidebar@0.19.1` | **0.19.1**,`^0.1.5-rc.1` |
|
||||
> | `0.1.5-alpha.2` | `dsh plugin --profile web add dsh-apes-sidebar@0.19.0-alpha.1` | `^0.1.5-alpha.2` |
|
||||
> | `0.1.2-rc.*` | `dsh plugin --profile web add dsh-apes-sidebar@0.18.1` | `^0.1.2-rc.1` |
|
||||
> | `0.1.2-alpha.2` | `dsh plugin --profile web add dsh-apes-sidebar@0.18.0-alpha.0` | `^0.1.2-alpha.2` |
|
||||
> | `0.1.0-rc.8` / `0.1.1` | `dsh plugin --profile web add dsh-apes-sidebar@0.17.1` | `^0.1.0-rc.8` |
|
||||
>
|
||||
> 命令里的 `web` 换成你自己的 profile 名即可。**旧版本一律写精确版本号**(`@0.19.1` 而不是 `@latest`),因为 `latest` 会随新正式版前移;反过来也**不要**在 0.1.7 的 alpha 上装 0.19.1,装上只会坏。
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add dsh-apes-sidebar@latest
|
||||
```
|
||||
|
||||
> 本版不依赖任何需要构建脚本的包(终端连同 `node-pty` 已整体交还 DSH),安装**一步到位**;装完后可在 DSH 自带的 **Plugins 页面**直接启停。
|
||||
|
||||
装完**硬刷新浏览器**(Cmd/Ctrl+Shift+R)即可看到侧边栏(DSH 对 client 改动热加载,无需重启;仅 host 半更新时需要重启)。
|
||||
|
||||
**方式二:让 DSH 自己装**——把下面这段提示词发给任意一个 DSH 会话:
|
||||
|
||||
```text
|
||||
帮我安装 dsh-apes-sidebar 插件(DSH 侧边栏工作台),步骤:
|
||||
1. 执行 dsh plugin --profile web add dsh-apes-sidebar@latest(latest 即当前正式版)
|
||||
2. 完成后提醒我硬刷新浏览器(Cmd/Ctrl+Shift+R)
|
||||
遇到报错先查 https://github.com/omdsh-dev/DSH-better-sidebar README 的常见问题表。
|
||||
```
|
||||
|
||||
**方式三:一键脚本**——克隆本仓库后执行 `bash scripts/install.sh`(macOS / Linux / Windows Git Bash;Windows 原生环境用 `install.ps1`;`-h` 查看参数),自动完成安装 + bundle 注册(含幂等清理旧的手动挂载行)。
|
||||
|
||||
<details>
|
||||
<summary><b>更新</b></summary>
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add dsh-apes-sidebar@latest
|
||||
```
|
||||
|
||||
也可把 `~/.dsh/profiles/web/package.json` 里的版本号改高后 `pnpm install`。改完**硬刷新浏览器**(Cmd/Ctrl+Shift+R)即可(client 改动无需重启 DSH)。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>常见问题</b></summary>
|
||||
|
||||
| 现象 | 原因与解决 |
|
||||
|---|---|
|
||||
| 报 `Ignored build scripts` | pnpm 11 拦截了某个传递依赖的构建脚本。在 profile 目录(`~/.dsh/profiles/web`)跑 `pnpm approve-builds` 按提示放行——本插件自身已无构建脚本依赖(终端删除后 `node-pty` 不在依赖里)。 |
|
||||
| 报 `minimum release age` / 版本不足 24h | 装的版本发布不足 24 小时。等 24h 或重跑一次(pnpm 会自动补 `minimumReleaseAgeExclude`)。 |
|
||||
| 报「找不到 profile 目录」 | 先跑一次 `dsh web`,让它初始化 `~/.dsh/profiles/web`。 |
|
||||
| 页面出现**两个侧边栏** | 双挂载。旧的手动挂载行:`~/.dsh/profiles/web/cordis.patch.yml` 还留着 `- insert: ... better-sidebar ...`,删掉那段(同 id 重复挂载 loader 会直接报 `duplicate loader entry id`)。聚合包(如 `@linxin666/dsh-web-ui-all`)以**不同 id** 挂载本包时,0.13.x 起插件自身 bundle patch 会自动退让(检测到已有启用中的同包名挂载就不挂自己),无需手动处理;若仍双挂载,先确认聚合包的 bundle 顺序在 `dsh-apes-sidebar` 之前。 |
|
||||
| 升级后设置页的值去哪了 | DSH 0.1.7 删除了插件可注册的设置命名空间:偏好现在写在 profile 里本插件的**挂载行**上(默认 entry id `better-sidebar`),不再是 `~/.dsh/settings.yaml`。插件会在首次启动时把旧 `settings.yaml`(已被宿主改名为 `settings.yaml.imported`)里 `dsh-apes-sidebar` 段一次性回迁,只迁移当前 schema 仍声明的字段、且只在该行还没有用户值时执行,不会覆盖升级后新设的值。 |
|
||||
| 终端无法使用 / 提示 shell 启动失败 | 终端由 **DSH 自身的 `ui-sidebar-terminal`** 提供(本插件不再自带终端与 `node-pty`,也没有终端相关设置项)。遇到问题请查 DSH 侧文档;若报错提到构建脚本,见上一行。 |
|
||||
| 提示 `dsh: command not found` | 先安装 DSH;或直接用 `npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-apes-sidebar@latest`。 |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>从源码安装 / 开发(可选,替代 npm 方式)</b></summary>
|
||||
|
||||
调试本地改动或跟随开发分支时,把依赖指向本地克隆并自行构建:
|
||||
|
||||
```text
|
||||
1. git clone https://github.com/omdsh-dev/DSH-better-sidebar.git ~/Code/DSH-better-sidebar
|
||||
cd ~/Code/DSH-better-sidebar && pnpm install && pnpm build
|
||||
2. ~/.dsh/profiles/web/package.json 的 dependencies 写 "dsh-apes-sidebar": "link:<克隆目录绝对路径>"
|
||||
3. ~/.dsh/profiles/web/cordis.patch.yml 追加挂载行(这一行的 `config` 就是本插件的设置表单:部署限额 `readLimit` / `mediaLimit` / `uploadLimit` / `listLimit` 加用户偏好字段,设置页写的就是它;不写则全部用 schema 默认值):
|
||||
- insert:
|
||||
- id: better-sidebar
|
||||
name: 'dsh-apes-sidebar'
|
||||
config:
|
||||
readLimit: 524288
|
||||
4. 在 ~/.dsh/profiles/web 执行 pnpm install
|
||||
5. 硬刷新浏览器(Cmd/Ctrl+Shift+R)即可看到效果(client 改动无需重启 DSH;host 半改动才需重启)
|
||||
```
|
||||
|
||||
更新:`git pull && pnpm install && pnpm build` → 硬刷新浏览器即可(client 改动热加载生效,无需重启 DSH;host 半改动才需重启)。切回 npm 通道时,把依赖改回 npm 上的对应版本(稳定线 `"^0.19.1"`;本线 `"^0.22.1"`)再 `pnpm install`。
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>通过 plugin-registry 安装(可选,与上述二选一)</b></summary>
|
||||
|
||||
前置:DSH 已集成 [plugin-registry](https://github.com/dsh-external/plugin-registry)(`dsh registry` 可用)。**同时启用两个通道会双挂载**(Node 半挂两次、页面两个侧边栏)。
|
||||
|
||||
```sh
|
||||
git clone https://github.com/omdsh-dev/DSH-better-sidebar.git && cd DSH-better-sidebar
|
||||
pnpm install && pnpm build
|
||||
node scripts/package-registry.mjs # 组装 registry/ 暂存(含清单 + 产物 + README,不入库)
|
||||
dsh registry install ./registry # 安装(默认禁用)
|
||||
dsh registry enable dsh-external/dsh-apes-sidebar
|
||||
```
|
||||
|
||||
更新:`git pull && pnpm install && pnpm build` → `node scripts/package-registry.mjs` → `dsh registry uninstall/install/enable`。切换通道前先移除另一通道的挂载。
|
||||
|
||||
</details>
|
||||
|
||||
## 🖼️ 特性巡礼
|
||||
|
||||
> 以下均为真实界面实拍(每行两张,点击可放大)。
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **🗂️ 文件工作台:资源管理器**<br/><sub>支持两种格式的资源管理器:内嵌在文件预览中 / 独立显示文件树。懒加载目录树、**展开的目录由宿主按目录 watch、改动后自动重列**、软链接按目标类型展示(目录软链接可展开、失效链接标红)、全局文件名搜索、上传文件/文件夹与拖放上传、右键菜单(在新 Tab 打开 / 在侧边打开 / 新建文件夹 / 打开方式:**宿主探测到的系统关联应用**(open-in-app)+ **插件自研目标**(资源管理器 / VS Code / Cursor / Zed / 自定义编辑器,支持 SSH 远端与固定到菜单)/ 复制路径 / 重命名 / 删除)、**Ctrl/Cmd 多选与 Shift 连选**(批量复制路径 / 批量删除 / **压缩并下载**)、**Git 变更按状态着色并带 M/A/D/U 字母**、悬浮 `@文件` 一键引用进输入框。</sub><br/><div align="center"><img width="420" alt="文件资源管理器" src="https://github.com/user-attachments/assets/a410bfd2-a8ba-43e6-873e-22417756e94d" /></div> | **📝 Markdown · HTML 内联预览**<br/><sub>Markdown 预览支持 **Mermaid 图表**(`securityLevel: 'strict'` 安全渲染 + 二次清洗;点击图表弹窗放大、滚轮缩放、拖拽平移)、**README 级内嵌 HTML**(徽章墙 `<div align=center>`、`<details>` 折叠块内嵌 markdown、表格单元格内联标签——DOMPurify 白名单消毒真实渲染,`<script>` 等活性内容剥除,本地图片经会话媒体路由重写)与**浮动目录大纲**(≥3 标题出现,点击平滑跳转、自动展开折叠块);HTML 走插件自带的**沙箱预览**,并带 `htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` 两个宿主没有的逃生门开关。**图片 / PDF / 表格 / Office 不再是插件能力**——那些格式由 DSH 自己的文档预览渲染。</sub><br/><div align="center"><img width="420" alt="Markdown + Mermaid 预览" src="https://github.com/user-attachments/assets/fe0e5182-55bb-45cc-b98b-a2877c2bdd38" /></div> |
|
||||
| **🖥️ CodeMirror 代码编辑器**<br/><sub>**可编辑**的文本 / 代码编辑器(保存、语法高亮、预览切换)——宿主自己的文档预览是**只读**的,这是插件保留 catch-all viewer 的理由。</sub><br/><div align="center"><img width="420" alt="CodeMirror 代码编辑器" src="https://github.com/user-attachments/assets/b44b488e-568c-4ee0-b96c-e9c906598a77" /></div> | **🖼️ 图片 / PDF / 表格 / Office 预览(由 DSH 内置提供)**<br/><sub>这些只读格式由 DSH 自己的 `ui-sidebar-documentpreview` 渲染:宿主侧 Office→PDF 转换、电子表格 worker 表格、图片 / PDF 缩放视口,并**按目录自动刷新**。插件已删除自己的 image / pdf / 下载兜底 viewer,也不再认领这些扩展名。</sub><br/><div align="center"><img width="420" alt="图片内联预览" src="https://github.com/user-attachments/assets/f9a58c30-5b7a-48b5-9e22-37d7e071f593" /></div> |
|
||||
| **💻 终端(由 DSH 内置提供)**<br/><sub>右侧栏终端由 DSH 自己的 `ui-sidebar-terminal` 提供:shell 选择、双击重命名、断线重连、刷新后恢复、主题与对比度跟随。插件不再自带终端实现。<br/><br/>⚠️ **模型侧提示**:插件原来自带的 8 个 `terminal_*` 工具(默认关)是模型唯一的**跨调用持久**终端;上游等价物 `@deepseek-ai/dsh-tool-terminal` 未被任何内置 bundle 默认挂载,若你需要该能力,请在 profile 的 `cordis.patch.yml` 里自行插入一行 `tool-terminal`。</sub><br/><div align="center"><img width="420" alt="真实终端" src="https://github.com/user-attachments/assets/0dad6ad3-ff3f-4b5a-86d2-f832ce65323e" /></div> | **🌿 文件变动:Git 视角 + 本轮文件**<br/><sub>双视角合一:**Git 视角**保留完整源代码管理(暂存 / 取消暂存 / 提交(`Ctrl+Enter`)/ 还原、历史、worktree 与子仓库选择);**本轮文件视角**实时折叠会话事件日志,记录模型读 / 写 / 编辑的每个文件(按文件分组、按类型筛选、操作数角标)。点击任意改动在底部**可拖拽预览面板**查看统一 diff——删红 / 增绿 / 改蓝配对 + 行内字符级高亮 + 语法着色 + 上下文折叠——也可一键展开为 VSCode 式独立 diff tab(同一渲染栈)。两个视角共用 36px 头(宿主 SegmentedControl 切换)、28px 行、单一空态与吸底提交条;**Git 视角按目录层级(树)展示变更**——单子目录链压缩成一行、目录行可折叠并带下级变更数、文件行带状态字母与文件图标;git 状态与文件树共享同一份快照,暂存/提交后文件树着色同步刷新。</sub><br/><div align="center"><img width="420" alt="文件变动" src="https://github.com/user-attachments/assets/e7fc1220-305f-4bca-8583-e77ab4f4fa78" /></div> |
|
||||
| **🌐 外链接管(浏览器视图由 DSH 提供)**<br/><sub>网页 tab 是 DSH 自己的 `ui-sidebar-browser`(多开 / 后退前进刷新 / 地址栏 / 沙箱 iframe),**0.1.7 起只在 desktop profile 挂载**——Web profile 里没有这个 kind。插件保留宿主没有的那一半:**只认领有 tab 类型通过 `urlTarget` 明确声明的链接**(Ctrl/Cmd 点击始终放行),**其余一律放行给宿主**(正文链接的去向由宿主的用户设置 `linkOpening` 决定);按协议分流的三个外链接管设置项已删除,认领成功但目标类型此刻不可用时兜底到 `window.open`。</sub><br/><div align="center"><img width="420" alt="内嵌浏览器" src="https://github.com/user-attachments/assets/9bc6b65a-64fc-4942-a685-76e391e55606" /></div> | **🧩 任务页:子代理拓扑 + 后台任务**<br/><sub>子代理树实时拓扑(运行状态、批量实时预览)+ 后台任务清单(退出码 / 实时输出 / 强制终止);新子代理 / 新任务可自动激活任务页,宽屏同时展开侧边栏,窄屏不强制展开全屏抽屉(可关)。</sub><br/><div align="center"><img width="420" alt="任务页:子代理拓扑" src="https://github.com/user-attachments/assets/dcd8ed2f-59fa-405b-937b-2d250f5034dd" /></div> |
|
||||
| **💬 侧边对话(beta)**<br/><sub>Codex 风格侧边线程:**每个对话一个独立 Tab**;线程继承主会话完整上下文(含进行中回合,以 interrupted 诚实冻结)独立运行,不污染主会话;可持续追问、重启冷恢复;一键「保存为新会话」提升为顶层会话。</sub><br/><div align="center"><img width="420" alt="侧边对话(beta)" src="https://github.com/user-attachments/assets/3a338c36-f5de-4000-95f3-4b1cd04f60fc" /></div> | **🖥️ DSH 原生右侧栏 + 插件底部工作台**<br/><sub>右列是 DSH 自己的右侧栏:插件把每个 tab 类型注册成原生 tab(含接管内置「文件」页),聊天里的文件点击直接落到原生栏——**宿主自己的文档预览已覆盖的格式由宿主渲染**,插件只认领 Markdown / HTML / 可编辑代码;插件自有底部面板可与其同时展开,拖 Tab 到分栏边缘**拆分**、拖到中间**合并**,高度拖上缘调节;开合按钮在会话头右侧。</sub><br/><div align="center"><img width="420" alt="双工作台(右侧栏 + 底部面板)" src="https://github.com/user-attachments/assets/dfdb875e-a1a8-4d4b-8340-353736b1708f" /></div> |
|
||||
| **⚙️ 声明式设置**<br/><sub>设置页「侧边卡片」分区:每个 tab / 预览器一张小卡片,独立开关(高亮启用态 + 品牌开关滑块);二级设置经卡片底部「功能设置」条弹窗(开关 / 文本 / 数字 / 下拉);插件自有设置持久化在 `pluginSettings`,整份偏好则写在 profile 里本插件的**挂载行**上(DSH 0.1.7 起设置按 Loader entry id 寻址)。</sub><br/><div align="center"><img width="420" alt="声明式设置:侧边卡片" src="https://github.com/user-attachments/assets/0800ca64-621e-48da-b7df-aecfddc3ec29" /></div> | **📱 移动端**<br/><sub>窄屏(<768px)自动切换为全宽抽屉:底栏 tab 一次性并入右侧栏,触屏拖拽可调。</sub><br/><div align="center"><img width="360" alt="移动端全宽抽屉" src="https://github.com/user-attachments/assets/a82ba78a-f4cf-4d85-80e8-050a05beb144" /></div> |
|
||||
|
||||
## 💬 社区
|
||||
|
||||
推荐添加QQ群(577011007)
|
||||
|
||||
<div align="center">
|
||||
<img width="220" alt="微信群二维码" src="https://github.com/user-attachments/assets/5d727d52-7fff-4526-8b36-fb7203fb1dce" />
|
||||
<img width="220" alt="QQ群二维码" src="https://github.com/user-attachments/assets/9be34629-26ef-4537-aad4-1393c147f81c" />
|
||||
</div>
|
||||
|
||||
|
||||
## 🆕 最近更新
|
||||
|
||||
**支持的 DSH 版本**:<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本(v0.24.0):0.2.0-rc.1+" src="https://img.shields.io/badge/DSH-0.2.0--rc.1%2B-4d6bfe" /></a> · 完整发布历史见 [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases)
|
||||
|
||||
### v0.24.1
|
||||
|
||||
> 🐞 **修复版**:修掉两处会让文件树「一操作就整体刷新」的缺陷——① 原生承载面在**每次会话状态写入**时都会重建 `files` 接管项的 slot 注册,宿主因此换掉 slot entry、把整个 tab body 卸载重挂(展开/收起、切 tab、拖底部工作台都会触发)→ 文件树丢失层缓存、滚动位置与目录 watcher 并重列整棵可见树;② 目录实时刷新的重列**先把该层缓存删掉**再请求,导致行被「Loading…」占位替换后重建(构建/格式化/模型跑 bash 时整层闪空)。现在展开只请求被展开那一层、tab body 不再重建、变更只增量更新行。
|
||||
|
||||
### v0.23.0
|
||||
|
||||
> 🧭 **开发线版本(从未发布到 npm,内容随 v0.24.1 一并发布)**:文件页与文件变动页的整体 UI/UX 重构。文件页新增 **Ctrl/Cmd 与 Shift 多选**、批量条、**Git 变更着色**、**新建文件夹**、拖拽上传重做,**多选可右键「压缩并下载」**(宿主侧带进度的打包任务);「打开方式」改为宿主本机关联应用与插件自研目标**双源并存**(可用设置 `openWithPluginTargets` 强制并存),右键菜单收敛层级;文件变动页**重构为层级树**(Git 视角 + Agent 视角,目录可暂存)。性能:`fs.tree` 实测 **22.8ms → 3.8ms**(10k 条目),新增批量路由 `fs.trees`(挂载/刷新从 N+1 请求变 1 个),菜单打开不再重列目录。⚠️ **行为变更(安全相关)**:**删除工作区路径检测**,插件 fs 路由可读写宿主用户能访问的任意路径(仅受 OS 权限约束)。详见 [CHANGELOG](./CHANGELOG.md#v0230)。
|
||||
|
||||
### v0.24.0
|
||||
|
||||
> 📦 **支持线前移**:仅适配 DSH **0.2.0-rc.1+**(peer 下限 `^0.2.0-rc.1`,CI 钉 `@deepseek-ai/dsh@0.2.0-rc.1`)。**0.1.7 线(含 npm `latest` 的 0.1.7-rc.2)请固定 v0.22.1(0.1.7 线最后发布的版本)**:caret 范围跨 minor 不成立,`^0.1.7-rc.1` 在 0.2.0 宿主上会被启动兼容性预检整行禁用(实测 `semver.satisfies('0.2.0-rc.1','^0.1.7-rc.1',{includePrerelease:true}) === false`)。
|
||||
|
||||
- 📦 **基线整体抬到 `0.2.0-rc.1`**:14 条 DSH peer 与 27 个 `@deepseek-ai/*` devDependencies 同步;`dsh.plugin.json` 的 `engines.dsh` 随之前移。
|
||||
- 🔍 **实测确认是纯增量**:本插件用到的 19 个宿主包里**零个值导出被删除**;类型面只有 `ui-primitives`(`DisclosureRow` / `TextShimmer` / `Tooltip` 新增可选 prop、overlay 顶部内距)、`dsh-session`(新增 `ToolCallRecovery`)与 `dsh-api-remotes`(新增 product-analytics remote)变化;会话格式仍 v4、`SUBAGENT_DESCRIPTOR_VERSION` 仍 3、`dsh/lib/bin.js` 与 `dsh-client-modules` 运行时逐字未变。因此**没有为 0.1.7 保留任何兼容分支**。
|
||||
- 🧪 **挂载 lane 与 CI 钉版同步到 `0.2.0-rc.1`**;`tests/market-manifest.spec.ts` 的 peer 形状规则改为钉当前基线 tuple,并记下「caret 跨 minor 必失效」这条教训。
|
||||
- ⚠️ **生态连带**:`@huanlin/dsh-plugin-better-locale`(可选集成,非本插件依赖面)的 peer 钉在 `^0.1.x` 线上,在 0.2.0 上无法加载,其 5 条 unmet peer 是本次 `pnpm peers check` 唯一残留(上游未适配,与本插件的 14 条 peer 无关)。
|
||||
**支持的 DSH 版本**:<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本(v0.22.1):0.1.7-rc.1+" src="https://img.shields.io/badge/DSH-0.1.7--rc.1%2B-4d6bfe" /></a> · 完整发布历史见 [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases)
|
||||
|
||||
### v0.22.1
|
||||
|
||||
> 📦 **正式版**(npm `latest`):支持线**不变**——仍仅支持 **DSH 0.1.7-rc.1+**(peer 下限 `^0.1.7-rc.1`,CI 钉 `@deepseek-ai/dsh@0.1.7-rc.1`),0.21.1 / 0.22.0 的用户直接升级即可。修掉两个**真机可复现、单测却全绿**的缺陷;**DSH 0.1.6-alpha.2 及更早请继续固定 v0.19.1**。
|
||||
|
||||
- 🐛 **`files` 接管被孤儿化 → 报错刷屏 + 文件树空态**(社区 #770 / #771,官方桌面壳日志实证):客户端条目替换(插件市场更新 / Plugins 页禁用→启用 / HMR 重打)时,`sync()` 的清理循环会把**不属于描述符**的 `files` 接管释放掉、又在同一轮里重建——而重建发生在**已经 inactive** 的插件上下文上:`tabs.register` 建在宿主上下文上照样取走了 id,紧随的 `ctx.slots.inject` 却抛 `cannot create effect on inactive context`,于是 disposer 丢失、该 id 在**整个页面生命周期内不可再注册**(表现就是 `native register files error: … already registered` 刷屏 + 文件树落到宿主空态,只有刷新页面才恢复)。现在清理循环**跳过 `FILES_KIND`**(接管的寿命只由编辑器类型开关与 seat disposer 决定),并且**任何在宿主取走 id 之后失败的注册都会回滚释放**(含已建好的槽位),失败只留一个「下次通知可重试」的状态。修复取自社区 PR #777(@yanzhaohui1999)。
|
||||
- 🖥️ **macOS 桌面版窗口拖拽 / 双击标题栏缩放失效**(#772):插件宿主是直挂 `body` 的子元素,宿主的 `html[data-platform=darwin] body > :not(#root) { -webkit-app-region: no-drag }` 命中它,而 app-region **无视 `pointer-events`**——铺满视口的面板层把下面每条拖拽带一起抵消(拖第一次还行、之后全失效)。现在 `[data-dsh-apes-sidebar]` / `[data-dsh-panel-host]` / 放大视图 `.mermaidModal` 都用中性值 `initial !important` 退出计算,层内的面板与控件保持 `no-drag`(点击不被吞);合并社区 PR #773 并补齐放大视图这最后一个铺满视口的 body 直挂层。
|
||||
- ✅ **守住它们**:新增单元用例把「接管不得被通知拆建」「注册失败必须回滚已占用 id 与已建槽位」钉在注册表**事件日志**上(未修复代码上 4/4 红),并新增**部署级回归门** `tests/e2e/native-reload.e2e.ts`(在 npm 0.22.0 上连续 3 次运行全红、修复版连续 3 次全绿(1 个用例重复跑三次));拖拽契约由单元用例 + 挂载 lane 的**真实级联探针**(按宿主规则读计算值)守护。验证:`pnpm test` 122 files / 1293 passed / 9 skipped,`pnpm test:mount` 与 `test:mount:aggregate` 绿。事故记录见 [docs/plans/2026-09-28-native-files-takeover-reload-leak.md](./docs/plans/2026-09-28-native-files-takeover-reload-leak.md)。
|
||||
|
||||
### v0.22.0
|
||||
|
||||
> 📦 **正式版**(npm `latest`):支持线**不变**——仍仅支持 **DSH 0.1.7-rc.1+**(peer 下限 `^0.1.7-rc.1`,CI 钉 `@deepseek-ai/dsh@0.1.7-rc.1`),0.21.1 的用户直接升级即可。**DSH 0.1.6-alpha.2 及更早请继续固定 v0.19.1**。
|
||||
|
||||
- 🧩 **任务管理页重做成工作流图**(主显示模式):会话树渲染为分层节点 + 贝塞尔连线——拖拽平移、滚轮缩放到光标、内容包围盒居中适配、右下角控制条(图/树切换 + 折叠开关 + 缩放 + 适配);**经典缩进树保留**(键盘可导航),两种模式共享同一个视图模型,折叠状态与团队富化不会视觉漂移。
|
||||
- 🃏 **双段式节点卡**:上段是类型徽章(主代理 / 子代理 / 成员 / 工作流 / 已完成聚合)+ 相位徽标 + 名称 + 元信息;下段小条是状态点 + 状态词 + **主 Agent 同款合并活动行**(并发工具按类别归并 + 计数 + 在跑那条的细节,措辞取宿主 `chat` 词条)+ 已完成节点的折叠按钮;运行中小条从左到右**完整扫过**(`prefers-reduced-motion` 下关闭)。8px 圆角、层级只用上段极淡底色表达、当前会话加粗 accent 描边。
|
||||
- 🔀 **工作流 run 入图**:从 `tool-workflow/*` 事件折叠出 run(与官方面板同一批),run 挂在发起代理下、成员 agent 重挂到 run 下并按相位分框、同色相位徽标;catalog 里没有的成员用 run 数据合成占位节点,跑完的 run 仍能看到成员。
|
||||
- 🗂 **折叠分两组、各自说清是什么**:`✓ N 已完成`(含出错,失败单独报 `出错 N`)与 `N 个待命`(跑完一轮、随时可被叫起来的 teammate)是**两行**;手动折叠按钮永远有效,自动聚合只在待命成员 ≥3 时收空闲成员;聚合卡名字行写「前两个名字 + `+N`」,点聚合全部展开。
|
||||
- 🪟 **两个常驻浮动窗**(抽出可复用的 `FloatingWindow`):后台任务输出与共享任务详情/编辑——可拖拽、可四边拉伸、内容区自滚动,只靠关闭按钮或 Escape 结束(外部点击 / 失焦 / 锚点离屏都不关);任务窗把余量交给描述区,拉大是给内容更多空间而不是留白,动作行固定在底部。
|
||||
- 👥 **Agent Teams 任务板(实验层)**:成员富化到对应节点、常驻任务条列出成员与共享任务;状态机跟随宿主(待办 → 认领 → 进行中 → 完成 → 重开)+ 改派 / 编辑 / 两击删除,CAS 过期修订单独提示;成员活动由 `subagents.live` 的 running 叠加。
|
||||
- 🔄 **后台任务改读宿主客户端 `ctx.jobs`**(推送 roster + 非消费输出流 + kill):删掉自建的 `jobs.list` / `jobs.output` / `jobs.kill` 三条路由与事件回放镜像,彻底不碰模型 `job_output` 游标;输出在常驻浮动窗里流式显示并尾随,代理数 ≥8 时抽屉自动折叠。
|
||||
- 🛠 **DSH 0.1.7 数据面重写**:上游删掉了 `agentTeams.remoteView` 三个 Remote 方法 → 团队改为读 Lead Session 的 **`agentTeam` Session projection**(推送式,删掉 `teams.view` 路由与 5 秒轮询);写路径两条路由保留,拒绝从「返回联合」变为「抛 `TeamError`」,过期修订映射 409 `team-conflict`。**修掉的真实故障**:0.1.7 上团队条从来不渲染(路由报 `remoteView is not a function`,页面静默无提示)。
|
||||
- 🐛 **真机抓到、单测全绿的四个缺陷**:逐节点折叠按钮点了没反应(被自动折叠的守卫卡住);「待命」卡片从不画折叠按钮;认领后标签错显「阻塞」;队列任务上「完成」必失败(需先认领)。
|
||||
- 🎨 **窄屏与手机设置**:按原生右侧栏窄宽重新定档卡片与行距;设置页新增**手机**分组——窄屏(≤768px)不自动弹出新任务页、任务页默认树状图。
|
||||
|
||||
> 📜 **更早版本**:完整发布历史见 [CHANGELOG.md](./CHANGELOG.md)(v0.21.1 → v0.12.3)与 [GitHub Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases)。
|
||||
|
||||
## ⌨️ 快捷键
|
||||
|
||||
| 操作 | 按键 |
|
||||
|---|---|
|
||||
| 保存编辑 | `Ctrl/Cmd + S` |
|
||||
| Git 提交 | `Ctrl + Enter` |
|
||||
| 关闭 Tab | 鼠标中键 |
|
||||
| Tab 右键菜单 | 关闭 / 关闭其他页签 / 关闭左侧页签 / 关闭右侧页签(当前标签组) |
|
||||
| 拆分/合并分栏 | 拖 Tab 到分栏边缘 / 中间 |
|
||||
| 引用文件到输入框 | 悬浮行尾 `@文件` 按钮 |
|
||||
| 复制文件路径 | 右键行 → 复制相对/绝对地址 |
|
||||
|
||||
## 🔌 服务化扩展
|
||||
|
||||
从 v0.4.0 起暴露 `ctx.betterSidebar` 服务,其他插件可注册侧边栏页面与文件预览器(内置 5 tab + 3 viewer 亦通过同一服务注册)。v0.12.1 补齐基座能力(完整类型导出、能力探测、状态订阅、tab 角标、生命周期回调、定向打开、插件自有设置等)。v0.19.0 起新增文件图标注册:`registerFileIcon` 按扩展名(或保留的 `'folder'` / `'folder-open'` 目录扩展名、`exts: []` 全局默认)替换文件树与文件 tab 的图标,彩色 ReactNode 亦可——内置消费、注册即生效,无需自己接线。
|
||||
|
||||
完整接入文档(全字段、匹配算法、HMR 陷阱、声明式设置、版本探测、原生栏承载面与皮肤契约):**[`docs/external-plugin-guide.md`](./docs/external-plugin-guide.md)**;仓库开发规则(硬约束 / CI / 发版)见 [`AGENTS.md`](./AGENTS.md)。
|
||||
|
||||
### ➕ 添加插件(推荐插件目录)
|
||||
|
||||
设置页「侧边卡片」两个网格末尾的**虚线卡片**分别打开 Tab / 预览插件弹窗:声明扩展点、「**在 GitHub 上浏览更多插件**」按钮([GitHub topic `dsh-apes-sidebar`](https://github.com/topics/dsh-apes-sidebar))、推荐插件目录(名字 / 仓库 / 简介 / 安装脚本),每个条目「**跳转**」直达仓库、「**复制**」把安装命令写入剪贴板。
|
||||
|
||||
**收录新插件**:向 [`src/client/plugins-tabs.ts`](./src/client/plugins-tabs.ts)(Tab 注册)或 [`src/client/plugins-viewers.ts`](./src/client/plugins-viewers.ts)(文件预览注册)追加一条 `PluginEntry`,并把仓库打上 `dsh-apes-sidebar` topic;数据完整性由 `tests/plugin-list.spec.ts` 守护。
|
||||
|
||||
## 🛠️ 开发与构建
|
||||
|
||||
```sh
|
||||
pnpm install # @deepseek-ai/* devDependencies 已发布(基线 0.2.0-rc.1,走 npm `next` 通道),直接解析、无需令牌
|
||||
pnpm typecheck # tsc --noEmit
|
||||
pnpm lint # eslint .(flat config:js + typescript-eslint + react-hooks recommended)
|
||||
pnpm build # → lib/index.js + lib/invariant.js + lib/client.js + lib/client-registry.js + lib/types
|
||||
pnpm test # vitest(含 manifest 一致性守卫,需先 build)
|
||||
pnpm watch # tsdown --watch
|
||||
```
|
||||
|
||||
**Make 薄封装**(`make help` 查看全部目标;package.json 仍是唯一事实源):
|
||||
|
||||
```sh
|
||||
make check # 聚合校验门禁:typecheck → build → test → check:consumer-types(对齐 CI)
|
||||
make mount # 真机挂载冒烟:build + pack → 安装 Chromium → pnpm test:mount
|
||||
make clean # 清理 lib/、*.tgz、playwright-report/、test-results/
|
||||
```
|
||||
|
||||
`pnpm check:consumer-types`:对外类型声明面守卫——以浏览器-only 消费者(无 `@types/node`、`skipLibCheck: false`)的视角对构建出的 `lib/types` 做类型检查,需先 `pnpm build`。
|
||||
|
||||
**架构**:单 npm 包、host/client 双半结构——host(`src/index.ts`):`/sidebar/api/*` JSON API、`/sidebar/file` 媒体路由、`/sidebar/html` 预览路由、`/sidebar/upload` 上传路由,以及两条 WebSocket(`/sidebar/ws/agent-opens` 模型打开推送、`/sidebar/ws/fs-watch` 文件树目录 watch;fs / git / 预览全部会话级 + 信任围栏);client(`src/client/index.tsx`):portal 侧边栏 + 各视图 + 链接接管;状态按会话持久化 localStorage。插件按 DSH 官方规范组织(无 default 导出、双 client bundle),运行期不依赖 npm / checkout(`@deepseek-ai/*` 由 web profile 提供)。
|
||||
|
||||
## 🔐 安全
|
||||
|
||||
- 路由受 Host 头信任围栏保护(与 `/api` 一致);`fs.write` 原子写入;git 只调 CLI、绝不设置身份
|
||||
- ⚠️ **v0.23.0 起文件系统路由不再做工作区包含检查**:`fs.tree` / `fs.trees` / `fs.read` / `fs.write` / `fs.rename` / `fs.remove` / `fs.mkdir` / 媒体 / HTML 预览 / `/sidebar/upload` / `archive.build` 能读写**宿主用户可访问的任意路径**(只受 OS 权限约束,`workspaceFence` 开关与 403 分支已删除)——调用方不能再把这些路由当作被围栏保护的接口
|
||||
- HTML 预览的内容在**不透明源沙箱 iframe** 中渲染(无 `allow-same-origin`/`allow-top-navigation`、`no-referrer`、权限策略全禁);`/sidebar/html` 路由带 CSP `sandbox` + 大小/路径边界
|
||||
- 设置页可按功能关闭 HTML 预览的沙箱(`htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe`,默认关闭,带警告文案)——关闭后内容与界面同源,仅建议对完全可信内容使用。**网页 tab 的沙箱不再是插件的面**:浏览器视图由宿主提供(desktop profile),其沙箱与导航策略见 DSH 侧文档
|
||||
|
||||
## ⚠️ 已知限制
|
||||
|
||||
- Git 无 push/pull/fetch;Markdown 预览提供手动刷新按钮,刷新未保存编辑前会确认是否丢弃草稿;文件树只对**已展开**的目录做 watch(折叠的目录不订阅,也不做全工作区递归扫描);工具行内文件打开按钮不可拦截
|
||||
- **只读预览的格式由宿主决定**:表格 / PDF / 图片 / Office 走 DSH 自己的 `ui-sidebar-documentpreview`,插件只渲染 Markdown / HTML 与可编辑的文本代码;宿主的实现(渲染细节、缩放、刷新时机)随 DSH 版本走
|
||||
- **浏览器视图只在 desktop profile 存在**:Web profile 没有宿主 `browser` kind,插件也不再自带浏览器 tab,因此网页 tab 只在 desktop profile 可用;登录态 / 第三方 Cookie / `X-Frame-Options` 等限制随宿主实现
|
||||
- HTML 预览渲染的是已保存文件(不反映未保存草稿)
|
||||
- 移动端(<768px)无底部面板:进入窄屏时其标签页一次性并入右侧栏(迁移后回桌面仍保留在右侧栏),桌面端的底部面板只在宽视口下可用。未选中会话时,点按弱化开关会显示选择会话提示;选中会话后开关打开全宽抽屉
|
||||
|
||||
## 🖥️ 平台支持
|
||||
|
||||
Windows / Linux / macOS 三平台适配(macOS 日常验证;其余经单元测试覆盖)。插件不再包含原生依赖(终端与 `node-pty` 已整体交还 DSH 自身),构建只需 Node + pnpm,无需编译工具链。
|
||||
|
||||
## 🌐 插件生态
|
||||
|
||||
`ctx.betterSidebar` 服务向所有插件开放两个扩展点:**`registerTab`(注册侧边栏页面)** 与 **`registerFileViewer`(注册文件预览器)**。内置的 5 tab + 3 viewer 与第三方插件走同一套 API,能力完全对等。
|
||||
|
||||
```ts
|
||||
import type {} from 'dsh-apes-sidebar' // 触发 ctx.betterSidebar 类型合并
|
||||
export const inject = ['betterSidebar']
|
||||
export function apply(ctx: Context) {
|
||||
ctx.effect(() => ctx.betterSidebar.registerTab({
|
||||
id: 'my-plugin:db', title: 'Database', component: ({ scope }) => <DbView sessionId={scope.sessionId} />,
|
||||
}))
|
||||
ctx.effect(() => ctx.betterSidebar.registerFileViewer({
|
||||
id: 'my-plugin:csv', exts: ['csv'], fetchStrategy: 'custom',
|
||||
load: async (path, scope) => parseCsv(await fetchText(scope, path)),
|
||||
component: ({ customData }) => <CsvGrid rows={customData} />,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
GitHub topic [`dsh-apes-sidebar`](https://github.com/topics/dsh-apes-sidebar) 下已有 **28+ 生态插件**(持续增长中):
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e"><img width="66%" alt="设置页「添加插件」弹窗:推荐插件目录 + 一键复制安装命令" src="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e" /></a><br />
|
||||
<i>设置页「侧边卡片」内置「添加插件」弹窗:推荐目录 + 一键复制安装命令 + 直达 GitHub topic</i>
|
||||
</div>
|
||||
|
||||
### 📑 Tab 插件(注册侧边栏页面)
|
||||
|
||||
<details>
|
||||
<summary><b>24 个插件(点击展开)</b></summary>
|
||||
|
||||
| 插件 | ⭐ | 简介 |
|
||||
|---|---|---|
|
||||
| [ChenRuoT/dsh-sidebar-qa](https://github.com/ChenRuoT/dsh-sidebar-qa) | <img alt="stars" src="https://img.shields.io/github/stars/ChenRuoT/dsh-sidebar-qa?style=flat&color=4d6bfe" /> | 划选追问侧边页:类 Codex 侧边提问 / Claude Code `/btw` |
|
||||
| [fuhefei/dsh-sentinel](https://github.com/fuhefei/dsh-sentinel) | <img alt="stars" src="https://img.shields.io/github/stars/fuhefei/dsh-sentinel?style=flat&color=4d6bfe" /> | 条件驱动唤醒系统:文件 / 命令 / HTTP / 进程 / Webhook 监视,到点唤醒 agent;dock + 侧栏分支 + 全局仪表盘 |
|
||||
| [Fisfzy/ego-browser](https://github.com/Fisfzy/ego-browser) | <img alt="stars" src="https://img.shields.io/github/stars/Fisfzy/ego-browser?style=flat&color=4d6bfe" /> | Agent 浏览器:i18n 感知的本机浏览器 Tab(`@dsh-external/ego-browser`,装了 better-sidebar 自动注册侧边栏页,未装回退浮动浮窗观察) |
|
||||
| [jiuge2467/dsh-studio](https://github.com/jiuge2467/dsh-studio) | <img alt="stars" src="https://img.shields.io/github/stars/jiuge2467/dsh-studio?style=flat&color=4d6bfe" /> | 全栈增强工作台:多源 MCP 可视化调试中枢、视觉思考引擎 |
|
||||
| [Iwctwbh/dsh-flowglass](https://github.com/Iwctwbh/dsh-flowglass) | <img alt="stars" src="https://img.shields.io/github/stars/Iwctwbh/dsh-flowglass?style=flat&color=4d6bfe" /> | 流镜 Flowglass:会话流程图实时可视化(消息 / 工具组 / 子代理分支) |
|
||||
| [FeatherHunter/dsh-mattpocock-skills-deck](https://github.com/FeatherHunter/dsh-mattpocock-skills-deck) | <img alt="stars" src="https://img.shields.io/github/stars/FeatherHunter/dsh-mattpocock-skills-deck?style=flat&color=4d6bfe" /> | mattpocock/skills 游戏化任务系统:地图拨迷雾、任务栏推进 |
|
||||
| [GULI-lab/DSH-element-source](https://github.com/GULI-lab/DSH-element-source) | <img alt="stars" src="https://img.shields.io/github/stars/GULI-lab/DSH-element-source?style=flat&color=4d6bfe" /> | 点击页面任意 UI 元素直达 Vue / React / Svelte / Angular 源码并送入会话 |
|
||||
| [Lzh3070/dsh-file-review-tab](https://github.com/Lzh3070/dsh-file-review-tab) | <img alt="stars" src="https://img.shields.io/github/stars/Lzh3070/dsh-file-review-tab?style=flat&color=4d6bfe" /> | 文件改动审查页:行级红绿 diff + 撤销 + chat 行深链 |
|
||||
| [yq04/dsh-git-remotes](https://github.com/yq04/dsh-git-remotes) | <img alt="stars" src="https://img.shields.io/github/stars/yq04/dsh-git-remotes?style=flat&color=4d6bfe" /> | Git 远程页:分支 / 上游 / ahead-behind,fetch 可 prune、ff-only pull、确认后 push |
|
||||
| [ztyhehe/dsh-apes-sidebar-svn](https://github.com/ztyhehe/dsh-apes-sidebar-svn) | <img alt="stars" src="https://img.shields.io/github/stars/ztyhehe/dsh-apes-sidebar-svn?style=flat&color=4d6bfe" /> | SVN 源码管理页:status / diff / log / commit / update / revert / 冲突解决,与内置 Git 面板对称 |
|
||||
| [Melody-max114/dsh-excel-panel](https://github.com/Melody-max114/dsh-excel-panel) | <img alt="stars" src="https://img.shields.io/github/stars/Melody-max114/dsh-excel-panel?style=flat&color=4d6bfe" /> | Excel 编辑页:xlsx 预览 / 编辑、公式实时计算、合并单元格、保存回原文件 |
|
||||
| [v587d/dsh-anysearch-refs](https://github.com/v587d/dsh-anysearch-refs) | <img alt="stars" src="https://img.shields.io/github/stars/v587d/dsh-anysearch-refs?style=flat&color=4d6bfe" /> | AnySearch 搜索结果引用卡片:搜索词、来源摘要、关键词高亮 |
|
||||
| [mlosun/dsh-docs-panel](https://github.com/mlosun/dsh-docs-panel) | <img alt="stars" src="https://img.shields.io/github/stars/mlosun/dsh-docs-panel?style=flat&color=4d6bfe" /> | 全局文档面板:随身 Markdown 笔记,任何工作区随时可读 |
|
||||
| [lnyuqian/dsh-skill-sidebar](https://github.com/lnyuqian/dsh-skill-sidebar) | <img alt="stars" src="https://img.shields.io/github/stars/lnyuqian/dsh-skill-sidebar?style=flat&color=4d6bfe" /> | 技能面板:扫描本机技能目录,4-6 字功能短语 + 一键复制调用 + 置顶 |
|
||||
| [g-yixuan/dsh-sidenote](https://github.com/g-yixuan/dsh-sidenote) | <img alt="stars" src="https://img.shields.io/github/stars/g-yixuan/dsh-sidenote?style=flat&color=4d6bfe" /> | Codex 风格侧边对话 + 划选引用注释(轻量消费插件) |
|
||||
| [thirsty5034/dsh-ssh-tunnel](https://github.com/thirsty5034/dsh-ssh-tunnel) | <img alt="stars" src="https://img.shields.io/github/stars/thirsty5034/dsh-ssh-tunnel?style=flat&color=4d6bfe" /> | 多主机 SSH 隧道 + SSH 管理器页 |
|
||||
| [thirsty5034/dsh-git-forge](https://github.com/thirsty5034/dsh-git-forge) | <img alt="stars" src="https://img.shields.io/github/stars/thirsty5034/dsh-git-forge?style=flat&color=4d6bfe" /> | GitHub / Gitea 账号、项目授权与推送策略 |
|
||||
| [YesSanSan/dsh-conversation-outline](https://github.com/YesSanSan/dsh-conversation-outline) | <img alt="stars" src="https://img.shields.io/github/stars/YesSanSan/dsh-conversation-outline?style=flat&color=4d6bfe" /> | 对话大纲页:按轮次结构化展示、一键跳转、LLM 一句话标题 |
|
||||
| [Wulabalabo/dsh-sidebar-Explorer-Plus](https://github.com/Wulabalabo/dsh-sidebar-Explorer-Plus) | <img alt="stars" src="https://img.shields.io/github/stars/Wulabalabo/dsh-sidebar-Explorer-Plus?style=flat&color=4d6bfe" /> | 文件管理页:上传 / 移动 / 删除 / 重命名 / 新建文件夹(补全写操作) |
|
||||
| [yq04/dsh-turn-review](https://github.com/yq04/dsh-turn-review) | <img alt="stars" src="https://img.shields.io/github/stars/yq04/dsh-turn-review?style=flat&color=4d6bfe" /> | 本轮审查:逐回合审查 agent 改动 |
|
||||
| [Ghz114514/dsh-refpics](https://github.com/Ghz114514/dsh-refpics) | <img alt="stars" src="https://img.shields.io/github/stars/Ghz114514/dsh-refpics?style=flat&color=4d6bfe" /> | Pinterest 风格参考图搜索:瀑布流、侧栏画板、下载与 Eagle 收藏 |
|
||||
| [yzlin499/dsh-yzlin499-easy-plugins](https://github.com/yzlin499/dsh-yzlin499-easy-plugins) | <img alt="stars" src="https://img.shields.io/github/stars/yzlin499/dsh-yzlin499-easy-plugins?style=flat&color=4d6bfe" /> | 实用小工具集(毛坯房 DSH 友好) |
|
||||
| [dong-victor/dsh-apes-sidebar-starter](https://github.com/dong-victor/dsh-apes-sidebar-starter) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-apes-sidebar-starter?style=flat&color=4d6bfe" /> | 运行配置页:IDEA 式 Run/Debug 配置(npm / springboot / python / custom)——一键启动、历史保存、WebSocket 实时日志(ANSI 彩色)、多实例并行、进程树跨平台杀死 |
|
||||
| [baosfeng/my-dsh-plugins](https://github.com/baosfeng/my-dsh-plugins) | <img alt="stars" src="https://img.shields.io/github/stars/baosfeng/my-dsh-plugins?style=flat&color=4d6bfe" /> | 个人多插件合集(`dsh-file-activity`):侧边栏文件活动页——记录文件读取 / 新增 / 修改历史与统计,按文件夹平铺,点击用原生预览打开 |
|
||||
| [Hoemr/dsh-better-overleaf](https://github.com/Hoemr/dsh-better-overleaf) | <img alt="stars" src="https://img.shields.io/github/stars/Hoemr/dsh-better-overleaf?style=flat&color=4d6bfe" /> | Overleaf 标签页:直连 CDP 浏览器登录(支持第三方 Chromium)、项目切换、工作区下 overleaf/ 目录本地 git 镜像与双向同步 |
|
||||
|
||||
</details>
|
||||
|
||||
### 🖼️ 预览插件(注册文件预览器)
|
||||
|
||||
<details>
|
||||
<summary><b>3 个插件(点击展开)</b></summary>
|
||||
|
||||
| 插件 | ⭐ | 简介 |
|
||||
|---|---|---|
|
||||
| [HuanLinOTO/dsh-plugin-better-sidebar-plugin-office](https://github.com/HuanLinOTO/dsh-plugin-better-sidebar-plugin-office) | <img alt="stars" src="https://img.shields.io/github/stars/HuanLinOTO/dsh-plugin-better-sidebar-plugin-office?style=flat&color=4d6bfe" /> | Office 三件套预览(.docx / .xlsx / .pptx),独立 bundle 瘦身主体(官方推荐目录收录) |
|
||||
| [zemul/dsh-video-preview](https://github.com/zemul/dsh-video-preview) | <img alt="stars" src="https://img.shields.io/github/stars/zemul/dsh-video-preview?style=flat&color=4d6bfe" /> | 视频内联预览:.mp4 / .webm / .mov / .mkv / .avi,自带 /video 路由支持 HTTP Range 拖进度条 |
|
||||
| [dong-victor/dsh-apes-sidebar-jupyter](https://github.com/dong-victor/dsh-apes-sidebar-jupyter) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-apes-sidebar-jupyter?style=flat&color=4d6bfe" /> | `.ipynb` 可运行 Notebook 视图:懒启动 Python kernel、流式输出、保存回写 |
|
||||
|
||||
</details>
|
||||
|
||||
### 🧰 增强与工具
|
||||
|
||||
<details>
|
||||
<summary><b>3 个插件(点击展开)</b></summary>
|
||||
|
||||
| 插件 | ⭐ | 简介 |
|
||||
|---|---|---|
|
||||
| [eg-bole/dsh-apes-sidebar-icons](https://github.com/eg-bole/dsh-apes-sidebar-icons) | <img alt="stars" src="https://img.shields.io/github/stars/eg-bole/dsh-apes-sidebar-icons?style=flat&color=4d6bfe" /> | VSCode 风格文件 / 文件夹图标主题:文件树与编辑器 Tab 换上熟悉的开发环境图标(vscode-icons 移植,纯 DOM 覆盖零侵入,安装 / 卸载零残留) |
|
||||
| [dong-victor/dsh-apes-sidebar-terminal-plus](https://github.com/dong-victor/dsh-apes-sidebar-terminal-plus) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-apes-sidebar-terminal-plus?style=flat&color=4d6bfe" /> | 终端增强:内嵌 Nerd Font 图标字体、修复 xterm 图标渲染、稳定终端 cwd |
|
||||
| [Max-Null/dsh-sidebar-preview-select](https://github.com/Max-Null/dsh-sidebar-preview-select) | <img alt="stars" src="https://img.shields.io/github/stars/Max-Null/dsh-sidebar-preview-select?style=flat&color=4d6bfe" /> | 预览划选增强:侧边栏预览里划选文本 → 浮动「发送到会话」 |
|
||||
| [Hoemr/dsh-quicklook](https://github.com/Hoemr/dsh-quicklook) | <img alt="stars" src="https://img.shields.io/github/stars/Hoemr/dsh-quicklook?style=flat&color=4d6bfe" /> | QuickLook 式空格预览:活动文件标签页按 Space 全尺寸查看图片 / PDF / 文本,Space 或 Esc 关闭 |
|
||||
|
||||
</details>
|
||||
|
||||
> 📣 **上架你的插件**:给仓库打上 `dsh-apes-sidebar` topic 即出现在 [topic 页](https://github.com/topics/dsh-apes-sidebar);再向 [`src/client/plugins-tabs.ts`](./src/client/plugins-tabs.ts) / [`src/client/plugins-viewers.ts`](./src/client/plugins-viewers.ts) 提一条 `PluginEntry` PR,即可进入设置页内置推荐目录(数据完整性由 `tests/plugin-list.spec.ts` 守护)。
|
||||
|
||||
## 🤝 参与贡献
|
||||
|
||||
- **代码改动走 PR**:`feat/*` / `fix/*` 分支开发 → `gh pr create`;纯文档改动可直接推 main
|
||||
- **收录生态插件**:给仓库打 `dsh-apes-sidebar` topic + 向 [`src/client/plugins-tabs.ts`](./src/client/plugins-tabs.ts) / [`plugins-viewers.ts`](./src/client/plugins-viewers.ts) 提 PR
|
||||
- **提交前自检**:`pnpm typecheck && pnpm build && pnpm test`(或 `make check` 一键聚合;CI 另有 npm 打包 → 真实挂载 → 无头渲染门禁 `pnpm test:mount`,及聚合双挂载回归 `pnpm test:mount:aggregate`)
|
||||
- 仓库工作规范见 [`AGENTS.md`](./AGENTS.md)(含仓库硬约束与 CI 说明)
|
||||
|
||||
## 👥 贡献者
|
||||
|
||||
感谢每一位贡献者:
|
||||
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=omdsh-dev/DSH-better-sidebar" alt="贡献者" />
|
||||
</a>
|
||||
|
||||
## 🔗 友情链接
|
||||
|
||||
- [dsh-tianshu-tui](https://github.com/huiliyi37/dsh-tianshu-tui):DeepSeek Harness 交互式终端 UI 插件(渲染核心由自研 harness agent Tianshu-Tui 演进而来),在官方基础上增加 TDD 与证据门等工作流
|
||||
- [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI):Claude Code 风格全屏交互终端插件——像素鲸鱼顶栏、实时工作状态行、思考流式展开、双击 Esc 回滚、上下文进度条 + TPS 仪表,npm 一键安装
|
||||
- [dshfind 插件超市](https://dshfind.com/zh/plugins):三方插件市场——GitHub topic `dsh-plugin` 下的公开仓库清单,每日同步 star、贡献者与增长数据
|
||||
- [DeepSeek Harness Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop):为 DeepSeek Harness 生态打造的现代化桌面端——无需配置 Node.js 或执行命令即可启动和管理本地 Harness 服务;[官网](https://www.dshdesktop.cn)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>MIT License · Built for the <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a> ecosystem · 在 <a href="https://github.com/topics/dsh-apes-sidebar">topic dsh-apes-sidebar</a> 发现更多生态插件</sub>
|
||||
</div>
|
||||
-429
@@ -1,429 +0,0 @@
|
||||
# dsh-apes-sidebar
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Built on DSH's native sidebar API** (since v0.19.0): the right column *is* DSH's own sidebar — the plugin registers every tab type as a native tab (no right panel of its own anymore) and keeps only its self-drawn bottom workbench and the `ctx.betterSidebar` service open to every plugin.
|
||||
>
|
||||
> **Since v0.24.1 the host support floor is DSH `0.2.0-rc.1+`** (peer floor `^0.2.0-rc.1`). Everything this plugin consumes from 0.2.0 is **purely additive** (zero removed exports, session format still v4, CLI and client runtime byte-identical), so this version carries no runtime compatibility branch — it only moves the support line. **Hosts on the 0.1.7 line should pin `dsh-apes-sidebar@0.22.1` — a caret never spans a minor bump, and `^0.1.7-rc.1` is silently disabled by the 0.2.0 startup preflight**; the DSH-to-plugin version table is in [Installation](#-installation).
|
||||
|
||||
<!-- Hero -->
|
||||
<div align="center">
|
||||
<b style="font-size: 1.15em;">A service-oriented sidebar framework, and a complete workbench out of the box</b><br /><br />
|
||||
<a href="https://www.npmjs.com/package/dsh-apes-sidebar"><img alt="npm version" src="https://img.shields.io/npm/v/dsh-apes-sidebar" /></a>
|
||||
<a href="https://www.npmjs.com/package/dsh-apes-sidebar"><img alt="npm downloads" src="https://img.shields.io/npm/dm/dsh-apes-sidebar" /></a>
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/omdsh-dev/DSH-better-sidebar/actions/workflows/ci.yml/badge.svg" /></a>
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/omdsh-dev/DSH-better-sidebar" /></a>
|
||||
<a href="https://opensource.org/licenses/MIT"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
|
||||
<a href="https://dshfind.com/en/plugins/omdsh-dev/DSH-better-sidebar?ref=badge"><img alt="dshfind" src="https://dshfind.com/api/badge/omdsh-dev/DSH-better-sidebar?lang=en" /></a><br /><br />
|
||||
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="Supported DSH versions (v0.24.1): 0.2.0-rc.1+" src="https://img.shields.io/badge/DSH-0.2.0--rc.1%2B-4d6bfe" /></a>
|
||||
<a href="https://github.com/topics/dsh-apes-sidebar"><img alt="Plugin ecosystem: GitHub topic dsh-apes-sidebar" src="https://img.shields.io/badge/plugin%20ecosystem-topic%20dsh--better--sidebar-4d6bfe" /></a><br /><br />
|
||||
<img alt="File management" src="https://img.shields.io/badge/-File%20management-4d6bfe" /> <img alt="Edit & preview" src="https://img.shields.io/badge/-Edit%20%26%20preview-4d6bfe" /> <img alt="Bottom workbench" src="https://img.shields.io/badge/-Bottom%20workbench-4d6bfe" /> <img alt="Changes" src="https://img.shields.io/badge/-Changes-4d6bfe" /> <img alt="Background tasks" src="https://img.shields.io/badge/-Background%20tasks-4d6bfe" /> <img alt="Side Chat" src="https://img.shields.io/badge/-Side%20Chat-4d6bfe" /> <img alt="Plugin integration" src="https://img.shields.io/badge/-Plugin%20integration-4d6bfe" /><br /><br />
|
||||
<b>A dual workbench (right sidebar + bottom panel)</b> that opens its <code>ctx.betterSidebar</code> service to every plugin —<br />
|
||||
register new sidebar pages and file viewers via <code>registerTab</code> / <code>registerFileViewer</code>.
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
🌏 <a href="./README.md">中文</a> · <a href="./README_EN.md"><b>English</b></a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<video src="https://github.com/user-attachments/assets/23187822-047e-45cc-b480-fe997bd55b86" muted autoplay loop playsinline controls width="100%"></video>
|
||||
<img alt="dsh-apes-sidebar workbench" src="https://github.com/user-attachments/assets/dfdb875e-a1a8-4d4b-8340-353736b1708f" />
|
||||
</div>
|
||||
|
||||
## 📑 Contents
|
||||
|
||||
- [✨ Features](#-features)
|
||||
- [🚀 Installation](#-installation)
|
||||
- [🖼️ Feature Tour](#-feature-tour)
|
||||
- [💬 Community](#-community)
|
||||
- [🆕 Recent Updates](#-recent-updates)
|
||||
- [⌨️ Keyboard Shortcuts](#-keyboard-shortcuts)
|
||||
- [🔌 Service API](#-service-api)
|
||||
- [🛠️ Development & Build](#-development--build)
|
||||
- [🔐 Security](#-security) · [⚠️ Known Limitations](#-known-limitations) · [🖥️ Platform Support](#-platform-support)
|
||||
- [🌐 Plugin Ecosystem](#-plugin-ecosystem) · [🤝 Contributing](#-contributing) · [👥 Contributors](#-contributors) · [🔗 Friends](#-friends)
|
||||
|
||||
## ✨ Features
|
||||
|
||||
What this plugin adds on top of DSH's stock sidebar:
|
||||
|
||||
- **✏️ Editable editor**: the host's document preview is **read-only** → the plugin keeps an **editable** CodeMirror editor (save, syntax highlighting, preview toggle); Markdown / HTML also render through the plugin's own pipeline (Mermaid diagrams with safe rendering + click-to-zoom, README-level inline HTML, floating table of contents, sandboxed HTML preview)
|
||||
- **🗂️ Enhanced file tree**: takes over the built-in Files page — lazy-loading tree, **expanded directories watched live and auto-refreshed**, symlink awareness, global filename search, drag-and-drop upload, hover `@file` to drop a reference into the input box; **Ctrl/Cmd multi-select and Shift range-select** (copy or delete a batch), **VS Code-style git status colors + M/A/D/U letters**, **new folder**, **multi-select “Zip and download”** (the host streams a ZIP built with no third-party dependency), and an “Open with” menu that COMBINES DSH’s own open-in-app (the host’s probed OS handlers + file-manager reveal) with the plugin’s own targets (file manager / VS Code / Cursor / Zed / custom editor URL templates, SSH remotes, pin-to-menu)
|
||||
- **🌿 Changes** (no Git panel in the stock sidebar): two lenses in one tab — **Git** (diff / history / stage·commit·revert) and **This Session** (every file the model touched) — with a unified diff renderer (intra-line character highlights, syntax coloring, secret redaction); the two lenses now share one 36px header (host SegmentedControl), 28px rows, a single empty state and a sticky commit bar, and the git snapshot is shared with the file tree so staging recolors the tree too
|
||||
- **🧩 Background Tasks** (absent upstream): agent topology preview + background task list (exit codes / live output / force-kill)
|
||||
- **💬 Side Chat** (absent upstream, beta): Codex-style side threads — inheriting the full parent context, running independently, promotable to a top-level session
|
||||
- **🖥️ Bottom workbench** (absent upstream): the right column belongs to DSH's native right sidebar; the plugin adds its own bottom workbench (drag-to-split panes, per-session persistence) that coexists with the native bar
|
||||
- **📂 Model-driven sidebar opens (opt-in)**: the `sidebar_open` tool lets the model actively open files / folders / web pages in the sidebar
|
||||
- **🔌 Service API**: `ctx.betterSidebar` is open to every plugin (`registerTab` / `registerFileViewer`); the built-in 5 tabs + 3 viewers go through the same API, and **28+ ecosystem plugins** already build on it (see "🌐 Plugin Ecosystem")
|
||||
- **⚡ On-demand loading**: ~325KB core at startup, editor / Mermaid / third-language dictionaries load on demand · **🌏 i18n** follows DSH's language · **🔁 Session isolation** persists layout per session
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
**Prerequisites**: DSH installed (`dsh web` boots), Node.js ≥ 20, pnpm ≥ 10.
|
||||
|
||||
**Supported DSH versions**:
|
||||
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="Supported DSH versions (v0.24.0): 0.2.0-rc.1+" src="https://img.shields.io/badge/DSH-0.2.0--rc.1%2B-4d6bfe" /></a>
|
||||
|
||||
> 📌 **Channel and support line**: `v0.24.1` targets DSH **0.2.0-rc.1+** (0.2.0's first candidate rides npm's `next` dist-tag; `latest` is still 0.1.7-rc.2). **Pin the DSH version exactly**: `npm i -g @deepseek-ai/dsh@0.2.0-rc.1`. **Hosts on the 0.1.7 line should pin `dsh-apes-sidebar@0.22.1`**: 0.2.0 is a host minor bump, and caret ranges such as `^0.1.7-rc.1` fail the host's startup compatibility preflight on 0.2.0, which disables the whole profile row silently.
|
||||
|
||||
> 🧭 **Pick the plugin version that matches your DSH**:
|
||||
>
|
||||
> | Your DSH | Install command | Version / peer declared |
|
||||
> | --- | --- | --- |
|
||||
> | **0.2.0-rc.1+** (including a later 0.2.0 stable) | `dsh plugin --profile web add dsh-apes-sidebar@latest` | **0.24.1**, `^0.2.0-rc.1` |
|
||||
> | **0.1.7-rc.1 – 0.1.7-rc.2** (and a later 0.1.7 stable; npm `latest` is still 0.1.7-rc.2) | `dsh plugin --profile web add dsh-apes-sidebar@0.22.1` | **0.22.1**, `^0.1.7-rc.1` |
|
||||
> | 0.1.7-alpha.1 / 0.1.7-alpha.2 | **nothing to install** — move DSH to rc.1 first, then run the row above:<br>`npm i -g @deepseek-ai/dsh@0.1.7-rc.1` | — |
|
||||
> | 0.1.6-alpha.2 and earlier, `0.1.5-rc.*` (including the 0.1.5-rc.3 that is npm's `latest`) | `dsh plugin --profile web add dsh-apes-sidebar@0.19.1` | **0.19.1**, `^0.1.5-rc.1` |
|
||||
> | `0.1.5-alpha.2` | `dsh plugin --profile web add dsh-apes-sidebar@0.19.0-alpha.1` | `^0.1.5-alpha.2` |
|
||||
> | `0.1.2-rc.*` | `dsh plugin --profile web add dsh-apes-sidebar@0.18.1` | `^0.1.2-rc.1` |
|
||||
> | `0.1.2-alpha.2` | `dsh plugin --profile web add dsh-apes-sidebar@0.18.0-alpha.0` | `^0.1.2-alpha.2` |
|
||||
> | `0.1.0-rc.8` / `0.1.1` | `dsh plugin --profile web add dsh-apes-sidebar@0.17.1` | `^0.1.0-rc.8` |
|
||||
>
|
||||
> Swap `web` for your own profile name. **Older versions are pinned exactly** (`@0.19.1`, not `@latest`), because `latest` moves forward with each new stable cut; conversely, do **not** install 0.19.1 on a 0.1.7 alpha — it would simply break.
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add dsh-apes-sidebar@latest
|
||||
```
|
||||
|
||||
> The plugin depends on no package that needs a build script (the terminal and `node-pty` went back to DSH wholesale), so installing is **one step**; once installed you can enable / disable it on DSH's own **Plugins page**.
|
||||
|
||||
Then **hard-refresh the browser** (Cmd/Ctrl+Shift+R) to see the sidebar (DSH hot-reloads client changes; only host-half updates need a restart).
|
||||
|
||||
**Or let DSH install it for you** — paste this prompt into any DSH session:
|
||||
|
||||
```text
|
||||
Install the dsh-apes-sidebar plugin (a sidebar workbench for DSH):
|
||||
1. Run: dsh plugin --profile web add dsh-apes-sidebar@latest (`latest` is the current stable)
|
||||
2. When done, remind me to hard-refresh the browser (Cmd/Ctrl+Shift+R)
|
||||
If anything fails, check the troubleshooting table in the README at https://github.com/omdsh-dev/DSH-better-sidebar
|
||||
```
|
||||
|
||||
**Option 3: one-shot script** — from a clone of this repo, run `bash scripts/install.sh` (macOS / Linux / Windows Git Bash; native Windows uses `install.ps1`; `-h` for options) — it installs and registers the bundle in one go (including idempotent cleanup of an old manual mount row).
|
||||
|
||||
<details>
|
||||
<summary><b>Updating</b></summary>
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add dsh-apes-sidebar@latest
|
||||
```
|
||||
|
||||
or bump the version in `~/.dsh/profiles/web/package.json` to the matching npm version (`"^0.22.1"`) and run `pnpm install`. Then hard-refresh the browser (Cmd/Ctrl+Shift+R) — client changes do not need a DSH restart.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Troubleshooting</b></summary>
|
||||
|
||||
| Symptom | Cause & fix |
|
||||
|---|---|
|
||||
| `Ignored build scripts` | pnpm 11 blocked a **transitive** dependency's build script. Run `pnpm approve-builds` (no `--all`) in the profile directory (`~/.dsh/profiles/web`) and follow its prompts — the plugin itself has no build-script dependency (`node-pty` left with the terminal). |
|
||||
| `minimum release age` / version `< 24h` | The release is younger than 24 hours. Wait, or re-run once (pnpm auto-adds `minimumReleaseAgeExclude`). |
|
||||
| "profile directory not found" | Run `dsh web` once so it initializes `~/.dsh/profiles/web`. |
|
||||
| Two sidebars on the page | Double-mount. Old hand-written line: `~/.dsh/profiles/web/cordis.patch.yml` still has `- insert: ... better-sidebar ...` — delete it (a same-id duplicate mount makes the loader fail loudly with `duplicate loader entry id`). When an aggregate bundle (e.g. `@linxin666/dsh-web-ui-all`) mounts this package under a **different** id, the plugin's own bundle patch backs off automatically since 0.13.x (it detects an already-enabled mount of the same package name and does not mount itself) — no manual fix needed; if it still double-mounts, make sure the aggregate bundle precedes `dsh-apes-sidebar` in `dsh.profile.bundles`. |
|
||||
| Where did my settings go after upgrading? | DSH 0.1.7 removed the registrable settings namespace: preferences now live on this plugin's **mount row** in the profile (entry id `better-sidebar` by default), not in `~/.dsh/settings.yaml`. On first boot the plugin imports the `dsh-apes-sidebar` section of the old `settings.yaml` (renamed `settings.yaml.imported` by the host) exactly once — only fields the current schema still declares, and only while that row has no user values yet, so it never overwrites values set after the upgrade. |
|
||||
| Terminal unusable / shell fails to start | The terminal comes from **DSH's own `ui-sidebar-terminal`** (this plugin no longer ships a terminal or `node-pty`, and has no terminal settings). Consult DSH's own docs for problems; if the error mentions build scripts, see the row above. |
|
||||
| `dsh: command not found` | Install DSH first, or run `npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-apes-sidebar@latest`. |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Install from source / develop (optional — alternative to the npm flow)</b></summary>
|
||||
|
||||
To debug local changes or track the dev branch, point the dependency at a local clone and build it yourself:
|
||||
|
||||
```text
|
||||
1. git clone https://github.com/omdsh-dev/DSH-better-sidebar.git ~/Code/DSH-better-sidebar
|
||||
cd ~/Code/DSH-better-sidebar && pnpm install && pnpm build
|
||||
2. In ~/.dsh/profiles/web/package.json dependencies write "dsh-apes-sidebar": "link:<absolute path of the clone>"
|
||||
3. Append this mount line to ~/.dsh/profiles/web/cordis.patch.yml (this row's `config` IS this plugin's settings form: the deployment limits `readLimit` / `mediaLimit` / `uploadLimit` / `listLimit` plus the user-preference fields — that is what the settings page writes; omit it and every field falls back to its schema default):
|
||||
- insert:
|
||||
- id: better-sidebar
|
||||
name: 'dsh-apes-sidebar'
|
||||
config:
|
||||
readLimit: 524288
|
||||
4. Run pnpm install in ~/.dsh/profiles/web
|
||||
5. Restart DSH and hard-refresh
|
||||
```
|
||||
|
||||
Update: `git pull && pnpm install && pnpm build` → just hard-refresh the browser (client changes hot-reload; only host-half changes need a DSH restart). To switch back to the npm channel, restore the matching npm version (`"^0.22.1"`) and re-run `pnpm install`.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Install via plugin-registry (optional — use either this or the main flow)</b></summary>
|
||||
|
||||
Prerequisite: DSH with [plugin-registry](https://github.com/dsh-external/plugin-registry) integrated (`dsh registry` available). **Enabling both channels double-mounts** (the Node half loads twice, the page gets two sidebars).
|
||||
|
||||
```sh
|
||||
git clone https://github.com/omdsh-dev/DSH-better-sidebar.git && cd DSH-better-sidebar
|
||||
pnpm install && pnpm build
|
||||
node scripts/package-registry.mjs # assemble the registry/ staging (manifest + artifacts + README, not committed)
|
||||
dsh registry install ./registry # install (disabled by default)
|
||||
dsh registry enable dsh-external/dsh-apes-sidebar
|
||||
```
|
||||
|
||||
Update: `git pull && pnpm install && pnpm build` → `node scripts/package-registry.mjs` → `dsh registry uninstall/install/enable`. Remove the other channel's mount before switching.
|
||||
|
||||
</details>
|
||||
|
||||
## 🖼️ Feature Tour
|
||||
|
||||
> Below are real UI screenshots (two per row; click to zoom).
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **🗂️ File Workbench: Explorer**<br/><sub>Two explorer modes: embedded in the file preview / standalone file tree. Lazy-loading directory tree whose **expanded directories the host watches per directory and re-lists on change**, symlinks classified by target kind (directory links expand, dangling links flagged), global filename search, file/folder upload buttons plus drag-drop upload, context menu (open in new tab / open to the side / new folder / Open with — the host’s probed OS handlers PLUS the plugin’s own targets (file manager / VS Code / Cursor / Zed / custom editors, SSH remotes, pin-to-menu) / copy paths / rename / delete), **Ctrl/Cmd multi-select with Shift range-select** (batch copy paths, batch delete, **zip and download**), **git changes colored by status with M/A/D/U letters**, and a hover `@file` button that references a file straight into the composer.</sub><br/><div align="center"><img width="420" alt="File explorer" src="https://github.com/user-attachments/assets/a410bfd2-a8ba-43e6-873e-22417756e94d" /></div> | **📝 Inline Preview: Markdown · HTML**<br/><sub>The Markdown preview renders **Mermaid diagrams** (strict-mode safe rendering + a second sanitize pass; click a diagram for a zoom modal with wheel-zoom and drag-pan), **README-level inline HTML** (badge walls `<div align=center>`, `<details>` blocks nesting markdown, inline tags in table cells — DOMPurify-sanitized, `<script>` stripped, local media rewritten to the session media route) and a floating **table of contents** (appears with ≥3 headings, smooth-scroll jumping, auto-expanding folded blocks); HTML uses the plugin's own **sandboxed preview** with the two escape hatches the host does not have (`htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe`). **Images / PDF / spreadsheets / Office are no longer a plugin capability** — the host's own document preview renders those formats.</sub><br/><div align="center"><img width="420" alt="Markdown + Mermaid preview" src="https://github.com/user-attachments/assets/fe0e5182-55bb-45cc-b98b-a2877c2bdd38" /></div> |
|
||||
| **🖥️ CodeMirror editor**<br/><sub>An **editable** text / code editor (save, syntax highlighting, preview toggle) — the host's own document preview is **read-only**, which is exactly why the plugin keeps its catch-all viewer.</sub><br/><div align="center"><img width="420" alt="CodeMirror editor" src="https://github.com/user-attachments/assets/b44b488e-568c-4ee0-b96c-e9c906598a77" /></div> | **🖼️ Images / PDF / spreadsheets / Office (provided by DSH)**<br/><sub>These read-only formats render in DSH's own `ui-sidebar-documentpreview`: host-side Office→PDF conversion, worker-backed spreadsheet tables, image / PDF zoom viewports, and **per-directory auto-refresh**. The plugin deleted its own image / pdf / download-fallback viewers and no longer claims those extensions.</sub><br/><div align="center"><img width="420" alt="Inline image preview" src="https://github.com/user-attachments/assets/f9a58c30-5b7a-48b5-9e22-37d7e071f593" /></div> |
|
||||
| **💻 Terminal (provided by DSH)**<br/><sub>The right-Sidebar terminal is provided by DSH's own `ui-sidebar-terminal`: shell picker, double-click rename, reconnect, restore after reload, theme and contrast following. The plugin no longer ships xterm + node-pty.<br/><br/>⚠️ **Model-side caveat**: the plugin's own 8 `terminal_*` tools (off by default) were the model's only **cross-call persistent** terminal; the upstream equivalent `@deepseek-ai/dsh-tool-terminal` is not mounted by any shipped bundle, so if you need that capability, add a `tool-terminal` row to your profile's `cordis.patch.yml` yourself.</sub><br/><div align="center"><img width="420" alt="Terminal" src="https://github.com/user-attachments/assets/0dad6ad3-ff3f-4b5a-86d2-f832ce65323e" /></div> | **🌿 Changes: Git lens + This-Session lens**<br/><sub>Two lenses on "what changed?": the **Git lens** keeps the full source-control surface (stage / unstage / commit (`Ctrl+Enter`) / revert, history, worktree and child-repo selectors); the **This Session** lens folds the session event log live, recording every file the model read / wrote / edited (grouped by file, kind-filtered, op-count badge). Both lenses now share one 36px header, 28px rows, a single empty state and a sticky commit bar; the **Git lens lists changes as a directory tree** (single-child chains collapsed into one row, collapsible folders with descendant counts, status letters and file icons on the leaves); the git snapshot is shared with the file tree, so staging recolors the tree as well. Clicking any change previews it in the **draggable bottom pane** with the unified diff — del red / add green / mod-blue pairing + intra-line character highlights + syntax coloring + context folding — or expands into a VSCode-style dedicated diff tab (same rendering stack).</sub><br/><div align="center"><img width="420" alt="Changes" src="https://github.com/user-attachments/assets/e7fc1220-305f-4bca-8583-e77ab4f4fa78" /></div> |
|
||||
| **🌐 External-link takeover (browser view provided by DSH)**<br/><sub>Web tabs are DSH's own `ui-sidebar-browser` (multiple tabs / back-forward-reload / address bar / sandboxed iframe), mounted **only in the desktop profile since 0.1.7** — the Web profile has no such kind. The plugin keeps the half the host does not provide: it takes over **only links a tab type explicitly claims through `urlTarget`** (Ctrl/Cmd-clicks always pass through) and **lets everything else through to the host** (whose `linkOpening` user setting decides where prose links go); the three protocol-routing settings are gone, and a claim whose target type is unavailable at open time falls back to `window.open`.</sub><br/><div align="center"><img width="420" alt="Browser" src="https://github.com/user-attachments/assets/9bc6b65a-64fc-4942-a685-76e391e55606" /></div> | **🧩 Tasks: Agent Topology + Background Jobs**<br/><sub>Live subagent-tree topology (run states, batched live previews) plus the background-jobs list (exit codes / live output / force-kill); new subagents / jobs can auto-activate the Tasks page, expanding the sidebar on wide viewports without forcing narrow full-screen drawers open (configurable).</sub><br/><div align="center"><img width="420" alt="Tasks: subagent topology" src="https://github.com/user-attachments/assets/dcd8ed2f-59fa-405b-937b-2d250f5034dd" /></div> |
|
||||
| **💬 Side Chat (beta)**<br/><sub>Codex-style side threads: **one independent tab per conversation**; the thread inherits the parent's full context (including the in-progress turn, honestly frozen as "interrupted") and runs independently without polluting the main session; follow-ups survive restarts; one click promotes the thread to a top-level session.</sub><br/><div align="center"><img width="420" alt="Side Chat (beta)" src="https://github.com/user-attachments/assets/3a338c36-f5de-4000-95f3-4b1cd04f60fc" /></div> | **🖥️ DSH's native right sidebar + plugin bottom workbench**<br/><sub>The right column is DSH's own sidebar: the plugin registers every tab type as a native tab (including taking over the built-in Files page), so clicking a file in the chat lands there directly — **formats the host's own document preview already covers are rendered by the host**, and the plugin claims only Markdown / HTML / editable code; the plugin's own bottom panel can stay open alongside it — drag a tab to a pane edge to **split**, to the middle to **merge**, drag the top edge to resize; the toggle lives in the session header.</sub><br/><div align="center"><img width="420" alt="Dual workbench (right sidebar + bottom panel)" src="https://github.com/user-attachments/assets/dfdb875e-a1a8-4d4b-8340-353736b1708f" /></div> |
|
||||
| **⚙️ Declarative Settings**<br/><sub>The "Side card" section in DSH settings: one small card per tab / viewer with an independent toggle (highlighted enabled state + brand switch); secondary settings open from the "Feature settings" strip at the card bottom (switch / text / number / select rows); plugin-owned settings persist under `pluginSettings`, while the whole preference set lives on this plugin's **mount row** in the profile (since DSH 0.1.7 settings are addressed by Loader entry id).</sub><br/><div align="center"><img width="420" alt="Declarative settings: side cards" src="https://github.com/user-attachments/assets/0800ca64-621e-48da-b7df-aecfddc3ec29" /></div> | **📱 Mobile**<br/><sub>On narrow screens (<768px) the panels become a full-width drawer: bottom-panel tabs merge into the sidebar once, with touch-friendly dragging.</sub><br/><div align="center"><img width="360" alt="Mobile full-width drawer" src="https://github.com/user-attachments/assets/a82ba78a-f4cf-4d85-80e8-050a05beb144" /></div> |
|
||||
|
||||
|
||||
|
||||
## 💬 Community
|
||||
|
||||
WeChat / QQ group QR codes will live here. After uploading the QR images (drag them into any issue/comment to get a `user-attachments` link), replace `src` below and uncomment:
|
||||
|
||||
<div align="center">
|
||||
<!-- WeChat group QR code
|
||||
<img width="220" alt="WeChat group QR code" src="https://github.com/user-attachments/assets/REPLACE_ME" />
|
||||
-->
|
||||
<!-- QQ group QR code
|
||||
<img width="220" alt="QQ group QR code" src="https://github.com/user-attachments/assets/REPLACE_ME" />
|
||||
-->
|
||||
</div>
|
||||
|
||||
## 🆕 Recent Updates
|
||||
<div align="center">
|
||||
<a href="https://github.com/user-attachments/assets/d2aea86b-a776-4f01-a6b8-b26b27314336"><img width="33%" alt="Sidebar" src="https://github.com/user-attachments/assets/d2aea86b-a776-4f01-a6b8-b26b27314336" /></a>
|
||||
<a href="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0"><img width="33%" alt="Service API base screenshot" src="https://github.com/user-attachments/assets/946f7028-4967-461e-a750-d1b5056b62d0" /></a>
|
||||
</div>
|
||||
|
||||
**Supported DSH versions**: <a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="Supported DSH versions (v0.24.0): 0.2.0-rc.1+" src="https://img.shields.io/badge/DSH-0.2.0--rc.1%2B-4d6bfe" /></a> · full release history on the [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases) page
|
||||
|
||||
### v0.24.1
|
||||
|
||||
> 🐞 **Fix release**: two defects made the file tree rebuild itself on every interaction — (1) the native surface re-created the `files` takeover’s slot registration on EVERY session-state write (folder toggles, tab switches, bottom-workbench drags), so the host swapped the slot entry and unmounted/remounted the whole tab body: the explorer lost its level cache, scroll position and directory watcher and re-listed the entire visible tree; (2) the live-refresh re-list deleted a level from the cache before refetching, replacing its rows with a loading placeholder (a build, a formatter or the model’s own bash made whole folders blink). Now a toggle asks only for the toggled level, the tab body survives, and disk changes update rows in place.
|
||||
|
||||
### v0.23.0
|
||||
|
||||
> 🧭 **Development-line version (never published to npm; its content ships with v0.24.1)**: the Files and Changes pages rebuilt. The Files page gains **Ctrl/Cmd and Shift multi-select**, a batch bar, **Git change coloring**, **new-folder creation**, reworked drag-and-drop upload and **"Zip and download" on a multi-selection** (a host-side archiving task with progress); "Open with" keeps the host-detected local apps and the plugin's own targets **side by side** (setting `openWithPluginTargets`), and the context menu was restructured; the Changes page is **a hierarchy tree** again (Git view + Agent view, directory staging). Performance: `fs.tree` measured **22.8ms → 3.8ms** (10k entries) with the new batch route `fs.trees` (mount/refresh: N+1 requests → one), and opening the context menu no longer re-lists directories. ⚠️ **Behavior change (security-relevant)**: the **workspace path fence was removed** — the plugin's fs routes can read and write any path the host user can reach (OS permissions only). Details in [CHANGELOG_EN](./CHANGELOG_EN.md#v0230).
|
||||
|
||||
### v0.24.0
|
||||
|
||||
> 📦 **Support line moved**: DSH **0.2.0-rc.1+** only (peer floor `^0.2.0-rc.1`, CI pins `@deepseek-ai/dsh@0.2.0-rc.1`). **The 0.1.7 line (including npm `latest` = 0.1.7-rc.2) should pin v0.22.1 (the last published release on the 0.1.7 line)**: a caret never spans a minor bump, so `^0.1.7-rc.1` fails the 0.2.0 host's startup compatibility preflight and the whole row is disabled (measured: `semver.satisfies('0.2.0-rc.1','^0.1.7-rc.1',{includePrerelease:true}) === false`).
|
||||
|
||||
- 📦 **Baseline lifted to `0.2.0-rc.1`**: 14 DSH peers and 27 `@deepseek-ai/*` devDependencies move together; `dsh.plugin.json`'s `engines.dsh` follows.
|
||||
- 🔍 **Measured as purely additive**: across the 19 host packages this plugin consumes, **zero value exports were removed**; the only type-surface changes are `ui-primitives` (optional props on `DisclosureRow` / `TextShimmer` / `Tooltip`, plus the overlay top inset), `dsh-session` (new `ToolCallRecovery`) and `dsh-api-remotes` (a new product-analytics remote). Session format stays v4, `SUBAGENT_DESCRIPTOR_VERSION` stays 3, and `dsh/lib/bin.js` plus the `dsh-client-modules` runtime are byte-identical — so **no compatibility branch for 0.1.7 is kept**.
|
||||
- 🧪 **Mount lane and CI pins follow to `0.2.0-rc.1`**; the peer-shape rule in `tests/market-manifest.spec.ts` now pins the current baseline tuple and records the "a caret never spans a minor bump" lesson.
|
||||
- ⚠️ **Ecosystem side effect**: the optional `@huanlin/dsh-plugin-better-locale` integration pins the `^0.1.x` line and cannot load on 0.2.0; its 5 unmet peers are the only residue in `pnpm peers check` (upstream has not adapted; unrelated to this plugin's own 14 peers).
|
||||
**Supported DSH versions**: <a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="Supported DSH versions (v0.22.1): 0.1.7-rc.1+" src="https://img.shields.io/badge/DSH-0.1.7--rc.1%2B-4d6bfe" /></a> · full release history on the [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases) page
|
||||
|
||||
### v0.22.1
|
||||
|
||||
> 📦 **Stable release** (npm `latest`): the support line is **unchanged** — DSH **0.1.7-rc.1+** only (peer floor `^0.1.7-rc.1`, CI pins `@deepseek-ai/dsh@0.1.7-rc.1`), so 0.21.1 / 0.22.0 users can upgrade straight away. Two defects that were **reproducible on a real host while every unit test stayed green** are fixed; **hosts on DSH 0.1.6-alpha.2 or earlier still pin v0.19.1**.
|
||||
|
||||
- 🐛 **The `files` takeover could be orphaned → error spam + an empty tree** (community issues #770 / #771, proven from the desktop shell's own log): during an in-page client entry replacement (plugin-market update, Plugins page disable→enable, HMR rebundle), `sync()`'s drop loop released the `files` takeover — which is **not a descriptor** — and re-created it in the same pass. That re-creation ran on an **already inactive** plugin context: `tabs.register` lives on the HOST context and took the id anyway, while the `ctx.slots.inject` right after it threw `cannot create effect on inactive context`, so the disposer was lost and the id became **unregistrable for the rest of the page's life** (`native register files error: … already registered` spam plus the Files window falling back to the host's empty state until a page refresh). The drop loop now **skips `FILES_KIND`** (the takeover lives and dies by the editor-type switch and the seat disposer only), and **any registration that fails after the host took the id is rolled back** (including slots already installed), leaving only a failure the next notification can retry. Taken from community PR #777 (@yanzhaohui1999).
|
||||
- 🖥️ **macOS desktop: window drag / double-click-title zoom stopped working** (#772): the plugin host is a direct `body` child, so the shell's `html[data-platform=darwin] body > :not(#root) { -webkit-app-region: no-drag }` applied to it — and app-region **ignores `pointer-events`** — leaving the viewport-sized panel layer cancelling every drag strip beneath it (the first drag worked, later ones did not). `[data-dsh-apes-sidebar]`, `[data-dsh-panel-host]` and the zoom modal `.mermaidModal` now opt out with the neutral `initial !important`, while panels and their controls stay `no-drag` so clicks are never swallowed. Merges community PR #773 and finishes the job for the zoom modal, the last viewport-sized body child.
|
||||
- ✅ **They stay fixed**: new unit cases pin "a notification must not tear the takeover down" and "a failed registration must release the type and the slots it installed" to the registry's **event log** (4/4 red on the unfixed code), plus a **deployment-level regression gate** `tests/e2e/native-reload.e2e.ts` (red on 3 consecutive runs against npm 0.22.0, green on 3 consecutive runs of the fixed build (one case, run repeatedly)). The drag contract is guarded by unit cases and by a real cascade probe in the mount lane that reads computed values against the shell's own rules. Verification: `pnpm test` 122 files / 1293 passed / 9 skipped, `pnpm test:mount` and `test:mount:aggregate` green. Incident write-up: [docs/plans/2026-09-28-native-files-takeover-reload-leak.md](./docs/plans/2026-09-28-native-files-takeover-reload-leak.md).
|
||||
|
||||
### v0.22.0
|
||||
|
||||
> 📦 **Stable release** (npm `latest`): the support line is **unchanged** — DSH **0.1.7-rc.1+** only (peer floor `^0.1.7-rc.1`, CI pins `@deepseek-ai/dsh@0.1.7-rc.1`), so v0.21.1 users can upgrade straight away. **Hosts on DSH 0.1.6-alpha.2 or earlier keep pinning v0.19.1.**
|
||||
|
||||
- 🧩 **The Tasks page is now a workflow graph** (the primary view): the session tree renders as layered nodes joined by bezier edges — drag to pan, wheel-zoom to the cursor, fit to the content box, a control cluster bottom-right (graph/tree toggle + fold switch + zoom + fit); the **classic indented tree is kept** (keyboard-navigable) and both modes share one view model, so fold state and team enrichment never drift apart.
|
||||
- 🃏 **Two-segment node cards**: the top segment is the kind badge (main agent / subagent / teammate / workflow / completed aggregate) + phase badge + name + meta; the bottom bar is the state dot + state word + **the same merged activity line the main agent shows** (concurrent tools grouped and counted, with the running call's detail — wording from the host's `chat` namespace) + a fold button on finished nodes; a running bar is swept across its **whole** width (disabled under `prefers-reduced-motion`). 8px-rounded, hierarchy carried by a faint top-segment tint only, the session you are on wearing a heavier accent border.
|
||||
- 🔀 **Workflow runs enter the graph**: runs folded from `tool-workflow/*` events (the same events the official panel folds) hang under their origin agent, with member agents re-parented below and boxed per phase with matching badges; members with no catalog row are synthesized from the run's own data, so a finished run still shows who took part.
|
||||
- 🗂 **Folding split into two groups that say what they hold**: `✓ N completed` (including failures, called out as `N failed`) and `N idle` (teammates that finished a turn and can be called back at any moment) are **two separate rows**; the manual fold button always works, the automatic rule only sweeps idle members once there are 3 or more, and an aggregate's name line reads "first two names + `+N`".
|
||||
- 🪟 **Two persistent floating windows** (extracted into a reusable `FloatingWindow`): background-job output and the shared task's detail/edit surface — draggable, resizable from every edge, a self-scrolling body, dismissed only by the close button or Escape (no outside click / blur / anchor observer); the task window hands its spare height to the description, so enlarging it gives the content room, and the action row is pinned to the bottom.
|
||||
- 👥 **Agent Teams board (experimental layer)**: members enrich their nodes and an always-visible strip lists the roster and shared tasks; the state machine follows the host (pending → claim → in progress → complete → reopen) with reassign / edit / two-step delete, and stale CAS revisions get their own notice; member activity is overlaid from `subagents.live`'s `running` flag.
|
||||
- 🔄 **Background jobs now read the host's client `ctx.jobs`** (a pushed roster plus a non-consuming output stream and `kill`): the plugin deleted its own `jobs.list` / `jobs.output` / `jobs.kill` routes and the event-replay mirror, never touching the model's `job_output` cursor; output streams in a persistent floating window with tail-follow, and the drawer auto-collapses at 8+ agents.
|
||||
- 🛠 **DSH 0.1.7 data-plane rewrite**: upstream deleted the three `agentTeams.remoteView`-style Remote methods → team reads moved to the Lead Session's **`agentTeam` Session projection** (push-based; the `teams.view` route and its 5-second poll are gone); the two write routes stay, with rejections moved from a result union to a thrown `TeamError` and a stale revision mapped to a 409 `team-conflict`. **The bug this fixed in the wild**: on 0.1.7 the team strip never rendered at all (the route answered `remoteView is not a function`, and the page degraded silently).
|
||||
- 🐛 **Four defects caught on a real host, all green in unit tests**: the per-node fold button did nothing (blocked by the automatic rule's guards); an idle card never drew a fold button; claiming a task mislabelled it "blocked"; and "complete" on a queued task always failed (a claim comes first).
|
||||
- 🎨 **Narrow panes and mobile settings**: card and row metrics re-tuned for the native right sidebar's narrow width; the settings page gained a **Mobile** group — on a narrow viewport (≤768px) the Tasks page no longer auto-opens and defaults to the tree view.
|
||||
|
||||
> 📜 **Earlier versions**: full release history in [CHANGELOG_EN.md](./CHANGELOG_EN.md) (v0.21.1 → v0.12.3) and on [GitHub Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases).
|
||||
|
||||
## ⌨️ Keyboard Shortcuts
|
||||
|
||||
| Action | Keys |
|
||||
|---|---|
|
||||
| Save edits | `Ctrl/Cmd + S` |
|
||||
| Git commit | `Ctrl + Enter` |
|
||||
| Close tab | Middle mouse button |
|
||||
| Tab context menu (right-click) | Close / Close Other Tabs / Close Tabs to the Left / Close Tabs to the Right (current pane) |
|
||||
| Split / merge panes | Drag tab to pane edge / middle |
|
||||
| Reference file to input | Hover the `@file` button at end of line |
|
||||
| Copy file path | Right-click row → copy relative/absolute path |
|
||||
|
||||
## 🔌 Service API
|
||||
|
||||
Since v0.4.0 the plugin exposes the `ctx.betterSidebar` service — other plugins can register sidebar pages and file viewers (the 5 built-in tabs + 3 viewers register through the same service). v0.12.1 completed the base capabilities (complete type exports, capability detection, state subscription, tab badges, lifecycle callbacks, targeted open, plugin-owned settings, etc.).
|
||||
|
||||
Full integration docs (complete fields, matching algorithm, HMR pitfalls, declarative settings, version detection, the native-sidebar surface and the skinning contract): **[`docs/external-plugin-guide.md`](./docs/external-plugin-guide.md)**; repository rules (hard constraints / CI / release) live in [`AGENTS.md`](./AGENTS.md).
|
||||
|
||||
### ➕ Add Plugins (recommended plugin catalog)
|
||||
|
||||
The dashed cards at the end of the "Sidebar content" / "File viewers" grids in the "Side Cards" settings section open the **Add tab plugins** / **Add preview plugins** modals: each declares its open extension point, offers a "**Browse more plugins on GitHub**" button (the [GitHub topic `dsh-apes-sidebar`](https://github.com/topics/dsh-apes-sidebar)), and lists the recommended catalog (name / repo / description / install script) — "**Open**" jumps to the repo, "**Copy**" writes the install command to the clipboard.
|
||||
|
||||
**Curating a new plugin**: append a `PluginEntry` to [`src/client/plugins-tabs.ts`](./src/client/plugins-tabs.ts) (tab registrations) or [`src/client/plugins-viewers.ts`](./src/client/plugins-viewers.ts) (file-previewer registrations) and tag your repo with the `dsh-apes-sidebar` topic; data integrity is guarded by `tests/plugin-list.spec.ts`.
|
||||
|
||||
## 🛠️ Development & Build
|
||||
|
||||
```sh
|
||||
pnpm install # @deepseek-ai/* devDependencies resolve (baseline 0.1.7-rc.1, alpha dist-tag) — no token needed
|
||||
pnpm typecheck # tsc --noEmit
|
||||
pnpm lint # eslint . (flat config: js + typescript-eslint + react-hooks recommended)
|
||||
pnpm build # → lib/index.js + lib/invariant.js + lib/client.js + lib/client-registry.js + lib/types
|
||||
pnpm test # vitest (includes manifest consistency guard; build first)
|
||||
pnpm watch # tsdown --watch
|
||||
```
|
||||
|
||||
**Make thin wrappers** (`make help` lists every target; package.json stays the single source of truth):
|
||||
|
||||
```sh
|
||||
make check # aggregate gate: typecheck → build → test → check:consumer-types (mirrors CI)
|
||||
make mount # real-mount smoke: build + pack → install Chromium → pnpm test:mount
|
||||
make clean # remove lib/, *.tgz, playwright-report/, test-results/
|
||||
```
|
||||
|
||||
`pnpm check:consumer-types`: the consumer-facing declaration-surface guard — type-checks the built `lib/types` from a browser-only consumer's perspective (no `@types/node`, `skipLibCheck: false`); run `pnpm build` first.
|
||||
|
||||
**Architecture**: a single npm package with host/client halves — host (`src/index.ts`): `/sidebar/api/*` JSON API, `/sidebar/file` media route, `/sidebar/html` preview route, `/sidebar/upload` upload route, and two WebSockets (`/sidebar/ws/agent-opens` for model-driven opens, `/sidebar/ws/fs-watch` for the file tree's directory watch; fs / git / preview are all session-scoped behind a trust fence); client (`src/client/index.tsx`): portal sidebar + views + link takeover; state persisted per session in localStorage. Organized per DSH official conventions (no default export, dual client bundles); no dependency on npm / checkout at runtime (`@deepseek-ai/*` provided by the web profile).
|
||||
|
||||
## 🔐 Security
|
||||
|
||||
- Routes protected by a Host-header trust fence (same as `/api`); `fs.write` is atomic; git only shells out to the CLI and never sets identity
|
||||
- ⚠️ **Since v0.23.0 the filesystem routes do NO workspace containment**: `fs.tree` / `fs.trees` / `fs.read` / `fs.write` / `fs.rename` / `fs.remove` / `fs.mkdir` / media / HTML preview / `/sidebar/upload` / `archive.build` reach **any path the host user can reach** (OS permissions are the only bound; the `workspaceFence` switch and the 403 branch are gone) — do not treat these routes as fenced
|
||||
- HTML preview content renders in an **opaque-origin sandboxed iframe** (no `allow-same-origin`/`allow-top-navigation`, `no-referrer`, all permission policies disabled); the `/sidebar/html` route carries a CSP `sandbox` + size/path bounds
|
||||
- The settings page can disable the HTML preview's sandbox per feature (`htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe`, off by default, with a warning) — when off, content shares the origin with the UI; only recommended for fully trusted content. **The web tab's sandbox is no longer this plugin's surface**: the browser view comes from the host (desktop profile); see DSH's own docs for its sandbox and navigation policy
|
||||
|
||||
## ⚠️ Known Limitations
|
||||
|
||||
- Git has no push/pull/fetch; Markdown previews provide a manual refresh button with confirmation before discarding unsaved edits; the file tree only watches **expanded** directories (collapsed folders are unsubscribed, and there is no recursive whole-workspace scan); tool inline file-open buttons cannot be intercepted
|
||||
- **Which read-only previews exist is the host's call**: spreadsheets / PDF / images / Office go to DSH's own `ui-sidebar-documentpreview`, while the plugin renders only Markdown / HTML and the editable text buffer; the host implementation (rendering details, zoom, refresh timing) follows the DSH version
|
||||
- **The browser view exists only in the desktop profile**: the Web profile has no host `browser` kind and the plugin no longer ships a browser tab, so web tabs are desktop-only; login state / third-party cookies / `X-Frame-Options` limits follow the host implementation
|
||||
- HTML preview renders the saved file (not unsaved drafts)
|
||||
- No bottom panel on mobile (<768px): on narrow screens its tabs merge into the right sidebar once (after migrating back to desktop they stay in the right sidebar); the desktop bottom panel is only available on wide viewports. Without a selected session, tapping the subdued toggle shows the select-session message; with a selected session, it opens the full-width drawer
|
||||
|
||||
## 🖥️ Platform Support
|
||||
|
||||
Windows / Linux / macOS (macOS validated daily; the rest covered by unit tests). The plugin carries no native dependencies (the terminal and `node-pty` went back to DSH wholesale), so building needs only Node + pnpm, with no compiler toolchain.
|
||||
|
||||
## 🌐 Plugin Ecosystem
|
||||
|
||||
The `ctx.betterSidebar` service opens two extension points to every plugin: **`registerTab` (sidebar pages)** and **`registerFileViewer` (file previewers)**. The 5 built-in tabs + 3 viewers register through the exact same API — fully equal capabilities.
|
||||
|
||||
```ts
|
||||
import type {} from 'dsh-apes-sidebar' // triggers the ctx.betterSidebar type merge
|
||||
export const inject = ['betterSidebar']
|
||||
export function apply(ctx: Context) {
|
||||
ctx.effect(() => ctx.betterSidebar.registerTab({
|
||||
id: 'my-plugin:db', title: 'Database', component: ({ scope }) => <DbView sessionId={scope.sessionId} />,
|
||||
}))
|
||||
ctx.effect(() => ctx.betterSidebar.registerFileViewer({
|
||||
id: 'my-plugin:csv', exts: ['csv'], fetchStrategy: 'custom',
|
||||
load: async (path, scope) => parseCsv(await fetchText(scope, path)),
|
||||
component: ({ customData }) => <CsvGrid rows={customData} />,
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
The GitHub topic [`dsh-apes-sidebar`](https://github.com/topics/dsh-apes-sidebar) already hosts **28+ ecosystem plugins** (and growing):
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e"><img width="66%" alt="The built-in Add Plugins modal: recommended catalog + one-click install command" src="https://github.com/user-attachments/assets/d4385b7e-aab4-425d-a5c4-2da5da81a34e" /></a><br />
|
||||
<i>The built-in "Add plugins" modal in settings: recommended catalog + one-click install command + a direct link to the GitHub topic</i>
|
||||
</div>
|
||||
|
||||
### 📑 Tab Plugins (sidebar pages)
|
||||
|
||||
<details>
|
||||
<summary><b>24 plugins (click to expand)</b></summary>
|
||||
|
||||
| Plugin | ⭐ | Description |
|
||||
|---|---|---|
|
||||
| [ChenRuoT/dsh-sidebar-qa](https://github.com/ChenRuoT/dsh-sidebar-qa) | <img alt="stars" src="https://img.shields.io/github/stars/ChenRuoT/dsh-sidebar-qa?style=flat&color=4d6bfe" /> | Selection-based side Q&A — Codex-style side questions / Claude Code `/btw` |
|
||||
| [fuhefei/dsh-sentinel](https://github.com/fuhefei/dsh-sentinel) | <img alt="stars" src="https://img.shields.io/github/stars/fuhefei/dsh-sentinel?style=flat&color=4d6bfe" /> | Condition-driven wakeup: file / command / HTTP / process / webhook watches that wake the agent; dock + sidebar branch + global dashboard |
|
||||
| [Fisfzy/ego-browser](https://github.com/Fisfzy/ego-browser) | <img alt="stars" src="https://img.shields.io/github/stars/Fisfzy/ego-browser?style=flat&color=4d6bfe" /> | Agent browser: a local browsing tab (`@dsh-external/ego-browser`, auto-registers the sidebar page when better-sidebar is present, floating-bubble fallback otherwise) |
|
||||
| [jiuge2467/dsh-studio](https://github.com/jiuge2467/dsh-studio) | <img alt="stars" src="https://img.shields.io/github/stars/jiuge2467/dsh-studio?style=flat&color=4d6bfe" /> | Full-stack enhancement workbench: multi-source MCP visual debugging hub, visual thinking engine |
|
||||
| [Iwctwbh/dsh-flowglass](https://github.com/Iwctwbh/dsh-flowglass) | <img alt="stars" src="https://img.shields.io/github/stars/Iwctwbh/dsh-flowglass?style=flat&color=4d6bfe" /> | Flowglass: live session flowgraph (messages / tool groups / subagent branches) |
|
||||
| [FeatherHunter/dsh-mattpocock-skills-deck](https://github.com/FeatherHunter/dsh-mattpocock-skills-deck) | <img alt="stars" src="https://img.shields.io/github/stars/FeatherHunter/dsh-mattpocock-skills-deck?style=flat&color=4d6bfe" /> | Game-like mission system for mattpocock/skills: fog-of-war map + task bar |
|
||||
| [GULI-lab/DSH-element-source](https://github.com/GULI-lab/DSH-element-source) | <img alt="stars" src="https://img.shields.io/github/stars/GULI-lab/DSH-element-source?style=flat&color=4d6bfe" /> | Click any UI element on your dev page to jump to its Vue / React / Svelte / Angular source, straight into the chat |
|
||||
| [Lzh3070/dsh-file-review-tab](https://github.com/Lzh3070/dsh-file-review-tab) | <img alt="stars" src="https://img.shields.io/github/stars/Lzh3070/dsh-file-review-tab?style=flat&color=4d6bfe" /> | File-change review tab: line-level red/green diffs + undo + chat-line deep links |
|
||||
| [yq04/dsh-git-remotes](https://github.com/yq04/dsh-git-remotes) | <img alt="stars" src="https://img.shields.io/github/stars/yq04/dsh-git-remotes?style=flat&color=4d6bfe" /> | Git remotes tab: branches / upstream / ahead-behind, fetch with prune, ff-only pull, confirm-before-push |
|
||||
| [ztyhehe/dsh-apes-sidebar-svn](https://github.com/ztyhehe/dsh-apes-sidebar-svn) | <img alt="stars" src="https://img.shields.io/github/stars/ztyhehe/dsh-apes-sidebar-svn?style=flat&color=4d6bfe" /> | SVN source-control tab: status / diff / log / commit / update / revert / conflict resolution — symmetric to the built-in Git panel |
|
||||
| [Melody-max114/dsh-excel-panel](https://github.com/Melody-max114/dsh-excel-panel) | <img alt="stars" src="https://img.shields.io/github/stars/Melody-max114/dsh-excel-panel?style=flat&color=4d6bfe" /> | Excel editing: xlsx preview/edit, live formula evaluation, merged cells, save back to the original file |
|
||||
| [v587d/dsh-anysearch-refs](https://github.com/v587d/dsh-anysearch-refs) | <img alt="stars" src="https://img.shields.io/github/stars/v587d/dsh-anysearch-refs?style=flat&color=4d6bfe" /> | AnySearch results as sidebar cards: query, source snippets, highlighted keywords |
|
||||
| [mlosun/dsh-docs-panel](https://github.com/mlosun/dsh-docs-panel) | <img alt="stars" src="https://img.shields.io/github/stars/mlosun/dsh-docs-panel?style=flat&color=4d6bfe" /> | Global docs panel: portable Markdown notes, readable from any workspace |
|
||||
| [lnyuqian/dsh-skill-sidebar](https://github.com/lnyuqian/dsh-skill-sidebar) | <img alt="stars" src="https://img.shields.io/github/stars/lnyuqian/dsh-skill-sidebar?style=flat&color=4d6bfe" /> | Skills panel: scans local skill directories, one-click invocation copy, pinning |
|
||||
| [g-yixuan/dsh-sidenote](https://github.com/g-yixuan/dsh-sidenote) | <img alt="stars" src="https://img.shields.io/github/stars/g-yixuan/dsh-sidenote?style=flat&color=4d6bfe" /> | Codex-style side chat + selection annotations (a thin consumer plugin) |
|
||||
| [thirsty5034/dsh-ssh-tunnel](https://github.com/thirsty5034/dsh-ssh-tunnel) | <img alt="stars" src="https://img.shields.io/github/stars/thirsty5034/dsh-ssh-tunnel?style=flat&color=4d6bfe" /> | Multi-host SSH tunnels + SSH manager tab |
|
||||
| [thirsty5034/dsh-git-forge](https://github.com/thirsty5034/dsh-git-forge) | <img alt="stars" src="https://img.shields.io/github/stars/thirsty5034/dsh-git-forge?style=flat&color=4d6bfe" /> | GitHub / Gitea accounts, project grants and push policy |
|
||||
| [YesSanSan/dsh-conversation-outline](https://github.com/YesSanSan/dsh-conversation-outline) | <img alt="stars" src="https://img.shields.io/github/stars/YesSanSan/dsh-conversation-outline?style=flat&color=4d6bfe" /> | Conversation outline tab: per-turn structure, quick jump, one-line LLM titles |
|
||||
| [Wulabalabo/dsh-sidebar-Explorer-Plus](https://github.com/Wulabalabo/dsh-sidebar-Explorer-Plus) | <img alt="stars" src="https://img.shields.io/github/stars/Wulabalabo/dsh-sidebar-Explorer-Plus?style=flat&color=4d6bfe" /> | File-manager tab: upload / move / delete / rename / new folder (write operations) |
|
||||
| [yq04/dsh-turn-review](https://github.com/yq04/dsh-turn-review) | <img alt="stars" src="https://img.shields.io/github/stars/yq04/dsh-turn-review?style=flat&color=4d6bfe" /> | Turn review: review agent changes turn by turn |
|
||||
| [Ghz114514/dsh-refpics](https://github.com/Ghz114514/dsh-refpics) | <img alt="stars" src="https://img.shields.io/github/stars/Ghz114514/dsh-refpics?style=flat&color=4d6bfe" /> | Pinterest-style reference-image search: masonry wall, sidebar board, downloads, save-to-Eagle |
|
||||
| [yzlin499/dsh-yzlin499-easy-plugins](https://github.com/yzlin499/dsh-yzlin499-easy-plugins) | <img alt="stars" src="https://img.shields.io/github/stars/yzlin499/dsh-yzlin499-easy-plugins?style=flat&color=4d6bfe" /> | A handy utility bundle for a bare-bones DSH |
|
||||
| [dong-victor/dsh-apes-sidebar-starter](https://github.com/dong-victor/dsh-apes-sidebar-starter) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-apes-sidebar-starter?style=flat&color=4d6bfe" /> | Run-configurations tab: IDEA-style Run/Debug configs (npm / springboot / python / custom) — one-click launch, history, WebSocket live logs (ANSI colors), parallel instances, cross-platform process-tree kill |
|
||||
| [baosfeng/my-dsh-plugins](https://github.com/baosfeng/my-dsh-plugins) | <img alt="stars" src="https://img.shields.io/github/stars/baosfeng/my-dsh-plugins?style=flat&color=4d6bfe" /> | Personal multi-plugin collection (`dsh-file-activity`): a sidebar file-activity tab recording read / added / modified history and stats, flat-browsed by folder, opened with the native preview |
|
||||
| [Hoemr/dsh-better-overleaf](https://github.com/Hoemr/dsh-better-overleaf) | <img alt="stars" src="https://img.shields.io/github/stars/Hoemr/dsh-better-overleaf?style=flat&color=4d6bfe" /> | Overleaf tab: direct-CDP browser login (third-party Chromium supported), project switching, local git mirrors under the workspace with two-way sync |
|
||||
|
||||
</details>
|
||||
|
||||
### 🖼️ Viewer Plugins (file previewers)
|
||||
|
||||
<details>
|
||||
<summary><b>3 plugins (click to expand)</b></summary>
|
||||
|
||||
| Plugin | ⭐ | Description |
|
||||
|---|---|---|
|
||||
| [HuanLinOTO/dsh-plugin-better-sidebar-plugin-office](https://github.com/HuanLinOTO/dsh-plugin-better-sidebar-plugin-office) | <img alt="stars" src="https://img.shields.io/github/stars/HuanLinOTO/dsh-plugin-better-sidebar-plugin-office?style=flat&color=4d6bfe" /> | Office-suite preview (.docx / .xlsx / .pptx) as a separate bundle to slim the core (in the official recommended catalog) |
|
||||
| [zemul/dsh-video-preview](https://github.com/zemul/dsh-video-preview) | <img alt="stars" src="https://img.shields.io/github/stars/zemul/dsh-video-preview?style=flat&color=4d6bfe" /> | Inline video preview: .mp4 / .webm / .mov / .mkv / .avi with a /video host route supporting HTTP Range scrubbing |
|
||||
| [dong-victor/dsh-apes-sidebar-jupyter](https://github.com/dong-victor/dsh-apes-sidebar-jupyter) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-apes-sidebar-jupyter?style=flat&color=4d6bfe" /> | Runnable `.ipynb` notebook view: lazy-start Python kernel, streaming outputs, save-back |
|
||||
|
||||
</details>
|
||||
|
||||
### 🧰 Enhancements & Tools
|
||||
|
||||
<details>
|
||||
<summary><b>3 plugins (click to expand)</b></summary>
|
||||
|
||||
| Plugin | ⭐ | Description |
|
||||
|---|---|---|
|
||||
| [dong-victor/dsh-apes-sidebar-terminal-plus](https://github.com/dong-victor/dsh-apes-sidebar-terminal-plus) | <img alt="stars" src="https://img.shields.io/github/stars/dong-victor/dsh-apes-sidebar-terminal-plus?style=flat&color=4d6bfe" /> | Terminal enhancement: bundled Nerd Font icons, xterm glyph fixes, stable terminal cwd |
|
||||
| [Max-Null/dsh-sidebar-preview-select](https://github.com/Max-Null/dsh-sidebar-preview-select) | <img alt="stars" src="https://img.shields.io/github/stars/Max-Null/dsh-sidebar-preview-select?style=flat&color=4d6bfe" /> | Preview selection boost: select text in any sidebar preview → floating "send to session" |
|
||||
| [Hoemr/dsh-quicklook](https://github.com/Hoemr/dsh-quicklook) | <img alt="stars" src="https://img.shields.io/github/stars/Hoemr/dsh-quicklook?style=flat&color=4d6bfe" /> | QuickLook-style Space preview: press Space on the active file tab for a full-size image / PDF / text overlay; Space or Esc closes |
|
||||
|
||||
</details>
|
||||
|
||||
> 📣 **List your plugin**: tag your repo with the `dsh-apes-sidebar` topic to appear on the [topic page](https://github.com/topics/dsh-apes-sidebar); then PR one `PluginEntry` into [`src/client/plugins-tabs.ts`](./src/client/plugins-tabs.ts) / [`src/client/plugins-viewers.ts`](./src/client/plugins-viewers.ts) to join the built-in recommended catalog (data integrity is guarded by `tests/plugin-list.spec.ts`).
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
- **Code changes go through PRs**: develop on a `feat/*` / `fix/*` branch, then `gh pr create`; docs-only changes may be pushed to main directly
|
||||
- **Curate an ecosystem plugin**: tag your repo with `dsh-apes-sidebar` + PR a `PluginEntry` into [`src/client/plugins-tabs.ts`](./src/client/plugins-tabs.ts) / [`plugins-viewers.ts`](./src/client/plugins-viewers.ts)
|
||||
- **Before submitting**: `pnpm typecheck && pnpm build && pnpm test` (or `make check` for the one-shot aggregate; CI additionally gates on npm-pack → real-mount → headless-render via `pnpm test:mount`, plus the aggregate double-mount regression `pnpm test:mount:aggregate`)
|
||||
- See [`AGENTS.md`](./AGENTS.md) for the repository rules (hard constraints, CI lanes, release flow)
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
Thanks to everyone who contributed:
|
||||
|
||||
<a href="https://github.com/omdsh-dev/DSH-better-sidebar/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=omdsh-dev/DSH-better-sidebar" alt="Contributors" />
|
||||
</a>
|
||||
|
||||
## 🔗 Friends
|
||||
|
||||
- [dsh-tianshu-tui](https://github.com/huiliyi37/dsh-tianshu-tui): an interactive terminal UI plugin for DeepSeek Harness (its rendering core evolved from the self-developed harness agent Tianshu-Tui), adding TDD and evidence-gate workflows on top of the official harness
|
||||
- [dsh-TUI](https://github.com/ccch1mneyyy/dsh-TUI): a Claude Code-style fullscreen interactive TUI plugin — pixel-whale top bar, live working-status row, streaming thought expansion, double-Esc rollback, context progress bar + TPS meter; one-command npm install
|
||||
- [dshfind Plugin Market](https://dshfind.com/zh/plugins): a third-party plugin marketplace — a listing of public repos under the GitHub topic `dsh-plugin`, with stars, contributors and growth data synced daily
|
||||
- [DeepSeek Harness Desktop](https://github.com/anywhere-labs/deepseek-harness-desktop): a modern desktop client for the DeepSeek Harness ecosystem — start and manage a local Harness service without configuring Node.js or running commands; [official site](https://www.dshdesktop.cn)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<sub>MIT License · Built for the <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a> ecosystem · discover more on the <a href="https://github.com/topics/dsh-apes-sidebar">dsh-apes-sidebar topic</a></sub>
|
||||
</div>
|
||||
+11
-40
@@ -1,42 +1,13 @@
|
||||
# dsh-apes-sidebar bundle patch
|
||||
# apes-sidebar bundle patch: inserts the host plugin row into the profile
|
||||
# roster. The client half (sidebar tabs: Tasks / Agents / Messages / Apps /
|
||||
# Profile) loads through the dsh.client.inject declaration in package.json;
|
||||
# this patch mounts the Node-side host entry only.
|
||||
#
|
||||
# This file is the `dsh.bundle.patch` layer of the published npm package: when
|
||||
# the plugin is installed through the official CLI —
|
||||
#
|
||||
# dsh plugin --profile <name> add dsh-apes-sidebar@<version>
|
||||
#
|
||||
# — the command reconciles `dsh.profile.bundles` against installed packages
|
||||
# and, seeing this declaration, appends `dsh-apes-sidebar` to the bundle
|
||||
# stack. The profile boot then merges THIS patch (a single `insert` of the
|
||||
# plugin row) exactly like the manual cordis.patch.yml mount line used
|
||||
# before. No profile file edits needed — one command installs and mounts.
|
||||
#
|
||||
# If the profile still carries the old manual mount line in its own
|
||||
# cordis.patch.yml, remove it before switching to the bundle channel to
|
||||
# avoid double-mounting (two Node halves, two sidebars).
|
||||
#
|
||||
# The `config.shell` / `config.shellArgs` rows are gone with the plugin's own
|
||||
# terminal (DSH 0.1.6 ships its own right-Sidebar terminal, and its shell is
|
||||
# chosen from the shell menu there):
|
||||
#
|
||||
# - insert:
|
||||
# - id: better-sidebar
|
||||
# name: 'dsh-apes-sidebar'
|
||||
#
|
||||
# Aggregate double-mount guard: aggregate bundles (e.g. @linxin666/dsh-web-ui-all)
|
||||
# may already mount this package under their own entry id (e.g.
|
||||
# `web-ui-better-sidebar`). Two mounts register /sidebar/api twice and fail the
|
||||
# whole plugin tree at boot ("duplicate prefix route"). The `!!js` disabled
|
||||
# expression backs THIS row off when another *enabled* entry already mounts
|
||||
# `dsh-apes-sidebar`; the aggregate instance then owns the sidebar. The
|
||||
# loader evaluates it when this row is processed (entry-list order, only rows
|
||||
# before this one are visible), so the aggregate bundle must precede this one
|
||||
# in `dsh.profile.bundles` — the standard install order, since `dsh plugin add`
|
||||
# appends new bundles last. The reverse order (this bundle before the
|
||||
# aggregate) is a known limitation: the guard cannot see rows that come after
|
||||
# it, and no runtime singleton guard is added. Same-id manual duplicates still
|
||||
# fail loudly at the loader as before.
|
||||
# Double-mount guard: if another *enabled* entry already mounts the same
|
||||
# package (e.g. via an aggregate bundle), this row backs off.
|
||||
- insert:
|
||||
- id: better-sidebar
|
||||
name: 'dsh-apes-sidebar'
|
||||
disabled: !!js "[...ctx.loader.entries()].some((e) => e.options.name === 'dsh-apes-sidebar' && e.options.id !== 'better-sidebar' && !e.disabled)"
|
||||
- id: apes-sidebar
|
||||
name: apes-sidebar
|
||||
disabled: !!js >
|
||||
[...ctx.loader.entries()].some((e) =>
|
||||
e.options.name === 'apes-sidebar' && e.options.id !== 'apes-sidebar' && !e.disabled)
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
# apes-sidebar 重写方案(v2)— 冲突检查通过,从零开始
|
||||
|
||||
> 目标:重写现有 `apes-sidebar`(v0.4.0,单文件手写 JS client bundle),
|
||||
> **代码规范/工程方案学习 [DSH-better-sidebar v0.18.0](https://github.com/omdsh-dev/DSH-better-sidebar)**,
|
||||
> **需求功能/交互能力学习 [tutti-os/tutti](https://github.com/tutti-os/tutti)**。
|
||||
>
|
||||
> 分析基线:DSH-better-sidebar `v0.18.0`(浅克隆 /tmp/DSH-better-sidebar)· Tutti(本地 /Users/figmarlite/Desktop/…/APES/tutti)
|
||||
> 生成时间:2026-09-03 · 用途:重构开工前对齐,供评审与拆单。
|
||||
|
||||
## 0. 冲突检查结论(2026-09-03)
|
||||
|
||||
**目标自洽,不冲突,无需推翻重规划。** 两个参考源互补:
|
||||
- better-sidebar v0.18.0 = 「怎么建」(工程规范/构建/测试/i18n/token/服务契约)
|
||||
- tutti-os/tutti = 「做什么」(面板模型/角标/agent 可编程驱动)
|
||||
|
||||
三处主动调整(借"从零开始"授权做干净化):
|
||||
1. **Tab id 换前缀**:旧 `wb:*` → 新 `apes:*`(apes:tasks / apes:agents / apes:apps / apes:messages / apes:profile),不留 `wb` 历史包袱。
|
||||
2. **定位收敛**:只做 **tasks/agents/apps/messages/profile** 五面板信息层;files/terminal/browser/editor 由 better-sidebar 宿主提供,**不重复造轮子**(复用 `openTab/openFile` + 宿主内置 tab 即可)。
|
||||
3. **放弃团队房间**:tutti 的 rooms/team 依赖 `dsh-background-agents` 0.7.x,与 DSH 基座 loader id 冲突会崩(历史教训),v2 不做。
|
||||
|
||||
唯一硬约束:peer 钉 `@deepseek-ai/cordis ^4.0.2` + `dsh-better-sidebar ^0.18.0`(better-sidebar 发 npm `next` dist-tag,随 DSH rc.x 漂移;升级前必须跑挂载冒烟)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状诊断(旧版 v0.4.0 的痛点)
|
||||
|
||||
### 1.1 现状结构
|
||||
|
||||
```
|
||||
figmar/apes-sidebar (v0.4.0)
|
||||
├── package.json # peer: cordis ^4.0.1 / better-sidebar ^0.16.0 / react 18
|
||||
├── cordis.patch.yml # insert apes-sidebar(双挂载守卫)
|
||||
├── lib/index.js # host 最小入口(~1KB,只打日志)
|
||||
└── lib/client.js # 45KB 单文件,window.__ModuleLoader__.load 手写 bundle
|
||||
# 纯 JS + React.createElement,无 JSX/TS/构建/测试
|
||||
```
|
||||
|
||||
注册 4 个 Tab(全部基于 better-sidebar `ctx.betterSidebar.registerTab`):
|
||||
|
||||
| Tab id | 作用 | 参照 |
|
||||
|---|---|---|
|
||||
| `wb:tasks` | 工作区+时间分组会话列表、搜索、运行中徽标、运行中任务区 | WorkBuddy |
|
||||
| `wb:agents` | 子代理/后台 agent 列表、状态、一键跳转 | Tutti multi-agent |
|
||||
| `wb:apps` | 应用中心(Design/PPT/Video 网格) | Tutti App Center |
|
||||
| `wb:profile` | Provider/模型信息 + 设置/帮助入口 | WorkBuddy 底部 |
|
||||
|
||||
配置:better-sidebar 设置页 `pluginSettings['apes-sidebar']`(`enableTasks`/`enableAgents`/`enableApps`/`enableProfile`/`displayName`/`showTime`/`showUngrouped`/`appsList`)。
|
||||
|
||||
### 1.2 主要痛点
|
||||
|
||||
1. **单文件 45KB 手写 JS bundle**:无类型、无模块拆分、无构建产物管理;
|
||||
CSS 以字符串内联注入(`APPS_CSS` 拼接),每次改样式要手工编辑字符串。
|
||||
2. **零测试**:无 spec,无 CI 挂载冒烟——任何 API 漂移(如 DSH rc 升级)静默崩。
|
||||
3. **i18n 手写**:本地 zh/en 两字典,不跟随宿主 `ctx.locale`,第三方语言直接缺。
|
||||
4. **皮肤脆弱**:CSS 硬编码 `#fff` 渐变等(`card-icon` 用 `#3370ff→#7c3aed`),不消费
|
||||
`--dsw-alias-*` token 链,深色/浅色/品牌换肤会破。
|
||||
5. **状态模型简单**:轮询/快照读取 sessions/workspaces,无订阅模型、无 memo、无懒加载。
|
||||
6. **无 host 能力**:只有日志入口。需要 host 路由(读会话日志、子代理拓扑、job 回放)
|
||||
时只能绕 DSH 网关或放弃。
|
||||
7. **版本基线落后**:peer 只到 better-sidebar ^0.16.0;而 better-sidebar v0.18.0
|
||||
已毕业正式版、只支持 DSH 0.1.2-rc.1+(协议、RPC 方言、client store 均大变)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 参考源 A:DSH-better-sidebar v0.18.0 工程规范(「怎么建」)
|
||||
|
||||
### 2.1 包结构(published npm package)
|
||||
|
||||
- `name: dsh-better-sidebar` / `version: 0.18.0` / `type: module` / `pnpm@11.8.0`
|
||||
- `main: lib/index.js`;exports 子路径:`.` / `./invariant` / `./client` / `./client/service` / `./client/api` / `./src/*` / `./package.json`
|
||||
- `dsh.bundle.patch: ./cordis.patch.yml`(bundle 激活的**硬性资格**)
|
||||
- `dsh.client.inject`: `[locale, ui-slots, ui-conversation, modules]`,`platform: web`
|
||||
- `dsh.plugin.json`:市场 manifest 形态(id/version/main/engines.dsh/contributes/client.main)
|
||||
- `files`: lib + src + cordis.patch.yml + scripts + README
|
||||
|
||||
### 2.2 构建与质量
|
||||
|
||||
- `build`: `tsc -p tsconfig.build.json && tsdown`(类型声明 + JS 产物)
|
||||
- `typecheck` / `test`(vitest) / `lint:ts`(oxlint) / `playwright`(e2e)
|
||||
- **懒加载 chunk**:重依赖(xterm/CodeMirror/mermaid)独立 bundle `lib/client-<name>.js`,
|
||||
经 `/sidebar/bundle` 按需下发、`globalThis.__dshChunks__` 物化(`src/client/chunk-loader.ts`);
|
||||
核心 bundle 禁止静态 import chunks。
|
||||
- **CI 挂载冒烟**(`plugin-mount` job):`pnpm pack` → scratch profile 真实安装 →
|
||||
`dsh web`(keyless)→ Playwright 断言 `[data-dsh-better-sidebar]` 挂载、无 pageerror、
|
||||
+ 菜单逐 tab 打开、editor chunk 强制加载。本地:`pnpm build && pnpm pack && pnpm test:mount`。
|
||||
|
||||
### 2.3 仓库硬约束(AGENTS.md)
|
||||
|
||||
- 禁止修改 DSH 源码(零写入 `~/.dsh/source/current`);改动必须 PR(`feat/*`/`fix/*`)。
|
||||
- 挂载只走 `cordis.patch.yml` + profile;**不反向侵入 DSH**。
|
||||
- 市场约束:`dependencies/peerDependencies/optionalDependencies` **一律不得出现 `cordis`**;
|
||||
`scripts` 不得含 `preinstall/install/postinstall/prepare`(`tests/market-manifest.spec.ts` 守护)。
|
||||
- 缺能力时用 DSH 现成只读/公开 API 或插件自有路由(如 jobs.output 事件回放),不改 DSH。
|
||||
|
||||
### 2.4 架构:host half + client half
|
||||
|
||||
**host half(Node 侧,`src/index.ts` ~1300 行)**
|
||||
- 导出 `export const name = 'dsh-better-sidebar'`、`inject = ['webServer','sessions','webRuntime','tools']`、`export function apply(ctx)`。
|
||||
- 插件自有路由全走同源 fence:`/sidebar/api/*`(JSON:fs.tree/search/read/write、git.*、session.cwd、
|
||||
changes.ops…)、`/sidebar/file`(媒体)、`/sidebar/html`(预览)、`/sidebar/bundle`(懒加载)、
|
||||
`/sidebar/ws/*`(终端 WebSocket upgrade)。
|
||||
- **信任围栏** `trust-fence.ts`:与 /api 网关同源(Host-header loopback / webRuntime trustedHosts),
|
||||
逐请求读取;**路径安全** `path-security.ts`:会话 cwd 内解析、workspaceFence 守卫、写路径原子写(tmp+rename)。
|
||||
- 会话隔离:所有请求带 sessionId,权威 cwd 来自 session store(header → client cwd → persistence → process.cwd 四级回退)。
|
||||
- PTY 管理:`pty-manager.ts` + `agent-pty.ts`(agent 终端注册表、resize gate、按会话配额)。
|
||||
- 工具注册:`registerTools`(agent 可调用)、`agent-opens.ts`(agent 主动开 tab 的 registry + open tool)。
|
||||
|
||||
**client half(浏览器侧,`src/client/index.tsx` ~446 行)**
|
||||
- `inject = ['slots','sessions','locale','modules','connection']`
|
||||
- `attachLocale(ctx.locale)` + `ctx.effect` 注册词典(zh/en 为主,第三语言走 better-locale 可选覆盖)。
|
||||
- `createSidebarStore()` 单例 per activation → `ctx.provide('betterSidebar', service)`。
|
||||
- `ctx.effect(() => registerBuiltins(ctx, service))`:内置 7 tab + 6 viewer **自己吃自己的狗粮**。
|
||||
- 挂载:`document.body` 追加 host div → `createRoot` 渲染 `RenderBoundary > Sidebar`;
|
||||
bodyObserver 防宿主移除;几何自检防页面 transform 漂移(degraded viewport sync)。
|
||||
- 拦截层:turn-tail 拦截、open-path 拦截(`remote.session.openWorkspacePath` 遮蔽)、外链拦截
|
||||
(urlTarget 认领,默认 browser tab)、**IME 守卫**(composition 键防第三方 UI 抢键)。
|
||||
- 设置面:`ctx.slots.inject('settings.section', …)` 声明「Side card」设置分区。
|
||||
|
||||
### 2.5 服务契约(消费插件 API,也是 apes-sidebar 重写后要面向的基座)
|
||||
|
||||
`ctx.betterSidebar`(client-only):
|
||||
|
||||
```ts
|
||||
registerTab(descriptor: TabDescriptor): () => void // 返回 disposer,HMR-safe
|
||||
registerFileViewer(descriptor: FileViewerDescriptor): () => void
|
||||
getTabs() / getFileViewers() / getTab(id)
|
||||
isTabEnabled(id) / isViewerEnabled(id)
|
||||
matchFileViewer(path, head?)
|
||||
openTab(seed: OpenTabSeed, scope?) / closeTab(tabId, scope?)
|
||||
openFile(scope, path, title?)
|
||||
updateTab / activateTab
|
||||
subscribe(listener) / getSnapshot() / subscribeState(listener)
|
||||
```
|
||||
|
||||
`TabDescriptor` 关键字段(v0.18.0 全量):
|
||||
|
||||
```ts
|
||||
interface TabDescriptor {
|
||||
id: string
|
||||
title: string | (() => string)
|
||||
icon?: ReactNode | ((size: number) => ReactNode)
|
||||
order?: number // + 菜单排序,默认 100
|
||||
hidden?: boolean
|
||||
available?: (ctx, scope, state) => boolean // + 菜单禁用谓词
|
||||
single?: boolean // 单实例(dedupeKey: () => id 语法糖)
|
||||
dedupeKey?: (tab) => string | undefined
|
||||
createTab?: (state) => { tab, patch? } | null // 自定义 mint tab id
|
||||
urlTarget?: (url: URL) => boolean // 外链认领
|
||||
settings?: SidebarSettingsDeclaration // toggles / pluginToggles / render
|
||||
badge?: (ctx, scope, state) => string | number | null | undefined // Tab 角标
|
||||
onOpen? / onActivate? / onClose? // 生命周期(仅 service 路径触发)
|
||||
component: (props: TabComponentProps) => ReactNode
|
||||
}
|
||||
```
|
||||
|
||||
声明式设置:`'switch' | 'text' | 'number' | 'select'`;select 支持 `multi` 与图标选项卡。
|
||||
`TabComponentProps = { ctx, store, scope, tab, visible, expanded?, revealed?, onToggleDir?/onOpenFile?/… }`。
|
||||
|
||||
**接入规范(外部插件视角)**:
|
||||
- peerDependencies: `@deepseek-ai/cordis` + `dsh-better-sidebar`(optional);
|
||||
type-only import 触发 `declare module '@deepseek-ai/cordis'` 类型合并;
|
||||
- 纯浏览器插件走 `import type {} from 'dsh-better-sidebar/client/service'`(零 Node 依赖图);
|
||||
- client half `inject: ['betterSidebar', …]`,`ctx.effect(() => ctx.betterSidebar.registerTab(...))`;
|
||||
- 宿主 half 无此服务——需要读状态走 better-sidebar 自己的 HTTP/WS 路由。
|
||||
|
||||
### 2.6 i18n / 皮肤 / 无障碍规范
|
||||
|
||||
- i18n:词典注册到 DSH `ctx.locale`(LOCALE_NS);新增 zh key 必须同步 ja(否则 ja 回退 en);
|
||||
渲染 MarkdownText 必须经 `markdownTextProps()`(labels 嵌套契约)。
|
||||
- 皮肤:**视觉值只消费 `--dsw-alias-*` / `--dsw-font-*` / `--ds-*` 令牌**,无硬编码颜色;
|
||||
硬编码 hex 只能做 token 链末端 fallback(`color:var(--x,#fff)`),并有 `tests/theme.spec.ts` 守护。
|
||||
- a11y:role=tablist/tab/separator、aria-selected/valuenow、focus-visible ring、IME guard。
|
||||
|
||||
---
|
||||
|
||||
## 3. 参考源 B:Tutti 侧边栏能力(「做什么」)
|
||||
|
||||
### 3.1 面板模型(packages/agent/gui/workbench/tool-sidebar/model.ts)
|
||||
|
||||
```ts
|
||||
export const agentToolPanelIds = ['files','terminal','browser','tasks','apps','messages','side'] as const
|
||||
```
|
||||
|
||||
- 每面板有 label、可开多实例(resourceId 区分,如 browser tab 带 agent session id)。
|
||||
- 每面板默认/最小/最大宽度(files 最大不限、side 默认 440 最大 600、其余默认 720/最小 420)。
|
||||
- `mainContentMinWidth = 280`:主内容区最小宽度保护。
|
||||
|
||||
### 3.2 行为层(AgentToolSidebar.tsx + useAgentToolSidebarController)
|
||||
|
||||
- **多标签**:TabBar(role=tablist),每个 tab = panel + resourceId;关闭/激活/拖拽。
|
||||
- **可编程 handle**(AgentToolSidebarHandle,agent/宿主可驱动):
|
||||
`addPanel / ensurePanel / openPanel / closeTab / close / collapseForContainerConstraint`
|
||||
- **容器约束自动折叠**:`shouldAutoCollapseAgentToolSidebar({containerWidth, mainContentMinWidth, sidebarWidth})`
|
||||
→ 窗口变窄时 `collapseForContainerConstraint()`(不关 tab,只收起到 Dock)。
|
||||
- **宽度记忆 + 动画**:`onLayoutWidthChange`、resize handle(键盘可操作 role=separator)、
|
||||
width transition 260ms。
|
||||
- **懒挂载渲染**:`contentReadyTabIds` + `renderLoading`(未就绪 tab 显示 loading,就绪才挂)。
|
||||
- **空态 Picker**:`isEmptySidebar` → AgentToolSidebarPicker(从 + 菜单选面板)。
|
||||
- **展开态**:expand 按钮让活动面板撑满(expanded panel 概念)。
|
||||
|
||||
### 3.3 信息架构与角标(StandaloneAgentToolSidebar.tsx)
|
||||
|
||||
- `panels` 条件组装:`side` 面板仅在有 side conversation identity 时出现(`canAdd: false`)。
|
||||
- `quickActionPanels`: 有自动化 browser 时把 browser 提到 quick action;常驻 tasks/apps/messages。
|
||||
- `reminders`(角标计数):
|
||||
- `browser: automationBrowserCount`(运行中 automation browser tab 数)
|
||||
- `messages: messageCenterWorkingCount`(工作会话中 working agent 消费者计数,订阅 sessionEngine)
|
||||
- 对应旧版 apes-sidebar 的 badge 概念:tasks 徽标 = 运行中会话数;agents 徽标 = 运行子代理数。
|
||||
|
||||
### 3.4 各面板承载(渲染层职责划分)
|
||||
|
||||
| 面板 | 承载 | apes-sidebar 对应/借鉴 |
|
||||
|---|---|---|
|
||||
| files | 文件树/编辑器 | (better-sidebar 已有 editor)不重复 |
|
||||
| terminal | 终端 | (better-sidebar 已有)不重复 |
|
||||
| browser | 内嵌浏览器 | (better-sidebar 已有)不重复 |
|
||||
| tasks | Issue Manager/任务编排 | **apes:wb:tasks 升级对象**:会话/任务拓扑 |
|
||||
| apps | App Center | **apes:wb:apps 对齐对象**:应用网格+调起 |
|
||||
| messages | 消息中心(审批/通知/后台工作汇总) | **apes 可新增 wb:messages** |
|
||||
| side | Side Conversation(子会话并排) | 子代理会话展示可借鉴 |
|
||||
|
||||
Tutti 其余值得迁移的交互:控制中心(一个视图看所有 agent 对话+待审批+运行任务)、
|
||||
goal→tasks 自动拆分、@ 深度引用、task 分配给人/agent。
|
||||
|
||||
### 3.5 进程/事件模型
|
||||
|
||||
- 会话与活动数据经 `sessionEngine`/activity 服务订阅(`subscribe + getSnapshot` 模式,
|
||||
即 React 19 `useSyncExternalStore` 形状)——ape-sidebar 应把「轮询」升级为订阅 + memo。
|
||||
- 子代理/后台工作 → `subagent-live-route` / jobs 回放(better-sidebar 已实现:
|
||||
`jobs-routes.ts` 从会话事件日志回放 job 输出、`subagent-live-route.ts` 批量子代理状态)。
|
||||
|
||||
---
|
||||
|
||||
## 4. apes-sidebar v2 目标架构
|
||||
|
||||
### 4.1 定位
|
||||
|
||||
> **apes-sidebar v2 = 构建在 better-sidebar v0.18.0 之上的「工作空间指挥层」**,
|
||||
> 不重复 files/terminal/browser/editor(better-sidebar 已有),聚焦 Tutti 风格
|
||||
> **任务 / Agent / 消息 / 应用**四类信息面板 + host 侧路由与 agent 可编程驱动。
|
||||
|
||||
仍是一个 **client-half 注册型插件**(注册到 `ctx.betterSidebar`),但工程规范全面对齐 better-sidebar。
|
||||
|
||||
### 4.2 目标仓库布局
|
||||
|
||||
```
|
||||
apes-sidebar/
|
||||
├── package.json # peer: @deepseek-ai/cordis ^4.0.2 / dsh-better-sidebar ^0.18.0 / react ^18
|
||||
├── cordis.patch.yml # insert apes-sidebar(双挂载守卫,抄 v0.18.0 写法)
|
||||
├── tsconfig.json / tsconfig.build.json
|
||||
├── tsdown.config.ts # 多产物:lib/index.js + lib/client.js + lib/client-<chunk>.js
|
||||
├── vitest.config.ts / playwright.config.ts
|
||||
├── scripts/e2e-mount.sh # scratch profile 真实挂载冒烟
|
||||
├── AGENTS.md # 仓库硬约束(参照 better-sidebar)
|
||||
├── src/
|
||||
│ ├── index.ts # host half:路由 + 工具(若需要)
|
||||
│ ├── context-types.ts # declare module '@deepseek-ai/cordis' 类型合并
|
||||
│ ├── client/
|
||||
│ │ ├── index.tsx # apply():attachLocale → 注册词典 → registerTabs
|
||||
│ │ ├── locales.ts # zh/en(跟随 ctx.locale)
|
||||
│ │ ├── api.ts # 插件自有路由 client
|
||||
│ │ ├── tabs/
|
||||
│ │ │ ├── TasksTab.tsx # 会话/任务导航(保留 wb:tasks id 兼容持久化)
|
||||
│ │ │ ├── AgentsTab.tsx # Agent/子代理拓扑(wb:agents)
|
||||
│ │ │ ├── AppsTab.tsx # 应用中心(wb:apps)
|
||||
│ │ │ ├── MessagesTab.tsx # (新)消息中心/审批汇总
|
||||
│ │ │ └── ProfileTab.tsx # 底部用户区(wb:profile)
|
||||
│ │ └── builtins.ts # 统一注册(吃自己的狗粮)
|
||||
│ └── (host routes 需要时) sidechat-routes.ts / subagent-routes.ts
|
||||
└── tests/
|
||||
├── tabs.spec.tsx / theme.spec.ts / market-manifest.spec.ts
|
||||
└── e2e/mount.e2e.ts
|
||||
```
|
||||
|
||||
### 4.3 关键工程决策
|
||||
|
||||
1. **TS + tsdown 双产物**:lib/index.js(host)、lib/client.js(client 入口)、
|
||||
chunks 按需(若引入 mermaid/图表等重依赖);`files` 白名单发布。
|
||||
2. **client inject 精简**:只声明实际 require 的宿主包
|
||||
(`@deepseek-ai/dsh-client-ui-primitives` + 服务注入 betterSidebar/sessions/workspaces/locale 经 cordis inject,不塞 dsh.client.inject)。
|
||||
3. **host half 保持最小**:纯展示可无 host;需要会话日志回放/子代理状态时新增
|
||||
better-sidebar 风格的自有路由(同源 fence + 会话隔离),不碰 DSH 源码。
|
||||
4. **Tab id 向后兼容**:`wb:tasks` / `wb:agents` / `wb:apps` / `wb:profile` 保持不变
|
||||
(避免破坏已持久化 pluginToggles / 用户布局);样式 data 属性带 `apes-sidebar-` 前缀。
|
||||
5. **配置迁移到声明式 settings**:沿用 `pluginToggles`(switch/select)机制,
|
||||
在 better-sidebar 设置页渲染;新增 `displayName` select(apes-sidebar/feyOS/自定义)。
|
||||
6. **i18n 对齐**:词典注册 `ctx.locale`(zh/en);新增 key 同步第三方语言或做 en 回退。
|
||||
7. **皮肤对齐**:全部视觉消费 `--dsw-alias-*`/`--ds-*` token,硬编码 hex 仅作链尾 fallback。
|
||||
8. **badge/reminders 对齐 Tutti**:TasksTab 徽标 = 运行中会话/任务数;AgentsTab 徽标 =
|
||||
运行子代理数(订阅 sessionEngine 而非轮询)。若可经 host 路由读 agent consumer counts,
|
||||
则消息中心徽标 = working count(对应 Tutti messageCenterWorkingCount)。
|
||||
9. **懒挂载**:未激活 tab 不渲染内容;首次打开再挂(contentReady + loading),
|
||||
与 better-sidebar TabContent 行为一致。
|
||||
10. **测试门禁**:至少 tabs 注册/开关/渲染 smoke + 挂载 e2e + 皮肤 token 守护 +
|
||||
market manifest 守护(cordis 不得出现在 dependencies)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 功能清单(Tutti → apes-sidebar 映射)
|
||||
|
||||
| # | 能力 | Tutti 参照 | apes-sidebar v2 落地 | 优先级 |
|
||||
|---|---|---|---|---|
|
||||
| F1 | 会话/任务导航(工作区+时间分组、搜索、运行徽标) | Tasks | `wb:tasks` 升级:订阅式 + 运行中任务区 + 拓扑 | P0 |
|
||||
| F2 | Agent 总览(子代理/后台 agent、状态、跳转) | multi-agent | `wb:agents` 升级:订阅式、subagent lineage | P0 |
|
||||
| F3 | 应用中心(已装应用网格 + 调起) | App Center | `wb:apps`:appsList 可配置、viewId 调起多级降级 | P0 |
|
||||
| F4 | 消息中心/审批/工作汇总(角标) | messages + messageCenterWorkingCount | `wb:messages`(新) | P1 |
|
||||
| F5 | 任务自动拆分(goal→tasks) | Tutti goal 编排 | 规划:输入目标→子任务草稿→一键建会话(DSH 能力内简化版) | P2 |
|
||||
| F6 | 显示名/开关/子设置 | — | 声明式 settings(pluginToggles) | P0 |
|
||||
| F7 | agent 可编程开 tab | AgentToolSidebarHandle | host route + better-sidebar openTab/openFile 组合 | P1 |
|
||||
| F8 | 自动折叠/宽度记忆 | shouldAutoCollapseAgentToolSidebar | 交 better-sidebar 容器(若支持)或本地监听 | P2 |
|
||||
| F9 | Profile 用户区 | — | `wb:profile`:模型/provider/连接状态 | P0 |
|
||||
| F10 | 深色/浅色/换肤 | skin tokens | 全 token 化 | P0 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 分阶段实施
|
||||
|
||||
### Phase 0:工程骨架(0.5d)
|
||||
- 建仓、tsconfig/tsdown/vitest/playwright、package.json(peer 钉 cordis ^4.0.2 + better-sidebar ^0.18.0)
|
||||
- cordis.patch.yml(v0.18.0 守卫写法)、AGENTS.md、最小 host+client 空注册冒烟
|
||||
- 通过 `pnpm test:mount` 在 scratch profile 验证挂载
|
||||
|
||||
### Phase 1:4 Tab 平移 + 质量化(1-1.5d)
|
||||
- 把旧 client.js 的 4 视图用 TSX 重写,行为不变(保留 wb:* id)
|
||||
- i18n → ctx.locale、皮肤 → token、样式 → CSS modules
|
||||
- 配置 → pluginToggles 声明式
|
||||
- 单测:注册数/开关/渲染/badge
|
||||
|
||||
### Phase 2:Tutti 能力增强(2-3d)
|
||||
- `wb:tasks`:订阅式 sessionEngine 快照 + 运行中任务区(子代理拓扑优先排序)
|
||||
- `wb:agents`:subagent lineage + 状态订阅
|
||||
- 新增 `wb:messages`(若 host 有 working count 数据源)
|
||||
- agent 可编程开 tab(F7)最小实现
|
||||
|
||||
### Phase 3:打磨与发布(1d)
|
||||
- 皮肤 token 全检、a11y、IME guard、错误边界
|
||||
- e2e 挂载冒烟入 CI、Gitea 发布 + npm 发版脚本
|
||||
|
||||
---
|
||||
|
||||
## 7. 验证清单(每个 Phase 完成时)
|
||||
|
||||
```bash
|
||||
pnpm typecheck
|
||||
pnpm test # vitest 单测
|
||||
pnpm lint:ts
|
||||
pnpm build && pnpm pack
|
||||
pnpm test:mount # scratch profile 真实挂载(需 dsh + playwright chromium)
|
||||
# 真机:dsh plugin --profile desktop add file:<tarball> → 硬刷新 → + 菜单见新 tab
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险与注意
|
||||
|
||||
- **DSH rc 漂移**:better-sidebar 是 0.1.2-rc.1 线,DSH 官方 rc.x 发 next dist-tag;
|
||||
重写期间锁定 devDeps peer,升级前跑 `pnpm peers check`。
|
||||
- **不要装 dsh-background-agents 0.7.x**:与 DSH 基座 loader 条目 id 冲突 → 启动崩溃
|
||||
(历史教训),团队房间类能力暂缓。
|
||||
- **apps 调起无公开一键 API**:维持多级防御(uiConversation.activateView → slots.set → 提示)。
|
||||
- **client 纯模块表**:运行时只能 require react/react-dom/primitives 等宿主提供模块;
|
||||
其余依赖必须内联或走懒加载 chunk,否则构建纯度门会拦。
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
# Word 缩放与 PDF 拖拽修复设计
|
||||
|
||||
> 日期: 2026-08-10
|
||||
> 状态: 已批准
|
||||
|
||||
## Word 缩放
|
||||
|
||||
- 默认 100%,范围 50%–200%,步进 10%。
|
||||
- `Alt + 滚轮` 调整缩放并阻止 viewport 默认滚动。
|
||||
- 底部固定 range slider,同步显示百分比。
|
||||
- 使用 CSS `zoom` 缩放 docx-preview 专属容器,使滚动尺寸同步变化。
|
||||
- 切换 Word 文件时恢复 100%。
|
||||
|
||||
## PDF 拖拽
|
||||
|
||||
- iframe 放入独立 stage。
|
||||
- stage 内常驻透明 drag shield。
|
||||
- Tab 拖拽或面板/分栏调整期间立即启用 shield;结束、取消或窗口失焦时关闭。
|
||||
- shield 捕获 Chromium 内置 PDF viewer 吞掉的 dragover/pointer 流,正常状态不影响 PDF 交互。
|
||||
@@ -1,366 +0,0 @@
|
||||
# Office 三件套预览 — 设计文档
|
||||
|
||||
> 日期:2026-08-10
|
||||
> 状态:已批准(待实现)
|
||||
> 范围:`dsh-better-sidebar` 插件,`EditorView` 文件类型扩展
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
当前 `EditorView.tsx` 对 `.docx/.xlsx/.pptx` 及旧格式 `.doc/.xls/.ppt` 一律显示 binary 占位(`EditorView.tsx:210`),因为 host 的 `readText` 检测到 NUL 字节就标记 binary(`src/index.ts:108`),而 Office 文件本质是 zip。
|
||||
|
||||
**目标**:为 Office 三件套新增有意义的预览能力,保持插件现有架构纪律(host 零改动、bundle 纯度门、跨平台、失败模式全覆盖)。
|
||||
|
||||
**非目标**:
|
||||
- 不做 Office 文件编辑(只预览)
|
||||
- 不做 `push/pull/fetch` 之类网络 Git 操作(与本次无关,沿用 v1 限制)
|
||||
- 不做文件 watcher
|
||||
|
||||
## 2. 范围与选型
|
||||
|
||||
| 类型 | 方案 | 库 | 加载策略 | host 改动 |
|
||||
|---|---|---|---|---|
|
||||
| `.docx` | 保真渲染(保留样式/页眉页脚/图片/表格) | `docx-preview` | 静态 import(~500KB,小) | 零 |
|
||||
| `.xlsx` | 完整电子表格(公式/图表/条件格式/多 sheet) | `@univerjs/*` 全家桶 + `xlsx`(SheetJS,导入) | `dynamic import()` 懒加载(~5MB,首次打开 .xlsx 才加载) | 零 |
|
||||
| `.pptx` | 降级下载按钮 | — | — | — |
|
||||
| `.doc/.xls/.ppt`(OLE 二进制) | 降级下载按钮 | 纯前端无成熟库 | — | — |
|
||||
|
||||
**决策依据**:
|
||||
- 纯前端路线符合现有 bundle 纯度门(`tsdown.config.ts:128` 的 purity gate 只禁 `@deepseek-ai/*` value import,普通 npm 包内联不受限)与跨平台原则(无需 host 装重依赖)。
|
||||
- `pptx` 纯前端无成熟渲染库( OOXML slides + layouts + masters + shapes 解析极复杂),YAGNI 降级。
|
||||
- 旧 OLE 二进制格式(`.doc/.xls/.ppt`)纯前端基本无解,降级。
|
||||
- Univer 选 dynamic import 懒加载:静态内联会让 client bundle 从 2MB→7MB(gzip 1.5MB→3MB),首屏负担过重。
|
||||
|
||||
## 3. 架构
|
||||
|
||||
### 3.1 host 半:零改动
|
||||
|
||||
复用现有 `/sidebar/file` media route(`src/index.ts:349-392`):
|
||||
- 该路由已支持任意文件字节(不限扩展名,只检查 `isWithin(cwd, path)` + `isFile` + `size <= mediaLimit`)
|
||||
- `MEDIA_TYPES`(`index.ts:44`)对 Office 扩展名 fallback 到 `application/octet-stream`,client 用 `fetch` 拿 ArrayBuffer,不依赖 content-type
|
||||
- `mediaLimit` 默认 20MB,对 Office 文件足够(超限降级下载)
|
||||
|
||||
**不改 `fs.read`**:Office 文件不走文本读取路径(会被 NUL 探测当 binary 拒绝),直接走 media route。
|
||||
|
||||
### 3.2 client 半:EditorView 类型扩展
|
||||
|
||||
`EditorLoad` 类型(`EditorView.tsx:26`)扩展:
|
||||
|
||||
```ts
|
||||
type EditorLoad =
|
||||
| { status: 'loading' }
|
||||
| { status: 'error'; message: string }
|
||||
| { status: 'ready'; kind: 'text' | 'image' | 'md' | 'docx' | 'xlsx'; content: string; truncated: boolean }
|
||||
| { status: 'binary' } // 现保留:未知二进制 / pptx / OLE 旧格式 → 下载按钮
|
||||
```
|
||||
|
||||
加载流程(`EditorView.tsx:60` 的 `useEffect`):
|
||||
1. `api.fsRead(scope, path)` 仍调(用于探测 binary/text)— 对 Office 文件会返回 `{kind:'binary'}`,据此分流
|
||||
2. 识别扩展名 ∈ `{.docx, .xlsx}` → `setLoad({ status:'ready', kind:'docx'|'xlsx', content:'', truncated:false })`
|
||||
3. 识别 `.pptx` / `.doc` / `.xls` / `.ppt` / 其他未知 binary → 保持 `{status:'binary'}`,但 binary 占位改为"下载查看"按钮(见 §3.5)
|
||||
4. `IMAGE_EXT` / `MD_EXT` 分支不变
|
||||
|
||||
> 备注:step 1 的 `fsRead` 对 Office 文件是冗余往返(必定返回 binary)。可选优化:client 侧先按扩展名短路,Office 扩展名直接走 media route 不调 `fsRead`。实现时取短路方案,省一次请求。
|
||||
|
||||
### 3.3 文件类型分发(纯函数,易测)
|
||||
|
||||
新增 `src/client/office-types.ts`:
|
||||
|
||||
```ts
|
||||
export const OFFICE_PREVIEWABLE = ['.docx', '.xlsx'] as const
|
||||
export const OFFICE_DOWNLOAD_ONLY = ['.pptx', '.doc', '.xls', '.ppt'] as const
|
||||
|
||||
export type OfficeKind = 'docx' | 'xlsx' | 'download-only' | null
|
||||
|
||||
export function officeKindForExt(ext: string): OfficeKind {
|
||||
if (ext === '.docx') return 'docx'
|
||||
if (ext === '.xlsx') return 'xlsx'
|
||||
if (['.pptx', '.doc', '.xls', '.ppt'].includes(ext)) return 'download-only'
|
||||
return null
|
||||
}
|
||||
```
|
||||
|
||||
`EditorView` 的 `extOfPath` 已有(`EditorView.tsx:235`),复用。
|
||||
|
||||
### 3.4 DocxView / XlsxView 组件
|
||||
|
||||
新增 `src/client/office-view.tsx`,导出 `DocxView` 与 `XlsxView`,各自管理库生命周期。
|
||||
|
||||
#### DocxView
|
||||
|
||||
```tsx
|
||||
export function DocxView(props: { scope: SessionScope; path: string; title: string }) {
|
||||
const hostRef = useRef<HTMLDivElement>(null)
|
||||
const [load, setLoad] = useState<{status:'loading'|'ready'|'error'; message?: string}>({status:'loading'})
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const container = hostRef.current
|
||||
if (container === null) return
|
||||
;(async () => {
|
||||
try {
|
||||
const buf = await fetch(mediaUrl(scope, path)).then(r => r.arrayBuffer())
|
||||
if (cancelled) return
|
||||
const { renderAsync } = await import('docx-preview') // 静态 import 亦可,此处用动态保持一致
|
||||
await renderAsync(buf, container, null, { className: 'docx', inWrapper: true })
|
||||
if (!cancelled) setLoad({status:'ready'})
|
||||
} catch (error) {
|
||||
if (!cancelled) setLoad({status:'error', message: error instanceof Error ? error.message : String(error)})
|
||||
}
|
||||
})()
|
||||
return () => { cancelled = true; container.innerHTML = '' }
|
||||
}, [scope.sessionId, scope.cwd, path])
|
||||
|
||||
// 渲染:host div + loading/error 占位
|
||||
}
|
||||
```
|
||||
|
||||
- `docx-preview` 的 `renderAsync(arrayBuffer, container, null, options)` 把 docx 渲染到 container,保留样式/页眉页脚/图片(图片库内部 base64 内联处理)
|
||||
- 卸载清空 `container.innerHTML`(docx-preview 无 dispose API,DOM 清空即可)
|
||||
|
||||
#### XlsxView
|
||||
|
||||
```tsx
|
||||
export function XlsxView(props: { scope: SessionScope; path: string; title: string }) {
|
||||
const hostRef = useRef<HTMLDivElement>(null)
|
||||
const univerRef = useRef<Univer | null>(null)
|
||||
const [load, setLoad] = useState<{status:'loading'|'ready'|'error'; message?: string}>({status:'loading'})
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const container = hostRef.current
|
||||
if (container === null) return
|
||||
;(async () => {
|
||||
try {
|
||||
const buf = await fetch(mediaUrl(scope, path)).then(r => r.arrayBuffer())
|
||||
if (cancelled) return
|
||||
// 懒加载 Univer 全家桶(首次打开 .xlsx 才下载 ~5MB chunk)
|
||||
const { createUniver, defaultLocale } = await import('@univerjs/core')
|
||||
const { UniverSheetsCorePlugin } = await import('@univerjs/sheets-ui')
|
||||
const { UniverSheetsFormulaPlugin } = await import('@univerjs/sheets-formula')
|
||||
const { UniverSheetsConditionalFormattingPlugin } = await import('@univerjs/sheets-conditional-formatting')
|
||||
const { UniverSheetsChartPlugin } = await import('@univerjs/sheets-chart')
|
||||
const { UniverDocsPlugin } = await import('@univerjs/docs-ui')
|
||||
// xlsx 导入:SheetJS 解析后转 Univer workbook data
|
||||
const XLSX = await import('xlsx')
|
||||
const wb = XLSX.read(buf, { type: 'array' })
|
||||
const workbookData = xlsxWorkbookToUniver(wb) // 转换函数,见 §3.6
|
||||
|
||||
const univer = createUniver({
|
||||
locale: defaultLocale,
|
||||
plugins: [
|
||||
UniverDocsPlugin,
|
||||
UniverSheetsCorePlugin,
|
||||
UniverSheetsFormulaPlugin,
|
||||
UniverSheetsConditionalFormattingPlugin,
|
||||
UniverSheetsChartPlugin,
|
||||
],
|
||||
})
|
||||
univer.createUniverSheet(workbookData)
|
||||
univerRef.current = univer
|
||||
if (!cancelled) setLoad({status:'ready'})
|
||||
} catch (error) {
|
||||
if (!cancelled) setLoad({status:'error', message: ...})
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
cancelled = true
|
||||
univerRef.current?.dispose() // 关键:防 canvas + worker 泄漏
|
||||
univerRef.current = null
|
||||
}
|
||||
}, [scope.sessionId, scope.cwd, path])
|
||||
|
||||
// 渲染:host div + loading/error 占位
|
||||
}
|
||||
```
|
||||
|
||||
- **dispose 纪律**:卸载必须 `univer.dispose()`,否则 canvas + worker 泄漏(类比 `TerminalView` 的 `term.dispose()` + `observer.disconnect()`)
|
||||
- Univer 的 UI 需要容器有尺寸(类似 xterm),复用 `ResizeObserver` 模式
|
||||
|
||||
### 3.5 binary 占位升级为下载按钮
|
||||
|
||||
`EditorView.tsx:210` 现状:`{t('binary')}` 纯文本占位。
|
||||
|
||||
改为:
|
||||
|
||||
```tsx
|
||||
{load.status === 'binary' && (
|
||||
<div className={css.editorBinary}>
|
||||
<span>{t('binaryNoPreview')}</span>
|
||||
<a className={css.editorDownloadLink} href={downloadUrl(scope, path)} download>
|
||||
{t('downloadToView')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
```
|
||||
|
||||
- 复用现有 `downloadUrl(scope, path)`(`api.ts:140`),走 `/sidebar/file?download=1`
|
||||
- 对所有 binary 统一(不只 Office)— PDF、zip、exe 等都受益
|
||||
|
||||
### 3.6 xlsx → Univer workbook data 转换
|
||||
|
||||
SheetJS 解析的 workbook 结构与 Univer 的 `IWorkbookData` 不同,需转换层 `src/client/xlsx-to-univer.ts`:
|
||||
|
||||
- SheetJS `wb.SheetNames[]` + `wb.Sheets[name]` → Univer `sheets: Record<sheetId, IWorksheetData>`
|
||||
- 单元格:`ws[addr]` → `cellData[row][col]`,映射类型(s/n/b/e)、值、样式(对齐/字体/背景/边框)
|
||||
- 合并单元格:`ws['!merges']` → `mergeData`
|
||||
- 列宽/行高:`ws['!cols']` / `ws['!rows']` → `columnData` / `rowData`
|
||||
- 公式:SheetJS `cell.f` → Univer 公式格式
|
||||
- 图表/条件格式:SheetJS 不解析这些,需 Univer importer 或跳过(首版可只迁数据+样式+合并+公式)
|
||||
|
||||
**首版范围**:数据 + 基本样式(字体/颜色/对齐/边框/合并)+ 公式(文本形式即可,Univer 引擎计算)。图表/条件格式若 Univer importer 直接支持则用,否则跳过。
|
||||
|
||||
> 备选:用 Univer 官方的 `@univerjs/sheets-import-export`(基于 SheetJS 封装,直接产出 `IWorkbookData`),省去自写转换层。实现时优先评估此包,若版本耦合/体积可接受则用,否则自写。
|
||||
|
||||
### 3.7 EditorView 集成
|
||||
|
||||
`EditorView.tsx` 的 `renderTab` / `TabContent` 分发(`Sidebar.tsx:49` 的 switch)无需改 — editor tab 仍走 `EditorView`。
|
||||
|
||||
`EditorView` 内部 `useEffect`(`EditorView.tsx:60`)加 Office 分支:
|
||||
|
||||
```ts
|
||||
const ext = extOfPath(path)
|
||||
const officeKind = officeKindForExt(ext)
|
||||
if (officeKind === 'docx' || officeKind === 'xlsx') {
|
||||
setLoad({ status: 'ready', kind: officeKind, content: '', truncated: false })
|
||||
return // 不再走 fsRead(短路,省一次 binary 往返)
|
||||
}
|
||||
// 原有 IMAGE / MD / text / binary 逻辑不变
|
||||
```
|
||||
|
||||
渲染区(`EditorView.tsx:220` 附近)加:
|
||||
|
||||
```tsx
|
||||
{load.status === 'ready' && load.kind === 'docx' && (
|
||||
<DocxView scope={scope} path={path} title={title} />
|
||||
)}
|
||||
{load.status === 'ready' && load.kind === 'xlsx' && (
|
||||
<XlsxView scope={scope} path={path} title={title} />
|
||||
)}
|
||||
```
|
||||
|
||||
## 4. 错误处理
|
||||
|
||||
| 场景 | 检测 | 响应 |
|
||||
|---|---|---|
|
||||
| 文件损坏(非有效 zip) | 库抛错 | 显示错误信息 + 下载按钮 |
|
||||
| `>20MB`(超 `mediaLimit`) | fetch 响应 / host 返回 400 | 提示"文件过大" + 下载按钮 |
|
||||
| 加密文件(密码保护) | SheetJS/docx-preview 抛特定错 | "不支持加密文件" + 下载按钮 |
|
||||
| Univer chunk 加载失败 | dynamic import reject | 重试按钮(类比 `TerminalView` 的 `FAILURE_LIMIT`) |
|
||||
| 网络失败(媒体路由 403) | fetch 非 200 | 错误信息(信任围栏拒绝) |
|
||||
| Univer dispose 后异步回调 | cancelled flag | 静默(类比现有 `TerminalView` 模式) |
|
||||
|
||||
**降级统一出口**:所有 Office 错误分支都附带"下载查看"链接,确保用户始终能拿到文件。
|
||||
|
||||
## 5. bundle 与构建
|
||||
|
||||
### 5.1 依赖新增(`package.json`)
|
||||
|
||||
`dependencies`(运行时,内联进 client bundle):
|
||||
- `docx-preview`(~500KB)
|
||||
- `xlsx`(SheetJS,~400KB,xlsx 导入)
|
||||
- `@univerjs/core` + `@univerjs/sheets` + `@univerjs/sheets-ui` + `@univerjs/sheets-formula` + `@univerjs/sheets-conditional-formatting` + `@univerjs/sheets-chart` + `@univerjs/docs-ui` + `@univerjs/design` + `@univerjs/engine-render`(~5MB 合计)
|
||||
|
||||
**不放 `peerDependencies`**:这些是插件自有运行时依赖,不由 web profile 提供(对比现有 `node-pty`/`CodeMirror` 也在 `dependencies`)。
|
||||
|
||||
> **实施偏差记录(#122,2026-08)**:上文"`xterm` 也在 `dependencies`"已过时——`xterm` 与 `@xterm/addon-fit` 自 v0.13.0 起从 `dependencies` 迁至 `devDependencies`:二者始终内联进 `lib/client-terminal.js`(tsdown `noExternal` 全内联 + CSS 构建期解析),运行时零依赖;同时代码与开发依赖已从已弃用的 `xterm` 迁移到官方新包名 `@xterm/xterm@^5.5.0`(addon-fit@0.10.0 的 peer 约束锁 `^5.0.0`,不用 6.x),消除用户安装时的 deprecated 与 missing-peer 警告。Office 预览若落地,其依赖按同一原则处理。
|
||||
|
||||
### 5.2 tsdown.config.ts 调整
|
||||
|
||||
现有 client bundle 配置(`tsdown.config.ts:103` 的 `clientBundle()`):
|
||||
- `format: 'cjs'` + `banner`/`footer` 包成 `__ModuleLoader__.load` 闭包工厂
|
||||
- `noExternal: (id) => (CLIENT_EXTERNALS.includes(id) ? undefined : true)` — 非 externals 全内联
|
||||
|
||||
**dynamic import 兼容性(最大实现风险)**:
|
||||
|
||||
Rollup 在 `format: 'cjs'` + `inlineDynamicImports: false` 时会拆 chunk,但 CJS chunk 在浏览器端无 `require`,动态导入链路可能断裂。需 spike 验证:
|
||||
|
||||
1. **首选**:保持 `format: 'cjs'`,让 rollup 拆 chunk,验证浏览器 `import()` 是否能加载子 chunk(rollup 会输出 `import()` 调用,浏览器原生支持 ESM dynamic import,但 CJS chunk 的 `module.exports` 形态需 wrapper)
|
||||
2. **fallback A**:退回静态内联(放弃懒加载,接受 7MB bundle)
|
||||
3. **fallback B**:Univer 单独打 IIFE bundle `lib/univer-bundle.js`,host 新增静态路由 `/sidebar/univer-bundle.js` serve,client 用 `<script>` 动态注入(破坏 host 零改动,最后手段)
|
||||
|
||||
**spike 必须是实现第一步**,结果决定后续构建配置。
|
||||
|
||||
### 5.3 CSS
|
||||
|
||||
Univer 自带样式,需确保:
|
||||
- Univer 的 CSS 被 `dsh-css-inline` plugin 正确内联(走 `import '@univerjs/.../style.css'`)
|
||||
- 或 Univer 的样式注入机制不与现有 `data-plugin-css` tag 冲突
|
||||
|
||||
docx-preview 的样式由 `className: 'docx'` 选项控制,可能需补 `src/client/office.module.css` 微调。
|
||||
|
||||
## 6. 测试
|
||||
|
||||
### 6.1 Unit(纯函数,加进 `tests/unit.spec.ts`)
|
||||
|
||||
- `officeKindForExt('.docx')` → `'docx'`
|
||||
- `officeKindForExt('.xlsx')` → `'xlsx'`
|
||||
- `officeKindForExt('.pptx')` → `'download-only'`
|
||||
- `officeKindForExt('.doc')` → `'download-only'`
|
||||
- `officeKindForExt('.txt')` → `null`
|
||||
- `officeKindForExt('')` → `null`
|
||||
|
||||
### 6.2 xlsx → Univer 转换(若自写转换层)
|
||||
|
||||
- fixture xlsx → 解析 → 转换 → 断言 `IWorkbookData` 结构(sheet 名/单元格值/合并/公式)
|
||||
- 用 `tests/fixtures/sample.xlsx` 作为输入
|
||||
|
||||
### 6.3 集成测试限制
|
||||
|
||||
- Univer 依赖 canvas API,jsdom 环境无法渲染 → 不测 Univer 渲染产物
|
||||
- 只测加载逻辑:mock `fetch` 返回 ArrayBuffer,验证 loading → ready/error 状态转换
|
||||
- docx-preview 同样依赖 DOM,jsdom 可跑 `renderAsync`(无 canvas 依赖)但渲染产物脆弱 → 只测"不抛错"
|
||||
|
||||
### 6.4 手动冒烟
|
||||
|
||||
- 真实 .docx(含图片/表格/页眉)→ 渲染保真度
|
||||
- 真实 .xlsx(多 sheet/公式/条件格式)→ 渲染 + 公式计算
|
||||
- .pptx → 下载按钮
|
||||
- 加密 docx/xlsx → 错误提示 + 下载
|
||||
- >20MB → 提示 + 下载
|
||||
|
||||
## 7. 国际化
|
||||
|
||||
`src/client/locales.ts` 新增 key:
|
||||
|
||||
- `downloadToView` — "下载查看" / "Download to view"
|
||||
- `binaryNoPreview` — "此文件类型不支持预览" / "This file type cannot be previewed"
|
||||
- `officeTooLarge` — "文件过大,无法预览" / "File too large to preview"
|
||||
- `officeCorrupt` — "文件损坏或格式无效" / "File is corrupt or in an invalid format"
|
||||
- `officeEncrypted` — "不支持加密文件" / "Encrypted files are not supported"
|
||||
- `officeLoadFailed` — "Office 预览组件加载失败" / "Office preview component failed to load"
|
||||
- `retry` — 复用现有 `terminalRetry` 或新增 `retry`
|
||||
|
||||
## 8. 文件改动清单
|
||||
|
||||
| 文件 | 改动 |
|
||||
|---|---|
|
||||
| `src/client/EditorView.tsx` | 加 docx/xlsx kind 分支 + 下载按钮 binary 占位 |
|
||||
| `src/client/office-view.tsx`(新) | `DocxView` / `XlsxView` 组件 |
|
||||
| `src/client/office-types.ts`(新) | 扩展名 → kind 纯函数 |
|
||||
| `src/client/xlsx-to-univer.ts`(新,若自写转换) | SheetJS workbook → Univer IWorkbookData |
|
||||
| `src/client/locales.ts` | 新文案 key |
|
||||
| `src/client/sidebar.module.css` | `.editorBinary` / `.editorDownloadLink` 样式 |
|
||||
| `package.json` | 加 docx-preview / @univerjs/* / xlsx 依赖 |
|
||||
| `tsdown.config.ts` | dynamic import 配置(spike 后定) |
|
||||
| `tests/unit.spec.ts` | officeKindForExt 单测 |
|
||||
| `tests/fixtures/`(新) | sample.docx / sample.xlsx 测试夹具 |
|
||||
|
||||
## 9. 风险登记
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解 |
|
||||
|---|---|---|---|
|
||||
| tsdown CJS 闭包不兼容 dynamic import | 中 | 高(阻塞懒加载) | spike 优先;fallback A 静态内联 / fallback B 独立 bundle |
|
||||
| Univer 体积超预期(>5MB) | 低 | 中 | 评估 tree-shaking;裁剪非必要插件(如 chart) |
|
||||
| xlsx → Univer 转换层复杂度爆表 | 中 | 中 | 优先用 `@univerjs/sheets-import-export` 官方包 |
|
||||
| Univer canvas 在某些浏览器渲染异常 | 低 | 中 | 错误边界捕获 + 下载降级 |
|
||||
| docx-preview 图片 base64 内存爆炸(大 docx) | 低 | 低 | 文件 size cap 已 20MB,可接受 |
|
||||
| Univer dispose 不彻底导致内存泄漏 | 中 | 中 | 严格 cleanup + 手动验证 Tab 切换/卸载 |
|
||||
|
||||
## 10. 实现里程碑(粗略,待 writing-plans 细化)
|
||||
|
||||
1. **Spike**:验证 tsdown CJS + dynamic import 兼容性(决定构建策略)
|
||||
2. **基础**:office-types.ts + EditorView 分发 + binary 下载按钮(最小可用,pptx/旧格式先受益)
|
||||
3. **docx**:DocxView + docx-preview 集成 + 错误处理
|
||||
4. **xlsx**:XlsxView + Univer 集成 + xlsx 导入(优先官方 importer)
|
||||
5. **测试**:unit + fixture + 手动冒烟
|
||||
6. **收尾**:CSS 打磨 + 国际化 + 文档更新(README 功能一览)
|
||||
@@ -1,31 +0,0 @@
|
||||
# PDF 原生预览设计
|
||||
|
||||
> 日期: 2026-08-10
|
||||
> 状态: 已批准
|
||||
> 范围: `dsh-better-sidebar` 的 `EditorView` 文件预览扩展
|
||||
|
||||
## 目标
|
||||
|
||||
为 `.pdf` 增加侧边栏内预览,同时保持零新增前端依赖和现有下载降级出口。
|
||||
|
||||
## 方案
|
||||
|
||||
- Client 按扩展名识别 `.pdf`,短路 `fsRead` 的二进制探测。
|
||||
- `PdfView` 通过 `fetch(mediaUrl(scope, path))` 读取字节,创建显式
|
||||
`application/pdf` Blob URL 后交给浏览器原生 `<iframe>`,避免旧 host
|
||||
或代理缓存 `application/octet-stream` 时触发直接下载。
|
||||
- Host media route 为 `.pdf` 返回 `application/pdf`。
|
||||
- PDF 预览工具条始终提供“下载查看”链接。
|
||||
- 不引入 PDF.js,不实现缩略图、文本搜索或自定义翻页工具栏。
|
||||
|
||||
## 错误与限制
|
||||
|
||||
- 是否可渲染取决于浏览器内置 PDF 查看器。
|
||||
- 文件仍受现有 `mediaLimit` 限制。
|
||||
- 浏览器原生 PDF iframe 对 HTTP 错误没有统一可观测事件;下载链接作为稳定降级出口。
|
||||
|
||||
## 测试
|
||||
|
||||
- `.pdf` 扩展名识别为 PDF 预览。
|
||||
- media type helper 对 `.pdf` 返回 `application/pdf`。
|
||||
- Typecheck、bundle purity、manifest/plugin-shape 测试通过。
|
||||
@@ -1,37 +0,0 @@
|
||||
# PPTX 高保真预览设计
|
||||
|
||||
> 日期: 2026-08-10
|
||||
> 状态: 已批准
|
||||
> 范围: `dsh-better-sidebar` 的 `.pptx` 浏览器预览
|
||||
|
||||
## 目标
|
||||
|
||||
使用 `@aiden0z/pptx-renderer` 在侧边栏内预览 `.pptx`,保留图片、表格、
|
||||
图表、SmartArt 与常见 OOXML 样式,同时保持下载降级出口。
|
||||
|
||||
## 架构
|
||||
|
||||
- `.pptx` 从 `download-only` 调整为独立 `pptx` 预览类型。
|
||||
- `PptxView` 从现有 media route 获取 `ArrayBuffer`。
|
||||
- 使用 `PptxViewer.open()` 渲染到库专属 DOM 容器。
|
||||
- 开启 `lazyMedia`、`lazySlides` 与推荐 ZIP 安全限制。
|
||||
- UI 提供上一页、下一页、页码和下载按钮。
|
||||
- 卸载时销毁 viewer 并清空库专属容器。
|
||||
- 旧 `.ppt/.doc/.xls` 继续仅下载。
|
||||
|
||||
## Bundle
|
||||
|
||||
- 先验证普通 bundler 入口不会遗留 Node builtin `require()`。
|
||||
- 若普通入口不纯,切换包提供的 standalone browser ESM 入口。
|
||||
- 不启用可选 PDF.js EMF fallback,避免额外体积。
|
||||
|
||||
## 错误处理
|
||||
|
||||
- 损坏、加密或超限文件显示错误信息和下载链接。
|
||||
- viewer 初始化失败时清理部分创建的实例与 DOM。
|
||||
|
||||
## 测试
|
||||
|
||||
- `.pptx` 类型分发。
|
||||
- bundle 模块表 require guard。
|
||||
- Typecheck、manifest/plugin-shape 与构建通过。
|
||||
@@ -1,162 +0,0 @@
|
||||
# 侧边栏设置声明式化 + 内置支持"插头化"设计
|
||||
|
||||
**日期**:2026-08-11
|
||||
**状态**:已实施(本文档含实施偏差记录)
|
||||
**目标版本**:v0.4.1
|
||||
|
||||
## 1. 目标
|
||||
|
||||
1. 把 DSH 设置页的「侧边卡片」分区从硬编码 4 行改为**注册表驱动的声明式清单**:
|
||||
- 展示当前注册的所有侧边栏内容(tab 类型)与文件查看器(viewer),每项可**直接打开/关闭**;
|
||||
- 每项声明式元数据:**类型(id)、图标(SVG)、相关设置**(如子代理的"检测到子代理就自动打开")。
|
||||
2. 内置支持(资源管理 / Git / 终端 / 子代理 / 编辑器 / 差异)与 8 个文件预览器继续以**插头(descriptor)形式**注册,并在描述符上携带上述声明。
|
||||
|
||||
## 2. 非目标
|
||||
|
||||
- 不改宿主路由 / pty / tools 的 gating 语义:`agentTerminalTools` 仍是独立布尔偏好(宿主 `syncToolsGate` 零改动),只是它的**设置行渲染位置**改由终端 tab 描述符声明。
|
||||
- 不做"已打开 tab 随禁用即时关闭"(保留式语义:禁用只影响新开,已打开 tab 继续渲染)。
|
||||
- 不实现外部插件自定义 settings 键的动态 schema(见 §6 限制)。
|
||||
- 不迁移 bundle 形态、不加依赖、不动 peerDependencies。
|
||||
|
||||
## 3. 现状回顾
|
||||
|
||||
- `SideCardSection.tsx` 硬编码 4 行:`openByDefault` / `defaultWidthPercent` / `autoOpenSubagent` / `agentTerminalTools`。
|
||||
- `PrefsSchema`(`config.ts`)为扁平对象,`parsePrefs`(`client/prefs.ts`)逐字段校验。
|
||||
- 内置 tab/viewer 已通过 `registerBuiltins` 走同一注册表(v0.4.0),但描述符缺少设置面元数据:viewer 无 `title`/`icon`,tab 无相关设置声明。
|
||||
- 服务无启用态概念:`getTabs`/`matchFileViewer`/`openTab` 不过滤。
|
||||
|
||||
## 4. 设计
|
||||
|
||||
### 4.1 偏好扩展(`prefs-shared.ts` / `config.ts` / `client/prefs.ts`)
|
||||
|
||||
```ts
|
||||
interface SidebarPrefs {
|
||||
openByDefault: boolean
|
||||
defaultWidthPercent: number
|
||||
autoOpenSubagent: boolean
|
||||
agentTerminalTools: boolean
|
||||
tabsEnabled: Record<string, boolean> // 按 tab descriptor id;缺省 = 启用
|
||||
viewersEnabled: Record<string, boolean> // 按 viewer descriptor id;缺省 = 启用
|
||||
}
|
||||
```
|
||||
|
||||
- schema:`z.dict(z.boolean()).default({})`(schemastery `dict`);旧文档自动解析为 `{}`(全开),零迁移。
|
||||
- `parsePrefs`:record-of-boolean 校验(非对象/非布尔值回退 `{}` / 丢弃该键)。
|
||||
- 设置页开关写入 `tabsEnabled: { ...prefs.tabsEnabled, [id]: next }`(viewers 同理)。
|
||||
|
||||
### 4.2 声明式描述符(`client/service.ts`)
|
||||
|
||||
```ts
|
||||
interface SidebarSettingToggle {
|
||||
key: string // SidebarPrefs 字段名('autoOpenSubagent')
|
||||
title: string | (() => string)
|
||||
desc?: string | (() => string)
|
||||
}
|
||||
interface SidebarSettingsDeclaration {
|
||||
toggles?: readonly SidebarSettingToggle[]
|
||||
}
|
||||
|
||||
// TabDescriptor 增加:
|
||||
settings?: SidebarSettingsDeclaration
|
||||
|
||||
// FileViewerDescriptor 增加(设置清单展示用):
|
||||
title?: string | (() => string) // 缺省回退 id
|
||||
icon?: ReactNode | ((size) => ReactNode)
|
||||
settings?: SidebarSettingsDeclaration
|
||||
```
|
||||
|
||||
服务新增:
|
||||
```ts
|
||||
isTabEnabled(id: string): boolean // store.getPrefs().tabsEnabled[id] !== false
|
||||
isViewerEnabled(id: string): boolean
|
||||
```
|
||||
|
||||
行为变化:
|
||||
- `openTab`:类型被禁用 → no-op(`console.warn` 提示);`matchFileViewer`:跳过禁用 viewer(文件落到下一个匹配或下载按钮)。
|
||||
- `getTabs()/getFileViewers()` 仍返回**全部**注册项(设置页清单必须看到已禁用的项)。
|
||||
|
||||
### 4.3 内置声明(`builtins/tabs.tsx` / `builtins/viewers.tsx`)
|
||||
|
||||
| tab | 相关设置声明 |
|
||||
|---|---|
|
||||
| subagent | `toggles: [{ key: 'autoOpenSubagent', ... }]` |
|
||||
| terminal | `toggles: [{ key: 'agentTerminalTools', ... }]` |
|
||||
| explorer/git/editor/diff | 无(仅开关) |
|
||||
|
||||
8 个 viewer 全部补 `title`(locales 新增 `viewerImage`…`viewerBinary`)+ `icon`(`icons.tsx` 新增 6 个手绘 outline SVG:image/pdf/docx/xlsx/pptx/markdown;code/binary-download 复用 `IconCodeOutline16` / `IconDownloadOutline16`)。
|
||||
|
||||
### 4.4 Store 快照携带 prefs(`client/state.ts`)
|
||||
|
||||
`SidebarSnapshot` 增加 `prefs` 字段;`setPrefs` 更新快照并 `notify()`(`setSession`/`update`/`reduce` 构造快照时带上当前 prefs)。效果:+ 菜单等 `useSyncExternalStore` 消费者在设置变更后**立即**重渲染(禁用 git → + 菜单马上消失)。
|
||||
|
||||
`makeDefaultState(width, panelOpen, seedExplorer = true)` 增加第三参;`loadState` 在 `tabsEnabled['explorer'] === false` 时传 `false`——新会话不再预置 explorer tab(与"已存在会话保持布局"语义并列:禁用只影响新布局)。
|
||||
|
||||
### 4.5 消费点 gating
|
||||
|
||||
| 消费点 | 行为 |
|
||||
|---|---|
|
||||
| + 菜单(`buildNewTabOptions`) | `filter(!d.hidden && service.isTabEnabled(d.id))` |
|
||||
| 子代理自动展开 effect | 追加 `isTabEnabled('subagent') === false` 早退(禁用时不展开面板) |
|
||||
| agent 终端 reconcile(WS 推送) | `isTabEnabled('terminal') === false` 时忽略推送(不自动补 tab;重新启用后下一次推送自愈) |
|
||||
| 产出文件拦截(`intercept.tsx` select) | `tabsEnabled['editor'] === false` 时 select 返回 null(回落默认交付行) |
|
||||
| `openTab` / `matchFileViewer` | 见 §4.2 |
|
||||
| 新会话默认布局 | explorer 禁用 → 空 pane |
|
||||
|
||||
### 4.6 设置页声明式渲染(`SideCardSection.tsx`)
|
||||
|
||||
- `inject: () => ({ store, service })`(`index.tsx` 闭包直接捕获 service)。
|
||||
- 注册表清单:本地 state + `service.subscribe` 同步(注册变化罕见,无需 external-store)。
|
||||
- 行结构:
|
||||
- **常规**:`openByDefault`、宽度(既有逻辑)。
|
||||
- **侧边栏内容**:按 `order` 升序、`hidden` 置后遍历 tabs——图标(16) + 标题 + desc=`tab.id`(类型)+ 开关;`settings.toggles` 渲染为缩进子行(仅父级启用时显示,绑定 `prefs[toggle.key]`)。
|
||||
- **文件预览**:按 priority 降序遍历 viewers——图标 + `title ?? id` + desc=`exts.join(' · ') || t('settingsViewerCatchAll')` + 开关。
|
||||
- 提交机制不变:串行化 + revision 守卫 + 失败回滚 + `store.setPrefs` 同步。
|
||||
|
||||
## 5. 测试
|
||||
|
||||
- `service.spec.ts`:启用态缺省 true;`openTab` 拒绝禁用类型;`matchFileViewer` 跳过禁用 viewer(含兜底 code 被关 → undefined)。
|
||||
- `builtins.spec.ts`:subagent/terminal 的 toggles 声明;全部 tab 有 icon;全部 viewer 有 title+icon。
|
||||
- `unit.spec.ts`:`parsePrefs` 的 map 校验(非对象/非布尔回退)+ 新字段默认值;`loadState` explorer 禁用 → 空 pane。
|
||||
- `plugin-shape.spec.ts` / `smoke.spec.ts`:schema 解析与 settings 路由断言补新字段。
|
||||
- `side-card-section.spec.tsx`(新增):`renderToString` 断言声明式行(图标/标题/id/exts/开关/嵌套开关隐藏规则)。
|
||||
|
||||
## 6. 限制与失败模式
|
||||
|
||||
- **外部插件的自定义 settings 键**:`toggles[].key` 必须是宿主 `PrefsSchema` 的字段(内置:`autoOpenSubagent` / `agentTerminalTools`);未知键会被 settings seam 丢弃(schemastery object 非严格模式放行未知键但 schema 不含其校验)。文档注明。
|
||||
- 设置路由失败 / 文档缺失:沿用既有降级(schema 默认 + 本地 state,开关默认全开)。
|
||||
- 已打开 tab 的类型被禁用:tab 保留可继续使用;关闭后无法重开。
|
||||
- 父级(子代理/终端)关闭时嵌套开关隐藏,其值保留在 prefs 不销毁。
|
||||
|
||||
## 7. 实施偏差记录
|
||||
|
||||
1. **嵌套开关的"父级关闭即隐藏"**:设计初稿曾考虑始终显示嵌套开关;实施决定仅在父级启用时渲染(避免"autoOpenSubagent 已开但子代理页面被关"的无效组合),值保留。
|
||||
2. **宿主 `syncToolsGate` 不改**:`agentTerminalTools` 仅随自身开关走,不与 `tabsEnabled['terminal']` 联动(模型终端工具是模型面向能力,与侧边栏 UI 开关解耦)。
|
||||
3. **`renderRow` 统一行渲染**:tab/viewer/嵌套开关共用一行渲染器(`sub` 参数区分缩进),CSS 新增 `.sectionHeading` / `.rowIcon` / `.titleLine` / `.subRow`。
|
||||
4. **清单排序**:tabs 按 `hidden` 置后 + `order` 升序(editor/diff 殿后);viewers 按 priority 降序(code 兜底最后)。
|
||||
5. **开关改为卡片点击(v0.4.1 追加)**:清单项与嵌套相关设置、`openByDefault` 全部从"复选框行"改为**可点击卡片**——`<button aria-pressed>`,整卡即开关,视觉状态即状态(启用 = 品牌描边 + 交互选中填充 + `IconCheckOutline16` 勾选徽标;禁用 = 中性 hairline + 文字降级)。嵌套相关设置渲染为更小的缩进卡片;宽度输入保持原生设置行(它是数值不是开关)。设置导航齿轮图标为宿主 shell 硬编码(`navIcon` 按 section id 映射,无插件注入点),用户选择不改宿主,保持现状。
|
||||
6. **小卡片响应式网格 + 二级设置弹窗(v0.4.1 再追加)**:清单项改为**小卡片**,置于 `repeat(auto-fill, minmax(148px, 1fr))` 响应式网格(一行自适应多个,随宽度换行);卡片结构 = 顶行(图标 + 标题省略 + **勾选徽标钉在最右端**)+ 类型/扩展名描述行(省略号截断,`title` 属性给出完整值)。声明了 `settings.toggles` 的卡片右下角渲染**齿轮角标按钮**(`IconSettingsOutline16`,仅父级启用时显示),点击打开**原生 `Modal` 弹窗**,内含 DSH 原生设置行(标题/描述 + 品牌强调原生复选框 + hairline 分隔,末行去分隔线)——二级设置不再内嵌于网格。实现细节:Modal 采用条件挂载(`settingsFor !== null && <Modal open …>`),因为 Modal 原语无条件调用 hooks,常闭挂载在测试的 dual-react(react 18.2 server / 18.3 bundled)分裂环境下会崩;弹窗行体抽成 `FeatureSettingsRows` 供 renderToString 直测。
|
||||
|
||||
---
|
||||
|
||||
## 8. v0.12.0 设置 seam 开放记录(2026-08-14,feat/plugin-api-v012)
|
||||
|
||||
设计正文「key 必须是宿主 PrefsSchema 的字段」的限制在 v0.12.0 被打开(纯增量,`toggles` 的宿主 key 语义不变):
|
||||
|
||||
- **持久化**:`SidebarPrefs` 增加 `pluginSettings: Record<string, Record<string, unknown>>`(开放嵌套 map,schema `z.dict(z.dict(z.any())).default({})`,按 descriptor id 分桶)——外部插件的自有设置不再需要宿主 schema 字段、不再被 settings seam 丢弃。`parsePrefs` 增加 `pluginSettingsMapOf` 校验(非对象整体回退 `{}`)。
|
||||
- **声明**:`SidebarSettingsDeclaration` 增加 `pluginToggles`(声明式行,控件同 toggles 的 switch/text/number,值须 JSON 可序列化)与 `render?: (props: SidebarSettingsRenderProps) => ReactNode`(自定义面板:store/service/prefs/pluginSettings/updatePluginSetting/close;抛错被吞并显示内联错误)。
|
||||
- **渲染**:齿轮弹窗体抽为 `SettingsBody`(`render` 存在时优先,否则 toggles 行 + pluginToggles 行,后者把 pluginSettings 投影到 prefs 面复用 `FeatureSettingsRows`);`settingsFor` 状态从 `TabDescriptor | null` 扩为 `TabDescriptor | FileViewerDescriptor | null`——viewer 卡片 v0.12.0 起同样有齿轮设置弹窗(§4.6 原仅 tab)。
|
||||
- **向后兼容**:旧设置文档无 `pluginSettings` 字段 → schema default `{}`;`toggles` 行为与 v0.11.0 完全一致。
|
||||
|
||||
---
|
||||
|
||||
## 9. 设置页 UI/UX 现代化(feat/settings-sidecard-uiux)
|
||||
|
||||
纯视觉/交互入口改造,无 API 与持久化语义变化(`settings.toggles` / `pluginToggles` / `render` / Modal 条件挂载契约全部不变):
|
||||
|
||||
- **二级设置入口:齿轮角标 → 卡片底部设置条**。原 `.cardGear`(16×16px 三级色透明幽灵按钮,`absolute; top:46px` 像素钉位 + `.cardWithGear` padding 耦合)可发现性不足;改为卡片底缘的**全宽设置条** `.cardSettings`(hairline 分隔、齿轮图标 12px + 「功能设置」文字标签 11px/500,hover 换 `--dsw-alias-interactive-bg-hover-accent` 品牌淡底 + brand 字色)。可见性条件不变(仅父级启用且声明 settings);aria-label 模式 `"${title} ${t('settingsPopup')}"` 不变(测试契约)。常规行齿轮 `.rowGear` 升级为 22×22 带 border-l2 边框的芯片按钮(hover 同样 accent 引导)。
|
||||
- **启用态指示(后续迭代:勾选 → 紧凑开关滑块)**:启用卡片描边从全强度品牌色柔化为 `color-mix(button-primary-fill 45%)`。第一版曾把勾选徽标改为 `--dsw-alias-state-success-primary` 实心绿(绿 = 已开启确认,brand = 激活强调);按反馈再迭代为**紧凑品牌开关滑块** `.cardSwitch`(30×16 track + 10px thumb,开 = brand 填充 + thumb 反色,关 = 中性且不渲染滑块)——纯视觉指示,卡片本身仍是开关(`aria-pressed` 语义不变),不再使用 success 色。全部颜色仍为 `--dsw-alias-*` 令牌派生,零硬编码。
|
||||
- **卡片等高**:`.card` 统一 `min-height: 106px` + `.cardMain { flex: 1 }` + `.cardTop { min-height: 28px }`——带设置条的卡片(天然更高)与不带设置条的卡片、无图标卡片全部等高;`prefers-reduced-motion` 覆盖新增的 `.cardSwitchTrack/Thumb` 过渡。
|
||||
- **二级弹窗长列表**:`.popupRows` 增加 `max-height: min(56vh, 480px)` + 内部 `overflow-y: auto`(终端等声明 6 行设置的场景下弹窗壳体与 Done 按钮固定,仅行区滚动);行密度收紧(padding 12/14、gap 8)。
|
||||
- **节奏**:section gap 16 / group padding 20 / 网格 `minmax(180px,1fr)` gap 12;计数徽章改为 `accent-soft` 淡底 pill(versionBadgeTag 同款令牌配方);开关关态 thumb 降一档(label-tertiary)+ track 底 layer-2。
|
||||
- **动效纪律**:全部过渡 ≤160ms 且仅 background/border-color/color;`prefers-reduced-motion` 扩展覆盖 `.cardSettings` / `.rowGear` / `.popupRow`。
|
||||
- **清理**:合并 `.pluginEntries` 重复定义(原 L598 与 L795 两处,保留带 max-height/overflow 的一处)。
|
||||
@@ -1,137 +0,0 @@
|
||||
# 内置 HTML 预览器 + 内嵌网页浏览器(沙箱化)设计
|
||||
|
||||
**日期**:2026-08-11
|
||||
**状态**:已批准,已实施
|
||||
**作者**:opencode + 用户
|
||||
**当前版本**:v0.4.3(main `35cde82`)
|
||||
**目标版本**:v0.5.0
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在 `dsh-better-sidebar` 内按既有 builtins 模式新增两个内置能力:
|
||||
|
||||
1. **`html` 文件预览器**:`.html`/`.htm` 文件在侧边栏编辑器中默认以真实渲染预览(相对资源可加载),可切换代码编辑(CodeMirror HTML 高亮 + 保存)。
|
||||
2. **`browser` 浏览器 tab**:`+` 菜单新增「浏览器」页面,地址栏输入 URL 在沙箱 iframe 中浏览,支持后退/前进/刷新,URL 与标题按 tab 持久化。
|
||||
|
||||
安全问题为第一优先级(用户显式要求"注意安全问题"):所有渲染走**不透明源沙箱 iframe**,host 侧新增 `/sidebar/html` 路由带 CSP + 信任围栏 + cwd 边界。另按用户追加要求提供**关闭沙箱的子设置**(默认关闭该设置;开启需明确勾选,设置项与界面都带安全警告文案)。
|
||||
|
||||
## 2. 复用门(dsh-reuse-first)
|
||||
|
||||
`NO EXISTING CAPABILITY`:已搜索 DSH checkout(docs/*-catalog、apps/web、packages 源码——sidecar/iframe/webview/html preview/CSP 均无等价物;`WorkspaceBrowser` 只是目录树)与 dsh-external org hub 目录(browser/html/webview/iframe/sidecar/viewer/preview 均 0 命中)。全新实现,落点为本仓库 builtins(用户已确认)。
|
||||
|
||||
## 3. 威胁模型与安全基线
|
||||
|
||||
| 威胁 | 对策 |
|
||||
|---|---|
|
||||
| 恶意 HTML/网页读取 GUI 数据(localStorage、`/sidebar/api`、同源 cookie) | iframe 一律 `sandbox` 且**不含 `allow-same-origin`** → 文档源为不透明源(null)。**禁止 srcdoc**(srcdoc 在无沙箱时继承父源),一律 route-src |
|
||||
| 预览/浏览内容劫持 GUI 页面(top 导航) | 不含 `allow-top-navigation` / `allow-top-navigation-by-user-activation` |
|
||||
| 表单 CSRF 打到本地服务 | html 预览器 iframe **不含 `allow-forms`**;浏览器 tab 含 `allow-forms`(登录流必需,残余风险见 §9) |
|
||||
| 浏览器 tab 访问内部地址(GUI 自身、localhost、127.0.0.0/8) | 地址栏导航前客户端阻断(`browser.ts` 纯函数);`javascript:`/`data:`/`file:` 等非 http(s) 一律拒绝 |
|
||||
| 预览页脚本请求 `/sidebar/api` | 三层:不透明源 fetch 被 CORS 拒读;信任围栏拒 `sec-fetch-site: cross-site`;`Origin: null` 无法伪造同源 |
|
||||
| 敏感信息泄露给被浏览站点 | iframe `referrerPolicy="no-referrer"`;权限策略 `allow=""`(拒绝 camera/mic/geolocation 等一切策略特性) |
|
||||
| 直接顶层打开 `/sidebar/html/*` URL(弹窗等) | 路由响应带 `Content-Security-Policy: sandbox ...; object-src 'none'`,顶层同样被沙箱化 |
|
||||
| 路径逃逸 | 路由复用 `requireAbsolute` + `isWithin(cwd, path)` + `mediaLimit` 上限 + 信任围栏 |
|
||||
| 关闭沙箱(用户主动选择) | 每功能独立子设置(`htmlViewerNoSandbox` / `browserNoSandbox`,默认 false);设置文案 + 渲染时持久警告条双警告 |
|
||||
|
||||
## 4. 变更清单
|
||||
|
||||
### 4.1 host:`/sidebar/html` 路由(src/html-route.ts 新 + src/index.ts)
|
||||
|
||||
URL 采用**路径编码**(无 query)——WHATSG URL 解析对纯路径相对引用会丢弃 base 的 query,query 式 URL 下 `./style.css` 会丢失 session 作用域:
|
||||
|
||||
```
|
||||
/sidebar/html/<sessionId>/<绝对路径逐段 encodeURIComponent>
|
||||
例:/sidebar/html/S/Users/me/proj/index.html
|
||||
./style.css → /sidebar/html/S/Users/me/proj/style.css ✓
|
||||
Windows: C:\Users\me\a.html → /sidebar/html/S/C%3A/Users/me/a.html
|
||||
```
|
||||
|
||||
- `src/html-route.ts`(无依赖纯模块,client 可导入不触发纯度门):`encodeHtmlUrl(sessionId, path)`、`decodeHtmlUrl(pathname)`(404 错前缀;400 空路径/双斜杠/畸形编码/缺 sessionId 或文件路径/空段)。
|
||||
- `src/index.ts`:注册 `/sidebar/html` prefix 路由(fence → GET 校验 → decode → `requireAbsolute` → `isWithin(cwd)` → stat isFile + `mediaLimit` → 读文件),响应头:
|
||||
`content-type`(新增 `.html`/`.htm` → `text/html` 到 MEDIA_TYPES)、`cache-control: no-cache`、`X-Content-Type-Options: nosniff`、`Referrer-Policy: no-referrer`、`Content-Security-Policy: sandbox allow-scripts allow-popups allow-downloads allow-modals; object-src 'none'`。
|
||||
- cwd 语义与媒体路由一致:session store 权威;会话附着前的首请求回退 `process.cwd()`,通常被 `isWithin` 拒绝(可接受)。
|
||||
|
||||
### 4.2 client:`html` 预览器(TextEditor.tsx 扩展 + builtins/viewers.tsx)
|
||||
|
||||
- descriptor:`{ id:'html', exts:['html','htm'], priority:0, fetchStrategy:'fsRead', settings.toggles:[htmlViewerNoSandbox] }`。
|
||||
- TextEditor:`markdown || html` 都显示预览/编辑切换;html 预览分支渲染:
|
||||
`<iframe src={htmlUrl(scope,path)} sandbox="allow-scripts allow-popups allow-downloads allow-modals" referrerPolicy="no-referrer" allow="" />`
|
||||
- 导出 `HTML_IFRAME_SANDBOX` 常量(测试钉死);沙箱关闭时 `sandbox` 属性整体移除 + 渲染 `sandboxWarning` 警告条。
|
||||
- 预览显示**已保存文件**(route-src),不反映未保存草稿(有意取舍,文档化)。
|
||||
|
||||
### 4.3 client:`browser` tab(BrowserView.tsx 新 + browser.ts 新 + builtins/tabs.tsx + state.ts)
|
||||
|
||||
- state.ts:`SidebarState.nextBrowser`(默认 1;sanitize 对缺失/畸形值回退 1,旧持久化布局继续加载);新 reducer `patchTab(state, tabId, {title?, path?})`。
|
||||
- tabs.tsx:`{ id:'browser', order:50, settings.toggles:[browserNoSandbox], createTab: 铸造 browser:<n> + nextBrowser 递增 }`。
|
||||
- browser.ts(无依赖纯模块):`isLoopbackHostname`(localhost/::1/0.0.0.0/127/8);`normalizeBrowserUrl(input, selfOrigin)`:
|
||||
- 无 scheme → 补 `https://`;`://` 完整 URL 走解析后协议兜底;已知危险 scheme 前缀(javascript/data/file/about/...)直接拒绝;`host:port` 形态(如 `example.com:8080`)不被误判为 scheme。
|
||||
- 拒绝顺序:协议 → 自身 origin **放行**(GUI 自身可在侧边栏打开,沙箱不透明源兜底;须先于 loopback 判定,因其主机通常就是 loopback)→ loopback 阻止 → ok。
|
||||
- BrowserView:地址栏(后退/前进/刷新/输入/前往)+ iframe + 状态行;URL 经 `patchTab` 持久化到 `tab.path`/`tab.title`(hostname);历史栈仅记录地址栏导航(iframe 内部跳转跨域不可见,已知限制);无 URL 时显示起始页(不自动加载任何站点)。
|
||||
- iframe:`sandbox="allow-scripts allow-forms allow-popups allow-downloads allow-modals allow-popups-to-escape-sandbox"`(导出 `BROWSER_IFRAME_SANDBOX`;无 allow-same-origin / allow-top-navigation;allow-popups-to-escape-sandbox 供 OAuth 弹窗);沙箱关闭时移除属性 + 警告条。
|
||||
- 常驻底部小字沙箱说明(`browserHint`)。
|
||||
|
||||
### 4.4 设置(prefs-shared.ts + config.ts + SideCardSection 声明式渲染)
|
||||
|
||||
- `SidebarPrefs` 新增 `htmlViewerNoSandbox` / `browserNoSandbox`(boolean,默认 false);`SIDEBAR_PREFS_DEFAULTS` 与 `PrefsSchema` 同步;`parsePrefs` 按字段校验回退。
|
||||
- 声明式设置:html viewer 行与 browser tab 行各自 `settings.toggles`,设置页自动渲染(父级关闭时隐藏),文案含「(不安全)」+ 后果说明。
|
||||
|
||||
### 4.5 其他
|
||||
|
||||
- `api.ts`:`htmlUrl(scope, path)` = `encodeHtmlUrl(sessionId, path)`。
|
||||
- `icons.tsx`:`IconHtmlOutline16`(文件 + `‹/›`)、`IconGlobeOutline16`(地球)。
|
||||
- `locales.ts`:zh/en 双份新键(viewerHtml/browser/浏览器栏/阻止原因/沙箱提示/设置文案)。
|
||||
- `sidebar.module.css`:`sandboxWarning`、`editorHtml`、`.browser*` 系列。
|
||||
|
||||
## 5. 测试
|
||||
|
||||
| 文件 | 覆盖 |
|
||||
|---|---|
|
||||
| tests/html-route.spec.ts(新) | 编码/解码往返(POSIX/Windows/特殊字符)、相对解析保持同路由同 session、404/400 拒绝、遍历段留给 isWithin |
|
||||
| tests/browser.spec.ts(新) | 补 https、host:port 不误判、危险 scheme/loopback 拒绝、自身 origin 放行、invalid |
|
||||
| tests/sandbox-views.spec.tsx(新) | iframe sandbox 常量钉死(无 allow-same-origin/top-navigation)、route-src 非 srcdoc、referrer/allow 属性、沙箱关闭 → 无 sandbox 属性 + 警告条、markdown 分支回归、浏览器起始页不自动加载 |
|
||||
| tests/builtins.spec.ts | 7 tab / 9 viewer 清单、html 扩展名匹配、browser createTab 铸造与 nextBrowser、两个 toggles 声明 |
|
||||
| tests/unit.spec.ts | nextBrowser sanitize(缺失/畸形回退 1)、patchTab 更新/无操作、parsePrefs 新字段默认与校验 |
|
||||
| tests/smoke.spec.ts | fenced routes 清单 + `/sidebar/html` |
|
||||
|
||||
## 6. 版本与文档
|
||||
|
||||
- 版本 0.4.3 → 0.5.0(package.json + dsh.plugin.json 同步,manifest-consistency 守卫)。
|
||||
- README/AGENTS 内置清单更新为 7 tab + 9 viewer。
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
1. `pnpm typecheck && pnpm build && pnpm test` 全绿(245 用例)。
|
||||
2. 手动:打开 `.html` 默认预览渲染(相对资源可加载),可切编辑/保存;`+` 菜单浏览器 tab 多开、地址栏浏览、后退/前进/刷新、刷新页面恢复 URL 与标题。
|
||||
3. 安全:预览/浏览 iframe 无 allow-same-origin/top-navigation;`javascript:`/`data:`/`file:`/localhost/127.x 均被阻止并提示(自身 origin 允许);`/sidebar/html` 越界 403、超限 400、跨站 403;预览页 fetch `/sidebar/api` 读不到(CORS),预览器表单被沙箱禁(无 allow-forms)。
|
||||
4. 关闭沙箱开关:设置页勾选后界面出现持久警告条,iframe 无 sandbox 属性。
|
||||
|
||||
## 8. 实施偏差记录
|
||||
|
||||
- 设计初期考虑过 srcdoc + `<base>` 注入以实现"预览反映未保存草稿",因 srcdoc 无沙箱时继承父源的致命风险与 base 注入的脆弱性,改采 route-src(预览 = 已保存文件);相对资源经路径编码 URL 天然解析,无需内容改写。
|
||||
- `normalizeBrowserUrl` 的 scheme 判别:朴素正则会把 `example.com:8080` 误判为 scheme,最终采用「`://` 完整 URL + 危险 scheme 黑名单 + 解析后协议兜底」三段式。
|
||||
- `nextBrowser` 在 sanitize 中为宽松字段(缺失/畸形回退 1),避免旧持久化布局整体失效(与严格校验的 `nextTerminal` 不同)。
|
||||
- **UNC 路径编码标记(PR #134 整合 #79/#115 时)**:`/sidebar/html` 对 `\\server\share\...` / `//server/share/...` 采用 sessionId 后的 `//` 标记,且解码统一重建为**前斜杠** `//server/share/...` 而非反斜杠——`node:path` 在 win32/POSIX 上分别解析为 `\\server\share\...` / `/server/share/...`,因此无需任何平台信号;原「400 双斜杠」约束改为「首个空段仅作 UNC 标记,其余空段仍 400」。
|
||||
|
||||
## 8.1 追加需求(2026-08-11,v0.5.0 内)
|
||||
|
||||
1. **设置页滚动**:`SideCardSection.module.css` 的 `.section` 填满设置壳 options 区并内部滚动(`height:100% + min-height:0 + overflow-y:auto`),7 tab + 9 viewer 卡片再多也不会被裁切。
|
||||
2. **沙箱状态实时显示 + 临时解锁**:新增 `SandboxStatusBar`(HTML 预览与浏览器 tab 共用)——沙箱开启显示绿色状态 + 「临时解锁(不安全)」按钮;关闭(全局设置或本页临时解锁)显示**红色警示**(`state-error-primary` 色系 + 危险文案)与「恢复沙箱」按钮(仅临时态)。临时解锁是组件内 state,不写回全局设置,随页面/标签卸载失效;iframe 随沙箱翻转重挂载。
|
||||
3. **聊天/界面外链在侧边栏打开**:新设置 `browserInterceptLinks`(默认开,受浏览器 tab 启用开关 gate)——`link-intercept.ts` 在 document capture 阶段拦截 http(s) **外部**链接点击(同源/非 http(s)/修饰键点击一律放行),经 `openTab({type:'browser', url, title: hostname})`(openTab seed 新增 `url` 字段:落地后 patchTab 预填 path)在侧边栏打开。浏览器 tab 地址栏新增「在浏览器中打开」按钮(`window.open(url,'_blank','noopener')`,用户手势触发,等价真实浏览器新标签)。
|
||||
|
||||
## 8.2 追加需求(2026-08-11 二轮,v0.5.0 内)
|
||||
|
||||
1. **HTML「默认非安全」二级设置**:`htmlViewerDefaultUnsafe`(默认 false)——打开 HTML 文件时预览**初始**处于非沙箱状态(localUnlock 初始值取自该 pref),状态行仍可一键「恢复沙箱」;与 `htmlViewerNoSandbox`(全局强制关闭,不可界面恢复)语义区分。
|
||||
2. **沙箱状态行单行化并取代底部说明**:浏览器 tab 底部常驻 `browserHint` 条移除,说明并入 `SandboxStatusBar` 开启态文案(「沙箱模式:已启用 · 页面无法访问界面数据与本地文件,登录态与第三方 Cookie 可能不可用」),状态行 `nowrap + ellipsis + title` 恒为一行;关闭态同样单行红色警示。
|
||||
3. **「在浏览器中打开」图标放大**:`IconRightUpOutline14` → `IconRightUpOutline16`(15px 渲染)。
|
||||
|
||||
## 8.3 追加需求(2026-08-11 三轮,v0.5.0 内)
|
||||
|
||||
1. **图标改 15px**:`<IconRightUpOutline16 size={15} />`。
|
||||
2. **嵌入拒绝面板**:站点通过 `X-Frame-Options` / CSP `frame-ancestors` 禁止嵌入时,浏览器 tab 不再显示空白拒绝框,而是显示 `BrowserEmbedBlocked` 面板——「{host} 拒绝了嵌入请求」+ 原因说明 + 「在浏览器中打开」(`window.open`)+ 「仍然加载」两个按钮。检测:新增 host 路由 `browser.probe`(HEAD→GET 兜底,8s 超时,仅 http(s) 非 loopback,返回目标响应头的 `x-frame-options`/`frame-ancestors`,受信任围栏保护)+ 客户端 `embeddabilityOf` 判定(XFO DENY/SAMEORIGIN 或 frame-ancestors 不含 `*` → blocked;探测失败 → unknown 保持普通 iframe)。探测在每次导航(含恢复的 path)时经 `useEffect(url)` 触发;「仍然加载」以 `forceEmbed` 保留普通 iframe。
|
||||
|
||||
## 9. 已知限制与残余风险
|
||||
|
||||
- 浏览器沙箱无登录态/第三方 Cookie 受限(SameSite=Lax 在跨站 iframe 不发送),部分站点登录需走弹窗(allow-popups-to-escape-sandbox 已开)。
|
||||
- iframe 内部链接跳转不进后退栈(跨域不可见);被 `X-Frame-Options`/`frame-ancestors` 拒绝的站点显示空白(站点策略,无法绕过)。
|
||||
- 用户主动浏览的恶意站点仍可在沙箱内发起盲 GET(无法读取响应)到本地服务;站点重定向链可把 iframe 带到 localhost(内容仍沙箱化、受围栏保护)。
|
||||
- 关闭沙箱后预览/浏览内容获得 GUI 同源权限(可读会话文件/本地存储/调内部接口)——仅建议对完全可信内容开启,设置与界面双重警告。
|
||||
@@ -1,432 +0,0 @@
|
||||
# DSH-better-sidebar 服务化改造设计
|
||||
|
||||
**日期**:2026-08-11
|
||||
**状态**:已批准,待实施
|
||||
**作者**:opencode + 用户
|
||||
**当前版本**:v0.3.0(main `069c754`)
|
||||
**目标版本**:v0.4.0
|
||||
|
||||
## 1. 目标
|
||||
|
||||
将 `dsh-better-sidebar` 从一个"纯消费方"插件改造为"服务提供者":
|
||||
|
||||
1. 允许其他 DSH 插件向 better-sidebar **注册新的侧边栏页面(tab 类型)**
|
||||
2. 允许其他 DSH 插件向 better-sidebar **注册文件类型预览器**
|
||||
3. 把现有 5 个内置 tab 和 6 种内置文件预览器**全部迁移到新注册表**(吃自己的狗粮)
|
||||
|
||||
## 2. 非目标(Out of Scope)
|
||||
|
||||
- **不动 host 半**(`src/index.ts` 的 HTTP 路由 / pty / tools / settings)
|
||||
- **不迁移到标准 bundle 形态**(不加 `cordis.patch.yml`、不弃用 `dsh.plugin.json`、不放弃 `client-registry.js`)—— 作为独立工作
|
||||
- 不动 server 端 `ctx.provide`:本次纯 client 端服务
|
||||
|
||||
## 3. 现状回顾
|
||||
|
||||
### 3.1 Tab 系统硬编码点(7 处)
|
||||
|
||||
| 文件:行 | 用途 |
|
||||
|---|---|
|
||||
| `src/client/state.ts:14` | `TabType` 联合类型(5 字面量) |
|
||||
| `src/client/state.ts:295-297` | `isSingle(type)` 单实例判定 |
|
||||
| `src/client/state.ts:555-561` | `sanitizeNode` 白名单校验 |
|
||||
| `src/client/Sidebar.tsx:56-83` | `TabContent` switch 渲染 |
|
||||
| `src/client/Sidebar.tsx:87-102` | `buildNewTabOptions` + 菜单项 |
|
||||
| `src/client/Sidebar.tsx:341-364` | `onNewTab` 创建分支 |
|
||||
| `src/client/icons.tsx:26-33` | `tabTypeIcon` 图标 switch |
|
||||
|
||||
### 3.2 文件预览硬编码点
|
||||
|
||||
- `src/client/EditorView.tsx:58-106`:顺序 if/else 选 `load.kind`
|
||||
- `src/client/EditorView.tsx:251-272`:if 链按 `load.kind` 渲染组件
|
||||
- 4 个预览组件(DocxView / XlsxView / PptxView / PdfView)静态 import 在 `EditorView.tsx:22-26`
|
||||
- 3 个纯函数模块(`office-types.ts` / `image-types.ts` / `pdf-types.ts`)+ `lang.ts` 已半抽象,可作为内置注册器的实现细节复用
|
||||
|
||||
### 3.3 服务暴露现状
|
||||
|
||||
- `src/index.ts` / `src/client/index.tsx` 全文搜 `ctx.provide` / `Context.service` / `extends Service` —— **零命中**
|
||||
- `package.json` 的 `exports` 没有暴露任何供外部 import 的注册 API
|
||||
- README:262 已承认 portal 挂载的限制(整面板 slot 不可用),但未提服务化方案
|
||||
|
||||
### 3.4 平台约束
|
||||
|
||||
- **构建期纯度门**(`tsdown.config.ts:177-180`):client bundle 禁止 value-import 别的插件代码;**type-only import 会被擦除,不触发门禁**
|
||||
- **双 cordis 实例**:外部插件解析不到 DSH monorepo 的 cordis augmentation,必须由 better-sidebar 自己导出 `declare module 'cordis'` augmentation
|
||||
- **ModuleLoader 运行时支持跨 bundle require,但被构建门挡** → 协作只能走 cordis 服务或 slot
|
||||
|
||||
## 4. 方案选型
|
||||
|
||||
| 方案 | 机制 | 评估 |
|
||||
|---|---|---|
|
||||
| **A. `ctx.provide` + 服务对象(采纳)** | client `apply()` 内 `ctx.provide('betterSidebar', service)`,service 暴露 `registerTab/registerFileViewer` 返回 disposer | registry 语义自然;disposer 生命周期贴合 cordis fiber;precedent:`dsh/packages/client/locale/src/client/index.ts:362` |
|
||||
| B. `ctx.slots.inject` 声明式 slot | 声明 `betterSidebar.tabType` 等 slot | 偏 UI 链替换,对带元数据 + disposer 的 registry 别扭 |
|
||||
| C. `extends Service` 类形式 | 完整 Service 类 + Config schema | client 半不支持 `extends Service`;本次纯 client,没必要 |
|
||||
|
||||
**采纳方案 A**。
|
||||
|
||||
## 5. 服务接口设计
|
||||
|
||||
### 5.1 主服务接口
|
||||
|
||||
```ts
|
||||
// 新文件 src/client/api.ts
|
||||
export interface BetterSidebarService {
|
||||
/** 注册一种 tab 类型;返回 disposer */
|
||||
registerTab(descriptor: TabDescriptor): () => void
|
||||
/** 注册一种文件预览器;返回 disposer */
|
||||
registerFileViewer(descriptor: FileViewerDescriptor): () => void
|
||||
/** 运行时快照(同步,供 React useSyncExternalStore 用) */
|
||||
getTabs(): readonly TabDescriptor[]
|
||||
getFileViewers(): readonly FileViewerDescriptor[]
|
||||
/** 匹配文件的预览器(按 priority 降序、exts/includes、detect() 三段匹配) */
|
||||
matchFileViewer(path: string, head?: Uint8Array): FileViewerDescriptor | undefined
|
||||
/** 工具方法:让外部 tab 也能开/关 tab */
|
||||
openTab(tab: { type: string; title: string; path?: string }): void
|
||||
closeTab(tabId: string): void
|
||||
/** 订阅 registry 变化(注册/卸载时触发) */
|
||||
subscribe(listener: () => void): () => void
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 TabDescriptor
|
||||
|
||||
```ts
|
||||
export interface TabDescriptor {
|
||||
id: string // 唯一,如 'explorer' / 'my-plugin:db'
|
||||
title: string | (() => string) // i18n 友好
|
||||
icon?: ReactNode | ((size: number) => ReactNode)
|
||||
order?: number // + 菜单排序,默认 100
|
||||
single?: boolean // 单实例(explorer/git/subagent)
|
||||
hidden?: boolean // 不出现在 + 菜单(editor 用)
|
||||
available?: (ctx: Context, scope: SessionScope) => boolean
|
||||
component: (props: TabComponentProps) => ReactNode
|
||||
}
|
||||
|
||||
export interface TabComponentProps {
|
||||
ctx: Context
|
||||
store: SidebarStore
|
||||
scope: SessionScope
|
||||
tab: SidebarTab
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 FileViewerDescriptor
|
||||
|
||||
```ts
|
||||
export interface FileViewerDescriptor {
|
||||
id: string // 'image' / 'pdf' / 'my-plugin:csv'
|
||||
exts: readonly string[] // 小写无点,如 ['png','jpg']
|
||||
priority?: number // 高优先;默认 0;内置默认 0;兜底 code 用 -100
|
||||
fetchStrategy: 'none' | 'fsRead' | 'mediaUrl' | 'custom' | 'binary-download'
|
||||
detect?: (path: string, head: Uint8Array) => boolean // 内容嗅探(覆盖 exts)
|
||||
load?: (path: string, scope: SessionScope) => Promise<unknown> // fetchStrategy='custom' 时
|
||||
component: (props: FileViewerProps) => ReactNode
|
||||
}
|
||||
|
||||
export interface FileViewerProps {
|
||||
ctx: Context
|
||||
store: SidebarStore
|
||||
scope: SessionScope
|
||||
path: string
|
||||
title: string
|
||||
content?: string // fetchStrategy='fsRead' 时
|
||||
truncated?: boolean
|
||||
mediaUrl?: string // fetchStrategy='mediaUrl' 时
|
||||
customData?: unknown // fetchStrategy='custom' 时 load() 的返回值
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Cordis 类型合并 outlet
|
||||
|
||||
```ts
|
||||
// 新文件 src/types.ts (纯类型)
|
||||
import type { BetterSidebarService } from './client/api'
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
betterSidebar: BetterSidebarService
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通过 `src/index.ts` 的 `export type * from './types.ts'` re-export 到主入口。Consumer 插件 `import type {} from 'dsh-better-sidebar'` 触发合并。
|
||||
|
||||
## 7. package.json exports 新增
|
||||
|
||||
```jsonc
|
||||
"exports": {
|
||||
".": { ... },
|
||||
"./invariant": { ... },
|
||||
"./client": { ... },
|
||||
"./client/api": { // 新增,纯类型导出
|
||||
"types": "./lib/types/client/api.d.ts",
|
||||
"default": "./lib/client.js" // 运行时落点回到 client bundle(不会被 value import)
|
||||
},
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json"
|
||||
}
|
||||
```
|
||||
|
||||
`files` 字段相应加 `lib/types/client/api.d.ts`。
|
||||
|
||||
## 8. 内置 tab 自我注册(吃狗粮)
|
||||
|
||||
`src/client/index.tsx` 的 `apply()` 内:
|
||||
|
||||
```ts
|
||||
const service = createBetterSidebarService(store)
|
||||
ctx.provide('betterSidebar', service)
|
||||
registerBuiltins(service) // 5 个内置 tab + 内置 viewer 全部用 service.registerTab/registerFileViewer
|
||||
```
|
||||
|
||||
### 8.1 5 个内置 tab
|
||||
|
||||
| id | order | single | hidden | available | 组件 |
|
||||
|---|---|---|---|---|---|
|
||||
| `editor` | -1 | false | true | always | `EditorView`(由 open-file 流程触发,不在 + 菜单) |
|
||||
| `explorer` | 10 | true | false | always | `ExplorerView` |
|
||||
| `git` | 20 | true | false | 检查 `.git` 存在 | `GitView` |
|
||||
| `subagent` | 30 | true | false | always | `SubagentView` |
|
||||
| `terminal` | 40 | false | false | 检查 terminal 配额 | `TerminalView` |
|
||||
|
||||
### 8.2 内置 file viewer
|
||||
|
||||
| id | exts | priority | fetchStrategy | 组件 |
|
||||
|---|---|---|---|---|
|
||||
| `image` | png/jpg/jpeg/gif/webp/svg/bmp/ico/avif | 0 | mediaUrl | `<img>` |
|
||||
| `pdf` | pdf | 0 | mediaUrl | `PdfView` |
|
||||
| `docx` | docx | 0 | mediaUrl | `DocxView` |
|
||||
| `xlsx` | xlsx | 0 | mediaUrl | `XlsxView` |
|
||||
| `pptx` | pptx | 0 | mediaUrl | `PptxView` |
|
||||
| `markdown` | md/markdown | 0 | fsRead | `<MarkdownText>` |
|
||||
| `code` | [](兜底) | -100 | fsRead | CodeMirror |
|
||||
| `binary-download` | doc/xls/ppt + NUL 探测为 binary | -50 | binary-download | 下载按钮 |
|
||||
|
||||
`matchFileViewer(path, head?)` 算法:
|
||||
1. 按 `priority` 降序遍历
|
||||
2. 若 `descriptor.detect` 提供且 `head` 可用 → 调 `detect(path, head)`,true 则命中
|
||||
3. 否则匹配 `exts`(小写无点)
|
||||
4. 全部 miss → 返回 `undefined`(caller 走兜底 `binary-download`)
|
||||
|
||||
## 9. 7 处硬编码 switch 改造点
|
||||
|
||||
| 文件:行 | 原状 | 改造后 |
|
||||
|---|---|---|
|
||||
| `src/client/state.ts:14` | `TabType` 联合 5 字面量 | `type TabType = string`;保留 `SIDEBAR_BUILTIN_TAB_TYPES` 常量数组供向后兼容 |
|
||||
| `src/client/state.ts:295-297` `isSingle` | switch 5 字面量 | `(type) => service.getTabs().find(t => t.id === type)?.single ?? false`;store 持有 service 引用 |
|
||||
| `src/client/state.ts:555-561` `sanitizeNode` | 白名单 5 type | 未注册 type 标记 `orphaned: true` 不丢弃,渲染占位卡 |
|
||||
| `src/client/Sidebar.tsx:56-83` `TabContent` | switch 5 分支 | 查 `service.getTabs().find(t => t.id === tab.type)`,调 `descriptor.component(props)`;未找到渲染 `<OrphanedTab/>` |
|
||||
| `src/client/Sidebar.tsx:87-102` `buildNewTabOptions` | 硬编码 4 项 | `service.getTabs().filter(t => !t.hidden && (t.available?.(ctx, scope) ?? true)).sort(byOrder).map(...)` |
|
||||
| `src/client/Sidebar.tsx:341-364` `onNewTab` | if optionId === 'explorer' 等 | `service.openTab({ type: optionId, title: descriptor.title() })` |
|
||||
| `src/client/icons.tsx:26-33` `tabTypeIcon` | switch 5 分支 | 废弃;调用方直接读 `descriptor.icon` |
|
||||
|
||||
## 10. 文件预览路由改造
|
||||
|
||||
### 10.1 `src/client/EditorView.tsx:58-106`(选预览器)
|
||||
|
||||
```ts
|
||||
const viewer = service.matchFileViewer(path, head?)
|
||||
if (!viewer || viewer.fetchStrategy === 'binary-download') {
|
||||
setLoad({ status: 'binary' })
|
||||
return
|
||||
}
|
||||
switch (viewer.fetchStrategy) {
|
||||
case 'mediaUrl': setLoad({ status: 'ready', kind: 'mediaUrl' }); break
|
||||
case 'fsRead':
|
||||
api.fsRead(scope, path).then(({ content, truncated }) =>
|
||||
setLoad({ status: 'ready', kind: 'fsRead', content, truncated })
|
||||
)
|
||||
break
|
||||
case 'custom':
|
||||
viewer.load(path, scope).then(data =>
|
||||
setLoad({ status: 'ready', kind: 'custom', customData: data })
|
||||
)
|
||||
break
|
||||
case 'none': setLoad({ status: 'ready', kind: 'none' }); break
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 `src/client/EditorView.tsx:251-272`(渲染)
|
||||
|
||||
```ts
|
||||
const viewer = service.matchFileViewer(path)
|
||||
if (!viewer) return <BinaryDownload ... />
|
||||
return <viewer.component {...props} />
|
||||
```
|
||||
|
||||
4 个内置预览组件(DocxView/XlsxView/PptxView/PdfView)从 `EditorView.tsx` 静态 import 移到 `registerBuiltins` 内部 import + 注册。
|
||||
|
||||
## 11. 持久化与降级
|
||||
|
||||
- localStorage 里 `orphaned` tab:渲染 `<OrphanedTab title="..." hint="Plugin not loaded: ${tab.type}"/>` + 关闭按钮
|
||||
- `sanitizeState` / `sanitizeNode` 不再丢弃未识别 type,改为标记 `orphaned`,保留 `title/path` 以备插件恢复时复用
|
||||
|
||||
## 12. Consumer 插件使用范例
|
||||
|
||||
### 12.1 Consumer `package.json`
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"peerDependencies": {
|
||||
"cordis": "^4.0.0-rc.8",
|
||||
"dsh-better-sidebar": "workspace:*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"dsh-better-sidebar": { "optional": true }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 12.2 Consumer client half
|
||||
|
||||
```ts
|
||||
// my-plugin/src/client/index.tsx
|
||||
import type {} from 'dsh-better-sidebar' // 触发 ctx.betterSidebar 类型合并
|
||||
|
||||
export const inject = ['betterSidebar', 'slots']
|
||||
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.effect(() =>
|
||||
ctx.betterSidebar.registerTab({
|
||||
id: 'my-plugin:db',
|
||||
title: () => 'Database',
|
||||
icon: <DbIcon />,
|
||||
order: 50,
|
||||
component: ({ scope, store, tab }) => <DbView scope={scope} />,
|
||||
})
|
||||
)
|
||||
|
||||
ctx.effect(() =>
|
||||
ctx.betterSidebar.registerFileViewer({
|
||||
id: 'my-plugin:csv',
|
||||
exts: ['csv'],
|
||||
fetchStrategy: 'custom',
|
||||
load: async (path, scope) => parseCsv(await fetchCsv(scope, path)),
|
||||
component: ({ customData, path }) => <CsvGrid data={customData} path={path} />,
|
||||
})
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 13. 测试策略
|
||||
|
||||
### 13.1 单元测试
|
||||
|
||||
- `tests/service.spec.ts`:`createBetterSidebarService` 的 register/dispose/subscribe 生命周期
|
||||
- `tests/match-file-viewer.spec.ts`:`matchFileViewer` 的 priority + exts + detect 三段匹配算法
|
||||
- `tests/builtins.spec.ts`:5 个内置 tab + 8 个内置 viewer 都正确注册
|
||||
- 现有 `tests/state.spec.ts` / `tests/office-types.spec.ts` / `tests/image-types.spec.ts` 保持通过
|
||||
|
||||
### 13.2 集成测试
|
||||
|
||||
- `tests/orphaned-tab.spec.ts`:localStorage 里有未注册 type 的 tab → 渲染 `<OrphanedTab/>`,注册后恢复
|
||||
|
||||
### 13.3 现有 manifest 守卫
|
||||
|
||||
- `tests/manifest-consistency.spec.ts` 的 `contributes.tools/skills` 空数组守卫保持通过
|
||||
- `CLIENT_EXTERNALS` 列表守卫保持通过(不新增外部 value import)
|
||||
|
||||
## 14. 风险与缓解
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| 双 cordis 实例导致 consumer 拿不到 `ctx.betterSidebar` 类型 | 由 dsh-better-sidebar 自己 `declare module 'cordis'`,consumer `import type {}` 触发合并 |
|
||||
| 注册顺序:consumer 在 better-sidebar 之前激活导致 inject 等待 | cordis `inject` 自带就绪等待,consumer 不会在服务就绪前激活 |
|
||||
| HMR 时 disposer 漏调导致注册项残留 | `ctx.effect(() => disposer)` 挂到 fiber,卸载时自动调 |
|
||||
| localStorage 里旧版本的 `TabType` 字面量 tab | `sanitizeNode` 改为标记 `orphaned` 不丢弃,向后兼容 |
|
||||
| `EditorView` 的 `kind` 字段类型从联合字面量变成 string | 内部 `load` 状态类型加 `kind: string` + `customData?: unknown`,TypeScript 收窄在渲染层做 |
|
||||
| 兜底 `code` viewer priority -100 与外部 `binary-download` -50 冲突 | `matchFileViewer` 先尝试 `detect`(NUL 探测为 binary),再走 exts;外部 `binary-download` 的 `exts: ['doc','xls','ppt']` 优先于兜底 `code` 的 `exts: []` |
|
||||
|
||||
## 15. 版本与发布
|
||||
|
||||
- 版本号:`v0.4.0`(破坏性:`TabType` 从联合字面量改 string)
|
||||
- peerDependencies 不变(不加新依赖)
|
||||
- `dsh.plugin.json` 的 `version` 同步更新到 `0.4.0`
|
||||
- 重建 `lib/` + 推送 main + profile 更新
|
||||
|
||||
## 16. 实施顺序(高层)
|
||||
|
||||
1. 拉取 main v0.3.0 到工作分支
|
||||
2. 新建 `src/client/api.ts`(类型 + `createBetterSidebarService` 工厂)
|
||||
3. 新建 `src/types.ts`(`declare module 'cordis'` augmentation)+ `src/index.ts` re-export
|
||||
4. `src/client/index.tsx` 改造:`ctx.provide('betterSidebar', service)` + `registerBuiltins(service)`
|
||||
5. 抽 `registerBuiltins.ts`:5 个内置 tab + 8 个内置 viewer 注册
|
||||
6. 改造 7 处硬编码 switch(state.ts / Sidebar.tsx / icons.tsx)
|
||||
7. 改造 `EditorView.tsx` 的选预览器 + 渲染两段
|
||||
8. `package.json` 加 `./client/api` exports + `files` 字段
|
||||
9. 加 `<OrphanedTab/>` 组件 + sanitize 降级
|
||||
10. 写测试(service / matchFileViewer / builtins / orphaned)
|
||||
11. typecheck + test + build 三件套
|
||||
12. 重建 lib/,提交,推送,更新 profile web
|
||||
|
||||
详细实施计划由 writing-plans skill 生成。
|
||||
|
||||
---
|
||||
|
||||
## 17. 实施偏差记录(2026-08-11,feat/modular-registry)
|
||||
|
||||
实现(PR #3 + 本分支)与上文设计有意或被迫不同的点,逐条记录。文档以本节为准。
|
||||
|
||||
### 17.1 去重机制:`dedupeKey` 为唯一机制,`single` 是其语法糖
|
||||
|
||||
§5.2 的 `single?: boolean` 与实现的 `dedupeKey` 合并为单一机制:
|
||||
- `dedupeKey?: (tab) => string | undefined` 是唯一去重规则(单实例 `() => id`、按 path、按 id 三种旧策略统一表达);
|
||||
- `single: true` ≡ `dedupeKey: () => id`(语法糖,显式 dedupeKey 优先)。
|
||||
|
||||
### 17.2 `available` 三参签名
|
||||
|
||||
§5.2 的 `available?: (ctx, scope) => boolean` 实现为 `(ctx, scope, state) => boolean`(state 用于 terminal 配额等 UI 状态判定,是设计签名的超集)。
|
||||
|
||||
### 17.3 匹配算法:单趟 per-descriptor,非三趟全局
|
||||
|
||||
§8.2 的算法(priority 降序逐 descriptor,detect 或 exts 二选一)为准;实现确认单趟,并补充一条规则:**`exts: []` + `detect` 是纯嗅探模式**——无 head 时不认领任何文件(避免 magic-sniffer 吞掉图片/PDF 等真实 viewer 的文件),有 head 时只认领 detect 命中的。盲 catch-all(无 detect)行为不变。
|
||||
|
||||
### 17.4 编辑器拆分:EditorHost + TextEditor(code/markdown 成为注册 viewer)
|
||||
|
||||
§10 的 EditorView 职责一分为二:
|
||||
- `EditorHost`('editor' tab 组件):标题栏 + `matchFileViewer` → 按 fetchStrategy 分发(纯逻辑在 `editor-load.ts` 的 `planFirstMatch`/`planFsReadOutcome`)→ 渲染 viewer.component;binary 无匹配渲染共享 `BinaryDownload`;
|
||||
- `TextEditor`(viewer 组件):code/markdown 的 CodeMirror 编辑 + md 预览/编辑切换 + 保存/脏点,内容由 host 经 fsRead 传入,按 `viewerId === 'markdown'` 分支。
|
||||
- 代码/保存按钮从标题行移到内容区第二行(VSCode 式工具栏,有意 UX 变更)。
|
||||
|
||||
### 17.5 `fs.read` 增加 `head` 字段(唯一 host 触碰)
|
||||
|
||||
§8.2/§14 的 detect 机制需要 head 字节:host `fs.read` 对二进制响应附加 `head`(base64,前 4KB,常量 `READ_HEAD_LIMIT`)。客户端在 fsRead 结果为二进制时用 head 重匹配一次(无额外 IO),NUL 探测因此成立。向后兼容(新字段)。纯文本文件不触发重匹配(文本嗅探请用 exts 或 custom 策略)。
|
||||
|
||||
### 17.6 导出路径:`./client/service` 为主,`./client/api` 为别名
|
||||
|
||||
§7 要求 `./client/api`,但 `api.ts` 已被既有的 fetch 助手占用;package.json exports 增加 `"./client/api"` 别名指向 `lib/types/client/service.d.ts`,公共导入路径满足设计,内部文件名不变。
|
||||
|
||||
### 17.7 + 菜单:disabled 行而非 filter
|
||||
|
||||
§9 的 `filter(!hidden && available)` 实现为"available 返回 false 显示 disabled 行"(与 v0.3.0 旧 UX 一致;终端配额满时用户能看到选项为何不可用)。行为有意保留,文档更新为现状。
|
||||
|
||||
### 17.8 保留的 store 级操作(非注册表职责)
|
||||
|
||||
- `openDiffTab`(state reducer):diff 的 split 树放置手术(粘性 diff pane、首次拆分下方),保留;其按 id 去重与 diff descriptor 的 `dedupeKey: (tab) => tab.id` 同规则(测试断言两者一致)。
|
||||
- `reconcileAgentTerminals`:agent 终端列表同步(host 推送驱动),直接落 state(`openTabInActivePane`),不经过 service(terminal 的 createTab 语义不适用)。
|
||||
- 命名:state.ts 的 `openTab` reducer 改名 `openTabInActivePane`(与 `service.openTab` 区分)。
|
||||
|
||||
### 17.9 内置清单
|
||||
|
||||
- tab 6 个不变(editor/explorer/git/subagent/terminal/diff),explorer/git/subagent 改用 `single: true`。
|
||||
- viewer 8 个:image/pdf/docx/xlsx/pptx(mediaUrl)+ markdown(fsRead)+ code(fsRead,catch-all -100)+ binary-download(-50,exts doc/xls/ppt + NUL detect)。
|
||||
- `SIDEBAR_BUILTIN_TAB_TYPES` 常量删除(零引用);`tabTypeIcon` 删除(图标归 descriptor.icon)。
|
||||
|
||||
### 17.10 测试
|
||||
|
||||
新增 `tests/builtins.spec.ts`(内置注册清单)、`tests/orphaned-tab.spec.tsx`(sanitize 保留未注册类型 + 占位渲染)、`tests/editor-load.spec.ts`(策略分发纯函数);`tests/browser-globals.ts` 为拉入 xterm/CodeMirror 的 spec 提供模块求值期浏览器全局 mock。`matchFileViewer` 相关断言按 17.3 语义重写。
|
||||
|
||||
---
|
||||
|
||||
## 18. v0.12.0 基座化扩展记录(2026-08-14,feat/plugin-api-v012)
|
||||
|
||||
§5 的服务接口在 v0.12.0 做了**纯增量**扩展(无任何签名删除 / 语义变更,向上兼容),全部记录于此,文档以本节为准:
|
||||
|
||||
- **类型导出补齐**:`./client/service` 入口 re-export 公共状态词汇——`SidebarTab` / `SidebarState` / `SidebarStore` / `SidebarSnapshot` / `SidebarDiffRef` / `TabType` / `SessionScope` / `SidebarPrefs` / `OpenTabSeed` / `SidebarSettingsRenderProps`。此前消费者无法命名这些类型(TS2459),只能靠推断。
|
||||
- **client 声明图零 Node 依赖**:`context-types.ts` 的 `SidebarWebRoute` / `SidebarWebUpgradeRoute` 改用结构镜像类型(`SidebarHttpRequest` / `SidebarHttpResponse` / `SidebarUpgradeSocket` / `SidebarUpgradeHead`),宿主在 ws 边界显式转型(`src/index.ts` 两处)。外部消费插件无需 `@types/node`、`skipLibCheck: false` 也能编译(`scripts/check-consumer-types.sh` 双层校验守护)。
|
||||
- **新服务方法**:`version` / `features`(单调能力清单,只增不删);`getSnapshot` / `subscribeState`(状态订阅,委托 store);`updateTab(tabId, patch)`(title/path/meta);`activateTab(tabId)`(跨树激活,tab 栏点击路径);`openFile(scope, path, title?)`(editor 打开,id 按路径派生与内置拦截一致);`openTab(seed, scope?)`(定向到指定 session:`SidebarStore.reduceFor` 加载/变更/持久化目标 session 而不切换 snapshot,UI 不扰动;scope 指向当前 session 时走原 reduce 路径保证 notify)。
|
||||
- **新 descriptor 字段**:`badge`(tab 角标:number 计数 99+ 封顶 / string 文本 / null 不显示,每次 tab 栏渲染调用,抛错吞掉);`onOpen` / `onActivate` / `onClose`(生命周期回调,service 路径派发——dedupe/id 安全网聚焦触发 onActivate 而非 onOpen;内置 diff 拆分与 agent 终端 reconcile 直接改 state 不触发,但只作用于内置类型;回调抛错只 console.error);`SidebarTab.meta`(JSON 可序列化自定义状态,`sanitizeNode` 宽松保留,跨刷新原样恢复)。
|
||||
- **设置 seam 开放**:`PrefsSchema` 增加 `pluginSettings: z.dict(z.dict(z.any())).default({})` 开放嵌套 map;`settings.pluginToggles`(插件自有声明式行)与 `settings.render`(自定义面板,props 见 §5)替代「自定义 key 被 seam 丢弃」的限制;viewer 卡片 v0.12.0 起同样有齿轮设置弹窗。
|
||||
- **`FileViewerDescriptor.load` 第三参 `signal?: AbortSignal`**:EditorHost 在卸载/重匹配时 abort(忽略 signal 的 load 照常工作)。
|
||||
- 测试:`tests/consumer-types.ts`(消费面编译门)、`tests/api-surface.spec.ts`(client 图零 node 依赖守护 + 能力常量)、`tests/service.spec.ts` 新增生命周期 / 定向打开 / updateTab / activateTab / openFile / meta 持久化用例、`tests/unit.spec.ts` 新增 `reduceFor` 与 sanitize meta 用例。
|
||||
|
||||
实现偏差:§5.1 的 `openTab(seed)` 单参签名保留为 `openTab(seed, scope?)` 的可选参数(非破坏);`SidebarSnapshot` 类型 re-export 自 `./client/service` 而非新建独立文件。
|
||||
@@ -1,65 +0,0 @@
|
||||
# 跟随 DSH 的 i18n(ctx.locale)设计
|
||||
|
||||
> 2026-08-12 · 随 v0.8.2 发布
|
||||
|
||||
## 背景
|
||||
|
||||
better-sidebar 的界面文案(约 260 处 `t()` 调用、28 个文件)此前自带 zh/en 词典并直接读
|
||||
`navigator.language`(`src/client/locales.ts` 头注释曾明言"renders outside the slot
|
||||
system's locale seat, so it reads the browser language directly")。问题:
|
||||
|
||||
- DSH 的语言偏好是 Host-backed(`locale.preference`,存于 `$DSH_HOME/settings.yaml`),
|
||||
用户设置与浏览器语言不一致时侧边栏不跟随(如偏好 zh、浏览器 en 时侧边栏仍为英文)。
|
||||
- 词典游离在 DSH 的 i18n 注册表之外,无法参与公共命名空间回退等机制。
|
||||
|
||||
DSH 的 i18n 系统(`@deepseek-ai/dsh-client-locale`):
|
||||
|
||||
- `ctx.locale` 服务(client-only):`getSnapshot()/subscribe()` 是 LocaleFace
|
||||
(uSES 安全,revision 递增),`locale/change` 事件在切换时触发;
|
||||
- `ctx.locale.register(ns, {zh, en})` 命名空间词典注册(typed 变体要求合并
|
||||
`LocaleNamespaceMap`);查询链 ns → common → zh → key;
|
||||
- 所有 DSH 客户端包以 `inject = [..., 'locale']` 消费。
|
||||
|
||||
## 设计决策
|
||||
|
||||
1. **结构镜像服务**(仓库惯例,`src/context-types.ts` 头注释):本插件解析在 DSH
|
||||
monorepo 单一 cordis 实例之外,上游 `declare module 'cordis'` augmentation 不达此
|
||||
Context。新增 `SidebarLocaleService` 结构脸(只镜像用到的切片:
|
||||
`getSnapshot` / `subscribe` / `register`),merge `Context.locale`。不引入
|
||||
`@deepseek-ai/dsh-client-locale` 依赖、不做 value import(client bundle 纯度门)。
|
||||
2. **保留模块级 `t()`**:`t()` 被 28 个文件使用,穿 props 不可行。改为模块级 holder:
|
||||
`apply()` 调 `attachLocale(ctx.locale)`,`t()`/`isZh()` 从 holder 读活跃 locale;
|
||||
无 holder 时回退既有 `navigator.language` 检测(行为与改动前完全一致,也是 jsdom
|
||||
测试与非标准组合的降级路径)。
|
||||
3. **根级重渲染触发**:侧边栏自行 `createRoot` 挂在 slot 系统之外,拿不到 `t` seat。
|
||||
在 `Sidebar.tsx` 根组件加 `useSyncExternalStore(ctx.locale.getSnapshot/subscribe)`
|
||||
(该文件已用此 API 订阅 sessions/store);树内无 `React.memo`,根重渲染即整树
|
||||
重渲染,所有渲染期 `t()` 求值读到新语言。事件期调用(intercept、api、subagent-tasks)
|
||||
在调用时刻读当前语言,天然正确。SideCardSection 由 settings shell 渲染,shell 自身
|
||||
订阅 locale revision(ui-settings-general 的 section rows 缓存含 locale revision),
|
||||
自动跟随。
|
||||
4. **命名空间 `'betterSidebar'`**:`'sidebar'` 已被 DSH 的 ui-sidebar 占用。注册走
|
||||
非类型化单语言重载 `register(ns, locale, dict)`(DSH 内 ui-permission 同款先例);
|
||||
键型安全由本地 `const en: Record<keyof typeof zh, string>` + 测试键集相等断言保证。
|
||||
5. **词典注册进 DSH 注册表**:`ctx.effect` 内注册 zh/en 并返回合并 disposer
|
||||
(fiber 卸载时撤销,HMR 重激活不抛 "already registered")。
|
||||
6. **xlsx 预览的 Univer locale pack**:`xlsx-view.tsx` 原内联 navigator 检测改走
|
||||
`isZh()`(跟随 DSH 语言;Univer 实例在挂载时创建,语言切换不重建实例——可接受)。
|
||||
|
||||
## 实施偏差记录
|
||||
|
||||
- 无。计划与实施一致。
|
||||
- 版本号随 v0.8.2 发版提交(`package.json` + `dsh.plugin.json`)。
|
||||
- v0.8.2 后补遗:md 预览(`TextEditor.tsx`)的代码块复制按钮文案——DSH 的
|
||||
`MarkdownText`/`CodeBlock` 是 cordis-free 组件,未传 `codeLabels` 时回退到硬编码中文
|
||||
「复制/复制成功」。修复为渲染时传入 `codeLabels={{ copyLabel: t('copy'), copiedLabel: t('copied') }}`
|
||||
(对齐 DSH 聊天区 `AssistantMarkdown` 的做法),新增 `tests/markdown-copy-labels.spec.tsx`
|
||||
钉住 zh/en 跟随。
|
||||
|
||||
## 验收
|
||||
|
||||
- `pnpm typecheck` / `pnpm test` / `pnpm build`(纯度门)全绿;`tests/locales.spec.ts`
|
||||
覆盖:无服务回退、attach 后跟随活跃 locale 实时切换、detach 恢复回退、占位符插值、
|
||||
relativeTime 链路、命名空间名、zh/en 键集相等。
|
||||
- 手动:设置页切换语言 → 侧边栏全部文案实时切换;`locale.preference: zh` 且浏览器 en
|
||||
→ 侧边栏中文。
|
||||
@@ -1,89 +0,0 @@
|
||||
# 客户端懒加载分块(lazy chunks)设计
|
||||
|
||||
> 日期:2026-08-12
|
||||
> 状态:已实现
|
||||
> 范围:`dsh-better-sidebar` 插件,客户端 bundle 拆分
|
||||
|
||||
## 1. 背景与问题
|
||||
|
||||
改造前客户端是单文件 bundle(`lib/client.js`,约 24.6MB),页面启动时全部**下载 + 解析 + 执行**。实测构成(按 sourcemap 源字节):
|
||||
|
||||
| 依赖 | 体积 | 用途 |
|
||||
|---|---|---|
|
||||
| `@univerjs/*` 全家桶 + echarts/zrender 等 | ~18MB | .xlsx 预览 |
|
||||
| `@codemirror/*` + `@lezer/*` | ~1.9MB | 文本编辑器高亮 |
|
||||
| `xlsx`(SheetJS) | ~0.6MB | .xlsx 解析 |
|
||||
| `@aiden0z/pptx-renderer` | ~0.5MB | .pptx 预览 |
|
||||
| `xterm` | ~0.3MB | 终端 |
|
||||
| `docx-preview` + `jszip` | ~0.2MB | .docx 预览 |
|
||||
| 插件自身代码 | ~0.35MB | — |
|
||||
|
||||
虽然 `office-view.tsx` 用了动态 `import()`,但 `codeSplitting: false` 只保证**延迟执行**(`Promise.resolve().then(() => (init_docx_preview(), ...))`),下载与解析成本仍在启动时;且 `xlsx-to-univer.ts` 对 `@univerjs/presets` 的静态 value-import 让 Univer 核心实际在启动时执行。
|
||||
|
||||
**目标**:重依赖拆成独立脚本,首次打开对应功能才下载/解析/执行;启动只拉核心(~325KB)。
|
||||
|
||||
## 2. 机制
|
||||
|
||||
chunk 脚本**不经过** `window.__ModuleLoader__` / `ClientModuleSystem.import()` 的物化路径——模块系统的 import() 只解析 seed word / shell-own 模块 / 已注册 factory / boot graph row,chunk id 四者皆非,解析行为依赖 DSH 版本(实测某些版本在 factory 已注册时仍抛 "cannot resolve")。因此改为插件自有的全局 factory 注册表:
|
||||
|
||||
1. **构建期**:每个 chunk 是独立 tsdown 浏览器 bundle(`lib/client-<name>.js`),脚本首行为:
|
||||
```js
|
||||
globalThis.__dshChunks__ = globalThis.__dshChunks__ || {};
|
||||
globalThis.__dshChunks__["terminal"] = (require) => { /* CJS 闭包体 */ };
|
||||
```
|
||||
与主 bundle 相同的 externals(react 等走 module table)、纯度门、CSS 内联管线;`codeSplitting: false` 保持单文件。
|
||||
2. **运行期** `loadChunk(name)`:
|
||||
- 注入 `<script src="/sidebar/bundle/<name>.js">`(经典同源脚本;官方 `/plugins/<id>/client.js` 路由的路径白名单无法服务任意文件名,故走插件自有路由);
|
||||
- 从 `globalThis.__dshChunks__[name]` 读取 factory(脚本执行即赋值,幂等——重复执行覆盖槽位,无 "duplicate factory registration" 错误类);
|
||||
- 用**自定义 require** 物化:externals(CLIENT_EXTERNALS 清单)经 `__DSH_MODULES__.import(spec)` 的 **seed 分支**逐一解析(页面级记忆化;解析失败的 spec 标记未解析,仅当 chunk 实际 require 时才报错)——seed 分支是模块系统最稳定、跨版本一致的部分。
|
||||
|
||||
## 3. 分块清单
|
||||
|
||||
| chunk 名 | 入口 | 内容 | 构建产物 | 触发 |
|
||||
|---|---|---|---|---|
|
||||
| `xlsx` | `src/client/chunks/xlsx.tsx` | `XlsxView` + Univer 全家桶 + xlsx + Univer CSS | lib/client-xlsx.js(~20MB) | 打开 .xlsx |
|
||||
| `docx` | `src/client/chunks/docx.tsx` | `DocxView` + docx-preview + jszip | lib/client-docx.js(~330KB) | 打开 .docx |
|
||||
| `pptx` | `src/client/chunks/pptx.tsx` | `PptxView` + pptx-renderer | lib/client-pptx.js(~2.4MB) | 打开 .pptx |
|
||||
| `terminal` | `src/client/chunks/terminal.tsx` | `TerminalView` + xterm + addon-fit + xterm.css | lib/client-terminal.js(~450KB) | 打开终端 tab |
|
||||
| `editor` | `src/client/chunks/editor.tsx` | `TextEditor` + @codemirror/* + lang.ts + cm-themes | lib/client-editor.js(~1.6MB) | 打开 code/md/html 文件 |
|
||||
| `mermaid` | `src/client/chunks/mermaid.tsx` | `MermaidBlocks` + mermaid + d3/dagre/cytoscape | lib/client-mermaid.js(~7MB) | 预览含 mermaid fence 的 md |
|
||||
| 核心 | `src/client/index.tsx` | 其余全部 | lib/client.js(~325KB) | 启动 |
|
||||
|
||||
关键拆分:**`office-view.tsx` 拆为 `docx-view.tsx` + `xlsx-view.tsx` + `office-shared.tsx`**(共享 `LoadState`/`BinaryFallback`)。若不拆,docx chunk 会经共享文件静态拖入 `xlsx-to-univer` → Univer 14.5MB。原路径 `office-view.tsx` 保留为 re-export 垫片,兼容 `./src/*` 深导入。
|
||||
|
||||
## 4. 缓存契约(三层,各带失败路径)
|
||||
|
||||
| 层 | 机制 | 失败路径 |
|
||||
|---|---|---|
|
||||
| 内存 | `loadChunk` 每 chunk 一个 in-flight promise,并发去重,常驻到 `resetChunks` | 失败删除缓存项 → 下次调用重试 |
|
||||
| 执行 | 脚本执行即覆盖全局注册表槽位(赋值幂等) | 物化失败清缓存 → 重试重新注入 + 重新执行脚本 |
|
||||
| HTTP | `/sidebar/bundle` 路由 `cache-control: no-cache` + ETag(内容哈希,按 mtime/size 记忆化),`If-None-Match` 命中返回 304 | 页面刷新/HMR 重激活时 304 免重下载 17MB;文件变化 ETag 轮转返回 200 |
|
||||
|
||||
**HMR**:`apply()` 每次激活调用 `resetChunks()`(清内存缓存 + externals 记忆),热更新后重新拉取新代码。chunk 源码单独改动需手动刷新页面(HMR 轮询只盯 client.js)。
|
||||
|
||||
## 5. 对外 API 兼容性(硬约束)
|
||||
|
||||
- `ctx.betterSidebar` 服务方法签名与行为零变化;懒加载只作用于**内置** descriptor,外部插件注册的原样存储/渲染。
|
||||
- `component` 契约 `(props) => ReactNode` 保持:懒包装是**纯渲染函数**(函数体无 hooks,hooks 在内部 `LazyChunkView`),兼容 Sidebar 的函数式调用与 EditorHost 的 `createElement` 渲染两种风格。
|
||||
- 内置 viewer/tab 的 id、exts、priority、detect、icon、title、settings 全部不变 → `matchFileViewer` 语义、外部插件同扩展名高优先级覆盖、设置页清单不变。
|
||||
- 公开类型面(`./client/service`、`./client/api`)不变;无新增 peerDeps。
|
||||
- 契约测试:`tests/lazy-chunk.spec.tsx` 钉住"component 可直接函数调用不抛错"。
|
||||
|
||||
## 6. 边界与失败模式
|
||||
|
||||
- chunk 加载/执行/物化失败 → 视图级错误 + 重试 UI(`css.editorError` + `css.terminalRetry`),面板其余部分不受影响;
|
||||
- host 旧版本无 `/sidebar/bundle` 路由(404) → 同样的错误 + 重试,升级 host 后重试即可;
|
||||
- `window.__DSH_MODULES__` 缺失(异常环境) → 明确报错,不触网;
|
||||
- chunk 注册 id 恒定 `dsh-better-sidebar/<name>`,两渠道共用(每页只活一个渠道——双渠道并存本就双挂载,属既有破损组合);
|
||||
- 跨 chunk 复用:`t()`/`api.ts`/`SandboxStatusBar`/`clsx`/`sidebar.module.css` 等无状态小模块允许在各 chunk 内联(几十 KB 级重复),避免 chunk→主 bundle require 的脆弱耦合;
|
||||
- CSS:`sidebar.module.css` 的 tagId 与官方渠道核心一致 → 幂等跳过注入;registry 渠道会多注入一份同内容样式(数据标签差异,无功能影响);xterm.css/Univer.css 随各自 chunk 注入。
|
||||
|
||||
## 7. 实施偏差记录
|
||||
|
||||
- 原设想 React.lazy + Suspense 被否决:错误处理需 error boundary,重试语义差;改用有状态包装组件(loading/error/retry 三态),与本库"每个视图自带 loading/error/retry"风格一致。
|
||||
- 原设想 chunk 注册 id 按渠道区分(`__DSH_PLUGIN_ID__` define)被否决:chunk 文件两渠道共用,恒定槽位即可;避免重复构建 6 份产物。
|
||||
- `resetChunks` 同时清空测试注册表(测试隔离需要)。
|
||||
- `lazy-chunk.tsx` 内部渲染用 `ComponentType<any>` 状态成员规避 React 18 createElement 泛型重载限制(仅内部一处显式 any)。
|
||||
- **v2(运行时修复)**:最初实现经 `window.__ModuleLoader__.load({id})` 注册 chunk factory、再 `__DSH_MODULES__.import(id)` 物化——真实环境报 `client-modules: cannot resolve "dsh-better-sidebar/terminal"`(某 DSH 版本的 import() 不解析已注册的非 boot 图 id)。改为全局 factory 注册表(`globalThis.__dshChunks__`) + 自定义 externals require(seed 分支),彻底不依赖模块系统的 factory 解析行为;`resetChunks` 不再需要 invalidate。
|
||||
- **v3(运行时修复)**:terminal 懒包装把整个 `TabComponentProps` 原样透传,但 `TerminalView` 的 props 是 `{ scope, tabId, store }` 而 `TabComponentProps` 只有 `tab` 对象、没有 `tabId` → `tabId` 为 undefined → `isAgentTabId` 里 `undefined.startsWith` 崩溃(`dsh-better-sidebar: Cannot read properties of undefined (reading 'startsWith')`)。修复:descriptor 显式映射 `tabId={tab.id}`,懒包装的 props 泛型 = TerminalView 真实签名(`{ scope, tabId, store }`);回归测试钉住映射(lazy-chunk.spec.tsx)。**教训:懒包装的 props 泛型必须等于目标组件真实 props,而非描述符 props 全集**。
|
||||
- **v2 附加**:bundled 产物经真实 `ClientModuleSystem`(带真实 react seed)端到端验证——5 个 chunk 均能物化出真实 React 组件;产物契约由 `tests/chunk-artifact.spec.ts` 长期钉住(脚本执行赋值槽位 + factory 可被 externals require 物化)。
|
||||
@@ -1,77 +0,0 @@
|
||||
# 移动端(窄视口 <768px)侧边栏布局设计
|
||||
|
||||
**日期**:2026-08-12
|
||||
**状态**:已实施(本文档含实施偏差记录)
|
||||
**目标版本**:v0.7.0
|
||||
|
||||
## 1. 目标
|
||||
|
||||
按**视口宽度**(<768px——"真正的移动端宽度",刻意不对齐宿主 1024 断点)切换移动端侧边栏体验:
|
||||
|
||||
1. 移动端**不提供底部面板按钮**(右上角按钮簇只剩一枚右侧面板开关)。
|
||||
2. **合并显示 = 只显示右侧栏**:进入窄屏时底部面板的标签页**直接并入右侧栏**(一次性状态迁移),右侧栏成为全宽抽屉,底部面板浮层整体不存在。
|
||||
3. 附带移动端优化:新会话默认收起抽屉、文件/外链打开自动展开、宽度拖动条禁用、布局不挤压(抽屉悬浮)、窄屏标签收窄。
|
||||
|
||||
## 2. 非目标
|
||||
|
||||
- 不改持久化 schema:`bottomOpen` / `bottomHeight` / `bottomSplits` 字段原样保留。
|
||||
- 不改桌面行为:≥768px 时双面板布局与之前完全一致(含布局挤压、拐角、底部首展自动终端)。
|
||||
- 不改公开服务 API、PrefsSchema、host 半。
|
||||
- 不处理 DSH 详情栏(宿主让步链在 <~996px 自动关闭详情栏,与本改动无交互)。
|
||||
|
||||
## 3. 现状回顾
|
||||
|
||||
- `Sidebar.tsx` 渲染两个独立浮层:右侧面板(全高、宽度可拖、布局挤压 `--dsh-sidebar-width`)与底部面板(只挤中间列、高度可拖、右上角按钮簇双按钮、共享拐角)。
|
||||
- 底部面板状态字段:`bottomOpen` / `bottomHeight`(钳制 `[BOTTOM_MIN, innerHeight-PANEL_MIN]`)/ `bottomOpenedOnce` / `bottomSplits`(独立 split 树,pane/tab id 全局唯一)。
|
||||
- 首次展开底部面板自动开终端(`bottomPanelAutoTerminal` pref)。
|
||||
|
||||
## 4. 设计
|
||||
|
||||
### 4.1 断点模块(新 `src/client/breakpoints.ts`)
|
||||
|
||||
- `NARROW_MAX_WIDTH = 768`;`isNarrowWidth(width)` 纯函数;`useNarrowViewport()` hook(初始读 `window.innerWidth`,resize + rAF 节流,`typeof window` 守卫;不用 matchMedia——jsdom 未实现)。
|
||||
- CSS 侧配对 `@media (max-width: 767px)`(767 ≡ <768),两端注释互指。
|
||||
- 刻意**不对齐**宿主 `SIDEBAR_AUTO_COLLAPSE = 1024`:1024px 窗口(小笔记本、分屏)保留桌面双面板;只有手机/竖屏平板才进移动布局。
|
||||
|
||||
### 4.2 合并 = 底部标签并入右侧栏(状态迁移)
|
||||
|
||||
- **新 reducer `migrateBottomTabs(state)`**(`state.ts`):
|
||||
- 底部树所有标签按深度优先序**追加到右侧树第一个 leaf**(`firstLeaf(splits)`)的标签条;
|
||||
- 底部树清空(结构保留——桌面端回显欢迎卡片)、`bottomOpen=false`、`activePane` 指向右侧树第一个 leaf(保证迁移后新开的标签落在可见面板);
|
||||
- 幂等:底部无标签且面板已关且 activePane 不在底部树时返回同一引用。
|
||||
- **Sidebar 触发 effect**:`narrow && sessionId 已定义` 时执行(覆盖:窄屏挂载、会话切换、桌面→窄屏过渡);幂等性保证其自然收敛,不循环。
|
||||
- 迁移是**永久**的(符合"直接丢到右侧栏中"):回桌面后标签仍留在右侧树,底部面板为空(欢迎卡片);用户可在桌面重新往底部面板开标签,再进窄屏会再次迁移。
|
||||
- 迁移后右侧栏用**现有 `Workbench` 原样渲染**——零视图层特判、零 action 路由修补(标签真实住在右侧树,close/activate/drag 全部走原路径)。
|
||||
|
||||
### 4.3 窄屏渲染(`Sidebar.tsx`)
|
||||
|
||||
- 面板宽度 `100vw`(全宽抽屉);`--dsh-sidebar-*` 布局变量恒 0(抽屉悬浮,不挤压 AppFrame);宽度拖动条、底部浮层、`bottomClose`、拐角、底部面板按钮全部不渲染。
|
||||
- `visible` 语义回到原始:底部树只在桌面底部面板场景(`bottomOpen && active`)——窄屏下底部树为空且不渲染。
|
||||
- 底部首展自动终端 effect 加 `narrow` 守卫。
|
||||
|
||||
### 4.4 行为修正
|
||||
|
||||
- `state.ts` `loadState`:窄屏加载一律以 `panelOpen=false` 开始(包括持久化布局恢复);仅覆盖抽屉可见性,右侧/底部 pane、tab 与自由窗口状态原样恢复。宽屏仍按持久化值或新会话偏好决定展开状态。
|
||||
- `service.ts` `openTab`:窄屏且 seed 带 `path`/`url` 且面板收起 → 自动展开抽屉(dedupe-focus 分支同样生效);纯类型打开不展开;宽屏行为不变。
|
||||
|
||||
### 4.5 样式(`sidebar.module.css`)
|
||||
|
||||
- `@media (max-width: 767px)`:开放面板标签条预留宽度 72px → 40px(按钮簇只剩一枚);`.tab` min/max 宽收窄(48/128px)。
|
||||
|
||||
### 4.6 测试
|
||||
|
||||
- `tests/breakpoints.spec.ts`:断点边界(767 窄 / 768 宽;1024 明确为宽)。
|
||||
- `tests/unit.spec.ts`:`migrateBottomTabs` 4 例(并入第一 leaf 且底部清空/面板关/activePane 重指、右树为 split 时并入最左 leaf、幂等性、空底部树但 activePane 在底部树时重指)+ 窄屏新会话默认收起;`tests/state.spec.ts` 守护窄屏恢复强制收起且 pane / free-window 状态不丢,768px 宽屏仍恢复持久化展开值。
|
||||
- `tests/service.spec.ts`:窄屏 auto-expand 5 例(390 < 768 仍窄)。
|
||||
|
||||
## 5. 边界情况
|
||||
|
||||
- 迁移触发:窄屏挂载 / 会话切换 / 桌面→窄屏过渡,均由同一 effect 覆盖;幂等保证收敛。
|
||||
- 迁移后新开标签:`activePane` 已重指右侧树第一 leaf,`openTabInActivePane` 必落在可见面板(含"空底部树但 activePane 指向它"的陈旧指针场景)。
|
||||
- 跨断点往返:迁移永久;桌面底部面板空显欢迎卡片;`bottomHeight` 不受影响。
|
||||
- jsdom 兼容:hook 不用 matchMedia;既有测试 stub `innerWidth=1024` 自动走桌面路径,零行为变化。
|
||||
|
||||
## 6. 实施偏差记录
|
||||
|
||||
- **v1(已提交 b391ebf,用户反馈推翻)**:合并 = 单面板内上下堆叠两个工作台(`MobileWorkbench` + 可拖分隔条,高度复用 `bottomHeight`),断点 1024(对齐宿主)。用户明确:合并 = 只显示右侧栏、底部标签直接丢进右侧栏;窄屏 = 真窄屏、不对齐 1024。
|
||||
- **v2(本文档,最终形态)**:删除 `MobileWorkbench`(组件 + CSS + spec),改为 `migrateBottomTabs` 状态迁移 + 现有 `Workbench` 渲染;断点 768。`bottomHeight` 不再参与移动端布局(仅桌面底部面板使用)。
|
||||
@@ -1,43 +0,0 @@
|
||||
# 任务管理页后台命令区块(查看输出 + 强制终止)设计
|
||||
|
||||
> 2026-08-12 · v0.8.0
|
||||
|
||||
## 目标
|
||||
|
||||
任务管理页(当前树的主代理拓扑,原「子代理」页)在拓扑树下方同页显示**当前树全部会话**(主代理 + 子代理)的后台任务:
|
||||
|
||||
- 点击任务行,输出显示在**侧边栏底部共享 Dock**(sticky 终端式,滚动时保持可见;运行中每 2s 轮询,页面不可见时暂停;自动跟随底部);
|
||||
- 运行中的任务行带两击确认的终止按钮;
|
||||
- **不破坏模型契约、不修改 DSH 源码**(本仓库规则:对官方源码 checkout 零写入,挂载只走 cordis.patch.yml + profile 机制):输出查看是**模型已读输出的回放**,绝不推进 `task_output` 的读取游标。
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
bash/tool run_in_background → ctx.tasks.start(owner = 执行 agent)
|
||||
api-proxy session/tasks 帧(TaskView[],无输出)→ 客户端 tasksBySession 镜像
|
||||
SubagentView 读镜像 → collectTreeTasks(树内会话) → 行渲染(单 Dock,选中行)
|
||||
点行 → POST /sidebar/api/tasks.output(事件回放)→ 扫描属主会话 events 里
|
||||
task_output 的 tool/call + tool/result 对 → 模型已读文本
|
||||
终止 → POST /sidebar/api/tasks.kill → ctx.tasks.kill(DSH 现成 API)
|
||||
```
|
||||
|
||||
## 关键决策
|
||||
|
||||
1. **输出 = 会话事件回放,零 DSH 写入**:`TaskService.read()` 是单一消费游标——UI 直接读会偷走模型 `task_output` 的字节(或读到空)。而 DSH 源码禁止修改,所以 `tasks.output` **不碰注册表**:扫描属主会话的 append-only 事件日志,取 `task_output` 调用的 `tool/call`(`arguments.task_id` 配对)与其 `tool/result`(finalize 后的文本,即模型实际收到的内容)按序拼接;`"(no new output)"` 增量与错误结果跳过;`read: false` 表示模型尚未读过。模型游标天然不被触碰。
|
||||
- 语义代价(用户已确认接受):面板只在模型读过之后才有内容——没有实时流。
|
||||
2. **列表不新增路由**:任务清单由 harness 的 `session/tasks` 推送镜像提供;只加 `tasks.output`(回放)与 `tasks.kill`(注册表原生 `kill`)两个路由(信任围栏与其余路由一致)。注册表缺席时 kill 降级 503(output 不需要注册表)。
|
||||
3. **终止的访问围栏**:caller 由请求的 `sessionId` 现场解析 `ctx.agents.get(sessionId)`,注册表按 owner session id 拒绝 foreign 任务;未知/foreign 统一映射 404 `task-error`。
|
||||
4. **展示范围 = 整棵树**:与页面作用域一致(拓扑即当前树);树跨多会话时行尾显示属主标题。
|
||||
5. **单共享 Dock(大量任务的形态)**:行内展开会让列表随任务数膨胀、排序变化时面板跳动;改为一个底部 sticky Dock + 单一轮询实例,行保持紧凑、排序变动不惊吓,选中行高亮。
|
||||
6. **两击确认终止**:首次点击进入「再次点击确认」态(3s 自动解除),防止误杀长任务。
|
||||
7. **输出上限**:路由按 `readLimit`(默认 512KB)截断并置 `truncated` 标记。
|
||||
|
||||
## 实施偏差
|
||||
|
||||
- 曾实现 harness 侧非消费 peek 缝隙(`BashProcess.peekOutput` / `TaskService.peek`),因「禁止修改 DSH 源码」规则**整体回退**(harness 提交已 reset,checkout 恢复纯净快照),改用事件回放。
|
||||
- 宿主不新增 `@deepseek-ai/*` 依赖(`ctx.tasks`/`ctx.agents` 走结构性镜像 + 运行时 `ctx.get`,可选服务降级 503);`ctx.sessions.get(id).events` 为只读访问。
|
||||
|
||||
## 验证
|
||||
|
||||
- 插件单测:tasks-routes(回放拼接/过滤/read 标志/截断/404/503 降级);subagent-tasks 纯函数;jsdom 视图(行渲染、单 Dock 切换、关闭、两击终止、read:false 提示、大量任务、隐藏不轮询、镜像清空 hook 顺序回归 #300)。
|
||||
- 集成(真实 dsh web):启动 `run_in_background` bash → 模型 `task_output` 读取 → `/sidebar/api/tasks.output` 返回模型已读内容 → `/sidebar/api/tasks.kill` 后进程退出、任务结算 `killed (signal: SIGTERM)`。
|
||||
@@ -1,69 +0,0 @@
|
||||
# 「添加插件」弹窗 + 推荐插件列表 + 复制安装命令 设计
|
||||
|
||||
**日期**:2026-08-14
|
||||
**状态**:已批准,已实施
|
||||
**作者**:opencode + 用户
|
||||
**当前版本**:v0.12.0(分支 `feat/plugin-api-v012`,已合入 main 后 rebase)
|
||||
**目标版本**:v0.12.0(不 bump)
|
||||
|
||||
## 1. 目标
|
||||
|
||||
1. 为「侧边栏内容(Tab)」与「文件预览(viewer)」两类扩展点分别增加**添加入口**:DSH 设置页「Side card」分区的两个响应式网格(侧边栏内容 / 文件预览)末尾各新增一个与其他卡片同尺寸的**虚线边框**添加卡片(「添加 Tab 插件」/「添加预览插件」)。
|
||||
2. 点击后弹出对应 kind 的原生 Modal:声明该类扩展点可由插件扩展(`ctx.betterSidebar` 服务),提供「在 GitHub 上浏览更多插件」**按钮**(打开 `https://github.com/topics/dsh-better-sidebar`,新标签页),并展示仓库**推荐**的同 kind 插件列表(名字 / url / 简介 / 安装脚本)。
|
||||
3. 每个条目两个动作:**「跳转」按钮**(`window.open` 新标签页直达插件仓库,绕过链接接管)与**「复制」按钮**——点击只把安装脚本写入剪贴板(`writeClipboard`,按钮闪现「已复制」反馈),用户自行粘贴到 DSH 所在环境的终端执行。
|
||||
4. **不关闭任何窗口、不打开终端、无失败路径**:复制是纯 client 动作,不依赖会话/终端/配额,弹窗保持打开可连续复制多个插件。
|
||||
5. 推荐列表为仓库静态数据、**分为两个文件**(`plugins-tabs.ts` Tab 注册目录 / `plugins-viewers.ts` 文件预览注册目录),初始在 viewer 目录收录唯一真实三方插件 `@huanlin/dsh-plugin-better-sidebar-plugin-office`(简介不注版本前提);后续插件只加一条数据。
|
||||
6. 内置 Office 预览(docx/xlsx/pptx)**从本体移除**——已迁至上述推荐插件(见偏差记录),`docx/xlsx/pptx` 文件在未装插件时落 code/下载兜底。
|
||||
|
||||
## 2. 非目标(Out of Scope)
|
||||
|
||||
- **不打开终端**:不做终端预填安装流(曾实现的 `OpenTabSeed.expand`、`TerminalSeedMeta` 预填/提示条、service meta 透传等已全部删除回滚)。
|
||||
- 不加 + 菜单入口、不加编辑器空态入口(入口只在设置页)。
|
||||
- 不做插件在线拉取 / 动态发现(列表为仓库推荐静态数据)。
|
||||
- 不改 host 半的服务契约(`service.ts` 相对 v0.12.0 无 API 变化;host 半仅在 office 移除时缩减了 chunk 白名单)。
|
||||
|
||||
## 3. 实施要点
|
||||
|
||||
### 3.1 数据层:两个目录文件 + 共享词汇
|
||||
|
||||
- `src/client/plugins-shared.ts`:`PluginEntry { id, name, url, description, install }` + `PLUGIN_TOPIC_URL`。
|
||||
- `src/client/plugins-tabs.ts`:`builtinTabPlugins`(收录 dsh-sentinel——条件驱动的 agent 唤醒系统,注册 `dsh-sentinel:watches` Tab;install 用官方 github: 源一键命令)。
|
||||
- `src/client/plugins-viewers.ts`:`builtinViewerPlugins`(种子:office 插件,install = `cd ~/.dsh && dsh plugin --profile web add @huanlin/dsh-plugin-better-sidebar-plugin-office`)。
|
||||
|
||||
### 3.2 UI 层:`src/client/add-plugin-modal.tsx` + `SideCardSection`
|
||||
|
||||
- 两个网格末尾各一个虚线卡片(复用 `.card` 配方 + `IconPlusOutline16`),`addPluginsOpen: PluginKind | null` 状态;Modal 仅在打开时条件挂载(Modal 原语无条件跑 hooks,SSR 规则同 settingsFor)。
|
||||
- `PluginListBody`(提取组件,直接可测):topic 按钮(`window.open`)+「推荐插件」分组(`groupHeading` + count)+ 条目(名字链接 / 简介 / mono 安装命令 / 跳转按钮 / 复制按钮)+ 空态。
|
||||
- **复制**:`void writeClipboard(entry.install)` + `copiedId` 状态闪现 `t('copied')`(1.5s 后还原)。不关闭、不打开、不抛错。
|
||||
|
||||
### 3.3 i18n
|
||||
|
||||
新增键:`addPluginsTabCard/TabCardDesc/ViewerCard/ViewerCardDesc/TabDesc/ViewerDesc`、`addPluginsBrowseMore`、`addPluginsRecommended`(推荐插件)、`addPluginsEmpty`、`openPlugin`(跳转)、`copyInstall`(复制安装命令,按钮 aria-label);复用 `copy`/`copied`/`close`/`settingsDone`。已删除废弃键:`install`、`installNotice`、`installError*`、office 相关键(`viewerDocx/Xlsx/Pptx`、`officeTooLarge/Corrupt/Encrypted/LoadFailed`、`previousSlide/nextSlide/zoom/zoomHint`)。
|
||||
|
||||
### 3.4 内置 Office 预览移除(与推荐插件衔接)
|
||||
|
||||
删除 3 个 viewer 描述符 + 10 个源文件(docx-view/xlsx-view/PptxView/office-view/office-shared/office-types/xlsx-to-univer + chunks/docx|xlsx|pptx)+ 3 个图标 + office CSS + office locale 键;chunk 白名单 5→2(terminal/editor,同步 `bundle-route.ts` / `chunk-loader.ts` / `tsdown.config.ts` / `scripts/package-registry.mjs`);`tests/builtins.spec.ts` 断言 6 个 viewer 且不含 office id。`.docx`(zip、无 NUL)未装插件时落 code 兜底。
|
||||
|
||||
## 4. 边界情况与失败模式
|
||||
|
||||
- **复制失败**:`writeClipboard` 尽力而为(navigator.clipboard → execCommand 兜底),失败静默;按钮反馈照常闪现。无会话/无终端/配额等一切前置条件均不涉及——复制与它们无关。
|
||||
- **空目录**:目录可为空(弹窗渲染空态 + topic 按钮);当前 Tab/Viewer 目录各有 1 条。
|
||||
- **版本号**:v0.12.0(`package.json` / `dsh.plugin.json` / `service.ts` 的 `SIDEBAR_SERVICE_VERSION` 三处一致,manifest-consistency 测试守护)。
|
||||
|
||||
## 5. 实施偏差记录
|
||||
|
||||
- **评审修改(第一批)**:入口由 + 菜单/编辑器空态改为**仅设置页虚线卡片**;安装命令并入「安装按钮」单一交互。
|
||||
- **评审修改(第二批)**:① topic 链接改为**按钮**(`window.open` 新标签页);② 添加按钮拆为**两个**(Tab 注册 / 文件预览注册),列表数据拆为**两个文件**;③ 安装错误只内联显示在设置内(预检 + try/catch);④ 终端提示条用品牌高亮色。
|
||||
- **评审修改(第三批)**:内置 Office 预览整体移除(迁至推荐插件);「内置插件」全部改名**「推荐插件」**。
|
||||
- **评审修改(第四批,最终形态)**:设置窗口关闭问题无法根治(openTab 的 store 涟漪会复活弹窗),且用户实测弹窗关闭不可靠——**放弃终端安装流**:安装按钮改为**只复制安装脚本**(`writeClipboard` + 「已复制」反馈),弹窗保持打开;随之删除整个终端预填流:`terminal-meta.ts`、`TerminalView` 的 meta/预填/提示条、`tabs.tsx` 的 meta 透传与 `uiTerminalCount` 导出、`service.ts` 的 `OpenTabSeed.expand` 与 createTab meta 透传(全部回滚到 v0.12.0 原始 API 面)、`.terminalNotice` CSS、相关测试与文档;`install`/`installNotice`/`installError*` 文案键删除,新增 `copyInstall`。
|
||||
- 测试中 `AddPluginModal` 不能 renderToString(React 18 server renderer 拒绝 portal),改为 createRoot + act 挂载后断言 `document.body`(portal 落点)。
|
||||
|
||||
## 6. 验收清单
|
||||
|
||||
- [x] 设置页两个网格末尾各出现虚线边框添加卡片,尺寸与既有卡片一致
|
||||
- [x] 两个弹窗(kind 区分):声明文案 + GitHub topic **按钮** + 对应推荐插件条目(名字/url/简介/安装命令/跳转按钮/复制按钮)
|
||||
- [x] 点「复制」:安装脚本写入剪贴板,按钮闪现「已复制」,弹窗不关闭、不打开终端、无失败路径
|
||||
- [x] 点「跳转」:`window.open` 新标签页直达插件仓库
|
||||
- [x] 内置 Office 预览已移除;chunk 白名单 2 个;无「内置插件」残留文案
|
||||
- [x] 版本号 v0.12.0 三处一致;`pnpm typecheck` / `pnpm test` / `pnpm build` 全绿
|
||||
- [x] README/README_EN/AGENTS.md 更新到位
|
||||
@@ -1,63 +0,0 @@
|
||||
# Issue #42 诊断与收尾记录(底部面板首展自动终端 → 整面板空白)
|
||||
|
||||
**日期**:2026-08-14
|
||||
**状态**:已诊断、已收尾(无新代码修复;回归测试 + 文档交付)
|
||||
**关联**:issue #42、issue #25(同根因)、issue #31(放大器)、PR #27 / commit 37d91be、commit 9aeb046
|
||||
**目标版本**:v0.11.0(根因修复已发布)
|
||||
|
||||
## 1. 问题报告
|
||||
|
||||
[issue #42](https://github.com/omdsh-dev/DSH-better-sidebar/issues/42)(Windows 11 / QQBrowser / dsh-launcher 宿主 / 插件 0.10.3):
|
||||
|
||||
1. 全新安装后重启 + 硬刷新,首次展开底部面板(`bottomPanelAutoTerminal` 默认开)→ **整个底部面板空白,连标签栏都没有**;
|
||||
2. 控制台两条关键报错:
|
||||
- `Uncaught TypeError: Cannot read properties of undefined (reading 'dimensions')`(`Viewport._innerRefresh`)
|
||||
- `Uncaught TypeError: Cannot read properties of undefined (reading 'handleResize')`(`IdleTaskQueue._process`)
|
||||
3. 关闭「底部面板首次展开自动开终端」后一切正常;右侧栏终端一直正常;
|
||||
4. 附带问题:Windows 无控制台宿主下每次关闭终端,服务器日志出现 `Error: AttachConsole failed`(node-pty `conpty_console_list_agent.js`)。
|
||||
|
||||
## 2. 诊断结论
|
||||
|
||||
### 2.1 根因 = issue #25 的同一 bug
|
||||
|
||||
与 [issue #25](https://github.com/omdsh-dev/DSH-better-sidebar/issues/25)(WKWebView 底部面板终端空白)完全同源,证据链:
|
||||
|
||||
- **触发路径一致**:`Sidebar.tsx` 的「底部面板首次展开自动开终端」effect(`bottomOpenedOnce` 过渡)在面板 slide-in transition 的**同一次 commit** 挂载 `TerminalView`。`docs/plans/2026-08-14-terminal-open-when-sized-design.md` §3 在设计 #25 修复时已明确把该路径列为零尺寸挂载路径之一。
|
||||
- **崩溃签名逐字一致**:0.10.3 的 `TerminalView` 挂载 effect 同步执行 `term.open(host); fit.fit()`(无尺寸守卫)。xterm 5.3.0 在零尺寸容器 open 时渲染器创建失败,`RenderService._renderer` 保持 undefined → `Viewport._innerRefresh` 读 `_renderService.dimensions`(getter 即 `_renderer.value.dimensions`)、idle 任务读 `handleResize` 全部抛 "Cannot read properties of undefined"——与 #42 两条报错逐字对应。
|
||||
- **平台差异只是时序**:#25 设计文档注明「Chrome(Blink)对零尺寸布局更宽容所以不崩;WKWebView 稳定复现」。QQBrowser(Chromium fork)在 Windows + launcher 启动的组合下命中同一零尺寸窗口期,属边界时序,不是新缺陷。
|
||||
- **版本差**:#42 报告针对 **0.10.3**(2026-08-13 发布);根因修复 37d91be(PR #27)于 2026-08-14 合并并随 **v0.11.0** 发布(早于 #42 提交约 1 小时,晚于 reporter 安装的版本)。
|
||||
|
||||
### 2.2 修复覆盖路径(无需新代码)
|
||||
|
||||
| 层 | 机制 | 位置 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 根因(零尺寸 open) | `openWhenSized`:`term.open + fit + sendResize` 推迟到容器有尺寸后恰好执行一次;RO/字体订阅的 `fit()` 全部 try/catch 加固 | `src/client/open-when-sized.ts` + `TerminalView.tsx`(37d91be,v0.11.0) | 已发布 |
|
||||
| 根因单测 | 手动步进 rAF 的 6 个用例(延迟/一次性/单维缺失/cancel/脱离文档/异常传播) | `tests/open-when-sized.spec.ts` | 已提交 |
|
||||
| 「整面板空白」放大器 | per-tab `RenderBoundary` 崩溃隔离(单 tab 崩溃只显示该 pane 内条带,toggle 簇/其他 tab/面板存活)+ 根 boundary 兜底 + layout-push CSS 变量卸载清理 | `src/client/RenderBoundary.tsx`、`Sidebar.tsx`、`index.tsx`(9aeb046,#31) | 已提交 |
|
||||
| 隔离测试 | 崩溃条带/重试恢复/布局变量清理 | `tests/sidebar-crash.spec.tsx` | 已提交 |
|
||||
| **#42 触发链回归** | 底部面板首展自动开终端:恰好一个 terminal tab 落在底部工作树、`bottomOpenedOnce` 原子置位、pref / 类型禁用双门控、面板完整存活 | `tests/bottom-auto-terminal.spec.tsx`(本次新增,4 例) | 本次提交 |
|
||||
|
||||
三者合起来构成 #42 全链回归保护:触发链(本测试)→ 零尺寸延迟(openWhenSized 单测)→ 崩溃隔离(containment 测试)。
|
||||
|
||||
## 3. 附带问题:AttachConsole 报错(仅文档化)
|
||||
|
||||
`Error: AttachConsole failed`(node-pty `conpty_console_list_agent.js`)是 **node-pty 上游行为**:`kill()` 清理路径 fork 的 console-list 辅助进程在无控制台宿主(launcher/服务方式启动的 `dsh`)下 `AttachConsole(shellPid)` 失败。终端功能本身不受影响(spawn/输入输出正常),仅刷服务器日志。
|
||||
|
||||
- 插件侧不可消除:不改 DSH 源码、不 patch node-pty 依赖是仓库硬约束(见 AGENTS.md §0),且 `IPty.kill()` 是 node-pty 的唯一 kill 通道;
|
||||
- 建议:保持现状(无功能影响),上游如提供无控制台规避选项再跟进;reporter 若在意日志噪音,可反馈 microsoft/node-pty。
|
||||
|
||||
> 交叉引用(issue #140):与本文档「仅刷日志」不同,**依赖缺失/损坏**(node-pty 安装不完整导致模块加载失败)已有插件侧降级处理——宿主半懒加载 node-pty,失败时终端 tab 显示可复制的修复命令(`scripts/install.sh --repair` / `install.ps1 -Repair`),详见 `src/pty-deps.ts` 与 `tests/pty-deps.spec.ts`。
|
||||
|
||||
## 4. 验证
|
||||
|
||||
- 全量测试:`pnpm test` → 34 文件 / 430 例全部通过(基线 425 + 同期 PR #43 的 html 盘符修复 1 例 + 本次新增 4 例);
|
||||
- `pnpm typecheck`(`tsc --noEmit`)通过;
|
||||
- `pnpm build`(`tsc -p tsconfig.build.json && tsdown`)通过。
|
||||
|
||||
## 5. Issue #42 回复草稿(供 maintainer 发布/关闭用)
|
||||
|
||||
> 感谢复现与详细报告。诊断结论:**这是 issue #25 同一根因在不同平台的复现**——底部面板首次展开自动开终端时,终端在容器尚为零尺寸(面板 slide-in 窗口期)就执行了 xterm `Terminal.open()`,渲染器创建失败导致后续 `Viewport._innerRefresh` / 空闲任务读取 `.dimensions` / `.handleResize` 崩溃(与 #25 的控制台报错逐字一致)。
|
||||
>
|
||||
> 该根因已在 **v0.11.0** 修复(终端延迟到容器有尺寸后再 open,PR #27);「整个面板空白」的放大器也已在最新 main 消除(单 tab 崩溃隔离 + 布局变量清理,issue #31)。**请升级到 v0.11.0 验证**:预期首次展开自动开终端不再空白;若仍复现,请提供 v0.11.0 下的控制台报错。
|
||||
>
|
||||
> 附带问题(`AttachConsole failed` 日志噪音)确认为 node-pty 在无控制台宿主(launcher 启动)下的上游行为,无功能影响,插件侧无法在不改动 node-pty/DSH 的前提下消除;已文档化(`docs/plans/2026-08-14-issue-42-verify-design.md`),并已补 #42 触发链回归测试。
|
||||
@@ -1,117 +0,0 @@
|
||||
# 终端自定义字体(设置页二级设置)设计
|
||||
|
||||
**日期**:2026-08-14
|
||||
**状态**:已实施(本文档含实施偏差记录)
|
||||
**目标版本**:v0.11.0
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在 DSH 设置页「侧边卡片」分区中,终端 tab 卡片的二级设置弹窗(齿轮按钮,README 所称"终端卡片二级设置")新增两行:
|
||||
|
||||
1. **终端字体**(text 输入):自定义 CSS font-family stack,如 `"JetBrains Mono", monospace`;留空 = 跟随主题等宽字体(`--ds-font-family-code` token)。
|
||||
2. **终端字号**(number 输入):9–32px,默认 13,带 `px` 后缀。
|
||||
|
||||
配置持久化到 `SidebarPrefs`(`dsh-better-sidebar` 命名空间,host 侧 PrefsSchema 校验),对**所有已打开终端实时生效**(UI tab 与 agent tab 共用 TerminalView):设置页提交 → `store.setPrefs` 通知 → xterm `options.fontFamily/fontSize` 更新 + 重排网格。
|
||||
|
||||
## 2. 非目标
|
||||
|
||||
- 不在终端视图内加菜单/按钮(用户明确选择仅设置页齿轮弹窗)。
|
||||
- 不做 per-tab / per-terminal 隔离(全局偏好,与 SidebarPrefs 现有语义一致)。
|
||||
- 不自动给字体族加引号、不做字体合法性嗅探(用户负责合法 CSS font-family;placeholder 示范格式)。
|
||||
- 不改 pty / WS / 终端连接生命周期:字体变化不重连、不重启 shell。
|
||||
|
||||
## 3. 现状回顾
|
||||
|
||||
- 终端描述符(`builtins/tabs.tsx`)的 `settings.toggles` 只支持**布尔开关**(`SidebarSettingToggle` 无类型字段),`FeatureSettingsRows`(`SideCardSection.tsx`)只渲染 Switch 行。
|
||||
- `SidebarPrefs` / `PrefsSchema` / `parsePrefs` 三处同步维护(`prefs-shared.ts` / `config.ts` / `client/prefs.ts`)。
|
||||
- `TerminalView` 硬编码 `fontSize: 13` + `fontFamily: tokenValue('--ds-font-family-code') || 内置栈`,且不响应偏好变化。
|
||||
|
||||
## 4. 设计
|
||||
|
||||
### 4.1 偏好扩展(`prefs-shared.ts` / `config.ts` / `client/prefs.ts`)
|
||||
|
||||
```ts
|
||||
interface SidebarPrefs {
|
||||
// …
|
||||
terminalFontFamily: string // '' = 跟随主题等宽字体
|
||||
terminalFontSize: number // 9–32,默认 13
|
||||
}
|
||||
|
||||
// prefs-shared.ts
|
||||
export const TERMINAL_FONT_SIZE_MIN = 9
|
||||
export const TERMINAL_FONT_SIZE_MAX = 32
|
||||
export const TERMINAL_FONT_SIZE_DEFAULT = 13
|
||||
export function clampTerminalFontSize(value: number): number // round + clamp(仿 clampWidthPercent)
|
||||
```
|
||||
|
||||
- `PrefsSchema`:`terminalFontFamily: z.string().default('')`、`terminalFontSize: z.number().step(1).min(9).max(32).default(13)` —— 旧文档解析自动补默认,无迁移。
|
||||
- `parsePrefs`:family 需 string 否则默认;size 需有限数并 `clampTerminalFontSize` 否则默认。
|
||||
|
||||
### 4.2 声明式 API 扩展(`client/service.ts`,向后兼容)
|
||||
|
||||
```ts
|
||||
type SidebarSettingToggleType = 'switch' | 'text' | 'number'
|
||||
|
||||
interface SidebarSettingToggle {
|
||||
key: string
|
||||
title: string | (() => string)
|
||||
desc?: string | (() => string)
|
||||
type?: SidebarSettingToggleType // 缺省 'switch',外部插件现有用法不变
|
||||
min?: number // number 行的下限(提交时钳制)
|
||||
max?: number // number 行的上限
|
||||
placeholder?: string // text 行的占位符
|
||||
unit?: string // 输入框后的单位后缀(如 'px')
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 终端描述符声明(`builtins/tabs.tsx`)
|
||||
|
||||
`settings.toggles` 追加两行:`{ key: 'terminalFontFamily', type: 'text', placeholder: … }` 与 `{ key: 'terminalFontSize', type: 'number', min: 9, max: 32, unit: 'px' }`(标题/描述走 locales)。
|
||||
|
||||
### 4.4 设置行渲染(`SideCardSection.tsx` + CSS)
|
||||
|
||||
- `FeatureSettingsRows` 按 `toggle.type` 分支:`switch` 走现有 Switch 行;`text`/`number` 走新 `TypedRow`(局部 draft state,`Input` 原语,blur/Enter 提交,number 行带 unit 后缀)。
|
||||
- `TypedRow` 的 `key` 含**已提交值**:提交失败 → prefs 回滚 → key 变化 → 行重挂载为存储值(输入中 draft 不因 key 变化被打断,因为 key 只随已提交值变)。
|
||||
- 父级 `onCommitSetting(toggle, raw): string`:number 解析+钳制(非法/空 → 走 `Number('')=0` 钳到下限,与宽度行 `commitWidth` 先例一致);text 原样;乐观 `setPrefs` → `commit` → 失败 `applyOutcome` 回滚(复用既有 `applyPref` 管道,原 `togglePref` 更名)。
|
||||
|
||||
### 4.5 终端消费(`client/terminal-font.ts` 新模块 + `TerminalView.tsx`)
|
||||
|
||||
```ts
|
||||
// terminal-font.ts(纯函数,可单测)
|
||||
export const DEFAULT_TERMINAL_FONT_FAMILY = '"SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
|
||||
export function resolveTerminalFont(prefs, themeFontFamily):
|
||||
{ fontFamily: 非空用户值 ?? themeFontFamily ?? 内置栈, fontSize: clampTerminalFontSize(prefs.terminalFontSize) }
|
||||
```
|
||||
|
||||
- `TerminalView` 构造 Terminal 时用解析值;主 effect 内 `store.subscribe` 回调 diff `term.options.fontFamily/fontSize`,变化则更新 options + `fit.fit()` + `sendResize()`(网格尺寸随字体变化);cleanup 退订。懒加载 chunk 无新增依赖(store 为运行时 prop)。
|
||||
|
||||
> **实施偏差(2026-08-17)**:`resolveTerminalFont` 不再**逐字**返回胜出的基础字体,而是经 `withIconFontFallbacks()` 追加 Nerd Font 图标族后返回。
|
||||
>
|
||||
> 起因:starship / powerlevel10k 等提示符的图标取自 Nerd Font 私有区,Chromium 的隐式系统回退能覆盖 BMP PUA(`U+E000–U+F8FF`,如 `U+E0B0`)但覆盖不到补充平面 PUA-B(`U+F0000+`,Nerd Fonts v3 把 Material Design 图标集迁到了这里),导致后者渲染为豆腐块。显式列出字体族可使浏览器逐字符查询,两个平面都能命中。
|
||||
>
|
||||
> 三条约束:①**只追加不前置**——xterm 用字体栈首项测量单元格宽度;②插入到**第一个**通用族(`monospace` 等)之前——通用族是 catch-all,其后的字体永不被查询;③按族名去重(引号/大小写/空白不敏感),用户自列的字体保持其原有优先级。基础字体的优先级链(用户 pref > 主题 > 内置栈)不变,另新增 CSS 全局关键字(`inherit`/`initial`/`unset`/`revert`/`revert-layer`)过滤——这类值只在作为整条声明值时合法,追加后会得到非法 `font-family` 而被 CSSOM 静默丢弃。
|
||||
|
||||
### 4.6 i18n(`locales.ts`)
|
||||
|
||||
zh/en 各新增:`settingsFontFamilyTitle/Desc/Placeholder`、`settingsFontSizeTitle/Desc/Suffix`(px)。
|
||||
|
||||
## 5. 测试
|
||||
|
||||
- `tests/unit.spec.ts`:`parsePrefs`/`loadPrefs` 新字段(合法 string 直通、越界钳制 9/32、非 number 回退、缺省默认);全等字面量补字段。
|
||||
- `tests/plugin-shape.spec.ts`:PrefsSchema 默认值 + overridden 全等。
|
||||
- `tests/builtins.spec.ts`:终端 toggles keys = 4 个,后两个 type/min/max 断言。
|
||||
- `tests/smoke.spec.ts`:settings.get 全默认字面量补字段。
|
||||
- `tests/side-card-section.spec.tsx`(SSR):text/number 行渲染(value/min/max/placeholder/unit,非 checkbox)。
|
||||
- `tests/side-card-section-rows.spec.tsx`(新,jsdom + createRoot):blur 提交回调、number 钳制、空输入钳到下限、提交失败回滚草稿(key 重挂载)。
|
||||
- `tests/terminal-font.spec.ts`(新):`resolveTerminalFont` 回退链 + `clampTerminalFontSize`。
|
||||
|
||||
## 6. 限制与取舍
|
||||
|
||||
- 外部插件的自定义 toggle key 照旧被 host PrefsSchema 校验丢弃(现有 seam 行为);`type: 'text'/'number'` 是公开 API 扩展,外部插件可用但 key 仍须是宿主 schema 字段。
|
||||
- number 输入的"非法值"在浏览器里只会以空串出现(number input 拒绝非数字字符),因此 `Number('')=0 → 钳到下限` 是唯一可达路径,与宽度行先例一致。
|
||||
- 字号实时生效依赖 xterm 的 options setter + re-fit;若未来 xterm 版本行为变化,回退方案是重建 Terminal(不推荐,会丢滚动缓冲)。
|
||||
|
||||
## 7. 实施偏差记录
|
||||
|
||||
- 计划中的"非法输入回退当前存储值"用例在实现中发现不可达(浏览器 number input 的值清洗),改为断言"空输入钳到下限"(与宽度行一致)。
|
||||
- `togglePref` 更名为 `applyPref`(它本就接受任意 patch,不只是布尔翻转)。
|
||||
@@ -1,90 +0,0 @@
|
||||
# 终端延迟初始化(零尺寸容器 xterm open 崩溃修复)设计
|
||||
|
||||
**日期**:2026-08-14
|
||||
**状态**:已实施(本文档含实施偏差记录)
|
||||
**目标版本**:v0.11.x(issue #25 修复)
|
||||
|
||||
## 1. 目标
|
||||
|
||||
修复 [issue #25](https://github.com/omdsh-dev/DSH-better-sidebar/issues/25):WKWebView 下点击「展开底部面板」后底部面板内容空白、无终端,控制台报 `Cannot read properties of undefined (reading 'dimensions')` 与 `(reading 'handleResize')`。
|
||||
|
||||
根因:`TerminalView` 的挂载 effect 在容器可能为零尺寸时**同步**执行 `term.open(host); fit.fit()`。xterm 5.3.0 在零尺寸容器里 open 时渲染器创建失败,`RenderService._renderer.value` 保持 undefined,随后 Viewport 的 `_innerRefresh` 读 `_renderService.dimensions`(getter 即 `_renderer.value.dimensions`)崩溃。Chrome(Blink)对零尺寸布局更宽容所以不崩;WKWebView 稳定复现。
|
||||
|
||||
修复:把 `term.open(host)` + `fit.fit()` 推迟到容器有实际尺寸之后再执行(xterm 在隐藏/零尺寸容器初始化的标准修法),open 恰好执行一次。
|
||||
|
||||
## 2. 非目标
|
||||
|
||||
- 不换 xterm 渲染器(打包产物只有 DomRenderer 是现状,不是本次根因;`rendererType` 保持默认)。
|
||||
- 不改 pty / WS / 终端连接生命周期:推迟 open 不推迟连接、不重启 shell、不丢 transcript。
|
||||
- 不改 host 半与 chunk 打包结构(新模块随 terminal chunk 打入 `lib/client-terminal.js`)。
|
||||
- 不 bump 版本号(release 流程负责)。
|
||||
|
||||
## 3. 现状回顾
|
||||
|
||||
- `src/client/TerminalView.tsx` 第 99–101 行同步 `term.open(host); fit.fit()`。
|
||||
- 零尺寸挂载路径真实存在且不止一条:
|
||||
- 底部面板首次展开自动开终端(`Sidebar.tsx` 的 `bottomOpenedOnce` effect)在面板 slide-in transition 的同一次 commit 挂载 TerminalView,WKWebView 下此刻 host 高度为 0;
|
||||
- 所有 tab 保持挂载(`split-pane.tsx`:非激活 tab 用 `.paneTabHidden { display: none }` 隐藏),面板折叠时保持挂载(`.panelHidden`)——任何 `display:none` / 零尺寸祖先都会踩同一坑。
|
||||
- 前提已验证(xterm 5.3.0 与 @xterm/addon-fit 0.10.0 源码):
|
||||
- `FitAddon.fit()` 在 open 之前是安全 no-op(`proposeDimensions` 先查 `this._terminal.element`,undefined 即返回)。
|
||||
- `WriteBuffer.write()` 不依赖 open 状态:数据解析进 buffer,open 后渲染——推迟 open 不丢初始 transcript。
|
||||
- `TerminalView` 已有的 ResizeObserver / 字体订阅里的 `fit.fit()` 在 open 前 no-op、open 后照常 re-fit,无需改动。
|
||||
|
||||
## 4. 设计
|
||||
|
||||
### 4.1 新模块 `src/client/open-when-sized.ts`(纯逻辑,可单测)
|
||||
|
||||
```ts
|
||||
export function openWhenSized(
|
||||
host: HTMLElement,
|
||||
open: () => void,
|
||||
raf: (cb: FrameRequestCallback) => number = requestAnimationFrame,
|
||||
caf: (id: number) => void = cancelAnimationFrame,
|
||||
): () => void
|
||||
```
|
||||
|
||||
- 单条代码路径:`raf(step)` → step 检查 `host.isConnected`(false 则停止,防卸载后空转)→ `clientWidth > 0 && clientHeight > 0` 则调 `open()` 并停止;否则再排一帧。
|
||||
- 返回 cancel:`caf(frame)` 并置空(幂等),cleanup 调用。
|
||||
- helper 不 try/catch(保持纯净),异常由调用方处理。
|
||||
|
||||
### 4.2 消费点 `src/client/TerminalView.tsx`
|
||||
|
||||
- 移除 effect 顶部的 `term.open(host); fit.fit()`(保留 `term.loadAddon(fit)`)。
|
||||
- 在 effect 尾部(`sendResize` / `connect` 定义之后、`connect()` 调用之前)注册:
|
||||
|
||||
```ts
|
||||
const cancelOpen = openWhenSized(host, () => {
|
||||
try {
|
||||
term.open(host)
|
||||
fit.fit()
|
||||
sendResize()
|
||||
} catch (error) {
|
||||
console.error('[dsh-better-sidebar] xterm open failed:', error)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- 回调里补 `sendResize()`:推迟路径下 socket 可能已先连上(`connect()` 在 effect 尾部同步调用),open+fit 后必须把真实 cols/rows 发出去,否则 pty 停留在默认 80x24 直到下次 resize;已尺寸快速路径下 socket 尚未 open,`sendResize` 内部守卫(`readyState === OPEN`)自然 no-op,无行为变化。
|
||||
- cleanup 里在 `term.dispose()` 前加 `cancelOpen()`。
|
||||
- 其余(connect / observer / fontSub / schemeSub / onData / 主题)不动。
|
||||
|
||||
## 5. 测试
|
||||
|
||||
`tests/open-when-sized.spec.ts`(jsdom + 注入手动步进的假 raf/caf;`Object.defineProperty` 桩 `clientWidth/clientHeight`;`appendChild/remove` 控制 `isConnected`):
|
||||
|
||||
1. 已尺寸 → tick 一次后 open 恰好一次,之后不再排帧;
|
||||
2. 零尺寸 → 多次 tick 不 open,设尺寸后下一次 tick open 一次,后续 tick 不重复;
|
||||
3. 只有一维缺失 → 补齐后 open;
|
||||
4. cancel 后永不 open、无残留帧、cancel 幂等;
|
||||
5. host 脱离文档 → 轮询停止、不 open;
|
||||
6. open 抛错 → 异常向调用方传播(调用方负责 try/catch),不重复调用。
|
||||
|
||||
## 6. 限制与取舍
|
||||
|
||||
- rAF 轮询在 host 长期隐藏(`display:none` 祖先)期间持续排帧,每帧只读两个属性,开销可忽略;open 后立即停止。用 ResizeObserver 触发也可行,但轮询是 issue 中建议的标准修法,且不依赖 observer 对 transition 中间态的回调时序。
|
||||
- 已尺寸快速路径比原行为晚一帧(rAF 回调时机),无感知差异。
|
||||
- 若容器永远零尺寸(异常布局),终端永不 open、不崩溃——对不可见内容是可接受的降级。
|
||||
|
||||
## 7. 实施偏差记录
|
||||
|
||||
无(按设计实施)。
|
||||
@@ -1,68 +0,0 @@
|
||||
# 皮肤兼容(令牌驱动):面板表面、终端透明度回退与独立 CSS 修复
|
||||
|
||||
**日期**:2026-08-15
|
||||
**状态**:已实施(本文档含实施偏差记录)
|
||||
**目标版本**:随发布 bump(本 PR 不改版本号、不更新 README——发布流程统一处理)
|
||||
|
||||
## 1. 目标
|
||||
|
||||
让 better-sidebar 与 **dsh-web-ui 的皮肤中心**兼容:用户切换皮肤时侧边栏自动跟随换肤,零每皮肤适配(KISS:不做 aionui 式"每款皮肤 remap 一套令牌"的重活)。附带修复同批 CSS 问题([#52](https://github.com/omdsh-dev/DSH-better-sidebar/issues/52) 弹出框遮挡、[#57](https://github.com/omdsh-dev/DSH-better-sidebar/issues/57) 前半图标尺寸、[#92](https://github.com/omdsh-dev/DSH-better-sidebar/issues/92) 拖拽回归防护),并覆盖 [#60](https://github.com/omdsh-dev/DSH-better-sidebar/issues/60)/[#105](https://github.com/omdsh-dev/DSH-better-sidebar/issues/105)/[#90](https://github.com/omdsh-dev/DSH-better-sidebar/issues/90) 的相关诉求。
|
||||
|
||||
## 2. 调研结论
|
||||
|
||||
dsh-web-ui(`zhu1090093659/dsh-web-ui`)的皮肤机制:
|
||||
|
||||
- 皮肤 = cordis 插件(`ui-skin-<id>`),皮肤中心通过 `cordis.patch.yml` 切换激活;皮肤插件 client half 在 `document.body` 上打 `data-dsh-<skin>` 属性并注入 CSS。
|
||||
- **10 款皮肤全部覆盖两层令牌**:`--dsw-static-*` 基础色板 + `--dsw-alias-*` 语义层(`--dsw-alias-bg-layer-1`、`--dsw-alias-bg-base` 等做成半透明玻璃 rgba 0.16–0.7,`--dsw-specific-sidebar-fill` 也全部覆盖)。
|
||||
- dsh-web-ui 自家右侧面板(aionui)用独立 `--aion-*` 令牌,每款皮肤单独 remap——**重**。
|
||||
|
||||
结论:better-sidebar 只要**消费 DSH 标准令牌**(alias 层),换肤就自动跟随,不需要任何每皮肤适配。此前围绕第三方皮肤插件(Aqua/deep-whale)设计的类名改名与 `data-bs-*` 语义钩子按 KISS 原则**全部回退**(见 §6)。
|
||||
|
||||
## 3. 设计
|
||||
|
||||
### 3.1 面板表面(`sidebar.module.css`)
|
||||
|
||||
```css
|
||||
.panel { background: var(--dsw-alias-bg-layer-1); }
|
||||
.bottomPanel { background: var(--dsw-alias-bg-layer-1); }
|
||||
```
|
||||
|
||||
- 直接消费通用卡片表面令牌,**不新增自有令牌、不加属性钩子**。
|
||||
- **绝不消费 `--dsw-specific-sidebar-fill`**:那是宿主左侧导航列专属令牌,皮肤系统按左导航语义覆盖它(dsh-web-ui 半透明玻璃/主题色、Aqua `transparent`),面板消费它会失去填充或与标签令牌冲突。
|
||||
- stock 外观色阶变化仅 ±8 RGB(bluish-50→75 / 900→875),视觉无差;dsh-web-ui 换肤后面板获得与宿主一致的玻璃效果。
|
||||
|
||||
### 3.2 终端/编辑器透明度回退(`theme.ts` / `TerminalView.tsx`)
|
||||
|
||||
`effectiveTokenValue` 现在把以下值视为"未设置"(触发调用方 `||` 回退):
|
||||
|
||||
- 空串与 CSS 重置关键字(`transparent`/`initial`/`inherit`/`unset`);
|
||||
- **alpha < 0.9 的半透明颜色**(dsh-web-ui 皮肤的 `--dsw-alias-bg-base` 是 rgba 0.16–0.7 玻璃)。
|
||||
|
||||
新增 `colorAlpha()` 解析 computed 颜色(rgb()/rgba()/hsl()/hsla() 逗号与空格语法、`#rgb/#rgba/#rrggbb/#rrggbbaa`);无法解析的格式(命名色、`color()`)视为不透明放行。阈值 0.9:≥ 0.9 的近不透明值(如皮肤作用域内 0.96 瓷器玻璃)放行,皮肤仍能控制终端表面。
|
||||
|
||||
### 3.3 独立修复(与皮肤兼容无关,保留)
|
||||
|
||||
- **z-index(#52)**:`.toggleCluster` 55→45、`.panel`/`.bottomPanel` 50→40;角手柄移入面板后为面板内局部 `z-index: 2`。依据:DSH 浮层栈为 100(Menu/HoverCard/Tooltip/PopupSelect/Modal=1000),30–99 无任何 DSH 元素。
|
||||
- **角手柄几何 CSS 化**:移入右面板 DOM,`position: absolute; left: -6px; bottom: calc(var(--dsh-sidebar-height, 0px) + 6px)`——复用拖拽逐帧写入的布局变量,删除 `cornerRef` JS 内联坐标。
|
||||
- **刷新图标统一 `size={14}`(#57 前半)**:Explorer/Git/Diff 的 `IconRefreshOutline16` 显式 14px,与 Subagent/Browser 一致。
|
||||
- **拖拽逐帧回归 e2e(#92)**:`tests/e2e/drag-layout.e2e.ts` rAF 采样,断言拖拽期间 `data-dsh-sidebar-dragging` 存在、`#root` transition 为 none、会话列与面板边单调 1:1 跟随(实测通过)。拖条以 `cursor: col-resize` 语义定位(无属性钩子可依赖)。
|
||||
|
||||
## 4. 测试
|
||||
|
||||
- `tests/theme.spec.ts`:`colorAlpha`(hex/rgb/hsl/不可解析)与 `effectiveTokenValue`(transparent/关键词/半透明回退/0.96 放行/缺失)。
|
||||
- `tests/e2e/mount.e2e.ts`:追加 `--dsh-sidebar-width` 布局变量随面板挂载生效的断言(令牌驱动契约的挂载级守护)。
|
||||
- `tests/e2e/drag-layout.e2e.ts`:拖拽逐帧回归。
|
||||
- 门禁:`pnpm typecheck && pnpm test && pnpm build && pnpm pack && pnpm test:mount`。
|
||||
|
||||
## 5. 文档
|
||||
|
||||
- AGENTS.md §8「皮肤兼容(令牌驱动)」:面板表面令牌、透明度阈值、z-index 层级、根锚点 `[data-dsh-better-sidebar]`、类名非契约。
|
||||
- README 不在本批更新(发布时随版本条目更新)。
|
||||
|
||||
## 6. 实施偏差记录
|
||||
|
||||
- **首版契约(已回退)**:初版围绕第三方皮肤插件设计了 `--dsh-sidebar-surface`/`--dsh-resize-strip-offset` 自有令牌(review 后已删)、panel 家族类名 kebab-case 改名(breaking)与 `data-bs-*` 语义钩子。确认主目标是 dsh-web-ui 皮肤中心后,按 KISS 全部回退:令牌驱动下这些都不需要,改名还引入无谓 breaking。最终契约 = 消费标准令牌 + 透明度阈值,无自有概念。
|
||||
- **角手柄 `z-index` 语义变化**:移入面板后 z-index 为面板内层叠(2,与拖条一致),对外绝对层级由面板(40)决定;两面板同开时手柄盒与底面板上沿无重叠(≥6px 间隙),行为不变。
|
||||
- **z-index 层级修正(2026-08-22)**:上文 "30–99 无任何 DSH 元素" 的假设不成立——DSH 的 ui-cordis 动态插件面板(`CordisPanel`,`position:fixed; z-index:30`,挂 `sidebar.footer.action` 槽)正落在该区间,且其清单/审批面会被 better-sidebar 的底部面板遮挡。修正:面板宿主层 40 → **25**(仍在 AppFrame overlayLayer 20 之上、ui-cordis 面板 30 与 DSH 浮层栈 100+ 之下),折叠按钮簇保持 host 内部 45(绝对层级随宿主 = 25)。
|
||||
- **拖拽禁用断言修正**:`transition: none` 的 computed 值为 `transition-property: none`(非 `all`),已按实测修正。
|
||||
- **面板默认表面色阶微调**:±8 RGB,未做像素级补偿(e2e 不依赖具体颜色)。
|
||||
@@ -1,89 +0,0 @@
|
||||
# 外链点击路由到插件 tab(urlTarget)+ 浏览器 HTTP/HTTPS 二级设置
|
||||
|
||||
**日期**:2026-08-15
|
||||
**状态**:已实施(本文档含实施偏差记录)
|
||||
**目标版本**:v0.13.0(API 兼容层,版本号未随本变更 bump,见 §7)
|
||||
|
||||
## 1. 目标
|
||||
|
||||
基于 [PR #74](https://github.com/omdsh-dev/DSH-better-sidebar/pull/74)(聊天区外链开关颗粒度讨论)向前兼容地拓展服务化 API:
|
||||
|
||||
1. **插件声明「点击打开的类型」**:`TabDescriptor` 新增可选 `urlTarget?: (url: URL) => boolean`——聊天区外链被拦截时,路由到第一个声明的、命中的、未被设置禁用的插件 tab 类型(`openTab({ type, url, title: hostname })`,URL 即全部载荷),内置浏览器 tab 退居隐式兜底。
|
||||
2. **浏览器链接设置改为二级菜单**(浏览器卡片齿轮弹窗内):
|
||||
- 侧边打开 HTTP 网页(默认开)
|
||||
- 侧边打开 HTTPS 网页(默认关)
|
||||
- `browserInterceptLinks` 保留为**总闸**(默认开,零迁移:旧文档显式 false 语义完整保留)。
|
||||
|
||||
## 2. 非目标
|
||||
|
||||
- 不实施 PR #74 的「外链开关提升到常规区」(用户确认:二级设置留在浏览器卡片齿轮弹窗,常规区不加行)。
|
||||
- 不做 per-host / per-domain 路由规则(只有 per-protocol 开关 + 插件谓词)。
|
||||
- 不改 + 菜单、不改手动打开的浏览器 tab、不改同源/修饰键点击放行规则。
|
||||
- 不改 host 半逻辑(仅 PrefsSchema 加字段);不修改 DSH 源码。
|
||||
- 不做版本号 bump(发布时决定;向前兼容机制 = `SIDEBAR_FEATURES` 追加 `'urlTarget'`)。
|
||||
|
||||
## 3. 现状回顾
|
||||
|
||||
- `link-intercept.ts` 的 `registerLinkInterception` 无参 `takeoverEnabled()` 只做布尔门;`index.tsx` 硬编码 `openTab({ type: 'browser', url, title: hostname })`。
|
||||
- `browserInterceptLinks`(默认 true)与 `browserNoSandbox` 并列于浏览器卡片的 `settings.toggles`;拦截 gating = 该 pref && `tabsEnabled['browser']`。
|
||||
- `openTab` seed 已有 `url` 字段(新建 tab 的 path 预填),插件 tab 无需新渲染管道。
|
||||
|
||||
## 4. 设计
|
||||
|
||||
### 4.1 Prefs(`prefs-shared.ts` / `config.ts` / `client/prefs.ts`)
|
||||
|
||||
```ts
|
||||
interface SidebarPrefs {
|
||||
// …
|
||||
browserInterceptLinks: boolean // 总闸,默认 true(保留,零迁移)
|
||||
browserInterceptHttp: boolean // HTTP 外链侧边栏接管,默认 true
|
||||
browserInterceptHttps: boolean // HTTPS 外链侧边栏接管,默认 false
|
||||
}
|
||||
```
|
||||
|
||||
三处同步维护(类型 + schemastery 默认 + parsePrefs 校验),旧文档无需迁移:总闸保留使显式关闭过的用户升级后仍不拦截。
|
||||
|
||||
### 4.2 服务 API(`service.ts`)
|
||||
|
||||
```ts
|
||||
interface TabDescriptor {
|
||||
// …
|
||||
urlTarget?: (url: URL) => boolean
|
||||
}
|
||||
|
||||
/** 注册顺序先到先得;谓词抛错吞掉(console.error)继续;返回 undefined 由调用方回退 browser。 */
|
||||
export function matchUrlTarget(tabs: readonly TabDescriptor[], url: URL): TabDescriptor | undefined
|
||||
```
|
||||
|
||||
- 匹配函数为**模块级纯函数**(不进 `BetterSidebarService` 接口,KISS:不扩公开面);启用态过滤由调用方(`index.tsx`)按 `tabsEnabled` 过滤后传入。
|
||||
- 内置 browser 不声明 `urlTarget`,永远不可能遮蔽插件声明;插件多 URL 并存需用 `createTab` 铸造 per-URL id(browser 同款模式),否则 id 安全网聚焦既有 tab 不覆写 path。
|
||||
- `SIDEBAR_FEATURES` 追加 `'urlTarget'`(消费者按能力 gate)。
|
||||
|
||||
### 4.3 拦截管线(`link-intercept.ts` / `index.tsx`)
|
||||
|
||||
- `shouldInterceptLink` 签名不变(协议/同源纯判定);`takeoverEnabled` 改为 `(url: URL) => boolean`,onClick 顺序:纯判定 → gate(持 URL)→ preventDefault → open。
|
||||
- `index.tsx` gate:总闸 `browserInterceptLinks` → 协议开关(https 读 `browserInterceptHttps`,否则读 `browserInterceptHttp`)→ 目标可开(插件命中即目标,否则 browser 的 `tabsEnabled`)。open 时再次解析目标并 `openTab({ type, url, title: hostname })`。
|
||||
|
||||
### 4.4 设置 UI(`builtins/tabs.tsx` / `locales.ts`)
|
||||
|
||||
浏览器卡片 `settings.toggles` 收敛为四行:`browserNoSandbox` / `browserInterceptLinks`(总闸)/ `browserInterceptHttp` / `browserInterceptHttps`;新增 zh/en 文案(HTTP 默认开、HTTPS 默认关及理由)。
|
||||
|
||||
## 5. 行为变化
|
||||
|
||||
- 默认设置下:HTTP 外链 → 侧边栏(不变);**HTTPS 外链 → 系统浏览器新窗口(变化,符合「默认关闭」)**。
|
||||
- 声明 `urlTarget` 的插件 tab 认领被拦截的链接(优先于浏览器兜底);插件被禁用/谓词抛错时跳过。
|
||||
- 总闸关闭 → 一律不拦截(旧文档语义保留)。
|
||||
|
||||
## 6. 测试
|
||||
|
||||
- `tests/builtins.spec.ts`:浏览器 toggles 断言收敛为四行 + 各行 title/desc 存在。
|
||||
- `tests/service.spec.ts`:`matchUrlTarget`(无声明 → undefined / 注册顺序 / 抛错吞掉 / browser 永不命中)。
|
||||
- `tests/link-intercept.spec.ts`(新增 jsdom):`takeoverEnabled(url)` 收到解析后的 URL、gate 决定是否接管、同源/非 http(s) 不 consult gate、修饰键绕过。
|
||||
- `tests/unit.spec.ts`:parsePrefs 新字段默认值(http true / https false / 非法回退);总闸与协议开关相互独立。
|
||||
- `tests/plugin-shape.spec.ts` / `tests/smoke.spec.ts`:全默认字面量补两个字段。
|
||||
|
||||
## 7. 实施偏差记录
|
||||
|
||||
- 计划曾考虑把 `matchUrlTarget` 作为 `BetterSidebarService` 接口方法(镜像 `matchFileViewer`);按 KISS 收敛为**模块级导出纯函数**,启用态过滤由调用方完成(匹配函数不感知 prefs),公开 API 面只剩 `urlTarget` 字段 + `'urlTarget'` feature 字符串。
|
||||
- 版本号未 bump(保持 0.12.1):`SIDEBAR_SERVICE_VERSION` 与 `package.json` 的配对断言继续成立;`'urlTarget'` feature 已入 `SIDEBAR_FEATURES`,发布时再决定版本。
|
||||
- PR #74 的 diff(常规区移动 + 其测试改动)被本方案取代,未合入。
|
||||
@@ -1,50 +0,0 @@
|
||||
# Edge 151 无端口 Origin 导致的侧边栏 403 forbidden 修复
|
||||
|
||||
日期:2026-08-17
|
||||
|
||||
## 现象
|
||||
|
||||
Windows + Edge 151(151.0.4129.86)下,`dsh web` 主应用一切正常,但 better-sidebar 的文件管理(Explorer / 文件树)显示 `forbidden`。抓包确认:`POST /sidebar/api/fs.tree` 返回 `403`,响应体为 `{"ok":false,"error":{"code":"forbidden","message":"forbidden"}}`——命中插件自身信任栅栏的拒绝分支,而不是业务错误。
|
||||
|
||||
## 根因
|
||||
|
||||
`src/trust-fence.ts` 是 DSH `/api` 网关信任栅栏(`@deepseek-ai/dsh-client-connection` 的 `api-request-trust.ts`)的行为镜像。Origin 校验一段,镜像版仍按 `host`(含端口)比较:
|
||||
|
||||
```ts
|
||||
return new URL(origin).host === hostUrl.host
|
||||
```
|
||||
|
||||
DSH 官方栅栏后来修复为按 `hostname` 比较,理由是:**部分 Chromium 构建(Edge 151)会把非默认端口 loopback 页面的 Origin 序列化为不带端口的形式**(`http://127.0.0.1:3080` 的页面,fetch 携带 `Origin: http://127.0.0.1`)。
|
||||
|
||||
于是 Edge 151 下:
|
||||
|
||||
- 页面 `http://127.0.0.1:3080` 发出的请求,`Origin: http://127.0.0.1`,`Host: 127.0.0.1:3080`;
|
||||
- 镜像栅栏比较 `new URL('http://127.0.0.1').host`(= `127.0.0.1`)与 Host `127.0.0.1:3080` → 不等 → 403;
|
||||
- DSH 自己的 `/api` 栅栏比较 hostname → 相等 → 放行,所以主应用正常,只有 `/sidebar/*` 全部 403。
|
||||
|
||||
镜像与官方行为漂移,正好打破镜像文件头声明的「behaviorally identical」契约。
|
||||
|
||||
## 证据
|
||||
|
||||
- 本机 Edge 版本 151.0.4129.86(DSH 注释点名的构建)。
|
||||
- curl 实测(对运行中的 `dsh web`,`/sidebar/api/fs.list` 用错方法名以区分 403 与方法分发):
|
||||
- `Origin: http://127.0.0.1:3080`(带端口)→ 200 `unknown sidebar API method`(过了栅栏);
|
||||
- `Origin: http://127.0.0.1`(Edge 151 形式)→ 403 `forbidden`。
|
||||
- DSH 修复参考:`packages/client/connection/src/api-request-trust.ts`(注释点名 Edge 151)及测试 `tests/api-request-trust.host.spec.ts` 的「accepts an Origin that names the Host hostname without its port (Edge 151 serialization)」。
|
||||
|
||||
## 方案
|
||||
|
||||
1. `src/trust-fence.ts` 的 Origin 校验改为 `new URL(origin).hostname === hostUrl.hostname`,注释写明与 DSH 官方栅栏保持一致的语义与理由(Host 栅栏已绑定 authority,端口不再重决信任)。
|
||||
2. `tests/trusted-hosts.spec.ts` 增加回归用例:loopback Host + 不带端口的 Origin → 200;同时保留带端口与跨域拒绝用例。
|
||||
|
||||
行为语义变化(有意为之,与 DSH 一致):Origin 与 Host 的 hostname 相同即认为同源,端口差异不再拒绝。安全前提不变:Host 栅栏仍要求 Host 为 loopback 或 `webRuntime.trustedHosts` 授权项,`sec-fetch-site: cross-site` 仍一律拒绝。
|
||||
|
||||
## 验证
|
||||
|
||||
1. `pnpm test`(vitest)新增用例通过,既有 trusted-hosts 用例不回归。
|
||||
2. `pnpm typecheck`。
|
||||
3. 本地挂载:`pnpm build` 后覆盖 web profile 的 node_modules 产物,重启 `dsh web`,curl 以 `Origin: http://127.0.0.1`(无端口)请求 `/sidebar/api/*` 应进入方法分发(404 unknown method)而非 403;浏览器硬刷新后文件管理恢复。
|
||||
|
||||
## 发布
|
||||
|
||||
按仓库 §0 流程:`fix/*` 分支 → PR → review 合并 → bump 版本打 tag 走 release workflow。即时修复可先本地覆盖 profile 产物验证,正式生效待 0.13.1 发布后 `dsh plugin add dsh-better-sidebar@latest`。
|
||||
@@ -1,82 +0,0 @@
|
||||
# Markdown 预览 Mermaid 安全渲染设计
|
||||
|
||||
> 日期:2026-08-17
|
||||
> 状态:已实现
|
||||
> 范围:`dsh-better-sidebar` 插件,Markdown 预览 + 新 `mermaid` 懒加载 chunk
|
||||
> 基线:社区 PR #99(`feat/mermaid-md-preview`)的架构与安全姿势;借鉴 PR #75(`feat/mermaid-rendering`)的点击放大交互
|
||||
|
||||
## 1. 背景与问题
|
||||
|
||||
Markdown 预览(`TextEditor.tsx` 的 preview 模式)直接把全文交给 DSH 的 `MarkdownText`(`@deepseek-ai/dsh-client-ui-primitives`)渲染。该渲染器的 fenced code 一律走 `CodeBlock`(Shiki 高亮 + 复制按钮),且**不支持自定义 fence 渲染器**——```` ```mermaid ```` 块只会显示成一段无高亮的代码,不会渲染成图(DSH 宿主内 `mermaid` 字样出现 0 次,Shiki 别名表也无此语言)。
|
||||
|
||||
社区出现过两个 mermaid PR,安全姿势差异极大:
|
||||
|
||||
| | PR #75 | PR #99 |
|
||||
|---|---|---|
|
||||
| 加载 | 运行时 jsdelivr CDN(第三方供应链、离线不可用、无版本锁) | 自有懒加载 chunk(mermaid 11.16.1 本地打包,`/sidebar/bundle` 同源下发) |
|
||||
| 安全 | `securityLevel: 'loose'`(标签可注入原始 HTML)+ 无清洗 + `innerHTML` 直插 | `securityLevel: 'strict'` + `htmlLabels: false` + SVG 二次清洗 + 不应用 `bindFunctions` |
|
||||
| 测试/文档 | 无 | 单测 + e2e + 设计文档 |
|
||||
| 亮点 | 点击放大弹窗(滚轮缩放/拖拽/快捷键) | 主题跟随、错误回退、CodeBlock 同款 chrome |
|
||||
|
||||
**结论:以 #99 为基线**。md 文件来自工作区(可能来自克隆仓库/下载),不可信;`loose` + 无清洗在 DSH GUI origin 下就是 XSS(可读会话数据)。**仅借鉴 #75 的点击放大交互**,在 #99 的已清洗 SVG 之上用 React 重写(克隆节点无事件面,弹窗不新增攻击面)。本地图片引用重写(#99 顺带功能)不在本 PR 范围,另行独立 PR。
|
||||
|
||||
## 2. 方案
|
||||
|
||||
### 2.1 检测:纯函数按 fence 切块
|
||||
|
||||
新增 `src/client/mermaid-blocks.ts`(`splitMermaidBlocks`,纯函数、无依赖),职责是**检测**预览是否需要 mermaid chunk:
|
||||
|
||||
- 逐行扫描,命中 info string 为 `mermaid` 的围栏(大小写不敏感,支持 `mermaid{...}` 属性后缀,缩进 ≤3 空格,CommonMark 语义)即"提出"为 `{ kind: 'mermaid', code }` 块;
|
||||
- 其余行原样保留在 `{ kind: 'markdown', text }` 块中(含非 mermaid 围栏);
|
||||
- 未闭合的 mermaid fence 吞掉文件剩余部分(与 CommonMark 对开 fence 的恢复一致);
|
||||
- **CommonMark fence 规则完整**:开 fence 为 3+ 反引号**或波浪号**;闭 fence 必须同字符且长度 ≥ 开 fence;反引号 fence 的 info string 含反引号时该行不是开 fence。
|
||||
|
||||
`TextEditor` 预览时先 `splitMermaidBlocks`,**无 mermaid 块则走原 `MarkdownText` 直渲路径(零行为变化)**;有则渲染 `MermaidMarkdown`(mermaid chunk 内)。
|
||||
|
||||
### 2.2 渲染架构:单次解析 + 占位替换(CR 第 3 轮 P1 修正)
|
||||
|
||||
早期方案把文档切成多个独立 `MarkdownText` 片段,会破坏跨 fence 的 Markdown 语义(引用式链接定义在 fence 之后、脚注、有序列表连续性)。当前实现:
|
||||
|
||||
- **整篇文档只走一次 `MarkdownText`**(`MermaidMarkdown`),跨 fence 语义完整;
|
||||
- 布局阶段(`useLayoutEffect`)扫描容器内 `.md-code-block`,识别 mermaid 块(双通道:shiki 路径的 `code.language-mermaid` 类,或 plain 路径的 banner infostring 文本 === `mermaid`——mermaid 无 shiki 语法必走 plain,语言只显示在 infostring 里),用 `createRoot` 把 `MermaidDiagram` 挂进替换后的子节点;**React 管理的 `.md-code-block` 宿主节点保留在树中**(仅替换其 children,CSS `display: contents` 抑制代码块自身盒子),React 协调不会丢失宿主;
|
||||
- 块内容变化(编辑后重新预览)按源码 diff 重渲;块不再是 mermaid fence 时还原原 children(普通代码块恢复显示);fence 消失时卸载对应 root;
|
||||
- DSH `MarkdownText` 无代码块渲染扩展入口(且仓库硬约束禁止改 DSH),这是约束内的最小侵入方案。
|
||||
|
||||
### 2.2 懒加载 chunk:`mermaid`
|
||||
|
||||
mermaid 及其图依赖(d3 / dagre-d3-es / cytoscape 等)打包进新的 `lib/client-mermaid.js`(~7MB),复用既有 chunk 机制(tsdown `CHUNKS` + `/sidebar/bundle` 路由 + `globalThis.__dshChunks__` 注册表 + `lazyChunkComponent` 懒包装),仅当预览的 md 文件实际含 mermaid fence 时才下载/执行。chunk 名在 4 处同步:`tsdown.config.ts` `CHUNKS`、`src/bundle-route.ts` `CHUNK_NAMES`、`src/client/chunk-loader.ts` `ChunkName`、`package.json` `files`(及各 spec 的硬编码清单)。构建侧对 mermaid chunk 注入 `mermaidChunkAliases`:把裸 `uuid` 说明符解析到浏览器入口(`uuid/dist/index.js`,Web Crypto 实现),绕过其 node 入口的 `node:crypto` 触发客户端纯度门。
|
||||
|
||||
### 2.3 渲染与安全(纵深防御)
|
||||
|
||||
- `mermaid.render(id, code)` 客户端渲染 → SVG 字符串注入;每个渲染调用用单调递增 id(文档内唯一);
|
||||
- `mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', htmlLabels: false, theme })`——strict 下标签被转义、`click` 指令失效;`htmlLabels: false` 强制节点文字走真实 SVG `<text>`(默认的 `foreignObject` HTML 标签通道会被清洗器整体删除,导致文字丢失),同时把 HTML 标签注入面关死;`bindFunctions` **不**应用(静态图,无点击交互);订阅 `subscribeColorScheme`,深浅色切换自动按新主题重渲;
|
||||
- **SVG 注入前二次清洗**(`src/client/mermaid-sanitize.ts`,独立纯函数、可单测):`DOMParser`(`image/svg+xml`)解析,删除 `foreignObject` 与 `script`/外来 HTML 元素(`img`/`iframe`/`object`/`embed`/`video`/`audio`/`input`/`button`/`form`/`link`/`meta`/`base`),剥离 `@*`/`on*` 属性,**移除全部 `href`/`xlink:href`**(静态图不需要链接,杜绝 javascript:/data: 乃至普通外链把整个 GUI 导航走);仅接受根元素为 `<svg>` 的文档;解析失败(parsererror/非 SVG 根)返回空串,调用方显示错误回退,**原始字符串永不过清洗器**;
|
||||
- 渲染失败(语法错误/清洗拒绝)展示错误条 + 原始代码块(便于对照修改),错误全文挂 tooltip;空 fence 不渲染;
|
||||
- 块头保留与 `CodeBlock` 一致的 chrome:info string「mermaid」+ 复制按钮(`writeClipboard`,沿用 `copy`/`copied` 字典)。块体带稳定锚点 `data-mermaid-diagram`(e2e 断言用)。
|
||||
|
||||
### 2.4 点击放大弹窗(借鉴 PR #75,React 重写)
|
||||
|
||||
点击 `[data-mermaid-diagram]` 内 SVG → 克隆节点(**已清洗**,无事件属性)→ portalled fixed overlay(`data-mermaid-modal`):
|
||||
|
||||
- 滚轮缩放(以鼠标位置为不动点,被动标志关闭防止页面滚动);
|
||||
- 拖拽平移(mousedown/mousemove/mouseup on window);
|
||||
- 工具栏按钮:− / + / ⟳ / ✕(i18n title);
|
||||
- 键盘:`+`/`=` 放大、`-` 缩小、`0` 重置、`Esc` 关闭;
|
||||
- 背景点击关闭;卸载时移除全部监听;
|
||||
- 样式全部走 CSS Module + DSH 令牌(遮罩 `--dsw-alias-bg-mask-1` + `blur(2px)`,与 DSH `Modal` 一致;z-index 1000 落在 DSH 浮层栈)。
|
||||
|
||||
## 3. 验证
|
||||
|
||||
- 单测:`tests/mermaid-blocks.spec.ts`(14 例:fence 识别大小写/属性/缩进、4 反引号/波浪号、同字符且不短于开 fence 的闭 fence、非 mermaid 围栏不动、交错顺序、开 fence 恢复、空文件);`tests/mermaid-sanitize.spec.ts`(10 例 XSS:foreignObject/script/外来元素整节点剥离(含混合大小写元素名与属性名)、`on*`/`@*` 剥离、href/xlink:href 全删、畸形 XML → 空串、非 SVG 根 → 空串、良性图保真);`tests/mermaid-markdown.spec.tsx`(3 例架构回归:mermaid 块被交换为图、**跨 fence 引用式链接在单次解析下解析**、非 mermaid 围栏不动);
|
||||
- 产物契约:`chunk-artifact.spec.ts`(mermaid chunk 执行 + registry 槽位 + 平台 externals require,`addEventListener` stub)/ `manifest-consistency.spec.ts` / `bundle-route.spec.ts` 的 chunk 清单同步加入 `mermaid`;
|
||||
- 挂载冒烟:`tests/e2e/mount.e2e.ts` 新增 seed `diagram.md`,打开后强制 `client-mermaid.js` 往返、断言 `[data-mermaid-diagram] svg` 含节点文字(真实 `<text>`)、**跨 fence 引用式链接解析为真实锚点(单次解析证明)**、点击弹窗出现且 Esc 关闭、预览模式编辑器隐藏;缺 chunk、文字丢失、语义破坏、弹窗失效任一即红;
|
||||
- 现有 `markdown-copy-labels.spec.tsx` 不回归(无 mermaid 文件走原路径)。
|
||||
|
||||
## 4. 风险与取舍
|
||||
|
||||
- 编辑器 chunk 之外新增 ~7MB 懒 chunk(mermaid + d3/dagre/cytoscape/katex 图依赖);仅预览含 mermaid fence 的 md 时下载,启动与普通文本打开路径零成本;
|
||||
- `splitMermaidBlocks` 对"mermaid 围栏嵌在其他代码块内"的极边缘情况会误提(只影响是否走 mermaid 路径,不影响渲染正确性)——预览级工具接受该取舍(与常见 md 渲染器行为一致);
|
||||
- 占位替换依赖 DSH `MarkdownText` 的 DOM 输出结构(`.md-code-block` + `language-mermaid`):宿主节点保留在 React 树中、仅换 children,协调安全;若未来 DSH 渲染器结构变化,`data-mermaid-processed` 扫描点需要同步(有 e2e 兜底);
|
||||
- 清洗器删除全部 `href`/`xlink:href`:`<use href>` 类图标引用可能不显示(纯装饰性损失),换取确定性的无导航/无脚本面;
|
||||
- mermaid 官方核心包持续演化,锁 `^11.16.1`,升级靠 Renovate 常规流程;
|
||||
- 回滚:删除 chunk 三处声明 + `files` 条目 + `TextEditor` 分支即可回到纯代码块展示。
|
||||
@@ -1,27 +0,0 @@
|
||||
# 编辑器预览刷新三件套(issue #167)
|
||||
|
||||
日期:2026-08-19
|
||||
|
||||
## 问题(#167,无评论无修复)
|
||||
|
||||
`.md` 文件编辑保存后,切到预览不立即渲染新内容,需关闭文件重新打开。根因:`EditorHost` 的加载 effect 只依赖 `[sessionId, cwd, path, ctx, showEmpty]`——保存(fsWrite)与模式切换(setMode)都不重跑加载,且工具栏没有手动刷新入口。
|
||||
|
||||
## 方案(最小三件套,全部在 EditorHost.tsx)
|
||||
|
||||
| 编号 | 行为 | 实现 |
|
||||
|------|------|------|
|
||||
| A | 手动刷新按钮(文本类 viewer,即 toolbar 非空时显示) | 新增 `reloadSeq` state;effect 依赖加 `reloadSeq`;header 刷新按钮 `setReloadSeq(s => s + 1)` |
|
||||
| B | 编辑保存后切回预览自动刷新 | header「预览」按钮 onClick:`toolbar.mode === 'edit'` 且非 dirty 且 saveState 非 failed → 先 `setReloadSeq(+1)` 再 `setMode('preview')`;dirty 时不刷新(草稿只在 CodeMirror 内,重载会丢) |
|
||||
| C | 预览模式下保存成功后刷新 | 监听 `toolbar.saveState` 从非 `'saved'` 变为 `'saved'` 且 `mode === 'preview'` → `setReloadSeq(+1)` |
|
||||
|
||||
刷新语义:重载走既有 load effect(AbortController 会中止旧加载;重挂载后 TextEditor 内部 state 重置为 preview 模式——与「关闭重开」等价,但保留 tab 与面板状态)。
|
||||
|
||||
## 范围外(记录,不实现)
|
||||
|
||||
- LLM 改文件后自动刷新(磁盘指纹对比 + silent 不闪屏)——分形有先例,另行立项;
|
||||
- image/pdf/binary viewer 的手动刷新(toolbar 为空,无入口;内容静态,低优先级)。
|
||||
|
||||
## 验证
|
||||
|
||||
- 单测:刷新按钮渲染 + 点击触发重载(mock fsRead 调用次数);B 的 dirty 抑制;C 的 saveState 边沿触发;
|
||||
- 手工:编辑 md → 保存 → 切预览即时渲染;预览模式 Ctrl+S 后刷新;刷新按钮任意 viewer 可用。
|
||||
@@ -1,262 +0,0 @@
|
||||
# 侧边栏面板注入方案设计:统一面板宿主(插件侧) + 官方槽位路线(上游侧)
|
||||
|
||||
**日期**:2026-08-19
|
||||
**状态**:评审中(设计文档先行,实现待批准)
|
||||
**作者**:DSH 工程模式会话
|
||||
**当前版本**:v0.13.0(main)
|
||||
**目标版本**:v0.13.x(不 bump 大版本)
|
||||
|
||||
## 1. 目标
|
||||
|
||||
1. 解决侧边栏面板的**注入方法**问题:找到一个同时兼容「纯浏览器 / Electron 套壳(DSH Desktop)/ 各类 Tauri/WebView 套壳 / 移动 webview」且**性能良好**的统一注入方案。
|
||||
2. 消灭现有症状级补丁 PR 簇(#226 desktop 布局推挤、#135 dsh-web-mobile、#82/#108/#111/#138/#153 桌面标题栏)背后的结构性根因:**插件选择器深入宿主 DOM 形状(nth-child 锚点)+ 桌面壳依赖手动设置(titleBarCompat)+ 几何写路径分散(拖拽/状态两条)+ 懒加载缓存每次激活清空(重注入重执行)**。
|
||||
3. 探索并文档化「官方槽位路线」:参考 DSH 内置插件 `ui-sidebar`(官方左侧边栏本身就是以插件方式占用 `sidebar` 槽),评估外部插件接管官方面板槽的可行性与代价,作为中期北极星;上游未落地前以插件侧方案为主线。
|
||||
|
||||
## 2. 非目标(Out of Scope)
|
||||
|
||||
- **不修改 DSH 官方源码**(仓库硬约束,`~/.dsh/source/current` 零写入);官方槽位路线只产出 issue/设计建议给上游。
|
||||
- 不做 shadow DOM / iframe 全隔离注入(令牌穿透、事件与 portal 代价高,仅在宿主样式污染实测出现时作为兜底启用)。
|
||||
- 不重构 tab/viewer 扩展点本身(`ctx.betterSidebar` 服务契约不变)。
|
||||
- 不承诺禁用官方 `ui-sidebar` 的行为由插件自动完成——那是 **profile 层用户决策**,插件只文档化。
|
||||
- 不处理 #69(browser 探测,独立域)。
|
||||
|
||||
## 3. 已验证事实(证据先行,2026-08-19 核对)
|
||||
|
||||
### 3.1 当前注入实现(本仓库)
|
||||
|
||||
| 事实 | 证据 |
|
||||
|---|---|
|
||||
| 注入 = body 直下 `div[data-dsh-better-sidebar]` + **第二个 React root**(非 portal) | `src/client/index.tsx:120-141` |
|
||||
| 宿主推挤 = html 上 `--dsh-sidebar-width/height` + `#root{margin-right}` + **`#root > [data-slot=root] > div > div:nth-child(2)` 锚点** | `src/client/layout.css:22-32` |
|
||||
| 拖拽:pointer 事件 + rAF 单帧写面板尺寸与 html 变量(无强制 reflow 读) | `src/client/Sidebar.tsx:494-518` |
|
||||
| 懒加载 chunk:每次激活 `resetChunks()` 清内存缓存 → 下次懒打开**重注入+重执行**脚本;HTTP 层已有 ETag 304(**不重下载**) | `src/client/index.tsx:120` 注释、`chunk-loader.ts` |
|
||||
| 会话切换:只换 snapshot,面板 DOM 不重建(保持) | `Sidebar.tsx` store 驱动 |
|
||||
| **已具备(实施勿重做)**:拖拽 rAF 批处理(`scheduleDrag` 单帧一次 `applyDrag`,无强制 reflow 读);窄屏不推挤宿主(narrow 时宽/高变量写 0);`measureCenter` ResizeObserver 已 rAF 节流;header 推挤已用弱锚点 `[data-slot="conversation.session.header"]` | `Sidebar.tsx:368-424,487-533,563-585`、`layout.css:45` |
|
||||
|
||||
### 3.2 DSH 官方架构(`~/.dsh/source/current`,只读)
|
||||
|
||||
| 事实 | 证据 |
|
||||
|---|---|
|
||||
| 外壳 = `#root` 单挂载点 + `__DSH_BOOT__` 引导;真实 UI 树全挂在 `root` 槽 | `apps/web/index.html:10-12`、`packages/client/web/src/app.tsx:38-43` |
|
||||
| `ui-layout` 声明 `root` 槽的 children:`sidebar`/`conversation`/`details` 三个 **single 独占槽**;single 槽重复声明**抛错** | `ui-layout/src/client/index.ts:80-96`、`ui-slots/src/index.ts:747` |
|
||||
| **官方左侧边栏 = 内置插件 `ui-sidebar`**:注册 `name:'sidebar'`,渲染在 AppFrame 的 grid 列内;几何经 layout store → `gridTemplateColumns` 内联传导(非 CSS 变量);**官方不持久化宽度/折叠**;折叠按钮在 SidebarRoot 内自渲染并调 `ctx.layout.toggleSidebar()`;拖拽把手由 AppFrame 免费提供 | `ui-sidebar/src/client/index.ts:39`、`ui-layout/src/client/AppFrame.tsx:164-196` |
|
||||
| `details` 槽(右侧面板)被 `ui-conversation` 的 DetailsPanel 占用;禁 ui-conversation = 会话列整体空置,代价极高 | `ui-conversation/src/client/apply.ts:445-458` |
|
||||
| 官方浮层(Menu/Modal/Toast/HoverCard)全部 `createPortal(document.body)`——**body 级 portal 是官方姿势** | `ui-primitives/Menu.tsx:299`、`Modal.tsx:55-85` |
|
||||
| 槽位注入消费者经 `slots.inject` 注入;槽未声明时**静默跳过**(禁用 ui-sidebar 不会炸树,但接管者需自实现会话/设置入口) | `runtime/src/client/slots.ts:130-192` |
|
||||
| profile 可禁用内置插件行:`cordis.patch.yml` `- id: <plugin-id>` + `disabled: true`(皮肤 10 款禁用先例) | `~/.dsh/profiles/web/cordis.patch.yml` |
|
||||
| 性能参考:拖拽 rAF 合并最新 delta 单帧应用 + `[data-dragging]` 禁过渡 + 纯函数让步结算 + 「关闭=宽 0 保活不卸载」 | `AppFrame.tsx:59-62`、`AppFrame.module.css:8-17`、`columns.ts:62-77` |
|
||||
|
||||
### 3.4 实证核对(真实 dsh web 页面,Playwright 探针,2026-08-19)
|
||||
|
||||
对 http://127.0.0.1:3080/(本机 profile,better-sidebar 已挂载)实测 DOM 与推挤行为:
|
||||
|
||||
| 事实 | 实测结果 |
|
||||
|---|---|
|
||||
| 外壳 DOM 链 | `#root > div[data-slot="root"]`(**display:contents**)`> div[data-dsh-frame]`(grid,CSS module 哈希类名) |
|
||||
| frame 子项 | **5 个**:`[data-pane=sidebar]` / `[data-pane=conversation]` / `[data-pane=details]` / `[data-shell-overlay]`(浮层)/ 拖拽 handle——`nth-child(2)` 当前仍命中 centerCol,但**位置脆弱性实锤**(浮层/handle 已混入子项序列) |
|
||||
| 稳定锚点 | centerCol 带 `data-pane="conversation"`;frame 带 `data-dsh-frame` → 弱锚点 = `#root [data-dsh-frame] > [data-pane="conversation"]` |
|
||||
| ~~`[data-slot="conversation"]` 锚点候选~~ | **作废**:该元素 `display: contents`(无盒,margin 无效,rect 0×0) |
|
||||
| 推挤无溢出(web) | 设 `--dsh-sidebar-width:300px` 后:`#root` margin-right 生效(过渡中 92px),`scrollWidth == clientWidth == 1440`,**无水平溢出** → `width: calc(100% - var)` 改动在 web 无副作用;桌面壳溢出修复(#226 目标)保留并标注「web 已验证无害、Desktop 待实测」 |
|
||||
| 当前 host 本体 | `[data-dsh-better-sidebar]` 为 `position: static` 的普通块(fixed 在**内层面板**上)→ 新增 `data-dsh-panel-host` 固定含块层是宿主内新元素,外部契约不变 |
|
||||
| 页面错误 | 0 个 pageerror(探针期间) |
|
||||
|
||||
### 3.3 桌面套壳(本机 DSH Desktop.app,Electron)
|
||||
|
||||
| 事实 | 证据 |
|
||||
|---|---|
|
||||
| 渲染 URL 注入信号:`?dsh-desktop-mode=<compatibility\|advanced>&dsh-desktop-platform=<…>` | `app.asar.unpacked/lib/src-B5373ech.js` |
|
||||
| win32:`titleBarStyle:"hidden"` + `titleBarOverlay({height:32})`(右上角 32px 为窗口控制钮) | `electron-runtime-C_i38SKA.js:338-348` |
|
||||
| darwin:`titleBarStyle:"hiddenInset"` + `trafficLightPosition(16,16)`(左上红绿灯区 ~78px) | 同文件 `:325-333` |
|
||||
| 安全基线:`contextIsolation/sandbox:true`、`nodeIntegration:false`;preload 仅暴露 `__DSH_DESKTOP_FILE_PATH__` | `preload.cjs:5-9` |
|
||||
| 社区 Tauri 套壳(kyorakuyk/dsh-desktop、Lotus-c/DSH-Desktop)用**原生系统标题栏**,fixed 语义与纯浏览器一致 | 子代理调研(来源 URL 见 §10) |
|
||||
| 套壳最大注入风险:祖先 `transform/will-change` 劫持 fixed 包含块、标题栏 overlay 遮顶、drag 区吞事件、移动键盘盖 `bottom:0` | 子代理调研 |
|
||||
|
||||
## 4. 方案总览:双路线
|
||||
|
||||
```
|
||||
路线 A(主线,本仓库可控)┐
|
||||
统一面板宿主:body 固定含块包裹层 ├─ 现在 → v0.13.x(已实施,分支 feat/injection-unified-host)
|
||||
+ 桌面信号自动适配 + 几何写路径统一 │
|
||||
路线 B(已关闭)┘
|
||||
ui-sidebar 模式:占用官方槽位(grid 内渲染) ─ 2026-08-19 用户决策关闭:无法向官方提 issue,
|
||||
纯插件侧自己优化注入方式(调研结论保留于 §6)
|
||||
```
|
||||
|
||||
- **路线 A 已实施**:不依赖 DSH 新能力,纯插件侧,直接吸收桌面/移动/性能问题。
|
||||
- **路线 B 已关闭**(2026-08-19 用户决策):不提交上游 issue;§6 的槽位可行性调研保留为后续参考,但不再推进。
|
||||
|
||||
## 5. 路线 A 详细设计:统一面板宿主
|
||||
|
||||
### 5.1 挂载层(兼容性根因修复)
|
||||
|
||||
**A1.1 固定含块包裹层**:保留 body 级锚点(官方 portal 姿势),但结构升级为:
|
||||
|
||||
```
|
||||
document.body
|
||||
└─ div[data-dsh-better-sidebar] ← 唯一锚点(属性不变,兼容既有契约/皮肤)
|
||||
└─ div[data-dsh-panel-host] ← 新增:position:fixed; inset:0; pointer-events:none; z-index:25(v0.15.x 起;初版为 40,2026-08-22 下调:ui-cordis 动态插件面板固定层 30,须在其上)
|
||||
├─ 右侧面板(absolute; right:0; pointer-events:auto)
|
||||
└─ 底部面板(absolute; bottom:0; pointer-events:auto)
|
||||
```
|
||||
|
||||
- 面板在**自己的固定含块**内 absolute 定位 → 宿主中间层(套壳注入的 wrapper)transform 不再劫持面板 fixed 语义。
|
||||
- 折叠按钮簇 z-index 45 不变(skin 契约 §8 同步)。
|
||||
- **挂载自检(一次)**:挂载后对比 `getBoundingClientRect()` 与 `window.innerWidth/Height`;若 html/body 本身被套壳 transform/偏移(罕见),自动降级为「absolute + 每帧 rAF 同步宿主 rect」模式并 console.warn 一次。
|
||||
- **MutationObserver 守卫(默认开,可关)**:body childList 变化检测锚点被宿主清空/挪位时自动重挂(SPA 导航/套壳重排兜底);观察器廉价(仅 childList、无 subtree)。
|
||||
|
||||
**A1.2 桌面信号自动探测**(替代手动 titleBarCompat):
|
||||
|
||||
- 信号源:URL `dsh-desktop-mode` / `dsh-desktop-platform`(Electron 官方注入)+ `window.__DSH_DESKTOP_FILE_PATH__` 存在性(preload 标记)+ UA 兜底。
|
||||
- 适配表:
|
||||
|
||||
| 探测结果 | 自动适配 |
|
||||
|---|---|
|
||||
| win32 + advanced | 右上角保留 32px 标题栏 overlay 区:折叠按钮簇/设置按钮下移,面板顶部内边距 +32px |
|
||||
| darwin | 左上角保留 ~78px 红绿灯区:左侧贴边控件(若存在)避让 |
|
||||
| compatibility / 无信号 | 原生 frame,不避让(现状行为) |
|
||||
| 移动 webview | 窄屏 overlay 抽屉 + `visualViewport` 键盘监听 + `env(safe-area-inset-*)` 内边距 |
|
||||
|
||||
- 保留现有手动开关(titleBarCompat)作为覆盖位,但默认值改为「自动」。
|
||||
|
||||
**A1.3 移动端**:窄屏(断点沿用 `breakpoints.ts`)面板从「推挤宿主」切「overlay 抽屉」:不写 `#root` margin、面板 transform 滑入滑出;`visualViewport` 变化(键盘弹起)时底部面板上移。
|
||||
|
||||
### 5.2 几何层(性能)
|
||||
|
||||
**A2.1 单一几何写入器**:现状有两条写路径——拖拽的 `applyDrag`(Sidebar.tsx:487-505)与打开态 effect(:563-585),都写同一组变量但互不感知。统一为 `writeGeometry({width,height})` 单函数,两条路径共用(拖拽仍走 `scheduleDrag` 的 rAF 单帧批写);折叠/会话恢复等非拖拽变更也收敛到它。**不改变对外契约**:`--dsh-sidebar-width/height` 变量与 `#root` margin 推挤机制不变。
|
||||
|
||||
**A2.2 拖拽**(现状已达标,微调即可):
|
||||
|
||||
- pointer capture + 最新 delta + 单帧 rAF 应用(**已具备**,保留);
|
||||
- 拖拽期 `body[data-dsh-sidebar-dragging]` 禁过渡(**已具备**,`layout.css:49`);
|
||||
- 新增:拖拽期面板根挂 `will-change: transform`;面板根常驻 `contain: layout style`(CSS 一处,隔离内部布局回流)。
|
||||
|
||||
**A2.3 chunk 缓存精确化**:HTTP 层已 304 不重下载;缺口只在「激活后内存缓存清空 → 重注入+重执行」。改为:缓存键 = chunk 名 + 脚本内容哈希;激活时若哈希未变则保留内存缓存(跳过重注入),仅 HMR 核心 bundle 变化(版本变化)才清。保留失败路径(失败清缓存重试)。
|
||||
|
||||
**A2.4 保活策略(保持现状并文档化)**:面板 DOM 跨会话不重建;tab 内容按 `visible` prop 懒挂载/卸载;树/终端等重组件不随会话切换重挂。
|
||||
|
||||
### 5.3 契约层(可维护性)
|
||||
|
||||
- **锚点弱化**:`layout.css:31` 的 `#root > [data-slot=root] > div > div:nth-child(2)` 替换为 `#root [data-dsh-frame] > [data-pane="conversation"]`(稳定 data 属性锚定,实证见 §3.4;`[data-slot="conversation"]` 为 display:contents 不可用);推挤逻辑收敛进几何写入器(一处实现)。
|
||||
- **推挤可关闭**:`SidebarPrefs` 新增(或复用既有)「宿主推挤」开关,默认开;桌面套壳/移动端自动切 overlay 时关闭推挤(修 #226 根因:不再依赖 `#root{width:100%}` 之外的宿主形状)。
|
||||
- **契约文档化**:挂载点(`data-dsh-better-sidebar` / `data-dsh-panel-host`)、z-index 栈、CSS 变量(`--dsh-sidebar-width/height` 保持对外)、桌面信号(URL/全局标记)、推挤锚点策略 —— 同步 AGENTS.md §8 与本文档。
|
||||
- **服务契约不变**:`ctx.betterSidebar` API 零变化;#135 的 service 读取问题(dsh-web-mobile 下 `ctx.betterSidebar` 直接读抛错)由「注入层与服务的解耦」顺带收敛(面板渲染不再假设服务存在即可读)。
|
||||
|
||||
### 5.4 性能清单(验收口径 + 实测基线)
|
||||
|
||||
基线(2026-08-19 真实页面探针,本机 3080,拖拽 12 步 ≈192ms):**40 帧、帧均 8.36ms / p95 10.2ms、1 帧超 16ms;62 次 `setProperty`(5 种几何属性:panel width / bottom height / bottom right / 两个 html 变量)**——即每帧 5 次写同一组元素,浏览器合并为单次样式重算。
|
||||
|
||||
| 指标 | 现状 | 目标(v0.13.1 实施后) |
|
||||
|---|---|---|
|
||||
| 拖拽期宿主 layout 次数 | 每帧 1 次批写(`applyDrag` rAF),帧均 8.36ms | 保持帧率 ≥ 基线;面板 `contain: layout style` 隔离内部重排;拖拽期 `will-change` 提升合成层 |
|
||||
| 几何写路径 | 拖拽(`applyDrag`)与打开态(effect)两条路径写同一组变量 | 统一 `writeGeometry` 单写入器(已实施) |
|
||||
| 懒加载 chunk 二次打开 | 不重下载(304)但重注入+重执行 | ETag 未变 → 内存缓存命中,0 脚本执行(已实施) |
|
||||
| 会话切换 | 面板 DOM 不重建(已满足) | 保持 + 重组件懒挂载 |
|
||||
| 面板显隐动画 | transform+width/margin 过渡 | transform 优先(overlay 模式),推挤模式过渡保留(`#root` width/margin 锁步过渡) |
|
||||
| 观察器 | ResizeObserver 已 rAF 节流 | 保持;挂载自检/MutationObserver 守卫均为一次性廉价检查 |
|
||||
|
||||
## 6. 路线 B 详细设计:官方槽位迁移(ui-sidebar 模式)
|
||||
|
||||
### 6.1 可行性结论(已验证)
|
||||
|
||||
- **`sidebar` 槽:可接管**。条件:profile 层 `cordis.patch.yml` 禁用 `ui-sidebar`(`disabled: true`),`ui-layout` 必须保留;接管者需自实现 `sidebar.workspaces`/`sidebar.settings` 子槽内容或留空(`slots.inject` 静默跳过,不炸树);折叠按钮自渲染 + `ctx.layout.toggleSidebar()`;宽度/折叠持久化**自做**(官方不持久化)。
|
||||
- **`details` 槽:不可行(当前)**。唯一占用者 ui-conversation 的 DetailsPanel 与其会话 UI 同包,禁用即空置会话列;需上游把 DetailsPanel 拆为可替换槽位或提供优先级机制。
|
||||
- **几何传导**:槽位渲染在 AppFrame grid 列内,官方拖拽把手/折叠/让步结算免费生效;session scope 槽的 sessionId 由框架注入为标准 prop。
|
||||
|
||||
### 6.2 上游 issue 建议(本仓库只出建议,不改源码)
|
||||
|
||||
1. 将 `details` 槽的占用者改为**可替换**(如 single 槽支持「替换注册」或 DetailsPanel 拆独立包),使外部插件能接管右侧面板。
|
||||
2. 新增(或声明)`bottom-panel` 槽,供底部面板类插件使用。
|
||||
3. ui-sidebar 宽度/折叠持久化下沉到 layout store(官方侧目前不持久化)。
|
||||
|
||||
**issue 草案(PR3 提交时使用,已备好)**:
|
||||
|
||||
> **Title**: External plugins cannot take over the `details` panel slot
|
||||
> **Repo**: deepseek-ai/dsh(或官方插件仓库)
|
||||
> **Body**:
|
||||
> - Context: `ui-layout` declares `root` → `{sidebar, conversation, details}` as single-exclusive child slots. `ui-conversation`'s DetailsPanel is the sole occupant of `details`; an external panel plugin (e.g. dsh-better-sidebar) cannot register into it without disabling `ui-conversation` (which empties the conversation column). Meanwhile `ui-sidebar` (the official left sidebar) is itself a plugin and CAN be replaced from the profile layer (`cordis.patch.yml` `disabled: true`), which proves the pattern.
|
||||
> - Ask: make the `details` occupant replaceable — either (a) allow single-slot "replacement registration" with explicit opt-out by the default occupant, (b) extract DetailsPanel into a standalone optional plugin so profiles can disable it without losing the conversation column, or (c) declare a new `bottom-panel` slot for bottom panel plugins.
|
||||
> - Reference implementation to mirror: `ui-sidebar/src/client/index.ts:39` (register `name:'sidebar'`), `ui-layout/src/client/AppFrame.tsx:164-196` (grid geometry + drag handles come free), `runtime/src/client/slots.ts:130-192` (`slots.inject` consumers skip silently when a slot is undeclared).
|
||||
|
||||
### 6.3 迁移路径(上游落地后)
|
||||
|
||||
1. better-sidebar 增加「槽位渲染后端」:注册 `details`/`bottom-panel` 槽,`Sidebar` 树渲染进 grid(同一套 store/组件,仅挂载点不同)。
|
||||
2. body 浮层收敛为纯浮层:菜单、弹窗、右键、拖出浮窗(与官方 ui-primitives portal 同姿势)。
|
||||
3. 保留路线 A 的固定含块层作为浮层宿主;推挤/几何写入器退役(grid 接管)。
|
||||
|
||||
### 6.4 收益与代价
|
||||
|
||||
| 收益 | 代价 |
|
||||
|---|---|
|
||||
| 零 body 注入、零宿主 DOM 依赖 | 契约绑定上游 SlotMap,DSH 升级需跟随 |
|
||||
| 官方几何/拖拽/折叠/让步结算免费 | 官方不持久化宽度,需自做持久化 |
|
||||
| 任何套壳天然一致(grid 与套壳无关) | 上游排期不可控(issue 周期) |
|
||||
| 拖拽性能与官方同源 | 迁移期间双后端并存(维护成本) |
|
||||
|
||||
## 7. 兼容矩阵(注入策略 × 场景)
|
||||
|
||||
| 场景 | body 固定含块(路线 A) | 官方槽位(路线 B) | shadow DOM | iframe |
|
||||
|---|---|---|---|---|
|
||||
| 纯浏览器 | ✅ 推荐 | ✅ | ✅(令牌穿透) | ⚠️ 令牌/交互断裂 |
|
||||
| Electron 套壳(DSH Desktop) | ✅ + 信号自动避让 | ✅ | ✅ | ⚠️ |
|
||||
| Tauri 套壳(原生标题栏) | ✅ | ✅ | ✅ | ⚠️ |
|
||||
| 移动 webview | ✅ + overlay 抽屉/visualViewport | 需上游槽位 | ✅ | ⚠️ |
|
||||
| 页面内嵌 iframe | ⚠️ 需宿主放行 | ❌ | ✅ | ✅(天然隔离) |
|
||||
| 祖先 transform 劫持 | ✅ 自检降级 | ✅ 天然免疫 | ✅ | ✅ |
|
||||
| 性能(拖拽/显隐) | rAF 批写 + contain | 官方 grid | 中 | 低 |
|
||||
|
||||
## 8. 实施拆分与验证
|
||||
|
||||
### 8.1 拆分(文件级改动清单)
|
||||
|
||||
**实施方式(2026-08-19 定稿)**:单分支 `feat/injection-unified-host`、按步骤顺序提交(每步 CI 绿);原 PR1/PR2 拆分为内部步骤,合并为**一个 PR** 交付(改动彼此耦合,且路线 B 关闭后无外部依赖)。各步骤:
|
||||
|
||||
1. **挂载层**(`src/client/index.tsx`、`src/client/Sidebar.tsx`、`src/client/sidebar.module.css`):`Sidebar` 渲染根包 `data-dsh-panel-host`(`position:fixed; inset:0; pointer-events:none; z-index:25`,初版 40,2026-08-22 起 25);`.panel/.toggleCluster/.bottomPanel` 由 fixed 改 absolute(层 inset:0 即视口坐标)+ `pointer-events:auto`;挂载后一次性几何自检(偏差 >8px → `data-dsh-panel-host-degraded` + rAF 视口同步 + warn 一次);`MutationObserver`(body childList)守卫锚点被清空时重挂。✅ 已实施(commit 9a4c160)
|
||||
2. **几何层**(`Sidebar.tsx`、`sidebar.module.css`):`writeGeometry` 统一拖拽/打开态两条推挤写路径;面板 `contain: layout style`;拖拽期(`body[data-dsh-sidebar-dragging]`)`will-change: transform`。✅ 已实施(commit 553e650)
|
||||
3. **chunk 缓存**(`chunk-loader.ts`、`index.tsx`):`revalidateChunksOnReactivate()`——激活时 HEAD 每个已加载 chunk 对比 ETag,未变保留已解析 exports(跳过重注入/重执行),变更/不可达 fail-open 重取;测试夹具与孤儿缓存不跨激活存活。✅ 已实施(commit 0264625)
|
||||
4. **锚点与防溢出**(`layout.css`):底部推挤锚点 `nth-child(2)` → `#root [data-dsh-frame] > [data-pane="conversation"]`(§3.4 实证);`#root` 加 `width: calc(100% - var(...))` 与 margin 锁步过渡(实测 1140+300=1440 无溢出;#226 桌面溢出根因吸收)。✅ 已实施(commit 9c9515d)
|
||||
5. **桌面/移动适配**(新增 `desktop-env.ts`、`Sidebar.tsx`、CSS):解析 `dsh-desktop-mode/platform` + `__DSH_DESKTOP_FILE_PATH__`;win32 advanced 自动 `titleBarCompat`(32px overlay,手动 pref 覆盖);`visualViewport` 键盘 inset(底部面板/窄屏抽屉上移);`env(safe-area-inset-*)`。✅ 已实施(commit a1906c5,单测 7 例)
|
||||
6. **测试与文档**:`tests/desktop-env.spec.ts`(7 例)、chunk-loader 重验证 4 例、`mount.e2e.ts` 增 `data-dsh-panel-host` 断言 + 桌面参数场景、AGENTS.md §8 契约同步、本文档更新。🔄 进行中
|
||||
7. **交付**:CI 全绿(含 plugin-mount)→ `gh pr create`。⏳ 待执行
|
||||
|
||||
### 8.2 验证
|
||||
|
||||
- **单元**:几何写入器 rAF 批处理(fake timers 断言单帧单写);桌面信号解析(URL param × platform × 全局标记);挂载自检降级路径;chunk 缓存命中/失效。
|
||||
- **e2e**:`tests/e2e/mount.e2e.ts` 深扫保持不变并新增断言:`data-dsh-panel-host` 存在、锚点属性不变;新增 desktop-mode URL 参数场景(模拟 win32 advanced → 顶栏避让类生效)。
|
||||
- **skin 契约**:`tests/theme.spec.ts` 令牌读取不变(§8 契约同步守护)。
|
||||
- **CI**:全绿门禁(build/typecheck/test/consumer-types/plugin-mount);#180 的 plugin-mount flake 与本次改动无关,重跑确认。
|
||||
- **度量**:拖拽期 PerformanceObserver 记录 layout 次数;懒加载二次打开网络计数为 0。
|
||||
|
||||
## 9. 风险与回滚
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| 固定含块改变既有 fixed 行为(罕见宿主) | 挂载自检 + 降级模式;锚点属性/CSS 变量不变,可一键回退旧实现 |
|
||||
| 几何写入器集中化引入回归 | PR1 内保留旧写入路径开关(feature-flag),e2e 深扫覆盖 |
|
||||
| 桌面自动探测误判(套壳改 URL 格式) | 信号解析白名单 + 手动覆盖位保留 |
|
||||
| 路线 B 上游不落地 | 路线 A 完整自洽,不阻塞 |
|
||||
| 禁用 ui-sidebar 属 profile 用户决策 | 插件仅文档化,不自动执行 |
|
||||
|
||||
## 10. 参考来源
|
||||
|
||||
- DSH 源码:`~/.dsh/source/current`(只读):`ui-layout/src/client/{index.ts,AppFrame.tsx,columns.ts,stores.ts,AppFrame.module.css}`、`ui-slots/src/index.ts`、`ui-sidebar/src/client/index.ts`、`ui-conversation/src/client/apply.ts`、`ui-primitives/Menu.tsx`、`runtime/src/client/slots.ts`
|
||||
- DSH Desktop 运行时(本机 .app,只读):`lib/src-B5373ech.js`、`lib/electron-runtime-C_i38SKA.js`、`lib/preload.cjs`
|
||||
- 社区套壳:kyorakuyk/dsh-desktop(https://github.com/kyorakuyk/dsh-desktop)、Lotus-c/DSH-Desktop(https://github.com/Lotus-c/DSH-Desktop)
|
||||
- 本仓库:`src/client/index.tsx`、`src/client/Sidebar.tsx`、`src/client/layout.css`、`docs/plans/2026-08-15-skin-compat-design.md`
|
||||
- 相关 PR:#226、#135、#82、#108、#111、#138、#153(症状级补丁,本方案吸收其目标);#208/#90(issue)
|
||||
|
||||
## 11. 决策记录与实施偏差记录
|
||||
|
||||
- 2026-08-19(第一轮):方案定稿——路线 A(统一面板宿主)为主线、路线 B(官方槽位)并行提 issue;先出设计文档,实现拆 PR1/PR2 待批准。
|
||||
- 2026-08-19(第二轮,实施就绪打磨):对照源码核实现状——拖拽 rAF 批处理/窄屏不推挤/观察器节流/header 弱锚点**已具备**(§3.1 已标注「实施勿重做」);chunk 缺口精确为「激活后内存缓存清空重注入」(HTTP 已 304);§8.1 落到文件级改动清单;§6.2 备好上游 issue 草案。
|
||||
- 2026-08-19(第三轮,实证核对):真实 dsh web 页面 Playwright 探针(§3.4)——frame 子项已含 overlay/handle(nth-child 脆弱性实锤);`data-pane="conversation"` 为新弱锚点(`[data-slot=conversation]` display:contents 作废);推挤在 web 无溢出(calc 改动无害);host 本体 static(fixed 在内层面板,含块层为新元素)。
|
||||
- 2026-08-19(第四轮,用户决策):**路线 B 关闭**——无法向官方提 issue,纯插件侧自己优化注入方式;实现合并为单 PR(分支 `feat/injection-unified-host`),按 §8.1 步骤顺序落地。
|
||||
- **实施偏差记录**:
|
||||
- 挂载自检降级实现为「`data-dsh-panel-host-degraded` 属性 + rAF 视口同步」(非独立 absolute 模式,见 §5.1 注释)。
|
||||
- chunk 缓存实现为 **ETag HEAD 重验证**而非内容哈希(bundle 路由已发 ETag,HEAD 更廉价);`resetChunks()` 保留(测试隔离),激活路径改用 `revalidateChunksOnReactivate()`。
|
||||
- `#root` 防溢出采用 `width: calc(100% - var)` + width/margin **锁步过渡**(避免动画断裂),并实测 1140+300=1440。
|
||||
- 桌面避让复用既有 `titleBarCompat/titleBarStripPx` 通道(未新增 `--dsh-desktop-top-inset` 变量);darwin 红绿灯区仅预留(当前无左侧贴边控件)。
|
||||
- 测试环境为 Node + 手写 shim(非 jsdom):`browser-globals.ts` 补 `location`;desktop-env 剥除 `location.search` 前导 `?`(真实 bug,顺带修复)。
|
||||
- 本地 Node 24 跑 `test:mount` 需 `node --expose-internals` 启动 dsh CLI(HMR 服务要求,CI Node 22 无此问题)——本地用 DSH_CMD 包装,不改仓库脚本。
|
||||
- **CR #232 修复(2026-08-19)**:① 降级同步改为按**未修正几何**判定(跟踪自身补偿量,祖先 transform 消失才退出循环);② chunk 重验证成为 `loadChunk` 屏障(重验证挂起期间不提供缓存,杜绝 HMR 竞态陈旧 exports);③ 键盘 inset 公式补 `visualViewport.offsetTop` 并监听 `scroll`。
|
||||
- **z-index 40→25(2026-08-22)**:设计初稿的"30–99 无任何 DSH 元素"(源自 #52 修复记录)低估了官方 ui-cordis 的动态插件面板(fixed z-index 30,挂 `sidebar.footer.action` 槽)——better-sidebar 底部面板曾盖住其清单/审批面。下调宿主层至 25(AppFrame overlayLayer 20 之上、ui-cordis 30 与 DSH 浮层栈 100+ 之下),折叠按钮簇 45 保留为 host 内部层叠;`AGENTS.md` §8 与 `2026-08-15-skin-compat-design.md` 已同步。
|
||||
- **桌面布局判定修正(2026-08-26)**:`#root` 的 width/margin 推挤会缩小 AppFrame 的 border box,触发 Harness `<1024px` 窄屏判定,使桌面左侧栏自动折叠、Session Log 退化为 fixed icon,并挤乱后台任务操作区。右面板宽度改为 AppFrame 内部 `padding-right`(新版 `[data-dsh-frame]` 与 rc.8 root-slot direct child 双锚点),Frame 与 `#root` 保持视口宽度;details 拖拽手柄按同一变量平移。`desktop-layout.e2e.ts` 守护 frame 宽度、桌面 Session Log 文本按钮与面板不重叠,`drag-layout.e2e.ts` 继续守护拖拽 1:1。
|
||||
@@ -1,79 +0,0 @@
|
||||
# 桌面端兼容统一方案:四选项下拉(自动检测 / DSH官方Web / 壳预设 / 自定义 CSS)
|
||||
|
||||
> 2026-08-20 · 对应分支 `feat/desktop-scheme-settings` · 关联 issue #257、#208、#103/#111;外部 PR #82/#89/#111/#153/#226/#243 处置见文末。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
better-sidebar 的桌面端适配历史上是"每壳一条补丁":右上角开关簇与原生标题栏按钮重叠(#87/#101)、点击被拖拽区吞掉(#103/#111)、推挤锚点随 DSH DOM 版本漂移(#208/#226/#262)、WCO 高度硬编码 32px 与实际不符(#257,Electron 版本间 32/36px 不一)。这些补丁全部针对**具体某个壳的 DOM/类名/数值**,换一个壳(Tauri、Pake、新壳)就失效或出错。
|
||||
|
||||
**全仓证据盘点**(121 issues + 159 PRs 全量 + 三个候选壳克隆取证):
|
||||
|
||||
| 壳 | ⭐ | issue/PR 提及 | 形态 | 需要什么 |
|
||||
|---|---|---|---|---|
|
||||
| anywhere-labs/deepseek-harness-desktop | 16.3K | ✅ 大量 | Electron;advanced: win32 WCO 32px / darwin caption 20px;compat 原生框 | win32 已被标准 WCO 覆盖;darwin 20px 让位 |
|
||||
| dataelement/dsh-desktop | 1.3K | ❌ 零提及 | Electron;win32 WCO 36px | 已被标准 WCO 覆盖 |
|
||||
| Deepseek-Harness-EAC | 1.0K | ❌ 零提及 | Electron Win-only;自绘 36px 玻璃栏(私有标记 `data-dsh-title-bar-height`) | 无 WCO,需 36px 让位(文档示例) |
|
||||
| hairyf Tauri 版 | 715 | ✅ pr#229 | Tauri2;44px 导航栏在 iframe **之上** | **无冲突**,无需适配 |
|
||||
|
||||
平台分布结论:兼容问题**不是 Windows 独有**(macOS 6 条、Web 3 条),但当前 main 上未闭环的 chrome 兼容基本就是 **Windows 顶栏**(WCO 几何 + 通用 no-drag 兜底);macOS 的坏 bug(#25/#208)已修复。
|
||||
|
||||
## 决策
|
||||
|
||||
1. **核心只留 Web 标准机制**:WCO(`navigator.windowControlsOverlay` + `geometrychange`)是唯一"不是适配、是标准"的信号;`-webkit-app-region: no-drag` 是标准属性(普通浏览器/无拖拽区壳惰性无害)。核心代码 grep 不到任何壳类名。
|
||||
2. **壳专属适配进用户空间**:「位置兼容模式」升级为**主行下拉**四选项:**自动检测(默认,保守,web 零修改)/ DSH官方Web(显式零适配)/ 壳兼容方案(内置预设,opt-in)/ 自定义方案(自由 CSS + 下移 px,保留齿轮弹窗)**。
|
||||
3. **预设准入规则**:issue/PR 中被提及(链接或明确名称)且 GitHub ⭐>100 的壳;v1 仅 anywhere-labs(唯一同时满足者)。预设只做**标题栏 strip 让位**,不做布局推挤/壳类名(尊重"不特意兼容 DSH Desktop 高级模式")。Tauri 无冲突只写文档;EAC 未提及但提供 custom CSS 示例。
|
||||
4. 不向 anywhere-labs 提跨仓库 PR;不修改 DSH 源码。
|
||||
|
||||
## 实施
|
||||
|
||||
### prefs 模型(`src/prefs-shared.ts` / `src/config.ts` / `src/client/prefs.ts`)
|
||||
|
||||
- 新增 `titleBarScheme: 'auto' | 'web' | 'preset' | 'custom'`(默认 `'auto'`)、`titleBarPresetId: string`、`customCss: string`。`web` = 显式「DSH官方Web」,取值链最优先强制 0(连 WCO 也不适用)。
|
||||
- schema 中三个新字段**无 default**(schemastery 验证:缺失字段不出现在解析结果里)→ `parsePrefs` 在字段缺失时由旧 `titleBarCompat` 派生:`true` → `custom`(保留 `titleBarStripPx`);`false`/缺失 → `auto`。写路径镜像写旧字段(`titleBarCompat = scheme !== 'auto'`)供降级。
|
||||
|
||||
### 标准件(`src/client/wco.ts` + `src/client/titlebar-strip.ts`)
|
||||
|
||||
- `wco.ts`:模块级反应式 store(subscribe/getSnapshot,`useSyncExternalStore` 消费),特性检测 `navigator.windowControlsOverlay`,订阅 `geometrychange` 重读矩形(最大化/还原实时更新);最后订阅者退订时解绑原生监听;`setWcoSourceForTests` 注入测试源。API 抛错视为 present+0(绝不崩布局)。
|
||||
- `titlebar-strip.ts`(唯一决策点,纯函数):**⓪ `web` 方案强制 0 → ① WCO 真实高度(权威,0 也权威;`visible=false` 幽灵 API 视为缺失)→ ② URL `dsh-desktop-titlebar-inset`(0–120 clamp)→ ③ 预设 `stripFor`(仅 preset 方案)→ ④ 手动 `titleBarStripPx`(仅 custom 方案)→ ⑤ 0**。结果驱动 `body[data-dsh-title-bar-compat]` + `--dsh-title-bar-strip`(CSS 契约不变)。
|
||||
- `desktop-env.ts`:只做环境报告(desktop/mode/platform/titlebarInset),SSR 安全;移除旧 `win32OverlayTop` 硬编码。
|
||||
|
||||
### 预设与用户 CSS(`src/client/shell-presets.ts` + `Sidebar.tsx` effect)
|
||||
|
||||
- `ShellPreset { id; title; desc; stripFor?; css?; detect? }`;v1 一条 `dsh-desktop`:darwin advanced 20px、win32 advanced 32px(无 WCO 兜底)、其余 undefined;`detect` 仅用于设置页「已检测」徽标。
|
||||
- 注入:预设 css → `<style data-dsh-preset-css="<id>">`;自定义 css → `<style data-dsh-custom-css="custom">`;追加到 `document.head` 末尾(后写胜出),fiber 卸载/变更即移除。
|
||||
- 稳定寻址面:`[data-dsh-toggle-cluster]`、`[data-dsh-panel]`、`[data-dsh-bottom-panel]` data 属性。
|
||||
|
||||
### no-drag 兜底(`sidebar.module.css`)
|
||||
|
||||
`.toggleCluster` / `.toggleButton` / `.tabBar` 统一 `-webkit-app-region: no-drag`(吸收 #111/#153,补 #103 插件侧缺口)。
|
||||
|
||||
### Web 推挤锚点加固(`layout.css`)
|
||||
|
||||
复合选择器(同元素双保险):`#root [data-dsh-frame] > [data-pane="conversation"]` 与 `#root :has(> [data-slot="conversation"])`;拖拽过渡/prefers-reduced-motion 同步;`drag-layout.e2e.ts` 新增两选择器同元素断言。
|
||||
|
||||
### 设置 UI(`SideCardSection.tsx` + locales)
|
||||
|
||||
「位置兼容模式」行改为**主行下拉**:自动检测(默认)/ DSH官方Web / 各壳预设(命中环境带「已检测」后缀,仅提示)/ 自定义方案(选中后行内出现齿轮,弹窗 = 下移距离 + CSS textarea(Cmd/Ctrl+Enter 提交))。
|
||||
|
||||
### 测试
|
||||
|
||||
- 单测:`wco.spec.ts`(7 例:检测/读取/取整/geometrychange 通知/抛错降级/换源)、`titlebar-strip.spec.ts`(取值链 5 例)、`shell-presets.spec.ts`(注册表完整性 + dsh-desktop strip 值)、`desktop-env.spec.ts`(重写:inset 参数 clamp + 移除 win32 硬编码)、`prefs.spec.ts`(新字段 + 旧字段迁移)、`plugin-shape.spec.ts`(schema 无默认值语义)、`smoke.spec.ts`(解析结果不含新字段)。
|
||||
- e2e(`mount.e2e.ts` 重写桌面场景):保守 auto(stamp 不触发修改 + data 属性 + no-drag 规则存在)、WCO mock 驱动 strip 且 geometrychange 反应、预设注入(settings.update 写入 scheme=preset → 32px + style 标签)、自定义 CSS 注入(marker 生效);`drag-layout.e2e.ts` 复合锚点。
|
||||
|
||||
## 验证
|
||||
|
||||
- `pnpm typecheck` / `pnpm test`(691 passed)/ `pnpm test:mount` 全绿。
|
||||
- 普通浏览器零行为变化(auto 无 WCO → 无 strip)。
|
||||
- Electron WCO win32(mock 实测):strip=真实高度(32/36),最大化/还原跟随。
|
||||
- anywhere-labs darwin advanced:启用预设后 20px 让位。
|
||||
- 核心代码 grep 无壳类名(`.dshDesktopFrame` 等仅存在于预设数据字符串与测试断言之外——v1 预设无 CSS,纯 strip 数据)。
|
||||
|
||||
## 实施偏差记录
|
||||
|
||||
- 无。计划与实施一致;仅补充:schema 新字段必须无 default 才能让旧文档迁移(否则 settings 服务会把 default 填进解析结果、吞掉迁移),`plugin-shape.spec.ts` 断言了这一点。
|
||||
|
||||
## 存量 PR/issue 处置(合并后执行,均含哲学声明)
|
||||
|
||||
- 实现后关闭:#257(WCO 标准件)。
|
||||
- 关闭:#111/#153(no-drag 吸收)、#226(复合锚点吸收,main 已修)、#243(布局推挤不采纳;标题栏部分进 dsh-desktop 预设)、#108(#232 已吸收 + WCO)、#82/#89/#138(不采纳,指向自定义 CSS/预设路径)。
|
||||
- 回复:#208(已修+回归)、#221(自定义 CSS 可微调)、#233(git.ts windowsHide 独立问题)、#167/#240(非 chrome 范畴/文档已有)。
|
||||
@@ -1,100 +0,0 @@
|
||||
# 侧边对话(Side Chat)Tab 设计
|
||||
|
||||
**日期**:2026-08-20
|
||||
**状态**:已批准,实施中
|
||||
**作者**:opencode + 用户
|
||||
**当前版本**:v0.14.1(分支 `feat/sidechat-tab`)
|
||||
**目标版本**:v0.14.1(不 bump)
|
||||
|
||||
## 1. 目标
|
||||
|
||||
1. 对标 Codex 的 side 对话,为 better-sidebar 新增内置 Tab **sidechat**:在当前主会话下开一个**继承主会话完整上下文**的独立子会话线程,线程在 Tab 内可续聊、可查看完整时间线(user / assistant / reasoning / tool)。
|
||||
2. **继承必须是结构化的**:线程种子 = 父会话点击时刻的全部事件——已完成回合 + 未回答的 user 消息 + **进行中回合的 assistant 流式输出与工具调用**(原样复制,合成 `step/end`+`turn/end{reason:'interrupted'}` 诚实闭合,子会话看到的是「被切断的进行中回合」而非完整定稿)。
|
||||
3. **前缀缓存复用**:子会话与父会话同组合(同 preset / 同 provider/model、无 persona/toolFilter、非 continuable 无 report 增量)→ 子会话首个请求的 token 前缀与父会话当前请求一致(到未回答 user 消息为止),命中 provider 前缀缓存。
|
||||
4. **即用即销语义 + 保存为新会话**:线程以 `origin:'subagent'` 创建(主会话列表不可见、子代理目录零噪音);用户可一键「保存为新会话」——`session.fork` 提升为顶层会话(自动命名、挂工作区、打开)。
|
||||
5. 零 dsh-subagent 依赖;dsh-subagent 缺失时功能不受影响。
|
||||
|
||||
## 2. 非目标(Out of Scope)
|
||||
|
||||
- 不做 `/btw` 一次性侧问(只做持续可续聊线程)。
|
||||
- 不做合并回主会话 / 重命名线程。
|
||||
- **不改 DSH 源码**:不加 promote RPC、不改 fork 边界、不改 continuation 机制。
|
||||
- 工具执行中途(悬挂 tool/call)的事件级继承不可行(provider 拒绝悬挂 assistant 调用)→ 该窗口回退为「截断 + 结构化文本转储」(见 §4)。
|
||||
|
||||
## 3. 为什么自建机制(重实现)
|
||||
|
||||
用户明确拍板:不走 `ctx.subagents.startContinuable`。原因(已核实源码):
|
||||
|
||||
1. **种子粒度受限**:fork provider 的 `completedTurnPrefix` 固定切在最后一个 `turn/end`(`subagent-fork-in-process/src/index.ts`),`session.fork` RPC 显式拒绝 open-turn 锚点(`core/session` `OPEN_TURN`)——进行中回合永远进不了种子。
|
||||
2. **continuable 组合破坏前缀复用**:continuable 子会话的请求头部在继承历史之前插入 `report` 工具 + `tool:report` 提示段(`.agents/notes/implemented/architecture/2026-08-10-fork-children-stay-one-shot.md`,issue #2124)——前缀从请求头即分叉,重发整个转录却零 cache 收益。DSH 官方因此把 fork 绑定为 one-shot。
|
||||
3. **无法注入自定义种子**:`ContinuableStartSpec` 无 seed 字段,创建路径无注入点。
|
||||
|
||||
自建机制只复用 DSH 公开 host 能力:`ctx.agents.create/resume`(AgentRegistry 公开方法,api-proxy fork 与 subagent driver 同用)、`agent.followup/cancel`(公开方法)、`ctx.get('agentPresets')`、`ctx.get('sessionTitle')`、`ctx.get('sessionPersistence')`。客户端复用 `ctx.sessions.list / fork / binding / open` 与通用 `session.history` RPC。
|
||||
|
||||
## 4. 种子算法(`src/sidechat-core.ts`)
|
||||
|
||||
1. 复制父会话**全部事件**(点击时刻快照;live 日志 seq === 数组下标,保持连续)。
|
||||
2. 日志结束于 turn 外(最后 turn 边界是 turn/end 或无 turn):种子 = 全部复制事件(可能以未回答 user 消息结尾,合法)。
|
||||
3. 结束于 open turn 内:
|
||||
- **当前 step 存在无配对 tool/call(悬挂)** → 回退:切到该 turn 的 `turn/start` 之前(保留已完成回合 + 未回答 user 消息);进行中内容(含执行中的工具调用,标注 executing)走 `buildOpenTurnSnapshot` 结构化文本转储并入 boundary 消息。原因:validator 不拒悬挂调用(step/end 清空 pendingCalls),但 **provider 拒绝悬挂 assistant 调用**(`core/session/src/repair.ts:108`)。
|
||||
- 否则:追加合成 `step/end{turn,step}`(若 step 未闭合)+ `turn/end{turn, reason:{kind:'interrupted'}}`(编号取自已复制事件的 turn/start、step/start)→ 种子平衡合法(种子逐事件过同一不变式检查器,`core/session/src/index.ts:517-541`;`turn/end` schema 要求 reason,`interrupted` 是合法持久化变体,语义即「回合未完成被切断」——诚实标记,客户端对冻结局部渲染「已停止」)。
|
||||
|
||||
子会话首个请求(常见路径)= [已完成回合][未回答 user][冻结的进行中回合:chunk 文本 + 已完成工具调用/结果][boundary+question]。前缀复用不受影响:共享前缀到未回答 user 消息为止,冻结回合位于共享前缀之后。
|
||||
|
||||
## 5. 线程生命周期(host 路由,`/sidebar/api/sidechat.*`,既有信任 fence)
|
||||
|
||||
- **start** `{sessionId, question}`:父 Agent 必须 live;`buildSidechatInheritance` 出种子 + 转储;`ctx.agents.create({meta:{cwd,parentSession,seedLength,origin:'subagent',delegationDepth:父+1,agentPreset}, seed, agentOptions:父 provider/model, setup:preset 组合})`;`agent.followup(createUserMessage({content:[boundary+转储?+question], source:{kind:'user'}}))`;`sessionTitle.rename(session,'Side: '+截断(question,48))` 钉死线程标签;返回 `{childId}`。
|
||||
- **prompt** `{childId, text}`:live → `followup`;absent(重启/关闭后)→ `ctx.agents.resume`(`resolvePresetId` 从 persisted header/events 解析 preset,`agentPresets.resolve/mount` 组合)→ `followup`。
|
||||
- **cancel** `{childId}`:`agent.cancel({kind:'user'},{keepInbox:true})`。
|
||||
- **dispose** `{childId}`:释放创建/恢复时保存的 AgentHandle disposer(会话与历史保留)。
|
||||
|
||||
为什么全走自有路由:`origin:'subagent'` 的会话被通用 RPC 的 agent-lookup 所有权 fence 全面封锁(`api/remotes/agent-lookup.ts` `hasApiRemoteSubagentOwner`),`subagents.prompt` 对我们的线程也无法寻址——自有路由直接调 registry/agent 绕开 fence,且只依赖公开方法。~~线程没有 subagent/descriptor(无目录注册、零噪音:`list-children.ts` 投影为 undefined 即丢弃)~~ **修正(v3 起)**:该判断只对 live 线程成立——cold 线程无 descriptor 时 `resolveColdIdentity` 确定性产出 `corrupt` 诊断行(`list-children.ts:405`),用户实测在 Subagent 页看到「目录损坏」。修复 = 线程种子尾部追加合法的 `subagent/descriptor`(`snapshotSubagentDescriptor`,version 2 / continuable / provider `'sidechat'` / label 即 `Side: ` 标题),线程成为目录里的健康行;SubagentView 与 `subagent-detect.ts` 按 `Side: ` 标签/标题前缀过滤,拓扑 UI 与自动打开触发保持零噪音(模型侧 `list_agents` 也会列出带标签的健康行,而非 corrupt 垃圾)。
|
||||
|
||||
## 6. 客户端 Tab(`src/client/SideChatView.tsx`)
|
||||
|
||||
- 内置 descriptor:`id:'sidechat'`、`order:35`、`single:true`、图标 `IconNewChatOutline16`。
|
||||
- 两栏布局:左线程列表(`sideThreadRows`:`origin==='subagent' && parentId===当前 && displayTitle 前缀 'Side: '`;订阅 `ctx.sessions.list`);右详情(时间线 + 输入框 + 保存/停止/关闭按钮)。
|
||||
- 时间线:轮询 `connection.api.sessions.history`(通用 RPC,未 fence;`session/end-seed` 截断 + boundary 行丢弃 + chunk 流式累加 + 工具配对——`src/client/sidechat-transcript.ts` 纯映射);可见且运行中 ~2s,seq 去重缓存;`visible=false` 停轮询。
|
||||
- 保存为新会话:`ctx.sessions.fork({sessionId: threadId, increaseTitle:true})` → `binding(newId).session.rename(标题去前缀)` → `ctx.sessions.open(newId)`。无已完成回合 → 禁用 + 提示;末条追问未完成 → 提示不包含(fork 边界=最后 turn/end)。
|
||||
- 选中线程持久化:`tab.meta.threadId`(既有 per-session localStorage 通道)。
|
||||
|
||||
## 7. 边界情况与失败模式
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 父会话无 live agent | 409 `sidechat-error` 内联提示 |
|
||||
| 父会话进行中(流式/思考/工具已返回) | 完整事件入种子 + interrupted 合成闭合 |
|
||||
| 父会话工具执行中途点击新建 | 悬挂 tool/call → 截断 + 结构化转储(工具标注 executing) |
|
||||
| seam 缺失(agentPresets/sessionTitle/sessionPersistence) | 降级:默认组合 / 自动标题 / 无 preset 组合的冷恢复 |
|
||||
| DSH 重启后续聊 / 线程关闭后追问 | `ctx.agents.resume`(persisted preset 组合) |
|
||||
| 线程无已完成回合 | 保存禁用 + 提示 |
|
||||
| 线程末条追问未完成 | 保存前提示不包含 |
|
||||
| 与 dsh-sidechain 并存 | 各自独立;双方线程都有 descriptor(目录健康行),SubagentView 按 `Side: ` 前缀过滤均不可见;双方共享 `Side: ` 标签约定,线程互见 |
|
||||
|
||||
## 8. 测试与自验
|
||||
|
||||
- 单测:`sidechat-core.spec.ts`(种子全分支:turn 外 / 空 / 仅 pending user / open turn 无工具 / 成对工具 / 悬挂回退 / 多 step 闭合 / seq 连续性;转储保真;线程过滤;保存资格;preset 解析;boundaryDelivered)、`sidechat-seed-validation.spec.ts`(真实 `Session.create` 校验器接受种子产物)、`sidechat-transcript.spec.ts`(种子截断、boundary 丢弃、chunk 流式、工具配对、孤儿失败工具)、`sidechat-routes.spec.ts`(start 带问/空问立即创建、首条 prompt 包裹边界+快照并改名、prompt live+cold、cancel、dispose、info live/cold/未知)、`builtins.spec.ts` 7 tab + sidechat 每线程一 Tab 铸造/去重。
|
||||
- e2e 挂载冒烟:+ 菜单深扫含 Side Chat;**host 路由级自验**(真实 keyless dsh web 中 sidechat.start/prompt/cancel/dispose/info 全链路 + 空问题立即创建流程,种子会话为父)。
|
||||
- 真实环境手测(实施后勾选):新建线程 → 追问往返 → 流式渲染 → 工具行 → 主会话流式中新建(冻结继承)→ 工具执行中途新建(回退转储)→ 重启后续聊 → 保存为新会话 → 刷新持久。
|
||||
|
||||
## 9. 复用门记录
|
||||
|
||||
搜索面:DSH checkout `docs/` + `packages/`(关键词 side conversation / sidetrack / thread / promote)+ dsh-external hub catalog(`search-org.mjs`,323 repos)。命中:`dsh-external/dsh-sidechain`(Codex 风格侧会话,/side & /btw,临时 fork + 侧栏面板),但**无 better-sidebar Tab、无保存为新会话**;用户拍板独立实现(「我们做独立实现」),sidechain 保持不动。本文档与 PR 记录该结论。
|
||||
|
||||
## 10. 实施偏差记录
|
||||
|
||||
- 路由键命名:`buildSidechatApi` 返回对象的键必须是**完整 wire 方法名**(`'sidechat.start'` 等,/sidebar/api 分发器按 `api[method]` 查找),而非短名 `start/prompt/…`。首次实现用了短名,单测直接调对象方法未暴露,真实挂载冒烟以 404 抓出(该冒烟因此新增了路由级自验,见 §8)。
|
||||
- 种子函数入参类型:sidechat-core 使用宽松结构类型 `SidechatLogEvent {type, seq, time, data: unknown}`(同时接受 host 真实 `SessionEvent` 与 client 的 `SidebarSessionEvent` 镜像),避免两半的类型图互相污染。
|
||||
- keyless 挂载冒烟无法验证 boundary 消息落盘:`agent.followup` 的消息先入 inbox,被 turn claim 后才写入 session 日志——keyless(无模型路由)下 turn 不会 claim。挂载冒烟的深度断言因此改为 **session.list 成员校验**(证明子会话真实创建,provider 无关);boundary 落盘与流式/回答在真实 provider 环境手测(§8 清单)。
|
||||
- **UI 两轮返工(用户实测驱动)**:
|
||||
1. 种子事件信封字段丢失(用户实测报 `invalid seed event … requires a surfaceOp marker`):`copyEvents` 重建事件时只保留 `{type,seq,time,data}`,剥掉了 `surfaceOp`/`sourceEventSeqs`/`ignorable`,而种子校验器要求 surface-eligible 事件必带 `surfaceOp`。修复 = 信封字段原样透传 + 新增 `sidechat-seed-validation.spec.ts`(用真实 `@deepseek-ai/dsh-session` 的 `Session.create` 校验器跑种子产物,回归守护)。
|
||||
2. 交互模型从「单 Tab + 内嵌线程列表 + 首问表单」改为 **Codex app 对齐的「每线程一 Tab」**:`createTab` 铸造 `sidechat:new-<uuid>`(meta `autoCreate`,视图挂载即调 `sidechat.start` 空问题立即建线程)或消费 `parkSidechatReopen` 停泊的 `sidechat:<threadId>` 重连 Tab;`dedupeKey = meta.threadId` 保证同线程聚焦不重复;`onClose` 释放 live agent(历史保留,头部菜单可重开)。配套后端变化:`sidechat.start` 的 `question` 变为可选(空 = 仅建线程,快照停泊在 `pendingSnapshots`);`sidechat.prompt` 检测边界未送达时(`boundaryDelivered` 扫日志)自动包裹边界 + 停泊快照并用首条消息改名;新增 `sidechat.info` 路由(live 状态 + provider/model/preset 身份,供头部 Agent 徽标)。UI 对齐主对话区:用户右对齐气泡(`--dsw-specific-bubble`)、assistant 通栏 markdown、胶囊 composer(`--dsw-specific-input-major` + 圆形 accent 发送/停止钮 + 自动增高 textarea)、运行扫光状态行与工具行(`StateDot` + shimmer),动效全部 ≤200ms 交叉淡化并受 `prefers-reduced-motion` 收敛。
|
||||
- (预留)实施过程中如有与本文档的其他偏差,在此记录。
|
||||
- **第三轮实测修复(4 bug)**:
|
||||
1. 「保存为新会话」报 `Cannot read properties of undefined (reading 'list')`:`handleSave` 把 `ctx.sessions.fork` 解构成自由函数再调用,丢失 `this`——client-runtime 的 `service.fork` 内部读 `this.list.getSnapshot()`(标题递增)。修复 = 保持方法调用形态(`ctx.sessions.fork({...})`)。
|
||||
2. 图标语义:线程切换菜单改自绘 `IconHistoryOutline16`(逆时针时钟)、保存按钮改自绘 `IconSaveOutline16`(软盘),沿用 1.5px 描边规范(primitives 无现成 history/save 图标)。
|
||||
3. 重开线程 Tab 丢工具行:根因**不在宿主**(`session.history` cold 路径原样返回 `tool/call`,见 `api-proxy.ts:749` 与 `chunk-rows.ts` 的无损 chunk 压缩)而在首挂回源窗口——旧实现 8 事件/页 × 32 页 = 256 事件上限,而 cold 读会把 chunk-rows 展开回每个流式 delta 一条事件,一轮回答动辄数百事件,较早的 `tool/call` 掉出窗口(settled 文本靠最终 `assistant/message` 幸存)。修复 = 抽出 `collectOwnEvents`(200 事件/页 × 40 页帽、日志穷尽/无标记时落定 `seedBoundary:0` 停止每轮重走),单测覆盖四分支。
|
||||
4. Subagent 页「目录损坏」行:见 §5 修正——种子追加 descriptor + SubagentView/subagent-detect 按前缀过滤(`isSideThreadSummary` 同时修好 autoOpenSubagent 误触发与计数膨胀)。旧版创建的线程仍是 corrupt 诊断行,由 SubagentView 的标题前缀过滤兜底隐藏。
|
||||
- **转录传输迁至自有路由(DSH 0.1.2-alpha.1 适配)**:§6 原设计的时间线轮询走通用 `connection.api.sessions.history` RPC——该客户端面(`ctx.connection.api`)在 0.1.2-alpha.1 的 Remote-gateway 迁移中被整体移除,且其继任者(`session/follow`/`session/page`)对 `origin:'subagent'` 会话强制 subagent 地址寻址(普通 `{kind:'session'}` 地址被 `agent-busy` 拒绝),纯分页也拿不到最新尾页(`throughSeq` 不得超当前游标)。修复 = 新增 **`sidechat.events`** 路由(`src/sidechat-routes.ts`):host 侧读线程日志(live 走 `agent.session.events`,冷线程走 `ctx.get('sessionPersistence').inspect`——两版 DSH 同形,alpha.1 的 inspect 返回展开后的逻辑日志而非磁盘上的打包 chunk-row),服务端切 `session/end-seed`(`threadOwnLogEvents`,`collectOwnEvents` 的 walk 语义下沉),`afterSeq` 增量 + 8000 事件尾部帽(对齐旧 walk 上限 40×200)。客户端 `fetchThread` 单路径化,删除 `collectOwnEvents` 与 `ctx.connection` 镜像/inject。种子切割仍由 `transcriptRows` 客户端二次防御。挂载冒烟补 live+冷读两条断言。
|
||||
- **种子缺 fork 标记 → 子会话继承父会话未领取的 inbox 消息(2026-09-05,用户实测「上下文很长时侧边对话会把之前的 User msg 先发送出去」)**:`sidechat.start` 原只传 `seed` 不传 `meta.isSeeded: true` + `inheritedEventCount`。dsh-session 契约:**只给 seed 不标 isSeeded,种子算重放历史而非继承前缀** → `Session.ownEvents()` 含整个种子 → 子会话 `Inbox` 构造器(重放 `ownEvents()` 中的 `agent/inbox/spliced`)继承父会话切割时刻**任何未领取的 inbox 输入**。两个触发窗口(均以真实 0.1.2-rc.1 包 `Session.create` + `Inbox` 实验证实):① next-turn——父会话排队中的用户消息(长回合运行中补发);② **next-step(用户实测场景:父会话运行中、用户队列已清空)**——长回合里工具结果上下文/steering/运行时快照在步骤边界之间插入 next-step、尚未领取;上下文越长回合越久窗口越宽。后果:首条 `sidechat.prompt` 的 claim 顺序为 `[幽灵消息, boundary, question]`——幽灵消息排在 boundary 之前最先发给模型(工具上下文常为「工具完成,接下来做 X」,模型先继续父会话任务),并在子会话日志落成 `user/message`。修复 = create 补 `meta.isSeeded: true` + `inheritedEventCount: SessionLogOffset(seed.length)`(宿主自己的 `session.fork`——api-session-controller——的规范调用形态;`createAgent` 工厂逐字段透传 `sessions.prepare`)。`threadOwnLogEvents` 按 end-seed 标记切割,标记位置不变,转录行为不变;冷恢复按持久化 header 切割,一致。回归测试进 `tests/sidechat-seed-validation.spec.ts`(真实 Session + Inbox 双向锁定:旧形态继承幽灵 / 新形态两队列皆空)。用户拍板:不迁移既有线程、不加 inbox.clear() 兜底(修复后新线程为无操作;旧「从未发过消息」空线程首次使用可能仍先答一次旧消息,之后自愈)。
|
||||
- **「更优雅 API」复评(2026-09-05,0.1.2-rc.1 = npm latest)**:无 sidechat/sidechain 宿主 API;`ContinuableStartSpec`(`ctx.subagents.startContinuable`)仍无 seed 字段;`session/create` RPC 只建全新会话。§3 的自建结论继续成立,`AgentRegistry.create` 接缝即宿主 fork 自用的公开接缝——补齐 fork 标记即为规范用法。
|
||||
@@ -1,116 +0,0 @@
|
||||
# 文件树「打开方式」子菜单设计(2026-08-22)
|
||||
|
||||
> 状态:已实现(分支 `feat/open-with-menu`)。本文档记录功能设计、关键取舍与实施偏差。
|
||||
|
||||
## 目标
|
||||
|
||||
文件树(`FileTree`)右键菜单新增「在应用中打开 >」子菜单:
|
||||
|
||||
- 内置 **资源管理器(在文件管理器中显示)、VS Code、Cursor、Zed** 四个打开方式;每个子菜单行右侧有**图钉**,点击可把该方式固定为右键菜单的**顶层直达项**(再点取消固定)。
|
||||
- **远端 SSH**:设置中配置可选 SSH host(`user@host` 或 `~/.ssh/config` 别名)。非空即整个工作区视为远端:VSCode 系条目(VS Code / Cursor / 自定义且勾选"VSCode 系")改用 `<scheme>://vscode-remote/ssh-remote+<host>/<path>` 打开;本地专用条目(资源管理器 / Zed / 非 VSCode 系自定义)从菜单隐藏。
|
||||
- **自定义编辑器**:名称 + URL 模板(`{path}` 占位符,如 `cursor://file/{path}`)+ 「是否 VSCode 系」开关;配置入口在**侧边栏设置 Files 卡片齿轮**(与既有 `editorExplorer` 下拉同一弹窗)。
|
||||
- 打开动作经宿主新路由 `POST /sidebar/api/open.external` 执行(argv 数组 spawn,无 shell 注入),浏览器模式与 DSH Desktop 行为一致;**不经过** `ctx.workspaces.openPath`(该入口已被 `wrapOpenPath` 劫持到侧边栏编辑器,见 `src/client/openpath-intercept.ts`)。
|
||||
|
||||
## 调研结论(reuse gate)
|
||||
|
||||
- DSH checkout(`~/.dsh/source/current`)无"文件树 open-with"、无 SSH 远程会话、无 `vscode://` 开启器;`host.openPath` 只能"默认应用打开",无法指定编辑器、无 reveal/select 语义 → 需要新增。
|
||||
- dsh-external hub 无同类成品;`dsh-open-in-vscode` 是工作区行菜单、依赖外部 slot,不适用。
|
||||
- primitives `Menu` 原生支持 `submenu?: readonly MenuItem[]`(子项点击走 `onSelect(child.id)`),`label: ReactNode` 可内嵌图钉;无现成图钉/品牌图标 → 引入 `react-icons`(`vsc` codicons 图钉 `VscPin`/`VscPinned` 与文件树 `VscFile`/`VscFolder(Opened)`、`si` 品牌剪影 Cursor/Zed、菜单父行 `VscLinkExternal`),VS Code 品牌剪影因 simple-icons 后期版本受微软商标政策下架而内嵌自 simple-icons@11.0.0(CC0,`IconVscode16`);菜单行图标统一 16px 与 DSH 原生菜单一致,子菜单父行的 `>` chevron 由 label 内右对齐元素提供(基元不渲染)。⚠️ react-icons 的 exports 把 `require` 排在 `import` 前,tsdown 共享 conditionNames 会选中不可树摇的 CJS 入口(bundle +6.4MB)——`tsdown.config.ts` 用 `resolve.alias` 钉死 `si`/`vsc` 的 ESM 入口,树摇后仅 +12KB。
|
||||
|
||||
## 数据模型
|
||||
|
||||
持久化在 `pluginSettings['editor'].openWith`(宿主 schema 的开放 map,无需改 PrefsSchema):
|
||||
|
||||
```json
|
||||
{
|
||||
"openWith": {
|
||||
"sshHost": "",
|
||||
"customEditors": [
|
||||
{ "id": "custom:<uuid>", "name": "Windsurf", "urlTemplate": "windsurf://file/{path}", "isVscodeFamily": false }
|
||||
],
|
||||
"pinned": ["vscode", "custom:<uuid>"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `parseOpenWithConfig`(`src/client/open-with.ts`)容错解析:坏行丢弃、未知 pinned 在解析目标时剪枝;**结构合法但未填完的行(空 name/模板)保留**——设置面板正在编辑的行必须能在弹窗往返中存活,由菜单侧的 `isValidCustomEditor` 过滤。
|
||||
- 目标 id:内置 `explorer` / `vscode` / `cursor` / `zed`;自定义 `custom:<uuid>`。
|
||||
- URL 构建:
|
||||
- 本地:模板原文替换 `{path}`(`vscode://file//home/u/f.ts`——POSIX 绝对路径自带前导 `/`,与 VS Code 官方文档 `vscode://file/c:/…` 的约定一致;反斜杠归一化为 `/`)。
|
||||
- SSH(VSCode 系):`<scheme>://vscode-remote/ssh-remote+<host>/<path>`,路径保留前导 `/`(如 `…+dev/home/u/f.ts`,与社区约定一致)。
|
||||
- 模板无 `scheme://` 或无 `{path}` → 返回 undefined(不打开)。
|
||||
|
||||
## 菜单结构(文件行与目录行一致)
|
||||
|
||||
```
|
||||
在新 Tab 中打开 ← 既有
|
||||
在侧边打开 ← 既有
|
||||
----------------------------
|
||||
VS Code (SSH) ← pinned 直达项(按内置顺序 + 自定义追加)
|
||||
Cursor (SSH)
|
||||
----------------------------
|
||||
在应用中打开 > ← 子菜单父项(始终在 pinned 之后)
|
||||
资源管理器 📌
|
||||
VS Code 📌(激活)
|
||||
Cursor 📌(激活)
|
||||
Zed 📌
|
||||
Windsurf 📌
|
||||
下载 ← 既有(仅文件行)
|
||||
复制相对地址 / 复制绝对地址 ← 既有
|
||||
```
|
||||
|
||||
- 图钉是 `label` 内的 `<span role="button" tabIndex={-1}>`(Menu 行本身是 `<button>`,嵌套原生 button 不合法);`stopPropagation` + `preventDefault` 使其**不关闭菜单、不触发行选择**,点击后经响应式 blob 重渲染立即翻转。
|
||||
- 未接线(props 缺省)或 SSH 过滤后无目标 → 整个区块隐藏,旧行为/旧测试不变。
|
||||
|
||||
## 设置面板:`settings.render` seam 扩展
|
||||
|
||||
**实施偏差(与最初"render 独占"契约不同,需同步文档)**:`SettingsBody`(`src/client/SideCardSection.tsx`)原先"有 `render` 就整个替换行列表"。本功能需要**行列表(`editorExplorer` 下拉)与自定义面板共存**,因此把 seam 改为:
|
||||
|
||||
> `render` 给定时**追加**渲染在行列表(`toggles` / `pluginToggles`)之后;未声明行列表时与旧行为一致(单独渲染)。
|
||||
|
||||
- 仓库内无既有 render 使用者,`tests/side-card-section.spec.tsx` 无"render 独占"断言,风险可控;`AGENTS.md` §3.1/§5 与本文档同步更新。
|
||||
- 面板组件 `OpenWithSettings`:SSH host 输入 + 自定义编辑器行(名称/模板/VSCode 系/删除)+「添加」;每次变更整体提交 `updatePluginSetting('openWith', next)`(含 pinned 原样保留)。
|
||||
- 图钉写入走 `src/client/plugin-settings.ts` 的 `updatePluginSettings`:Promise 链串行化 + `api.settingsUpdate` 后 `store.setPrefs(parsePrefs(view.value))`,避免快速连点丢写。(设置弹窗内部有自己独立的串行 commit,二者极少重叠。)
|
||||
|
||||
## 宿主路由
|
||||
|
||||
`POST /sidebar/api/open.external`(`src/index.ts` `buildApi`,与其他方法同 fence + 错误包裹):
|
||||
|
||||
```ts
|
||||
{ action: 'reveal', path } // 在文件管理器中显示/选中
|
||||
{ action: 'url', url } // 交给注册的协议处理器
|
||||
```
|
||||
|
||||
- `src/open-external.ts`:纯函数 `revealCommand` / `urlCommand`(platform 可注入,单测覆盖三平台)+ `launchExternal`(`spawn(..., { detached: true, stdio: 'ignore' })` + `unref`)。
|
||||
- `reveal`:darwin `open -R`;win32 `explorer.exe /select,`;linux `xdg-open <父目录>`(无统一 select 协议,KISS 打开所在目录)。
|
||||
- `url`:darwin `open`;win32 `rundll32 url.dll,FileProtocolHandler`(备选 `cmd /c start ""`);linux `xdg-open`。
|
||||
- 校验:`reveal` 路径必须绝对(`requireAbsolute`);`url` 必须是 `scheme://` 自定义协议(拒绝 http/https)。
|
||||
- **Windows 平台命令为约定实现,需在 Windows 实机验证**(本机为 macOS;CI 覆盖 linux)。
|
||||
|
||||
**实施偏差(2026-09,#517 / PR #522)**:SSH 远程编辑器链接**不再经宿主路由执行**。DSH 部署在无头远端服务器时,宿主侧 `xdg-open` 无 DISPLAY/无编辑器,`vscode://` 静默失败。`api.openExternal`(`src/client/api.ts`)现把 `<scheme>://vscode-remote/ssh-remote+…` 形态的 URL 在浏览器客户端同步触发 `window.location.assign`(处于用户点击链内,外部协议交给本机编辑器经 Remote-SSH 打开远端文件);reveal 与本地编辑器 URL 仍走本节宿主路由。普通浏览器可处理自定义协议;禁止/未处理 `vscode://` 的 WebView 壳客户端需各自适配(见 #517 补充信息)。
|
||||
|
||||
**实施偏差(2026-09,右键菜单减重与子菜单视口钳制)**:本菜单(及 TabBar / GitLens / FreeWindow 右键菜单、「+」菜单)整体切到 primitives `Menu` 的 **`compact` 密度**(行高 26px / 12px 字 / 164px 卡片 / 7px 圆角),菜单图标随槽位 16→14、子菜单图钉 20→16。同时发现宿主子菜单**不受视口钳制**(`bottom:-4px` 从父行向上生长、`left:calc(100%+10px)` 向右展开,主列表的钳制与 `max-height` 收缩均不覆盖它):树顶部右键时本子菜单伸出视口上沿,窄面板(280–400px)时伸出右沿。修复为插件侧方案 `src/client/menu-flip.ts`——菜单打开期间在 `<body>` 发布 `data-dsh-sidebar-submenu="down|left"` 方向 token(`y < vh/2` → down;`x + 400 > vw` → left,400 镜像宿主几何 218 卡片 + 10 间隙 + 165 子菜单 + 12 边距),`layout.css` 据此翻转子菜单生长方向与 `::before` 悬停桥;**属性的存续期即作用域**(仅我们的菜单打开时存在),宿主自有菜单不受影响。**不做** `max-height` + 滚动钳制:overflow 裁剪会切掉子菜单 `::before` 悬停桥(跨 10px 间隙会触发 mouseLeave 关卡)。残留见「已知限制」末两条。
|
||||
|
||||
## 已知限制
|
||||
|
||||
- 路径含 `#`/`?` 的文件名经 URL 打开可能被浏览器当作 fragment/query(不处理,注释说明)。
|
||||
- 编辑器未安装/协议未注册:由 OS 弹提示或静默失败,不做安装检测。SSH 客户端分支同理——浏览器端 `location.assign` 无法探测协议 handler 是否存在,仍返回 `{started: true}`。
|
||||
- 自定义编辑器仅支持 URL 模板式,不支持 CLI 命令式;无 `{dir}` 占位符。
|
||||
- Linux reveal 退化为打开所在目录(精确 select 需要各文件管理器私有协议)。
|
||||
- SSH 模式为**全局**(非每会话):DSH 无远程会话概念,文件树路径即宿主路径;该模型对应"DSH 跑在远端开发机上"的场景。
|
||||
- 子菜单钳制为方向翻转而非高度裁剪:约 >15 个打开方式目标 + 矮视口 + 不利光标位仍可能溢出(compact 密度已把常规 4–8 目标场景收进安全区)。
|
||||
- 翻转阈值镜像宿主 `Menu.module.css` 几何常量(218/10/165/12):宿主未来改尺寸时阈值偏保守(多翻一次方向),不会产生新越界;菜单打开期间窗口 resize 不重算 token,该次打开最坏退化为原始行为。
|
||||
|
||||
## 测试
|
||||
|
||||
- `tests/open-with.spec.ts`:解析容错、目标解析与 SSH 过滤、URL 构建(本地/SSH/custom/坏模板)、校验器。
|
||||
- `tests/open-external.spec.ts`:三平台命令表、URL 校验、spawn 前校验。
|
||||
- `tests/open-external-client.spec.ts`(#522):SSH remote URL 客户端分流(不触 fetch)、本地/reveal/http(s) 留宿主、导航抛错 reject。
|
||||
- `tests/file-tree-open-with.spec.tsx`(jsdom):右键 → 子菜单/图钉;pin 不选中不关闭;选子项回调关闭菜单;SSH 标签后缀;未接线隐藏;翻转向量(down / down left / 卸载清理)。
|
||||
- `tests/menu-flip.spec.tsx`:方向 token 纯函数(四象限、边界、窄面板)+ hook 属性生命周期 + layout.css 选择器与属性名的跨文件一致性。
|
||||
- `tests/tab-bar-context-menu.spec.tsx`:追加翻转向量(顶部条 down、右缘 down left、下半空 token、卸载清理)。
|
||||
- `tests/e2e/mount.e2e.ts`:「+」菜单首行 computed 高度 ≤30px(compact 密度的布局级证据)。
|
||||
- `tests/open-with-settings.spec.tsx`:SSH 输入、添加/删除(删除剪枝 pinned)、无效提示、VSCode 系开关。
|
||||
- `tests/side-card-section.spec.tsx`:`SettingsBody` 行列表 + render 共存、仅 render 时不变。
|
||||
- `tests/builtins.spec.ts`:editor 描述符断言补 `settings.render`。
|
||||
- 全量 `pnpm test`(810 passed)、`pnpm typecheck`、`pnpm build` 全绿;未跑挂载冒烟(由 CI plugin-mount 门禁覆盖)。
|
||||
@@ -1,96 +0,0 @@
|
||||
# 设计:模型主动在侧边栏打开(`sidebar_open` 工具 + `agentOpenTools` 设置)
|
||||
|
||||
> 日期:2026-08-23 · 状态:已实现 · 仓库:omdsh-dev/DSH-better-sidebar
|
||||
|
||||
## 需求
|
||||
|
||||
新增一个侧边栏**全局设置**(Side card 设置页「常规」分组):`agentOpenTools`,**默认关闭**。
|
||||
开启后向模型注入**一个**工具 `sidebar_open`,允许模型主动在**调用方会话**的侧边栏中打开:
|
||||
|
||||
- 本地**文件** → 内置 editor tab(按 path 去重,聚焦既有 tab)
|
||||
- 本地**文件夹** → 内置 editor tab 的**文件夹窗口**(文件树以该目录为根)
|
||||
- **HTTP(S) 网页** → 内置 browser tab(URL 预填)
|
||||
|
||||
## 关键决策
|
||||
|
||||
### 1. 作用域 = 调用方 agent 会话
|
||||
|
||||
与 `terminal_*` 工具一致:工具只绑定 `exec.agent.session.id`,模型不传 sessionId。
|
||||
子代理(或未激活会话)调用时若无客户端连接,打开请求排队,该会话侧边栏下次可见时重放。
|
||||
工具结果通过 `delivered` 如实报告「当前是否已推送到已连接的侧边栏视图」。
|
||||
|
||||
### 2. 投递通道:host→client WS 推送(无 ack 协议)
|
||||
|
||||
- 新增 WS 升级路由 `/sidebar/ws/agent-opens?sessionId=...`(与 `/sidebar/ws/agent-terminals` 同模式,过同一 trust fence)。
|
||||
- host 侧 `AgentOpenRegistry` 维护会话级队列 + 已连接视图集合:
|
||||
- `enqueue`:有订阅者 → 即时推送并**消费**(`delivered: true`);无订阅者 → 保留队列(`delivered: false`)。
|
||||
- `attach`:注册订阅者并**重放**队列(消费即出队),返回 disposer(close/error 时退订)。
|
||||
- `drainAll`:特性关闭时丢弃未投递队列。
|
||||
- **消费即出队**是关键:browser tab 无 per-URL 去重,重连重放会铸造重复 tab;消费语义保证每个请求恰好推送一次。
|
||||
- 不加 ack 回执、不加 `/sidebar/api` 路由——结果的最佳努力语义(`delivered`)已足够,符合 KISS 与 terminal 先例。
|
||||
|
||||
### 3. `sidebar_open` 工具(唯一新增工具)
|
||||
|
||||
- 参数:`target`(必填:绝对/相对路径,或 http(s) URL)、`title`(可选:tab 标题)。
|
||||
- 执行流程:`throwIfAborted` → 分类 → 目标 tab 启用检查 → 入队 → 返回规范结果。
|
||||
- 分类规则:
|
||||
- `http://` / `https://`(大小写不敏感)→ `url`,默认标题 = hostname;
|
||||
- 其他 scheme(`file:` / `javascript:` / `vscode:` 等)→ 报错(Windows 盘符前缀 `C:\`/`C:/` 除外,它是路径不是 scheme);
|
||||
- 本地路径 → 相对路径以会话 cwd(`sessionCwdOf`)解析为绝对路径,`stat` 定 `file`/`folder`;不存在/不可读 → 报错;
|
||||
- 目标 tab 类型被用户禁用(`tabsEnabled['editor'|'browser'] === false`)→ 报错并提示启用(避免客户端静默 no-op)。
|
||||
- 输出(canonical JSON,`render` 纯文本投影):`{ kind, target, title, delivered }`。
|
||||
- 工具描述/错误消息为英文(与 8 个 `terminal_*` 一致)。
|
||||
|
||||
### 4. 文件夹窗口:editor tab 的 `meta.dir`
|
||||
|
||||
- 打开文件夹 = `openTab({ type: 'editor', path: <dir>, id: 'editor:'+<dir>, title, meta: { dir: true } })`。
|
||||
- EditorHost 识别 `meta.dir === true`:不进入 viewer 加载流程,渲染**全窗 TreePanel**(`cwd = tab.path`);
|
||||
FileTree 根部即 `cwd`(`fs.tree` 接受绝对路径),搜索结果/上传仍受会话工作区约束(已知限制,见下)。
|
||||
- `meta` 为可持久化的 tab 状态(v0.12.0+),`sanitizeState` 原样保留——刷新后文件夹窗口恢复。
|
||||
- 折叠/展开、右键打开、拖拽上传等全部复用 TreePanel 既有能力。
|
||||
|
||||
### 5. 设置与门控
|
||||
|
||||
- 新 prefs 字段 `agentOpenTools: boolean`(`SidebarPrefs` + `PrefsSchema` + 客户端 `parsePrefs`),默认 `false`。
|
||||
- SideCardSection「常规」分组新增一行 Switch(标题/描述走新 locale key)。
|
||||
- host 门控完全拷贝 `agentTerminalTools` 的 `syncToolsGate` 模式:
|
||||
- 开启 → `registerOpenTool(...)`;关闭 → 注销工具 + `agentOpenRegistry.drainAll()`;
|
||||
- 一次 `scope.watch` 同时驱动两个门控(各持有自己的 disposer,互不干扰);
|
||||
- 工具不依赖 node-pty,降级模式下也可用。
|
||||
- 客户端防御:Sidebar 的 agent-opens 推送订阅在 `prefs.agentOpenTools !== true` 时忽略消息(host 已门控,此为双保险)。
|
||||
|
||||
### 6. 与既有拦截开关的关系
|
||||
|
||||
- URL 打开**不受** `browserInterceptLinks` / `browserInterceptHttp` / `browserInterceptHttps` 约束——那是用户点击外链的接管开关;
|
||||
模型显式调用 `sidebar_open` 只受 browser tab 自身启用开关约束(openTab 既有 gating)。
|
||||
- 文件打开**不受** `interceptOpenPath` 影响(那是聊天点击路径的接管开关)。
|
||||
|
||||
## 既有模式复用(零新公共 API)
|
||||
|
||||
| 新事物 | 复用/镜像的既有模式 |
|
||||
|---|---|
|
||||
| `agentOpenTools` 门控 | `agentTerminalTools` 门控(settings seam `watch` → 注册/注销) |
|
||||
| `/sidebar/ws/agent-opens` | `/sidebar/ws/agent-terminals` 推送(升级注册、fence、每会话 attach) |
|
||||
| 客户端推送订阅 | Sidebar 的 agent-terminals effect(重连上限 `FAILURE_LIMIT`、防抖/清理) |
|
||||
| 文件/URL 打开 | `service.openFile` / `openTab({ type:'browser', url })`(link-intercept 同款) |
|
||||
| 文件夹窗口 | path-less explorer 的全窗 TreePanel 分支(仅根目录改为 `tab.path`) |
|
||||
|
||||
不新增 `BetterSidebarService` 方法;不改变任何现有路由协议。
|
||||
|
||||
## 已知限制
|
||||
|
||||
- **多浏览器窗口**:推送按「有订阅者即发送并消费」执行,多窗口同时连接时后连接的窗口不会收到已消费的请求
|
||||
(其本体状态由 localStorage 恢复——与 agent-terminals 的全量列表 fan-out 不同,属尽力而为语义)。
|
||||
- **文件夹窗口的全局搜索/上传**:TreePanel 的搜索盒仍以**会话 cwd** 为根(`fs.search` 是有意的工作区限定),
|
||||
目录内上传经 host 会话工作区校验(目录在 cwd 内可上传,cwd 外拒绝)。
|
||||
- **无 ack**:`delivered: true` 只表示「推送已发出」,客户端处理失败(罕见)不重试。
|
||||
- **关闭特性不清已打开 tab**:与 tab 启用开关语义一致,只注销工具并清空未投递队列。
|
||||
|
||||
## 测试覆盖
|
||||
|
||||
- `tests/agent-opens.spec.ts`(新):注册表语义(排队/重放/消费/作用域隔离/drain/dispose)+ 工具行为
|
||||
(分类、相对路径、缺路径、非 http(s) scheme、tab 禁用、schema 校验、无 agent 错误)。
|
||||
- `tests/smoke.spec.ts`:WS 升级路由清单 + `settings.get` 默认值 + 独立 gating 测试(默认 off → 0 工具;on → 1;off → 注销)。
|
||||
- `tests/plugin-shape.spec.ts` / `tests/prefs.spec.ts`:schema/parse 默认值 + 全量 prefs 字面量。
|
||||
- `tests/editor-host.spec.tsx`:`meta.dir` 文件夹窗口(树以目录为根、无编辑器 chrome、不触发 viewer 加载)。
|
||||
- `tests/locales.spec.ts`:zh/en/ja 词典 key 集相等(自动覆盖新键)。
|
||||
@@ -1,408 +0,0 @@
|
||||
# Bug PR 一次性合并方案(merge train + 并行 subagent)
|
||||
|
||||
> 仓库:`omdsh-dev/DSH-better-sidebar`(本地 clone:`D:\Projects\deepseek-harness\DSH-better-sidebar`)
|
||||
> 基准:main `d6d8db0` (= 0.15.2 + #339 locale ja)
|
||||
> 目标:把 12 个 bug PR 一次性合入 main,**保留每个原作者的贡献归属**
|
||||
> 输入:`bug-pr-review.md` 中 12 个 NOT_FIXED PR
|
||||
> 日期:2026-08-23
|
||||
|
||||
---
|
||||
|
||||
## 0. 设计原则
|
||||
|
||||
| 原则 | 说明 |
|
||||
|---|---|
|
||||
| 保留作者归属 | 用 `git merge --no-ff`(**不 squash**),每个 PR 的原始 commits 全部进 main 历史;GitHub 通过 commit author email 匹配到 12 位作者 |
|
||||
| 跨平台 commit trailer | #285 作者邮箱 `fix@local.invalid` 不匹配 GitHub → 在 merge commit message 加 `Co-authored-by: xxxg0001 <xxxg0001@users.noreply.github.com>` |
|
||||
| 零源码 patch | train 分支所有改动都来自 PR 本身或冲突解决;**不修改任何 PR 的 commits** |
|
||||
| 并行加速 | 4 个 subagent 各自 worktree 独立合并簇内 PR;跨簇冲突由主 agent 串行解决 |
|
||||
| 每步验证 | 每个 subagent 完成簇内合并后跑 `pnpm typecheck`;主 agent 跨簇合并后跑 `pnpm typecheck && pnpm test` |
|
||||
|
||||
---
|
||||
|
||||
## 1. PR 清单与作者
|
||||
|
||||
| PR | 标题 | 作者 | author email |
|
||||
|---|---|---|---|
|
||||
| #345 | 限制文件 API 访问会话工作区 | wang-kaopu | 1970138194@qq.com |
|
||||
| #342 | 搜索跳过 node_modules 等噪声目录 | sunrain520 | sunrain520@users.noreply.github.com |
|
||||
| #341 | mermaid 抑制全局错误渲染 | duyiliu | 278950578@qq.com |
|
||||
| #338 | 移除公开版 cordis peer,迁移类型基底 | Menghuan1918 | menghuan2003@outlook.com |
|
||||
| #336 | tsdown 配置加载缺 unrun devDependency | MilchstraBe-Endless10 | milchstrasse.endless10@users.noreply.github.com |
|
||||
| #330 | panel host 降到 DSH dynamic-plugin 层下 | ZgblKylin | 13114351+ZgblKylin@users.noreply.github.com |
|
||||
| #326 | 支持选择子 git 仓库 | loeanxi | 2012065136@qq.com |
|
||||
| #310 | layout push 下保 composer 在视口内 | Zzy-min | zzy19812007@gmail.com |
|
||||
| #292 | markdown 预览本地/相对路径图片 | huzilin | huzilinha@gmail.com |
|
||||
| #285 | linked worktrees 移植到 RC8 | xxxg0001 | fix@local.invalid ⚠ |
|
||||
| #278 | 面板宿主裁剪视口、收起面板不再撑出双向滚动 | ch3n4y | ch3n4y@gmail.com |
|
||||
| #254 | mobile 说明不可用侧边栏并防溢出 | SparkElf | laoyh@chinatelecom.cn |
|
||||
|
||||
⚠ #285 author email 不匹配 GitHub 账号,需在 merge commit 加 `Co-authored-by` trailer 补 credit。
|
||||
|
||||
---
|
||||
|
||||
## 2. 文件冲突矩阵(跨簇)
|
||||
|
||||
> 仅列出被 ≥2 个 PR 修改、且**跨簇**的文件。簇内冲突由 subagent 解决。
|
||||
|
||||
| 文件 | 涉及 PR(按簇) |
|
||||
|---|---|
|
||||
| `README.md` / `README_EN.md` | A: #338 #345;B: #292;C: #254 |
|
||||
| `AGENTS.md` | A: #338;B: #342;C: #330 |
|
||||
| `docs/external-plugin-guide.md` | A: #338 #345(簇内解决) |
|
||||
| `package.json` | A: #338 #336;C: #254;D: #326 |
|
||||
| `dsh.plugin.json` | A: #338;C: #254 |
|
||||
| `src/index.ts` | A: #338 #345;D: #285 #326 |
|
||||
| `src/client/sidebar.module.css` | C: #330 #278 #254(簇内解决);D: #285 |
|
||||
| `src/client/Sidebar.tsx` | B: #310;C: #254 |
|
||||
| `src/client/service.ts` | A: #338;C: #254 |
|
||||
| `src/client/DiffTab.tsx` / `GitView.tsx` / `api.ts` / `state.ts` / `src/git.ts` | D: #285 #326(簇内解决) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 分簇(簇内串行,跨簇并行)
|
||||
|
||||
### Cluster A — 基础设施(package.json / cordis / path security)
|
||||
|
||||
**分支**:`train/base`
|
||||
**worktree**:`D:\Projects\deepseek-harness\dsb-train-base`
|
||||
**合并顺序**:
|
||||
1. `#338` (cordis peer 迁移) — 改 package.json/dsh.plugin.json/src/index.ts/service.ts/AGENTS.md/README/docs
|
||||
2. `#336` (unrun devDep) — 只改 package.json
|
||||
3. `#345` (path security) — 改 README/docs/src/index.ts/fs-operations/html-route/path-security 新增
|
||||
|
||||
**簇内冲突点**:
|
||||
- #336 vs #338:package.json(#338 移除 cordis peer,#336 加 unrun devDep,行不冲突但邻近)
|
||||
- #345 vs #338:README.md / README_EN.md / docs/external-plugin-guide.md / src/index.ts(#338 迁移基底,#345 加 path-security 引用,可能 src/index.ts 注册新路由撞)
|
||||
|
||||
### Cluster B — 独立修复
|
||||
|
||||
**分支**:`train/misc`
|
||||
**worktree**:`D:\Projects\deepseek-harness\dsb-train-misc`
|
||||
**合并顺序**:
|
||||
1. `#341` (mermaid suppressErrorRendering) — 改 src/client/mermaid.tsx + 测试
|
||||
2. `#310` (layout-push composer clip) — 改 Sidebar.tsx + layout-push.ts + breakpoints.ts + layout.css + 测试
|
||||
3. `#342` (fs-search skip dirs) — 改 fs-search.ts + AGENTS.md + 测试
|
||||
4. `#292` (markdown images) — 改 TextEditor.tsx + markdown-images.ts + README + 测试
|
||||
|
||||
**簇内冲突点**:基本无(彼此文件不重叠)。#342 的 AGENTS.md 和 #292 的 README 是跨簇冲突,本 worktree 内基于 main,无冲突。
|
||||
|
||||
### Cluster C — CSS 簇(sidebar.module.css)
|
||||
|
||||
**分支**:`train/css`
|
||||
**worktree**:`D:\Projects\deepseek-harness\dsb-train-css`
|
||||
**合并顺序**:
|
||||
1. `#330` (z-index drop) — 改 sidebar.module.css:35 + AGENTS.md + docs/plans + e2e 测试
|
||||
2. `#278` (overflow hidden + panelHidden transform) — 改 sidebar.module.css + e2e 测试
|
||||
3. `#254` (mobile no-session feedback + overflow) — 改 Sidebar.tsx + sidebar.module.css + README + dsh.plugin.json + package.json + service.ts
|
||||
|
||||
**簇内冲突点**:
|
||||
- #278 vs #330:sidebar.module.css(#330 改 z-index 行,#278 改 overflow/transform 行,可能行邻近但不直接冲突)
|
||||
- #254 vs #330/#278:sidebar.module.css(mobile media query 区域,可能撞 e2e 测试)
|
||||
|
||||
### Cluster D — git 簇
|
||||
|
||||
**分支**:`train/git`
|
||||
**worktree**:`D:\Projects\deepseek-harness\dsb-train-git`
|
||||
**合并顺序**:
|
||||
1. `#285` (worktree RC8 forward-port) — 改 DiffTab/GitView/api/state/git.ts/index.ts/builtins/tabs.tsx/locales.ts/sidebar.module.css + 测试
|
||||
2. `#326` (multi-repo selector) — 改 DiffTab/GitView/api/state/git.ts/index.ts/package.json + 截图 + 测试
|
||||
|
||||
**簇内冲突点**(严重):
|
||||
- #326 vs #285:5 个核心文件全撞(DiffTab/GitView/api/state/git.ts)+ src/index.ts
|
||||
- #326 应建立在 #285 的 worktree 支持基础上(多仓选择是 worktree list 的扩展)
|
||||
- 合并 #326 时需要手工缝合:保留 #285 的 worktree list 逻辑 + #326 的子仓选择 UI
|
||||
|
||||
---
|
||||
|
||||
## 4. 执行流程
|
||||
|
||||
### Phase 0 — 准备(主 agent,串行)
|
||||
|
||||
```powershell
|
||||
$repo = "D:\Projects\deepseek-harness\DSH-better-sidebar"
|
||||
|
||||
# 0.1 拉所有 PR 分支到本地 ref(避免 subagent 重复 fetch)
|
||||
$prs = 345,342,341,338,336,330,326,310,292,285,278,254
|
||||
foreach ($n in $prs) {
|
||||
git -C $repo fetch origin "pull/$n/head:pr-$n"
|
||||
}
|
||||
|
||||
# 0.2 创建 4 个 worktree(每个独立目录,独立分支基于 origin/main)
|
||||
git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-base" -b train/base origin/main
|
||||
git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-misc" -b train/misc origin/main
|
||||
git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-css" -b train/css origin/main
|
||||
git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-git" -b train/git origin/main
|
||||
|
||||
# 0.3 每个 worktree 都 pnpm install(独立 node_modules,避免锁文件竞争)
|
||||
foreach ($wt in @("dsb-train-base","dsb-train-misc","dsb-train-css","dsb-train-git")) {
|
||||
Push-Location "D:\Projects\deepseek-harness\$wt"
|
||||
pnpm install --prefer-offline
|
||||
Pop-Location
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 1 — 簇内并行合并(4 个 subagent 同时跑)
|
||||
|
||||
每个 subagent 接收:
|
||||
- 自己的 worktree 路径
|
||||
- 簇内 PR 编号和合并顺序
|
||||
- PR 的预期改动文件清单(见 §3)
|
||||
- 验证命令:`pnpm typecheck`(必跑)+ `pnpm test`(如簇内有相互依赖的测试)
|
||||
|
||||
#### Subagent A(base)
|
||||
|
||||
```
|
||||
worktree: D:\Projects\deepseek-harness\dsb-train-base
|
||||
合并顺序: #338 → #336 → #345
|
||||
命令:
|
||||
git merge --no-ff pr-338 -m "merge #338: 移除公开版 cordis peer,迁移类型基底到 @deepseek-ai/cordis"
|
||||
# 解决冲突(README/docs/package.json/src/index.ts)
|
||||
pnpm typecheck
|
||||
|
||||
git merge --no-ff pr-336 -m "merge #336: add unrun devDependency for tsdown config loading"
|
||||
# 解决 package.json 冲突
|
||||
pnpm typecheck
|
||||
|
||||
git merge --no-ff pr-345 -m "merge #345: 限制文件 API 访问会话工作区"
|
||||
# 解决 README/docs/src/index.ts 冲突
|
||||
pnpm typecheck
|
||||
|
||||
完成后输出:
|
||||
- train/base 的 HEAD SHA
|
||||
- 解决的冲突文件清单
|
||||
- 跨簇预期冲突点(README.md / package.json / src/index.ts / docs/external-plugin-guide.md / AGENTS.md / dsh.plugin.json / src/client/service.ts)
|
||||
```
|
||||
|
||||
#### Subagent B(misc)
|
||||
|
||||
```
|
||||
worktree: D:\Projects\deepseek-harness\dsb-train-misc
|
||||
合并顺序: #341 → #310 → #342 → #292
|
||||
命令:
|
||||
git merge --no-ff pr-341 -m "merge #341: fix(mermaid): suppress global error rendering"
|
||||
pnpm typecheck
|
||||
|
||||
git merge --no-ff pr-310 -m "merge #310: keep conversation composer in viewport under layout push"
|
||||
pnpm typecheck
|
||||
|
||||
git merge --no-ff pr-342 -m "merge #342: fs-search skip node_modules and other noise dirs"
|
||||
# AGENTS.md 可能无冲突(基于 main,A 簇的 AGENTS.md 改动在另一个 worktree)
|
||||
pnpm typecheck
|
||||
|
||||
git merge --no-ff pr-292 -m "merge #292: markdown preview local/relative images"
|
||||
pnpm typecheck
|
||||
|
||||
完成后输出:
|
||||
- train/misc 的 HEAD SHA
|
||||
- 跨簇预期冲突点(AGENTS.md 来自 #342;README.md 来自 #292;Sidebar.tsx 来自 #310)
|
||||
```
|
||||
|
||||
#### Subagent C(css)
|
||||
|
||||
```
|
||||
worktree: D:\Projects\deepseek-harness\dsb-train-css
|
||||
合并顺序: #330 → #278 → #254
|
||||
命令:
|
||||
git merge --no-ff pr-330 -m "merge #330: drop panel host below DSH cordis dynamic-plugin layer"
|
||||
pnpm typecheck
|
||||
|
||||
git merge --no-ff pr-278 -m "merge #278: panel host clip viewport, hidden panel no longer causes双向 overflow"
|
||||
# sidebar.module.css 可能冲突
|
||||
pnpm typecheck
|
||||
|
||||
git merge --no-ff pr-254 -m "merge #254: mobile explain unavailable sidebar and prevent overflow"
|
||||
# sidebar.module.css 可能冲突;其他文件基于 main 无冲突
|
||||
pnpm typecheck
|
||||
|
||||
完成后输出:
|
||||
- train/css 的 HEAD SHA
|
||||
- 跨簇预期冲突点(sidebar.module.css 来自 #285;README/package.json/dsh.plugin.json/Sidebar.tsx/service.ts 来自 #254)
|
||||
```
|
||||
|
||||
#### Subagent D(git)
|
||||
|
||||
```
|
||||
worktree: D:\Projects\deepseek-harness\dsb-train-git
|
||||
合并顺序: #285 → #326
|
||||
命令:
|
||||
git merge --no-ff pr-285 -m "merge #285: forward-port linked worktrees to RC8
|
||||
|
||||
Co-authored-by: xxxg0001 <xxxg0001@users.noreply.github.com>"
|
||||
# 注意:#285 author email = fix@local.invalid,必须加 Co-authored-by trailer
|
||||
pnpm typecheck
|
||||
|
||||
git merge --no-ff pr-326 -m "merge #326: support selecting child git repositories"
|
||||
# 严重冲突:DiffTab/GitView/api/state/git.ts/src/index.ts
|
||||
# 缝合策略:保留 #285 的 worktree list 逻辑 + #326 的子仓选择 UI
|
||||
pnpm typecheck
|
||||
|
||||
完成后输出:
|
||||
- train/git 的 HEAD SHA
|
||||
- 跨簇预期冲突点(sidebar.module.css 来自 #285;src/index.ts 与 base 簇撞;package.json 与 base 簇撞)
|
||||
- #326 缝合的关键决策(哪些函数来自 #285,哪些来自 #326)
|
||||
```
|
||||
|
||||
### Phase 2 — 跨簇串行合并(主 agent)
|
||||
|
||||
```powershell
|
||||
$repo = "D:\Projects\deepseek-harness\DSH-better-sidebar"
|
||||
|
||||
# 切回主 worktree
|
||||
# 2.1 创建 mega-train 分支
|
||||
git -C $repo checkout -b merge/bug-pr-train origin/main
|
||||
|
||||
# 2.2 merge train/base(应该 ff,因为 base 直接基于 main)
|
||||
git -C $repo merge --no-ff train/base -m "integrate train/base: cordis peer migration + unrun devDep + path security"
|
||||
|
||||
# 2.3 merge train/css
|
||||
git -C $repo merge --no-ff train/css -m "integrate train/css: panel z-index + overflow clip + mobile sidebar"
|
||||
# 解决冲突:
|
||||
# - sidebar.module.css(train/base 没改,应该无冲突)
|
||||
# - README.md(train/base 改了,train/css 的 #254 也改了 → 冲突)
|
||||
# - package.json(train/base 加了 unrun devDep,train/css 的 #254 可能加 mobile deps → 冲突)
|
||||
# - dsh.plugin.json(train/base 改了,train/css 的 #254 也改了 → 冲突)
|
||||
# - src/client/service.ts(train/base 迁移了 cordis,train/css 的 #254 可能改了 mobile 检测 → 冲突)
|
||||
pnpm install # 合并 package.json 后必须重装
|
||||
pnpm typecheck
|
||||
|
||||
# 2.4 merge train/git
|
||||
git -C $repo merge --no-ff train/git -m "integrate train/git: worktree RC8 forward-port + multi-repo selector"
|
||||
# 解决冲突:
|
||||
# - src/index.ts(train/base 改了 cordis 迁移,train/git 改了 worktree 注册 → 冲突)
|
||||
# - src/client/sidebar.module.css(train/css 改了 z-index/overflow/mobile,train/git 的 #285 可能改了 git 面板样式 → 冲突)
|
||||
# - package.json(train/git 的 #326 可能加了依赖 → 冲突)
|
||||
pnpm install
|
||||
pnpm typecheck
|
||||
|
||||
# 2.5 merge train/misc
|
||||
git -C $repo merge --no-ff train/misc -m "integrate train/misc: mermaid + layout-push + fs-search + markdown-images"
|
||||
# 解决冲突:
|
||||
# - AGENTS.md(train/base 改了,train/misc 的 #342 也改了 → 冲突)
|
||||
# - README.md(train/base + train/css 都改了,train/misc 的 #292 又改 → 三方合并冲突)
|
||||
# - src/client/Sidebar.tsx(train/misc 的 #310 改了 layout,train/css 的 #254 改了 mobile → 冲突)
|
||||
pnpm typecheck
|
||||
|
||||
# 2.6 全量验证
|
||||
pnpm typecheck
|
||||
pnpm test
|
||||
```
|
||||
|
||||
### Phase 3 — 推送与开 PR(主 agent)
|
||||
|
||||
```powershell
|
||||
# 3.1 推 mega-train 到 fork(不是 origin,因为 origin 是 omdsh-dev 上游)
|
||||
git -C $repo push fork merge/bug-pr-train
|
||||
|
||||
# 3.2 开 mega-PR(base = main,head = HuanLinOTO:merge/bug-pr-train)
|
||||
gh pr create `
|
||||
--repo omdsh-dev/DSH-better-sidebar `
|
||||
--base main `
|
||||
--head HuanLinOTO:merge/bug-pr-train `
|
||||
--title "merge: bug PR train (12 PRs, --no-ff to preserve authors)" `
|
||||
--body "Closes #345, #342, #341, #338, #336, #330, #326, #310, #292, #285, #278, #254
|
||||
|
||||
一次性合并 12 个 bug PR,使用 --no-ff 保留每个 PR 的原始 commits 和作者归属。
|
||||
|
||||
合并顺序与分簇:
|
||||
- Cluster A (base): #338 → #336 → #345
|
||||
- Cluster B (misc): #341 → #310 → #342 → #292
|
||||
- Cluster C (css): #330 → #278 → #254
|
||||
- Cluster D (git): #285 → #326
|
||||
|
||||
并行 subagent 在 4 个独立 worktree 中合并簇内 PR,主 agent 串行合并 4 个 train 子分支。
|
||||
|
||||
#285 author email 不匹配 GitHub,merge commit 加了 Co-authored-by: xxxg0001 trailer。
|
||||
|
||||
详细方案:docs/plans/2026-08-23-bug-pr-merge-train.md"
|
||||
|
||||
# 3.3 PR 创建后,13 个 PR(12 个原始 + 1 个 mega)会显示在 PR 列表
|
||||
# 等 CI 通过后用 --merge 合并(不要 squash):
|
||||
gh pr merge <mega-PR-number> --repo omdsh-dev/DSH-better-sidebar --merge
|
||||
# 合并后 12 个原始 PR 因 Closes 关键字自动 close
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 验证清单
|
||||
|
||||
### Phase 1 完成后(每个 subagent 自查)
|
||||
|
||||
- [ ] 簇内所有 PR 已 `--no-ff` 合并
|
||||
- [ ] `pnpm typecheck` 通过
|
||||
- [ ] git log 显示每个 PR 的原始 commits 保留
|
||||
- [ ] 输出 train 子分支的 HEAD SHA 给主 agent
|
||||
- [ ] 输出解决的冲突文件清单
|
||||
- [ ] 输出跨簇预期冲突点
|
||||
|
||||
### Phase 2 完成后(主 agent 自查)
|
||||
|
||||
- [ ] 4 个 train 子分支全部并入 `merge/bug-pr-train`
|
||||
- [ ] `pnpm install` 成功
|
||||
- [ ] `pnpm typecheck` 通过
|
||||
- [ ] `pnpm test` 通过(win32 平台已知失败项可记录,不算回归)
|
||||
- [ ] `git log --oneline d6d8db0..HEAD` 显示 12 个 merge commit + 各 PR 的原始 commits
|
||||
|
||||
### Phase 3 完成后
|
||||
|
||||
- [ ] mega-PR 在 omdsh-dev/DSH-better-sidebar 创建成功
|
||||
- [ ] PR body 包含 12 个 `Closes #N`
|
||||
- [ ] CI 通过(或失败项是已知非回归)
|
||||
- [ ] 用 `--merge` 合并(**不是 squash**)
|
||||
- [ ] 合并后 12 个原始 PR 全部 close
|
||||
- [ ] main 上 `git log --author=<每位作者>` 都能查到 commits
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险点与应对
|
||||
|
||||
| 风险 | 概率 | 应对 |
|
||||
|---|---|---|
|
||||
| Phase 1 某个 subagent 簇内冲突解决失败 | 中 | 主 agent 介入协助;最坏退回该簇重做 |
|
||||
| Phase 2 跨簇冲突比预期复杂(特别是 README 三方合并) | 高 | 手工缝合;以 #338 的 cordis 迁移为基底,叠加其他 PR 的章节 |
|
||||
| Phase 2 #326 缝合 #285 后逻辑不一致(worktree list + 子仓选择) | 中 | 优先保 #285 的 RC8 forward-port 完整性;#326 的 UI 适配其 API |
|
||||
| `pnpm test` 在 win32 平台已知失败 | 中 | 记录为已知非回归,不阻塞合并(参考 AGENTS.md "上游测试可能有 win32 平台假设失败") |
|
||||
| GitHub SSL 抖动(fetch 失败) | 低 | 重试 / 用 `gh pr diff` 替代 fetch / 走 fork remote |
|
||||
| `--merge` 合并 mega-PR 后 contributors 列表未立即更新 | 低 | GitHub 异步计算 contributors,等 24h;只要 commit author email 匹配即可 |
|
||||
| #285 的 `fix@local.invalid` 邮箱即使加 Co-authored-by 也不被 GitHub 识别 | 中 | 已用 noreply 邮箱作为 Co-authored-by,GitHub 会识别;最坏在 PR body 注明 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 时间预估
|
||||
|
||||
| 阶段 | 串行耗时 | 并行耗时 |
|
||||
|---|---|---|
|
||||
| Phase 0 准备 | 5 min | 5 min |
|
||||
| Phase 1 簇内合并 | 60 min | **20 min**(4 个 subagent 并行,取最慢的 git 簇) |
|
||||
| Phase 2 跨簇合并 | 30 min | 30 min(必须串行) |
|
||||
| Phase 3 推送 + PR | 5 min | 5 min |
|
||||
| **总计** | 100 min | **60 min** |
|
||||
|
||||
并行加速比 ≈ 1.7×(受 Phase 2 串行合并限制)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 回滚
|
||||
|
||||
```powershell
|
||||
# 任意阶段失败,回滚到 main
|
||||
git -C $repo checkout main
|
||||
git -C $repo branch -D merge/bug-pr-train train/base train/misc train/css train/git
|
||||
|
||||
# 清理 worktree
|
||||
git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-base" --force
|
||||
git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-misc" --force
|
||||
git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-css" --force
|
||||
git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-git" --force
|
||||
|
||||
# 删除本地 PR ref
|
||||
$prs = 345,342,341,338,336,330,326,310,292,285,278,254
|
||||
foreach ($n in $prs) { git -C $repo branch -D "pr-$n" }
|
||||
```
|
||||
|
||||
如果 mega-PR 已合并到 main 但发现严重回归:
|
||||
|
||||
```powershell
|
||||
# Revert mega-PR 的 merge commit(保留历史,不删除 PR 记录)
|
||||
gh pr merge <mega-PR-number> --repo omdsh-dev/DSH-better-sidebar --revert
|
||||
```
|
||||
@@ -1,82 +0,0 @@
|
||||
# 自由窗口(Free Window)设计文档
|
||||
|
||||
- 日期:2026-08-23
|
||||
- 版本:v0.16.0
|
||||
- 分支:`feat/free-window`
|
||||
- 状态:已实现(本文含实施偏差记录)
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
侧边栏的 tab 只能活在右面板 / 底面板的 pane 里。用户在主会话区域工作的同时想盯一个终端、一个文件或一个插件页面时,只能挤压面板宽度或来回切换 tab。
|
||||
|
||||
**目标**:把任一 tab 拖到主会话区域(conversation 列)上松开,它变成一个可自由移动/缩放的**自由窗口**,悬浮在会话区之上;随时可拖回侧边栏停靠或一键返回。
|
||||
|
||||
**非目标(KISS 排除项)**:
|
||||
|
||||
- 无四边/四角缩放(只有 SE 角)、无双击最大化;
|
||||
- 浮窗单 tab(无内嵌迷你标签栏);
|
||||
- 停靠不支持 edge split(只 center 合并进目标 pane);
|
||||
- 无窗口数量上限、无磁吸/网格对齐;
|
||||
- 窄视口(合并抽屉)禁用拖出手势(抽屉覆盖会话区,无处可放);右键菜单入口仍可用。
|
||||
|
||||
## 2. 交互设计(用户视角)
|
||||
|
||||
| 手势/入口 | 行为 |
|
||||
|---|---|
|
||||
| 按住 tab 拖到主会话区域 | 会话列出现虚线提示浮层「松开以在自由窗口中打开」;松开 → tab **移动**(pane 中移除)为浮窗,默认 390×780(手机竖屏比例;创建时按视口钳制)居中于松点、钳入视口 |
|
||||
| tab 右键菜单「移动到自由窗口」 | 同上,落点为会话列中心(右面板与底面板的 TabBar 共用组件,一处生效) |
|
||||
| 浮窗头部按住拖动 | 移动窗口(pointer capture + rAF 直写 DOM + 松手提交 store);拖到侧边栏 pane 上方时该 pane 高亮,松开即停靠(center 合并) |
|
||||
| 浮窗头部右键 | 菜单:「回到侧边栏」(停靠到激活 pane)/「关闭」 |
|
||||
| 浮窗右下角拖动 | SE 角缩放(≥320×200,锚定左上角,钳入视口) |
|
||||
| 点击浮窗任意处 | 置顶(层叠顺序 = `floats` 数组序,末位最上) |
|
||||
| 浮窗头部 X | 关闭该 tab(走 `closeTab` 正常流程:触发 `onClose`、释放终端 pty) |
|
||||
|
||||
刷新页面:浮窗随会话状态从 localStorage 原样恢复(tab + 几何)。
|
||||
|
||||
## 3. 状态模型(`src/client/state.ts`)
|
||||
|
||||
```ts
|
||||
interface FloatWindow {
|
||||
id: string // uid('float'),与 pane/tab/split 共享计数器
|
||||
tab: SidebarTab // 移动语义:浮窗拥有 tab,pane 不再持有
|
||||
x: number; y: number // 视口坐标(左上角)
|
||||
w: number; h: number
|
||||
}
|
||||
// SidebarState.floats: FloatWindow[](数组序 = 层叠序)
|
||||
```
|
||||
|
||||
新增纯 reducer:`floatTab`(两棵树定位 + 移除 + 清空折叠 + activePane 回落)、`moveFloat` / `resizeFloat`(钳制,no-op 返回原引用避免持久化搅动)、`raiseFloat`(移末尾,幂等)、`dockFloat`(目标 pane 缺省 activePane,stale 回落右树首叶)、`closeFloatByTab`、查找助手 `floatWithTab` / `floatById`。
|
||||
|
||||
**既有查找路径全部扩展到 floats**(一致性关键):`tabOpenIn`、`patchTab`(updateTab,浏览器浮窗导航持久化 URL)、`openTabInActivePane` 的 id 安全网(命中 → raise)、`reconcileAgentTerminals`(浮窗 agent 终端随宿主列表消失则整窗移除)、`maxCounterId`(`float:N` 前缀参与 uid 播种,防 reload 碰撞)。
|
||||
|
||||
**sanitize**:`floats` 缺省 → `[]`;逐条宽容校验(id/tab 复用与叶子共享的 `sanitizePersistedTab`;几何非有限数或重复 id → 丢弃该条,不动整体布局);几何钳入当前视口;diff/ephemeral tab 不持久化;`explorer` 旧类型在浮窗内同样迁移为 editor home。
|
||||
|
||||
## 4. 实现要点
|
||||
|
||||
- **拖出检测**(`Sidebar.tsx`,document 捕获层):`dragover`/`drop`/`dragend` 捕获监听,以 `body[data-dsh-tab-dragging']`(TabBar 维护的 tab 拖拽标记)门控;目标在 `[data-dsh-panel-host]` 内则无视(pane 的 split/merge 自治);在 conversation 列(`#root [data-slot="conversation"]` 父列实时 `getBoundingClientRect()`)内 `preventDefault()`(武装 drop)+ 显示提示浮层(同值 rect 跳过 setState,避免 60Hz 重渲染);`drop` 读 `application/x-dsh-tab` payload 调 `floatTab(tabId, clientX, clientY)`。
|
||||
- **FreeWindow**(新文件 `FreeWindow.tsx`):头部/SE 角 pointer capture 拖拽——照抄面板拖拽的「rAF 直写 `style.left/top/width/height` + 松手提交 store + pointercancel/lostpointercapture 取 last-applied 兜底」模式;拖动中每帧对 `[data-dsh-pane]` 矩形命中检测,命中 pane 直接 DOM 打 `data-dsh-float-dock-over` 高亮(不经 React state,避免拖拽期全树重渲染);松手命中 → `dockFloat(paneId)`,未命中 → `moveFloat`。jsdom 无 pointer capture API,取 `setPointerCapture?.()` 容忍模式(与 `EditorHost.tsx` 同款)。
|
||||
- **服务层**(`service.ts`):`closeTab`/`activateTab` 的浮窗分支;`openTab` 的 dedupeKey/id 匹配与生命周期分类纳入 floats——**聚焦浮动 tab = 置顶窗口**,且内容型打开不触发面板自动展开(浮窗与面板开合无关,恒可见);`features` 追加 `'floatWindows'`。
|
||||
- **渲染**:浮窗渲染在 `[data-dsh-panel-host]` 内(视口坐标、免疫壳层 transform 劫持),z-index 42(面板 40 之上、开关簇 45 与 DSH 浮层之下);内容复用 `renderTab`/`TabContent`(每 tab RenderBoundary、memo),`visible` 恒 true。
|
||||
- **样式**(`sidebar.module.css`):全令牌驱动(`--dsw-alias-bg-layer-1` 表面、`--dsw-alias-border-l2` 边、`--dsw-shadow-lv3` 阴影、accent 虚线提示);窗口盒无 transition(拖拽逐帧直写);头部 `-webkit-app-region: no-drag`。
|
||||
- **i18n**:`moveToFreeWindow` / `floatDropHint` / `dockToSidebar` 三键,19 语言词典全量同步(zh/en/ja 一致性由 `tests/locales.spec.ts` 守护)。
|
||||
|
||||
## 5. 测试
|
||||
|
||||
- `tests/state.spec.ts`(free windows 块):reducer 语义(移动/清空折叠/钳制/幂等/停靠回退/未知 id 严格 no-op)、查找路径扩展、agent 终端浮窗移除、sanitize(缺省/逐条丢弃/几何钳制/uid 播种/round-trip)。
|
||||
- `tests/service.spec.ts`(free windows 块):dedupe 聚焦浮窗 = 置顶不展开面板、closeTab 关窗 + onClose、activateTab 置顶 + onActivate。
|
||||
- `tests/free-window.spec.tsx`(新,jsdom 全 Sidebar 挂载):拖出检测(提示出现/消失、drop 落位、无武装 drop 不浮动)、右键菜单浮动落点、头部拖动(rAF 直写 + 提交)、SE 缩放、拖到 pane 停靠(高亮断言)、X 关闭、reload 恢复。
|
||||
- `tests/e2e/float-window.e2e.ts`(新,Playwright 真实 `dsh web`):右键浮动 → 拖动移动 → reload 恢复 → 右键停靠回归;HTML5 拖出(提示浮层中拖断言)→ 终端浮窗 + 标签栏移除;全程无 crash 标记。
|
||||
|
||||
## 6. 已知取舍
|
||||
|
||||
- 浮窗头部同时承担「移动」与「拖回停靠」——统一为一种 pointer 手势(落点命中 pane 即停靠,否则移动),未用 HTML5 DnD(浏览器接管后 pointer 流中断,与顺滑移动冲突)。
|
||||
- 停靠只支持 center 合并:从浮窗拖回不提供 edge split(KISS;pane 内拖拽仍支持全部分区操作)。
|
||||
- 浮窗内 tab 被 `openTab` 再次打开时聚焦的是**浮窗**(置顶),不会把 tab 拉回 pane——窗口就是 tab 的「pane」。
|
||||
- 窄视口下拖出手势禁用,但右键菜单入口保留(窗口钳入小视口仍可用)。
|
||||
|
||||
## 7. 实施偏差记录
|
||||
|
||||
- **portal 事件劫持拖拽(e2e 发现,jsdom 不复现)**:浮窗头部内嵌 `Menu`(portal 渲染到 `document.body`),但其 React 合成事件仍沿 React 树冒泡进头部的 `onPointerDown`——右键菜单行的**左键点击**会被误判为头部拖拽:`preventDefault` 吞掉行的兼容 mousedown,`setPointerCapture` 把后续 pointer 事件重定向到头部,行的 click 永不触发(「回到侧边栏」与 X 关闭在真实浏览器中双双失效)。修复:头部/缩放把手的 `onPointerDown` 加**同源守卫**——`event.currentTarget.contains(event.target)` 为假(portal 后代)或目标在按钮内(X)时直接返回。回归测试:`tests/free-window.spec.tsx`「a pointerdown on the portaled header menu must not start a header drag」。教训:任何承载 pointer 拖拽的表面,若其 React 子树里有 portal 覆盖层(菜单/弹窗),必须做 DOM 包含守卫——jsdom 合成事件不走这条路径,只有真实浏览器 e2e 能抓到。
|
||||
- **e2e tarball 选择修复**(`scripts/e2e-mount.sh`):缺省 tarball 解析原为 `ls | head -1`(字典序),仓库根残留旧版本号 tarball 时会把冒烟挂到**过期产物**上(本次 0.15.0 劫持了 0.16.0 的 lane,菜单项整体消失)。改为 `ls -t` 取 mtime 最新并在多候选时告警。
|
||||
- e2e 细节修正:conversation 列须取 `[data-slot="conversation"]` 的**父列**(slot 包裹器零尺寸,`boundingBox()` 返回 null);reload 后需重跑 DSH 首启 takeover 清场(takeover 在 settings join 后才挂载,须先有界等待再清);提示浮层断言按 2 计(`[class*="floatDropHint"]` 子串命中浮层与其文案 label 两个节点);拖拽后 reload 前留 500ms 让 store 的 200ms 防抖持久化落盘。
|
||||
- 其余与本文一致。
|
||||
@@ -1,58 +0,0 @@
|
||||
# Markdown 预览内嵌 HTML 渲染 + 目录大纲(TOC)设计
|
||||
|
||||
> 日期:2026-08-24
|
||||
> 状态:已实现
|
||||
> 范围:`dsh-better-sidebar` 插件,Markdown 文件预览(`TextEditor.tsx` preview 模式);Sidechat 与其他渲染面**零改动**
|
||||
> 基线:复用 mermaid 预览(PR #164)确立的「检测切分 + 单次 MarkdownText 解析 + commit 后 DOM 手术」架构与安全姿势
|
||||
|
||||
## 1. 背景与问题
|
||||
|
||||
DSH 宿主的 `MarkdownText` 出于聊天安全把 raw HTML 按字面文本渲染(协议白名单、相对链接禁用、图片仅绝对 http(s))。这对聊天是对的,但对**文件预览**是灾难:GitHub 风格的 README(本仓库自己的 README 即典型)大量使用内嵌 HTML——`<div align="center">` 徽章墙、`<details>` 折叠块内嵌 markdown、表格单元格里的 `<br/>`/`<sub>`/`<img>`、`<video>`/`<picture>`——预览时全部显示为源码文本。
|
||||
|
||||
用户需求(确认记录):覆盖 Markdown 文件预览;块级 + 内联 HTML 都做;安全模型选 DOMPurify 白名单内联渲染(非沙箱 iframe);TOC 选浮动按钮 + 弹出面板;Sidechat 暂不动;**不更换渲染器**(保住 MarkdownText 的 shiki 高亮 + KaTeX 数学 + GFM 表格)。
|
||||
|
||||
## 2. 方案
|
||||
|
||||
### 2.1 检测与切分:`src/client/markdown-html.ts`(纯函数)
|
||||
|
||||
- `splitHtmlBlocks(text)`:行扫描器把文档切成 `markdown | html` 段。HTML 段 = 围栏外的**块级标签行**(CommonMark type-6 名单 + `<summary>`,开或闭标签)开头、延伸到空行的连续行;`<!-- -->` 注释段在含 `-->` 的行结束。**围栏识别复用 `mermaid-blocks.ts` 导出的 fence 助手**——```html 围栏内的 `<div>` 示例不会被误切。段边界的空行是分隔符,被丢弃。
|
||||
- `analyzeHtmlSegment(source)`:段内标签 tokenizer(跳过注释,void/自闭合标签不进栈,错配闭标签按 HTML 解析器的隐式闭合弹出)归约为有序 parts:`html`(平衡的原始片段,渲染为消毒叶子)/ `open`(未闭合开标签 + 原始 attrs,成为包裹后续段的容器)/ `close`(多余闭标签,弹出一层)。
|
||||
- `collectReferenceDefinitions(segments)`:从 markdown 段收集 `[label]: dest` 行。**每个 markdown 段渲染时追加密全文档定义**——跨 HTML 段的 `[text][id]` 语义保真(追加副本在已含定义的段内因 first-match-wins 而惰性)。这是 mermaid 预览「单次解析」约束(引用定义跨 fence 解析,e2e 钉死)在切分渲染下的等价物。
|
||||
- `analyzeMarkdownHtml(text)`:整体入口,含 `hasInlineHtml` 廉价正则门(fence 内误报只多走增强路径,内联 pass 本身跳过代码块,无行为差异)。
|
||||
|
||||
### 2.2 渲染:`src/client/MarkdownHtml.tsx`
|
||||
|
||||
- **安全管线**(三个入口共用):块叶子 / 内联文本 / 包裹开标签 attrs 全部过 DOMPurify(显式 `FORBID_TAGS: script/style/iframe/object/embed/form/input/button/select/textarea/meta/link/base/frame/frameset/applet` + `FORBID_ATTR: srcdoc/formaction`,叠加在默认白名单之上);消毒后 DOM 遍历强制 `<a>` → `target=_blank rel=noopener noreferrer`;本地 `img/video/audio/source` 的 `src` 经 `resolveLocalMediaDest`(从 `markdown-images.ts` 抽出的共享助手)重写为 `/sidebar/file` 会话媒体 URL——与 markdown 图片重写同一信任围栏。包裹标签的 attrs 走「迷你文档消毒再读回」;`class/for` 映射 React 名,`style` 丢弃(React 需对象,不值得引入 CSS 解析器);整标签被禁时容器透明(子内容照常渲染)。
|
||||
- **`MarkdownDocument` 段渲染状态机**:markdown 段 → `MarkdownSegment`;平衡 html 段 → `dangerouslySetInnerHTML` 叶子(`[data-dsh-html-segment]`);`open`/`close` 部件驱动一个**跨段持久的帧栈**——`<details>` 开段把后续段降为自己的子元素,`</details>` 段弹栈(GitHub 线性 HTML 输出的同款嵌套)。顶层多余闭标签渲染为无(消毒器/解析器本就会丢弃);文档结束仍开着的帧按浏览器解析器语义收口。消毒在 `useMemo` 内按段缓存。
|
||||
- **`MarkdownSegment` 内联替换**:段内含 mermaid fence 走现有 `LazyMermaidMarkdown`(chunk 逻辑不变),否则 `MarkdownText`;`useLayoutEffect` + **MutationObserver**(微任务去抖、幂等、跳过 `code/pre/.md-code-block/[data-mermaid-processed]/[data-html-inline]`)把含标签样式文本的文本节点整体消毒后包进 `<span data-html-inline>` 替换——纯散文(`a < b`)消毒后无元素子节点则原样保留;observer 覆盖懒 chunk 迟挂载、shiki 异步高亮等迟到内容。与 mermaid 换装同款「React 持有宿主树、只动叶子」姿势。
|
||||
- **接线(`TextEditor.tsx`)**:`analyzeMarkdownHtml` 发现块级或内联 HTML 才走 `MarkdownDocument`;**纯 markdown 文档走原路径原样**(无 HTML → 单次 `MarkdownText`/mermaid chunk,现有 e2e「单次解析」守卫继续成立——种子 md 文件不含 HTML,断言一字未改)。
|
||||
|
||||
### 2.3 TOC:`src/client/md-toc.tsx`
|
||||
|
||||
- 零高度 sticky 条(`position: sticky; top: 0; height: 0; pointer-events: none`,不占布局、滚动常驻)+ 右上角按钮 `[data-dsh-md-toc]`(≥3 个标题才出现)+ 弹出面板 `[data-dsh-md-toc-panel]`(按层级缩进、独立滚动、Esc 关闭)。
|
||||
- 标题从**渲染后的 DOM** 收集(h1–h6,含 HTML 段内标题),MutationObserver 重扫(签名比对防重渲染循环)。
|
||||
- 跳转:展开 `closest('details:not([open])')` → `scrollIntoView({behavior:'smooth'})` → CSS 动画高亮 1.2s。
|
||||
- **⚠️ 关键实现陷阱(本次踩坑)**:MdToc **不能**通过传入父容器 ref 定位——React 的 layout 阶段子先于父,子组件的 `useLayoutEffect` 执行时父 div 的 ref **尚未挂上**(实测 `containerRef.current === null`,且依赖数组恒定导致 effect 永不重跑,按钮永不出现;jsdom 单测传预填 ref 对象会掩盖此 bug,只有真实挂载 e2e 能抓到)。修复:MdToc 用**自身 bar div 的 ref**(自身 effect 前必然已挂)取 `parentElement` 作为容器——挂载契约「渲染为目标滚动容器的直接子元素」写入组件文档;bar 常驻渲染(零高度无副作用)以保证 ref 恒在。
|
||||
|
||||
### 2.4 依赖与产物
|
||||
|
||||
- `dompurify ^3.4`(已是 mermaid 的传递依赖,提升为直接依赖后 pnpm 去重单版本,零额外体积)随 editor chunk 内联,未新增 chunk。
|
||||
- 市场清单守卫合规(dependencies 无 `cordis`、无 install 脚本)。
|
||||
|
||||
## 3. 测试
|
||||
|
||||
- `tests/markdown-html.spec.ts`:切分器(21 例:块级标签成段/空行终止/注释段/闭标签段/围栏免疫/CommonMark 恢复/README details 三段形)+ tokenizer 平衡分析(9 例)+ 引用定义收集 + 内联门。
|
||||
- `tests/markdown-html-render.spec.tsx`:真实 `MarkdownText` 渲染——消毒叶子(script 剥除、a 加固、本地 src 重写)、details 跨段嵌套(fence 在 details 元素内、后续段是兄弟)、跨段引用定义解析、禁用标签容器透明、内联替换(表格单元格 `<br/>` 生效、代码块与 `a < b` 不动、内联本地 img 重写)。
|
||||
- `tests/md-toc.spec.tsx`:阈值门、标题收集、跳转(mock `scrollIntoView`、折叠祖先展开、高亮清除)、observer 迟到标题、Esc 关闭。
|
||||
- `tests/e2e/mount.e2e.ts`:新增 `readme-style.md` 种子(徽章 div 含 script、details 包 fence+标题、表格内联 `<br/>`、跨段引用定义)+ 探针(徽章 img 真实元素、无 script、details 嵌套、内联 br、引用解析、TOC 按钮→面板→跳转展开)。**现有 mermaid 探针与断言一字不动**(纯 markdown 文档零回归证明)。
|
||||
|
||||
## 4. 已知取舍(明确不做)
|
||||
|
||||
- GFM 脚注跨段不保(宿主本就把脚注渲染为纯文本);`srcset` 不重写(实际只有远程 URL);HTML 实体不解码;标题锚点 permalink、TOC 滚动联动高亮不做;Sidechat 不动;不换渲染器。
|
||||
- CommonMark HTML 块类型 1(script/pre/style/textarea 按闭标签终止)与类型 7(单行完整标签)不做专门处理——块级名单 + 空行终止已覆盖 README 级真实用法。
|
||||
|
||||
## 5. 后续修正记录(bug 反馈)
|
||||
|
||||
- **代码块表头在预览中悬浮过长**:DSH `CodeBlock` 的 `bannerWrap` 是 `position: sticky; top: 0; z-index: 6`——在聊天里每个消息块拥有自己的滚动容器,表头粘住是对的;但在文件预览中所有 fence 共享一个 `.editorMd` 滚动容器,表头会钉在视口顶部直到整个块滚过,读作悬浮在文档上。修正:预览容器内对非 mermaid 的 `.md-code-block` 首子元素(即 `bannerWrap`)恢复 `position: static; z-index: auto`(mermaid 块已被 `data-mermaid-processed` 排除,其自身 chrome 取代了表头)。见 `src/client/sidebar.module.css` `.editorMd :global(.md-code-block:not([data-mermaid-processed])) > :global(div):first-child`。
|
||||
- **TOC 面板被代码块表头压住**:`.tocBar` 原 `z-index: 3`,低于代码块表头的 `6`,弹出面板(位于 bar 的层叠上下文内)会被 sticky 表头盖住。修正:`.tocBar` 提升到 `7`(仍低于传送门选择弹窗的 `60` 与 mermaid 模态的 `1000`)。
|
||||
- **TOC 只能再点按钮收起**:新增文档级 `pointerdown` 监听,点击落在按钮与面板之外(预览空白处)即收起;按钮/面板豁免,保证按钮仍可翻转。见 `src/client/md-toc.tsx`(与 Esc 关闭共用同一 effect)。
|
||||
@@ -1,173 +0,0 @@
|
||||
# 固定终端(Pinned Terminals)设计
|
||||
|
||||
> 2026-08-26 · 状态:设计已评审,待实施
|
||||
> 需求:现所有 Tab 跟随会话,切换会话后终端仍留在原会话(不可见但 PTY 保活)。
|
||||
> 希望右键终端 Tab 可「固定」→ 二级菜单「固定到工作区 / 固定到全局」,固定后切换会话不消失。
|
||||
|
||||
## 0. 决策记录(已确认)
|
||||
|
||||
| 决策点 | 结论 |
|
||||
|---|---|
|
||||
| 可 pin 的终端类型 | **UI 终端 + Agent 终端均可**(Agent 终端被 reconcile 移除时保留为「已断开」tab) |
|
||||
| 跨会话呈现形态 | **独立「固定区」列表**(PinnedRail,面板顶部紧凑条),不投影进目标会话 tab 条 |
|
||||
| Menu 二级菜单 | `ui-primitives` 的 `Menu` **原生支持 `submenu`**(`MenuItem.submenu`,hover/focus 展开右侧子卡),零改动可用 |
|
||||
|
||||
## 1. 现状关键事实(实施依据)
|
||||
|
||||
- **状态模型**(`src/client/state.ts`):`SidebarStore.bySession: Map<sessionId, SidebarState>`;`setSession()` 整体换快照。`SidebarTab = { id, type, title, path?, diff?, meta? }`。终端 tab:`type: 'terminal'`。
|
||||
- **终端两类**(id 前缀判别,无需新字段):
|
||||
- `terminal:<uuid>` —— UI 终端(用户 + 菜单开,`createTab` 铸造)
|
||||
- `agent:<uuid>` —— Agent 终端(模型 `terminal_create` 工具,`reconcileAgentTerminals()` 跟宿主 WS 列表同步;`isAgentTabId()` / `agentUuidOf()` / `agentTabId()` 已存在)
|
||||
- **PTY 保活**(`src/client/TerminalView.tsx` 卸载三分支):tab 关闭 → `{type:'close'}` 杀 PTY;**切会话 → `{type:'park'}` 宿主机无限保活**;同会话重挂载 → bare drop(30s 重连宽限)。Agent 终端 bare drop 已无限保活,无需 park。
|
||||
- **关闭路径**(`src/client/Sidebar.tsx` `actions.closeTab`):service `closeTab` + 终端额外 `api.ptyClose({sessionId,cwd},tabId)` / `api.agentPtyClose(uuid)`。
|
||||
- **持久化**:`localStorage` `dsh-sidebar:v1:<sessionId>`,`sanitizeState` 白名单校验;`sanitizePersistedTab` 逐 tab 校验(未知 type → OrphanedTab 占位)。
|
||||
- **TabBar 右键菜单**(`src/client/TabBar.tsx`):flat `items`,`onSelect(id)` 分发。Menu `MenuEntry = MenuItem | MenuSeparator | MenuLabel`。
|
||||
- **宿主会话切换**:Sidebar 只读 `ctx.sessions.list` 的 `current`(`store.setSession(current)` 跟随);「跳转到子代理会话」先例 = `subagentJumpRef` 记录目标 + 宿主列表侧点击导航,effect 在 `sessionId === pending` 时执行后续动作(PinnedRail 点击沿用此模式)。
|
||||
- **cwd 来源**:`sessionList.byId[sessionId]?.cwd`(`Sidebar.tsx:303` 先例)。
|
||||
|
||||
## 2. 数据模型
|
||||
|
||||
```ts
|
||||
// state.ts — SidebarTab 新增可选字段
|
||||
interface SidebarTab {
|
||||
// ...现有字段
|
||||
/** 固定标记(v0.x+)。scope=工作区|全局;homeCwd=pin 时刻会话 cwd(workspace 可见性判据)。 */
|
||||
pin?: { scope: 'workspace' | 'global'; homeCwd?: string }
|
||||
}
|
||||
```
|
||||
|
||||
- `sanitizePersistedTab` 白名单校验 `pin`(`scope` 枚举值校验,`homeCwd` string 可选;非法值降级为无 pin,不丢 tab)。缺字段 = 未 pin,旧持久化状态向后兼容。
|
||||
- pin 元数据**随宿主会话 state 持久化**,无独立 localStorage 注册表(YAGNI)。
|
||||
- **权威副本唯一**:pinned tab 只存在于宿主会话(home session)的 state 里,其他会话仅跨会话渲染——现有 park/保活语义零改动。
|
||||
|
||||
## 3. 跨会话解析层(新模块 `src/client/pinned.ts`,纯函数)
|
||||
|
||||
```ts
|
||||
/** 该 pinned tab 对 viewer 会话是否可见。cwd 未知(undefined)时保守可见,避免水合期闪烁消失。 */
|
||||
export function pinnedVisibleTo(
|
||||
tab: SidebarTab,
|
||||
viewer: { sessionId: string; cwd: string | undefined },
|
||||
): boolean
|
||||
|
||||
/** 从所有缓存会话的 state 收集对 viewer 可见的 pinned 终端 tab。
|
||||
* 返回 { tab, homeSessionId }[],按 pin 先后稳定排序;排除 viewer 自己会话的
|
||||
* (那些本就在 tab 条上,避免双渲染)。 */
|
||||
export function collectPinnedTabs(
|
||||
bySession: ReadonlyMap<string, SidebarState>,
|
||||
viewer: { sessionId: string; cwd: string | undefined },
|
||||
): { tab: SidebarTab; homeSessionId: string }[]
|
||||
```
|
||||
|
||||
可见性规则:
|
||||
|
||||
| pin.scope | 可见条件 |
|
||||
|---|---|
|
||||
| `global` | 任意会话(不同 cwd 也可见) |
|
||||
| `workspace` | `viewer.cwd === tab.pin.homeCwd`(两者皆 undefined 视为匹配;仅 viewer.cwd 未知时保守可见) |
|
||||
|
||||
`SidebarStore` 新增只读方法:
|
||||
|
||||
```ts
|
||||
/** 当前所有会话 state 的只读视图(供 collectPinnedTabs)。 */
|
||||
getSessionStates(): ReadonlyMap<string, SidebarState>
|
||||
```
|
||||
|
||||
渲染期解析,不写跨会话投影——每会话 state 保持纯净,reduce/reduceFor 语义不变。store 任意会话更新已全局 notify,rail 随 uSES 自动重渲染。
|
||||
|
||||
## 4. UI:PinnedRail(新组件 `src/client/PinnedRail.tsx`)
|
||||
|
||||
- **挂载**:`Sidebar.tsx` 内、右面板 tab 区之上(右/底面板共享一条,pinned tab 不归属任何树)。仅当可见 pinned ≥ 1 时渲染。稳定寻址面 `[data-dsh-pinned-rail]`、`[data-dsh-pinned-tab="<tabId>"]`。样式走 CSS Modules + `--dsw-alias-*` 令牌(§8 皮肤契约),hairline 分隔。
|
||||
- **条目**:pin 图标(📌 风格 SVG,icons.tsx 新增 `IconPinOutline16`)+ 终端类型图标(Agent 终端用机器人/思考图标,UI 终端用终端图标)+ 标题 + tooltip(`类型 + 范围 + 来源 cwd`,如「Agent 终端 · 固定到工作区 · D:\proj\x」)。
|
||||
- **交互**:
|
||||
- **点击**:当前会话含该 tab id → `activateTab`;否则记录 `pinnedJumpRef`(沿用 subagentJumpRef 模式)→ 触发宿主导航到 home 会话 → effect 命中后 `reduceFor` 激活 tab(面板折叠则展开)。
|
||||
- **右键菜单**(复用 Menu portal):`取消固定` / `关闭终端`。
|
||||
- **中键**:关闭(同右键关闭)。
|
||||
- **关闭语义**:走 home 会话路径——`reduceFor(homeSessionId, closeTab)` + `api.ptyClose({sessionId: homeSessionId, cwd: homeCwd}, tabId)` / `api.agentPtyClose(uuid)`。正处 home 会话时退化为现有 closeTab 路径。关闭同时清 pin(tab 消失即 pin 消失,无需单独解 pin 步骤)。
|
||||
|
||||
## 5. TabBar 右键菜单扩展(`src/client/TabBar.tsx`)
|
||||
|
||||
终端类型 tab(`tab.type === 'terminal'`,含 UI 与 Agent)菜单项变为:
|
||||
|
||||
```
|
||||
移动到自由窗口
|
||||
固定 ▸ ← 未 pin;MenuItem.submenu
|
||||
├ 固定到工作区
|
||||
└ 固定到全局
|
||||
取消固定 ← 已 pin(替换「固定 ▸」行)
|
||||
─────────────
|
||||
关闭 / 关闭其他页签 / 关闭左侧 / 关闭右侧
|
||||
```
|
||||
|
||||
- 菜单 label 按终端类型区分:`固定终端`(UI)/ `固定 Agent 终端`(Agent)。
|
||||
- TabBar 新增 props:`onPinTab(tabId: string, scope: 'workspace' | 'global' | null): void`(null = 取消)。`isPinned` 从 tab 对象自身读(`tab.pin !== undefined`),无需新 prop。
|
||||
- pin 实现(Sidebar 层):`store.reduce(s => patchTab(s, tabId, { meta: ... }))` 不够——`patchTab` 只 patch title/path/meta。**新增 reducer `setTabPin(state, tabId, pin | null)`**(纯函数,walk 两树 + floats,与 patchTab 同构),pin 时从当前会话 cwd 快照 homeCwd。
|
||||
|
||||
## 6. 生命周期与安全
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 关闭 pinned tab(tab 条 X 或 rail 关闭) | 清 pin + 正常关闭 + 杀 PTY(home 路径) |
|
||||
| Agent 终端被 reconcile 移除(宿主 uuid 消失) | `reconcileAgentTerminals` **豁免 pinned tab**:保留 tab,标题渲染时追加 `(已断开)`(不改持久化 title,reconcile uuid 匹配不受影响)。xterm 重连失败走现有 fatal banner |
|
||||
| UI 终端 PTY 死亡 | 现有行为(xterm 显示退出输出);pinned 不改变重连/重开语义 |
|
||||
| 取消固定 | 仅清 `tab.pin`;tab 留在 home 会话 tab 条,切走恢复旧行为 |
|
||||
| 刷新/重载 | pin 随会话 state 持久化恢复;rail 从各会话缓存 state 重建 |
|
||||
| 底部面板/窄屏迁移 | pinned tab 若在 bottomSplits,`migrateBottomTabs` 照常迁移(pin 语义不受影响) |
|
||||
| 自由窗口中的 pinned tab | 允许;rail 点击切回 home 会话后 `raiseFloat`(floatWithTab 命中走置顶,openTabInActivePane 已有此分支) |
|
||||
|
||||
## 12. 实施偏差记录
|
||||
|
||||
### M3:断开信号收敛
|
||||
|
||||
设计 §6 原表「Agent 终端被 reconcile 移除」行提到「标题渲染时追加 `(已断开)`」。实施时收敛为:**M3 只做豁免,断开的用户可见信号由 xterm 现有重连失败 banner 承担**。原因:rail/tab 标题渲染处无法知道宿主列表(`reconcileAgentTerminals` 的 uuid 匹配是 state 级的,渲染层不持有该信息),改 meta 会写持久化(违反「reconcile uuid 匹配不受影响」)。xterm 的 WS 连接 1011/超时已有 fatal UI,足以承担断开信号。设计 §6 表相应行实施时修正为「banner 承担断开信号」。
|
||||
|
||||
### M4:内联虚拟 Tab(取代 PinnedRail)
|
||||
|
||||
设计 §3 原描述「PinnedRail 组件——面板顶部的紧凑条,渲染其他会话的 pinned tab」。实施时改为**内联虚拟 Tab**:pinned tab 作为虚拟 `SidebarTab` 注入到当前会话 split tree 的第一个 leaf 的 `tabs` 数组尾部,与普通 tab 并排在 TabBar 中渲染。
|
||||
|
||||
**变更原因**:用户反馈「固定的终端是永远显示在 Tabs 栏处,而不是单独有一个地方让我点击后回到那个会话」。PinnedRail 是面板顶部的独立条,点击跳回宿主会话——这两个设计点都被否定。
|
||||
|
||||
**新设计**:
|
||||
- **虚拟 Tab 注入**:`injectPinnedIntoTree(state.splits, pinnedVirtualTabs, activePinnedTabId)` 将虚拟 tab 追加到第一个 leaf,并在 `activePinnedTabId` 设置时覆盖 leaf 的 `active`
|
||||
- **就地激活**:点击虚拟 tab 设 `activePinnedTabId`(本地 state),TerminalView 用 home session 的 scope(sessionId + cwd)连接宿主 PTY 的 WS(`/sidebar/ws/terminal?sessionId=<home>&tab=<originalId>`),不跳转会话
|
||||
- **effectiveTabId**:`TabContent` 新增 `effectiveTabId` prop,将虚拟 tab 的原始 id 传给 descriptor component(TerminalView 的 `tabId` 参数),虚拟 id 仅作 React key
|
||||
- **不可拖拽**:TabBar 检测 `isPinnedVirtualTab(tab)` → `draggable={false}`,跳过 drop handler
|
||||
- **右键菜单精简**:pinned 虚拟 tab 右键只有 Unpin / Close(无 float / close-others / close-left / close-right)
|
||||
- **reduceFor 不 notify**:`pinnedRevision` state bump 替代 `railRevision`,force `pinnedEntries` useMemo 重算
|
||||
|
||||
## 7. 测试计划(Unit + 组件,vitest)
|
||||
|
||||
| 文件 | 覆盖 |
|
||||
|---|---|
|
||||
| `tests/pinned.spec.ts`(新) | `pinnedVisibleTo` 全组合(global/workspace × cwd 匹配/不匹配/双方 undefined/单方 undefined);`collectPinnedTabs` 跨会话收集、排除 viewer 自身会话、稳定排序、home 会话 id 携带 |
|
||||
| `tests/state.spec.ts`(追加) | `sanitizePersistedTab` 接受合法 pin / 拒绝非法 scope 降级 / 缺字段兼容;`setTabPin` reducer(设置/取消/未知 tabId no-op/浮动窗口 tab);`reconcileAgentTerminals` 豁免 pinned agent tab(uuid 消失仍保留)+ 未 pin 的照旧移除 |
|
||||
| `tests/tab-bar-context-menu.spec.tsx`(追加) | 终端 tab 出现「固定 ▸」子菜单(UI 与 Agent 文案区分);非终端 tab 不出现;已 pin 显示「取消固定」;onPinTab 回调参数正确 |
|
||||
| `tests/pinned-rail.spec.tsx`(新) | rail 渲染条件(0 个不渲染);类型图标与 tooltip;点击切回 home 会话(reduceFor 激活);右键取消固定/关闭;「已断开」后缀渲染 |
|
||||
|
||||
回归:`pnpm typecheck && pnpm test`;终端相关改动加跑 `tests/agent-terminal-reconcile.spec.ts`、`tests/terminal-*.spec.ts`、`tests/bottom-auto-terminal.spec.tsx`。
|
||||
|
||||
## 8. i18n(19 语言词典同步,遵循 locales-*.ts 模式)
|
||||
|
||||
新增 key:`pinTerminal` / `pinAgentTerminal` / `pinToWorkspace` / `pinToGlobal` / `unpinTerminal` / `pinnedTerminalTooltip`(类型/范围/cwd 插值)/ `terminalDisconnected`。
|
||||
zh-CN 基准 + en;ja 必须同步(better-locale 契约:缺 ja key 回退 en)。其余语言沿用现有词典文件的同步方式。
|
||||
|
||||
## 9. 文档同步
|
||||
|
||||
- `README.md` / `README_EN.md`:功能清单加「固定终端」
|
||||
- `AGENTS.md` §3.4 内置 tab 表:terminal 行补充 pin 能力说明;§7 陷阱表视情况加「pinned tab 跨会话解析在渲染期,勿在 reducer 里投影」
|
||||
- 实施偏差记录在本文档追加
|
||||
|
||||
## 10. YAGNI(明确不做)
|
||||
|
||||
- ❌ 独立 localStorage pin 注册表
|
||||
- ❌ pinned tab 投影进目标会话 tab 条(独立固定区已选定)
|
||||
- ❌ pin 非终端 tab
|
||||
- ❌ 宿主机(host 半)改动——PTY 保活现有逻辑完备(park + agent 无限保活)
|
||||
- ❌ pin 数量上限(终端本身有 `TERMINAL_LIMIT` 配额兜底)
|
||||
|
||||
## 11. 风险与缓解
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|---|---|
|
||||
| rail 点击需触发**宿主**会话切换(非 sidebar store) | 沿用 subagentJumpRef 模式;PinnedRail 点击项渲染为宿主会话列表可识别的导航(或经 `ctx.sessions` 可用 API),effect 在 sessionId 命中后激活 tab。实施时先验证该链路(PoC 先行) |
|
||||
| 「已断开」标题与后续 reconcile push 兼容性 | 后缀在**渲染期**追加,不改持久化 title;reconcile 按 uuid 匹配不受影响 |
|
||||
| bySession 未缓存的会话(从未加载)其 pinned tab 不可见 | 接受:会话 state 在首次切换时 loadState 进缓存;rail 只覆盖本次运行内访问过的会话 + 当前会话。如需全覆盖需扫 localStorage 全量 key(复杂且慢),YAGNI |
|
||||
@@ -1,108 +0,0 @@
|
||||
# 固定终端(Pinned Terminals)实施计划
|
||||
|
||||
> 2026-08-26 · 设计文档:[2026-08-26-pinned-terminal-design.md](2026-08-26-pinned-terminal-design.md)
|
||||
> 仓库:dsh-better-sidebar · 分支策略:`feat/pinned-terminal`(仓库硬约束:代码改动必须走 PR)
|
||||
|
||||
## 里程碑总览
|
||||
|
||||
| # | 里程碑 | 产出 | 验证 |
|
||||
|---|---|---|---|
|
||||
| M1 | 数据模型 + 解析层 | `SidebarTab.pin`、`setTabPin` reducer、`sanitizePersistedTab` 校验、`src/client/pinned.ts`、`SidebarStore.getSessionStates()` | `tests/state.spec.ts` + `tests/pinned.spec.ts` 绿 |
|
||||
| M2 | TabBar 右键菜单 | 「固定 ▸」二级菜单(UI/Agent 文案区分)、「取消固定」、`onPinTab` prop 接线 | `tests/tab-bar-context-menu.spec.tsx` 绿 |
|
||||
| M3 | reconcile 豁免 + 断开标注 | pinned agent tab 豁免移除 + 渲染期「已断开」后缀 | `tests/agent-terminal-reconcile.spec.ts` 追加用例绿 |
|
||||
| M4 | PinnedRail 组件 | rail 渲染、类型图标、tooltip、点击切回 home 会话、右键取消固定/关闭 | `tests/pinned-rail.spec.tsx` 绿 |
|
||||
| M5 | i18n + 文档 + 收尾 | 19 语言 key、README/AGENTS.md、全量回归 | `pnpm typecheck && pnpm test` 全绿 |
|
||||
|
||||
## M1:数据模型 + 解析层
|
||||
|
||||
**改动文件**:
|
||||
- `src/client/state.ts`:`SidebarTab.pin?: { scope: 'workspace' | 'global'; homeCwd?: string }`;`sanitizePersistedTab` 白名单校验 pin(非法 scope → 降级无 pin,不丢 tab);新 reducer `setTabPin(state, tabId, pin | null)`(walk splits + bottomSplits + floats,同构 `patchTab`;未知 tabId 严格同引用 no-op)
|
||||
- `src/client/pinned.ts`(新):`pinnedVisibleTo(tab, viewer)` + `collectPinnedTabs(bySession, viewer)`(排除 viewer 自身会话、按收集顺序稳定)
|
||||
- `src/client/state.ts` `SidebarStore`:新增 `getSessionStates(): ReadonlyMap<string, SidebarState>`(暴露 `bySession` 只读视图)
|
||||
|
||||
**测试**:
|
||||
- `tests/state.spec.ts`:pin 字段 sanitize(合法/非法 scope/缺字段)、`setTabPin`(设置/取消/未知 id/浮动窗口 tab/同引用 no-op)
|
||||
- `tests/pinned.spec.ts`(新):`pinnedVisibleTo` 矩阵(global × 任意 cwd;workspace × cwd 匹配/不匹配/双方 undefined/仅 viewer undefined);`collectPinnedTabs`(多会话收集、排除自身、空 map)
|
||||
|
||||
**提交**:`feat: pin data model + cross-session resolver`
|
||||
|
||||
## M2:TabBar 右键菜单
|
||||
|
||||
**改动文件**:
|
||||
- `src/client/TabBar.tsx`:新增 prop `onPinTab?: (tabId: string, scope: 'workspace' | 'global' | null) => void`;终端 tab(`tab.type === 'terminal'`)菜单插入 pin 行——未 pin 用 `MenuItem.submenu`(`固定到工作区` / `固定到全局`),已 pin 用单行「取消固定」;label 经 t() 且按 `isAgentTabId` 区分文案
|
||||
- `src/client/Sidebar.tsx`:实现 `onPinTab` —— `store.reduce(s => setTabPin(s, tabId, scope === null ? null : { scope, homeCwd: cwd }))`(cwd 取当前会话 summaryCwd);传入 WorkbenchActions → 各 TabBar 实例
|
||||
- 检查 `FreeWindow.tsx` 头部右键菜单是否也需 pin 项(与 TabBar 保持一致;若 FreeWindow 菜单独立实现则同步加)
|
||||
|
||||
**测试**(`tests/tab-bar-context-menu.spec.tsx` 追加):
|
||||
- 终端 tab 菜单含「固定 ▸」且 hover 展开两项;点击回调 `(tabId, 'workspace' | 'global')`
|
||||
- 已 pin tab 显示「取消固定」,回调 `(tabId, null)`
|
||||
- 非终端 tab 无 pin 行;Agent 终端 label 为「固定 Agent 终端」
|
||||
|
||||
**提交**:`feat: tab context menu pin submenu`
|
||||
|
||||
## M3:reconcile 豁免 + 断开标注
|
||||
|
||||
**改动文件**:
|
||||
- `src/client/state.ts` `reconcileAgentTerminals`:`toRemove` 过滤掉 `tab.pin !== undefined` 的 tab(豁免移除)
|
||||
- 渲染期「已断开」:pinned agent tab 的标题后缀在**读取侧**追加——`TabBar`/rail 渲染处无法知道宿主列表,故在 `reconcileAgentTerminals` 豁免时给 tab meta 打标?**否**(改 meta 会写持久化)。方案:rail/tab 标题渲染保持原 title;「已断开」状态由 TerminalView 现有重连失败 banner 承担(WS 连接 1011/超时已有 fatal UI)。**收敛**:M3 只做豁免,断开的用户可见信号 = xterm banner,不加标题后缀(避免渲染期需要宿主列表的耦合)。设计文档 §6 表相应行实施时修正为「banner 承担断开信号」。
|
||||
|
||||
**测试**(`tests/agent-terminal-reconcile.spec.ts` 追加):
|
||||
- pinned agent tab 在 uuid 消失后保留;未 pin 的照旧移除;豁免不影响 toAdd 路径
|
||||
|
||||
**提交**:`feat: reconcile exempts pinned agent terminals`
|
||||
|
||||
## M4:PinnedRail 组件
|
||||
|
||||
**改动文件**:
|
||||
- `src/client/PinnedRail.tsx`(新):props `{ entries: { tab, homeSessionId }[], currentSessionId, onFocus(homeSessionId, tabId), onUnpin(tabId), onClose(homeSessionId, tabId) }`;`[data-dsh-pinned-rail]` 锚点;pin 图标 + 类型图标 + tooltip;右键 Menu(portal);中键关闭(沿用 TabBar middlePressed 模式)
|
||||
- `src/client/PinnedRail.module.css`(新):紧凑条样式,`--dsw-alias-*` 令牌,hairline 分隔,面板折叠时隐藏
|
||||
- `src/client/Sidebar.tsx`:
|
||||
- 渲染期 `collectPinnedTabs(store.getSessionStates(), { sessionId, cwd: summaryCwd })`(useMemo on snapshot + sessionList)
|
||||
- `pinnedJumpRef`(沿用 subagentJumpRef 模式)+ effect:`sessionId === pending` 时 `reduceFor` 激活 tab(`activateTab` 命中 float 则 raiseFloat)+ 面板折叠则 `togglePanel`
|
||||
- onClose:home 路径 `reduceFor(home, s => closeTab(...))` + `api.ptyClose({ sessionId: home, cwd: homeCwd }, tabId)` / `api.agentPtyClose(uuid)`;当前会话退化为现有 `actions.closeTab`
|
||||
- rail 挂载点:右面板内、第一个 TabBar 之上;窄视口(合并抽屉)同样渲染
|
||||
- `src/client/icons.tsx`:`IconPinOutline16`(若图标库无现成 pin)
|
||||
|
||||
**宿主会话切换链路(关键风险,先 PoC)**:rail 点击 → 宿主会话列表导航。调研 `ctx.sessions` 客户端 API 是否有主动切换方法(参考 subagent 跳转实际触发方式);若无公开 API,用 `location.hash` 导航(与宿主路由一致)。PoC 验证后再写组件。
|
||||
|
||||
**测试**(`tests/pinned-rail.spec.tsx` 新):
|
||||
- 0 条目不渲染;≥1 渲染条目(图标/tooltip/标题)
|
||||
- 点击非当前会话条目 → 记录 jump + 导航调用(stub);effect 命中后 reduceFor 激活
|
||||
- 右键取消固定(onUnpin)、关闭(onClose 参数 home 路径);中键关闭
|
||||
- 浮动 pinned tab 点击 → raiseFloat 分支
|
||||
|
||||
**提交**:`feat: pinned rail component + cross-session focus`
|
||||
|
||||
## M5:i18n + 文档 + 收尾
|
||||
|
||||
**i18n**(`src/client/locales.ts` + 19 个 `locales-*.ts`):
|
||||
- 新 key:`pinTerminal` / `pinAgentTerminal` / `pinToWorkspace` / `pinToGlobal` / `unpinTerminal` / `pinnedTerminalTooltip`(插值:类型/范围/cwd)
|
||||
- zh-CN 基准 + en 必写;ja 必同步(better-locale 契约);其余语言沿用既有同步惯例
|
||||
- `tests/locales.spec.ts` 守护同步性
|
||||
|
||||
**文档**:
|
||||
- `README.md` / `README_EN.md` 功能清单加「固定终端」
|
||||
- `AGENTS.md` §3.4 内置 tab 表 terminal 行补 pin 说明
|
||||
- 设计文档追加「实施偏差」节(如 M3 的断开信号收敛)
|
||||
|
||||
**回归门禁**:
|
||||
```powershell
|
||||
pnpm typecheck; pnpm test
|
||||
# 重点回归:agent-terminal-reconcile / terminal-* / bottom-auto-terminal / tab-bar-* / state / prefs
|
||||
```
|
||||
|
||||
**版本**:`package.json` minor bump(新功能);`src/client/service.ts` `features` 数组若对外暴露 pin 能力则加 `'pinnedTerminals'`(消费插件 gate 用)——本功能不开放服务 API,**不加**。
|
||||
|
||||
**提交**:`feat: pinned terminal i18n + docs` → PR `feat/pinned-terminal`
|
||||
|
||||
## 依赖与顺序
|
||||
|
||||
M1 →(M2 ∥ M3)→ M4 → M5。M2/M3 互不依赖可并行;M4 依赖 M1 的 pin 字段与 reducer;M5 最后。
|
||||
|
||||
## 风险登记
|
||||
|
||||
| 风险 | 缓解 | 状态 |
|
||||
|---|---|---|
|
||||
| 宿主会话切换无公开 API | M4 开工先做 PoC(读 subagent 跳转链路 + `ctx.sessions` 面);实在不行用 hash 导航 | 待验证 |
|
||||
| rail 在窄视口/底面板合并态的布局 | 复用现有面板 CSS 变量;`tests/breakpoints*.spec.*` 观察 | 实施期 |
|
||||
| bySession 未缓存会话的 pin 不可见 | 设计已接受(YAGNI 全量扫 localStorage) | 已确认 |
|
||||
@@ -1,55 +0,0 @@
|
||||
# 设计:工作区路径围栏开关(`workspaceFence` 设置 + 错误面一键关闭)
|
||||
|
||||
> 日期:2026-08-29 · 状态:已实现 · 仓库:omdsh-dev/DSH-better-sidebar
|
||||
|
||||
## 需求
|
||||
|
||||
侧栏所有文件系统路由(`fs.tree` / `fs.read` / `fs.write` / `/sidebar/file` 媒体 / `/sidebar/html` 预览 / `/sidebar/upload`)默认强制工作区包含检查:客户端路径经 realpath 解析后必须落在会话工作区内,否则 403 `path "..." is outside workspace`(`src/path-security.ts`,由 `tests/smoke.spec.ts` 的穿越/符号链接用例守护)。
|
||||
|
||||
两个真实痛点:
|
||||
|
||||
1. 打开工作区外的合法文件被拒——全局 `~/.dsh/AGENTS.md`、会话 cwd 之外的 linked worktree(GitView 为此隐藏了「在编辑器中打开」菜单项)。
|
||||
2. 拒绝时直接暴露原始 wire 文本 `path "/tmp/xxx.sh" is outside workspace`,用户不知道原因也不知道怎么办。
|
||||
|
||||
方案(用户拍板):
|
||||
|
||||
1. 新增**持久化**内置布尔设置 `workspaceFence`(默认 `true`),挂在设置页「文件」卡片的「功能设置」二级弹窗(`settings.toggles`)。
|
||||
2. 编辑器加载失败与文件树列目录失败两处错误面:命中围栏拒绝时不再显示原始文本,改为**本地化原因 + 「关闭工作区检测」按钮**——点击全局关闭并**自动重试**失败的操作。
|
||||
|
||||
## 关键决策
|
||||
|
||||
### 1. 全局开关而非 allowlist(取舍记录)
|
||||
|
||||
评估过「受控额外根 allowlist(如 `~/.dsh`)」方案:边界更细,但需要路径管理模式、UI 管理界面与 per-root 读写语义,超出本次需求。用户明确选择单一全局开关贯穿**全部**文件路由(树/读/写/媒体/HTML/上传一体放行),KISS 优先。
|
||||
|
||||
**安全代价已明示**:关闭期间,页面内任意同源脚本(包括第三方消费插件)都能通过 `/sidebar` 路由读写主机任意文件。默认 `true`、设置描述文案与接入指南 §8.1 均写明该风险。
|
||||
|
||||
### 2. host 侧读取:每次调用经 settings seam 同步读
|
||||
|
||||
`fenceEnabledOf(getSettings)`(仿 `shellOverridesOf`):`record.workspaceFence !== false`——**缺省即武装**(无 settings 服务 / 字段缺失 / 值非法一律 `true`,安全默认不依赖设置面可达性)。`ensureWorkspacePath` / `ensureWorkspaceWritePath` 增加可选 `fence = true` 参数:`false` 时跳过包含断言,**realpath 解析保留**(调用方拿到的 canonical 路径语义不变,符号链接仍被解析)。上传 `writeWorkspaceUpload` 以可选 `fence` 入参同语义透传。
|
||||
|
||||
### 3. 错误识别:wire 不变,客户端按消息片段匹配
|
||||
|
||||
不新增 wire 错误码:`/sidebar/api` 信封内 `code: 'forbidden'` 有两个来源——请求信任 fence(消息恒为 `forbidden`)与路径围栏(消息含 `outside workspace`)。客户端 `isOutsideWorkspaceError(e)` / `isOutsideWorkspaceMessage(msg)`(`src/client/api.ts`)以消息片段区分,既有 403 断言零改动。
|
||||
|
||||
### 4. 关闭按钮:写设置 → 更新 store → 自动重试
|
||||
|
||||
`FenceErrorNotice`(新组件):按钮点击 → `api.settingsUpdate({ workspaceFence: false })`(revision-free,同 `plugin-settings.ts` 队列的 last-write-wins 语义)→ `store.setPrefs(parsePrefs(view.value))`(GitView 的 open 预判、设置页等所有 prefs 读者随之翻转)→ `onDisabled()` 回调:
|
||||
|
||||
- EditorHost:`setReloadSeq(seq + 1)` 复用既有重载机制;
|
||||
- FileTree:`retryDir(dir)` 删该层缓存后重载。
|
||||
|
||||
失败路径:保持按钮可用并 console 记录(无乐观翻转)。
|
||||
|
||||
### 5. 客户端预判同步放行
|
||||
|
||||
GitView 两处 `isWithinWorkspace` 预判(菜单项隐藏 :578、纵深防御 no-op :601)改为仅在 `workspaceFence !== false` 时限制,组件新增 `store` prop。`fs.search` 不动——它锚定 cwd 向下走,本来就不会越界。
|
||||
|
||||
## 实施清单
|
||||
|
||||
- 字段三处声明 + 解析:`prefs-shared.ts`(含默认 `true`)/ `config.ts` PrefsSchema / `client/prefs.ts` parsePrefs。
|
||||
- host:`path-security.ts` fence 参数;`index.ts` `fenceEnabledOf` + 六个调用点(fs.tree/fs.read/fs.write 走 buildApi 的 `getSettings` 闭包,媒体/HTML/上传走模块级 `settingsFace`);`fs-operations.ts` 上传入参。
|
||||
- client:`api.ts` 识别 helper;`FenceErrorNotice.tsx`;EditorHost 错误分支;FileTree `retryDir` + 错误分支(store 经 EditorHost → TreePanel → FileTree 穿透);GitView 预判放行;tabs.tsx editor 卡片 toggles 追加开关行。
|
||||
- i18n:4 个 key(`settingsFenceTitle` / `settingsFenceDesc` / `fenceErrorReason` / `fenceDisableAction`)× 全部 21 个词典(zh/en/ja + 18 第三语言,`locales.spec` 强制 key 集相等)。
|
||||
- 测试:smoke「disarms the workspace fence…」(先拒后放,覆盖 tree/read/write);prefs 解析专测(缺省/非法 → `true`,显式 `false` 透传);plugin-shape schema 默认;builtins editor toggles 清单。
|
||||
- 文档:接入指南 §8 内置键清单 + 新增 §8.1(本特性对消费插件的可感知行为与风险)。
|
||||
@@ -1,72 +0,0 @@
|
||||
# 聊天文件打开拦截的 0.1.2-alpha 适配(openPath 漏斗迁移)
|
||||
|
||||
日期:2026-08-31 分支:`fix/openpath-intercept-alpha`
|
||||
|
||||
## 问题
|
||||
|
||||
0.1.2-alpha.x 宿主上,聊天里点击文件链接(工具行、产物行、正文行内代码路径)直接落到系统默认应用(Linux 即 `xdg-open`),「聊天区文件在侧边栏打开」(`interceptOpenPath`,默认开)形同虚设。
|
||||
|
||||
**根因**:alpha 架构把聊天打开文件的漏斗从客户端服务换成了远程调用——`ui-chat/src/client/apply.ts` 注入 ChatView 的 `openFile` 直连 `ctx.remote.session.openWorkspacePath({ path })` → 宿主 `session-controller` → `openNativePath`。插件的 `wrapOpenPath` 包的是旧漏斗 `ctx.workspaces.openPath`,而 alpha 的 `IWorkspaces`(`api/workspace-controller/src/client/service.ts`)**已删除 `openPath` 方法**——wrap 只是给 service 实例挂了个永无调用方的新属性,死代码。这是 #435/#472 适配的漏网项(挂载冒烟不点聊天文件链接,抓不到)。
|
||||
|
||||
**连带影响**:#158 的聊天路径/行号超链接(`owner.openFile` → openPath 假设)同样失效。(#171 的「以默认应用打开」旁路 `openPathWithSystem` 已随重构消失,现行走插件自有 `open.external` 路由,不受影响。)
|
||||
|
||||
**版本前提**:插件已进 alpha 通道(#472,peer `^0.1.2-alpha.2`,pre-alpha 兼容层已删)——本修复只面向 alpha 宿主,**整体替换**旧漏斗,不做双版本兼容。
|
||||
|
||||
## 新漏斗的事实(宿主源码核实)
|
||||
|
||||
1. **唯一生产调用方**:`ui-chat` apply 的 `openFile` 注入(工具行 / 产物行 / 正文提及 / 行内代码路径全部经 ChatView 的 `requestOpenFile` 汇入)。调用前路径已被 `resolveWorkspacePath(cwd, path)` 解析成绝对路径——与旧漏斗收到的形态一致。`canOpenWorkspacePath` 仅被 ui-deliverables 用于按钮可见性,无打开语义,不拦。
|
||||
2. **`remote.session` 是 cordis 服务**(gateway client 的 `RemoteNamespaceService`,service key `remote.session`,经 `ctx.plugin` 注册):
|
||||
- 方法以 `Object.defineProperty(service, method, { configurable: true, enumerable: true, get })` 安装,getter 在**访问时**读 `methods` 表返回调用闭包(因此直接赋值包装无效——无 setter);
|
||||
- 命名空间整组方法与 service 构造在**同一同步窗口**安装——`ctx.inject(['remote.session'], cb)` 回调触发时方法必已就位;
|
||||
- contribution 卸载/重挂载时服务 dispose/重建,inject 回调随之重跑——包装天然自愈。
|
||||
3. 宿主端成功返回 `{ opened: true }`;失败抛 TypertRemoteFailure(ChatView 弹错误框)。接管方必须返回同形成功值。
|
||||
|
||||
## 方案:`wrapOpenWorkspacePath`(沿用「包单漏斗」模式,defineProperty 版)
|
||||
|
||||
改写 `src/client/openpath-intercept.ts`:
|
||||
|
||||
```ts
|
||||
// 伪代码
|
||||
const descriptor = Object.getOwnPropertyDescriptor(service, 'openWorkspacePath')
|
||||
const original = service.openWorkspacePath // 访问 getter,捕获当前调用闭包
|
||||
Object.defineProperty(service, 'openWorkspacePath', {
|
||||
configurable: true, enumerable: true, writable: true,
|
||||
value: (request: { path: string }, signal?: AbortSignal) => {
|
||||
if (takeover()) { /* 接管 */ return Promise.resolve({ opened: true }) }
|
||||
return original.call(service, request, signal) // 放行 = 宿主 native open
|
||||
},
|
||||
})
|
||||
// dispose:descriptor 存在则原样 defineProperty 恢复,否则 delete —— HMR 安全
|
||||
```
|
||||
|
||||
- **接管语义不变**(复用现有 deps):`!suspended && interceptOpenPath !== false && tabsEnabled.editor !== false && 有 current session`;`isFolderRevealPath` → `revealInExplorer`;其余 → `openInSidebar`(`:line` 拆分在既有链路内,#158 功能随之恢复)。
|
||||
- **进入时机**:`registerOpenPathInterception` 改为 `ctx.inject(['remote.session'], ...)` 内包装;宿主裁掉该服务时回调永不触发 = 不拦截不报错。
|
||||
- **已知取舍**:包装期间若 contribution 重挂方法记录,捕获的 `original` 闭包指向旧记录(getter 访问期绑定语义决定);session 命名空间实际常驻,可接受,代码注释记录。
|
||||
|
||||
## 改动清单
|
||||
|
||||
| 文件 | 变更 |
|
||||
|------|------|
|
||||
| `src/client/openpath-intercept.ts` | 重写为 `wrapOpenWorkspacePath`(defineProperty 包装/恢复);`isFolderRevealPath` 保留 |
|
||||
| `src/client/intercept.tsx` | `registerOpenPathInterception` 改走 `ctx.inject(['remote.session'], …)`;deps 不变 |
|
||||
| `src/context-types.ts` | `SidebarWorkspacesService{openPath}` 替换为 `SidebarRemoteSessionService{openWorkspacePath(request): Promise<{opened: boolean}>}` |
|
||||
| `src/client/index.tsx` | inject 数组移除 `'workspaces'`(全仓唯一消费者是旧拦截,typecheck 验证后删) |
|
||||
| `tests/openpath-intercept.spec.ts` | 重写:getter 形态假服务、接管/放行/恢复、inject 晚到场景 |
|
||||
| `docs/external-plugin-guide.md` | §10 平台陷阱补「聊天文件打开拦截走 remote.session.openWorkspacePath」一条 |
|
||||
| AGENTS.md §3 | 新增一条 alpha 宿主契约记录(对齐 #463 的做法) |
|
||||
|
||||
## 验证
|
||||
|
||||
- 单测:接管 / 放行 / 文件夹揭示手势三态;dispose 恢复原 descriptor;inject 延迟出现。
|
||||
- 门禁:`pnpm typecheck` → `pnpm test` → `pnpm build` → `pnpm test:mount`(alpha.2 真机 14/14 不回归;mount lane 不驱动聊天点击,拦截正确性靠单测 + 手测)。
|
||||
- 真机手测:让模型在回复里输出一个工作区文件路径(行内代码)→ 点击落侧边栏编辑器;关闭「聊天区文件在侧边栏打开」后点击 → 系统默认应用;`path:line` 链接 → 侧边栏跳行(#158 回归)。
|
||||
|
||||
## 实施偏差记录
|
||||
|
||||
- **接管返回值必须是 typert `RemoteResult` 包络**(初版实现踩坑修正):plan 中「接管返回 `{ opened: true }`」有误——gateway client 的每个 remote 方法都把业务值折叠成 `{ ok: true, value }` / `{ ok: false, error }`,ChatView 的 `openFile` 按 `result.ok` 分支、falsy 时读 `result.error.message`。初版返回裸业务值导致「文件已在侧边栏打开,但仍弹『无法打开文件: Cannot read properties of undefined (reading 'message')』」。修正为接管时返回 `{ ok: true, value: { opened: true } }`(`OpenWorkspacePathResult` 类型钉住,单测断言包络形状)。
|
||||
|
||||
## 不做
|
||||
|
||||
- pre-alpha 双版本兼容(alpha-only 通道,#472 已删兼容层)。
|
||||
- `canOpenWorkspacePath` 包装(仅按钮可见性)。
|
||||
- DSH 侧任何改动(仓库硬约束 §1)。
|
||||
@@ -1,87 +0,0 @@
|
||||
# 侧边栏性能优化 + 底栏拖拽布局突变修复
|
||||
|
||||
**日期**:2026-08-31(2026-09-01 rebase 到 main c199212 并在 alpha.3 上复测)
|
||||
**状态**:已实施(分支 `feat/sidebar-perf`,单 PR 多 commit)
|
||||
**目标宿主**:DSH v0.1.2-alpha.2 起开发,宿主参考 `/Users/menghuan/Code/deepseek-harness`(只读);main 已切 alpha.3(v0.18.1-alpha.0),rebase 后在 alpha.3 真机全量复测(§3.1)
|
||||
**测量环境**:macOS arm64 本机,真实 `dsh web`(npx 钉版)+ scratch profile 挂载 tarball,Playwright Chromium 无头,每指标 3 次取中位数(`tests/e2e/perf.e2e.ts` 输出 `PERF_JSON`)。
|
||||
|
||||
## 1. 修复的 bug:底栏拖拽引发原生左侧边栏突变
|
||||
|
||||
**症状**:右侧面板关闭时拖动底栏 resizer,DSH 原生左侧边栏突然折叠/跳动;松手后弹回。插件自己的右侧边栏不受影响。
|
||||
|
||||
**根因**(源码级定位,`Sidebar.tsx`):底栏拖拽的 `applyDrag` 把「持久化的右栏宽度偏好」(如 400px)无条件写进 `--dsh-sidebar-width`,不看 `panelOpen`;而静止时的 push effect 走 `layoutPushSize` 门控(面板关闭 → 0)。于是拖拽首帧该变量 0 → ~400px:
|
||||
|
||||
1. `layout.css` 的 `#root { margin-right; width: calc(100% - …) }` 瞬间挤掉宿主 400px(拖拽期间 transition 被 `body[data-dsh-sidebar-dragging]` 关闭——瞬跳而非动画);
|
||||
2. 宿主 `AppFrame` 的 ResizeObserver → `setViewport` → viewport 跌破 1024px 断点 → `computeColumns` 把原生左栏折叠成 56px rail(0.3s grid 轨道动画);
|
||||
3. 松手 `commitDrag` → store 提交 → push effect 又写回 0 → `#root` 弹回 → 左栏再展开。
|
||||
|
||||
插件右栏在 `#root` 之外的 `document.body` fixed 层(`[data-dsh-panel-host]`),几何上碰不到——这正是「左栏突变、右栏无恙」的不对称来源。底栏是**唯一**在 `panelOpen === false` 时可达的拖拽(宽度/角点拖拽都要求面板开着),所以只有它触发。`abortDrag` 路径此前已修过(走 `layoutPushSize` 门控),move/up 路径漏网。
|
||||
|
||||
**修复**:`applyDrag` 里 `writeGeometry` 的 width 实参加上与 push effect 一致的门控(`!narrow && panelOpen ? min(width, vw) : 0`);面板 DOM 写入(`panelRef`/`bottomRef`/`lastDragSize`)保持原始 width(底栏 `right` 定位派生自它)。
|
||||
|
||||
**验证**:e2e 回归(`drag-layout.e2e.ts`)——右面板关闭 + 底栏打开拖 strip,逐帧断言 pushed width 恒 0、宿主 grid 模板 / 左栏渲染宽度 / `#root` 右边缘全程不变。旧代码上实测泄漏 504px(红),修复后通过(绿)。
|
||||
|
||||
## 2. 性能改动清单
|
||||
|
||||
| # | 改动 | 类别 | commit |
|
||||
|---|---|---|---|
|
||||
| 1 | boot 路径 `loadPrefs` + `loadExternalDisable` 合并为一次 `settingsGet`(原来同文档串行拉两次,第二个无超时——settings RPC 挂起则侧边栏永不出现) | 启动 | perf: cut the boot and poll overhead |
|
||||
| 2 | GitLens 2s silent 轮询不再每 tick 重列 worktrees(每 tick 少 spawn 1 个 git 进程;每 15 tick ≈30s 才重列+自动选择一次) | 运行时开销 | 同上 |
|
||||
| 3 | ChangesTab 的 `extractFileOps` fold 从「每 poll 两遍(badge 一遍 + render 一遍)」改为每 poll 一遍、结果进 ref 共享(4000 条事件 × 每条 JSON.parse) | CPU | 同上 |
|
||||
| 4 | SideChat 转录行引用复用:`transcriptRows` 接受上次结果,位置对齐内容相同的行复用旧对象——2s 轮询只重渲染变化的尾部(原来全部行连同 markdown 重解析每轮重渲染) | 渲染 | perf: stabilize renders |
|
||||
| 5 | FileTree `expanded`/`revealed` 数组 → Set(原来每行 `includes()` 线性扫描,整树 O(rows×expanded)) | 渲染 | 同上 |
|
||||
| 6 | `buildNewTabOptions` 从两处 JSX 内联调用改为共享 memo(原来每次 render 两个新数组,击穿 LeafView 的 props 比较) | 渲染 | 同上 |
|
||||
| 7 | DiffPane 高度拖拽、TerminalView ResizeObserver 走每帧批处理(原来每 pointer/resize 事件一次 setState / 一次含字形度量的 fit());SessionLens 的 `new Blob().size`(整段编辑内容的 UTF-8 编码)从每行每 render 移到每变化一次的 memo | 渲染 | 同上 |
|
||||
| 8 | 19 个非 zh/en 字典移入 lazy `locale` chunk(`lib/client-locale.js`,仅安装 better-locale 时按需拉取;核心包只留 zh/en——`t()` 本来也只查 zh/en + override store) | 启动体积 | perf: move the 19 dictionaries |
|
||||
| 9 | 底栏推挤锚点从 `:has(> [data-slot=conversation])` 换成 shell 定位器打的 `[data-dsh-center-col]` 标注(`:has()` 匹配缓存随流式聊天的 #root 子树变动不断失效,每次布局推挤帧全文档重估) | 拖拽帧成本 | perf: tag the center column |
|
||||
| 10 | TextEditor 不再每键 `doc.toString()` 全文字符串化进 React state(O(docLength)/键);listener 只翻 dirty,进预览/换文件时从 live view 快照一次;保存本就现读 | 编辑 | 同上 |
|
||||
|
||||
## 3. Before / After 对比
|
||||
|
||||
测量说明:`mount-sweep` = 冷导航 → 插件挂载 → 展开面板 → 依次打开全部 6 个内置 tab;`bottom-drag` = 面板关闭 + 底栏打开时拖 12 步的 rAF 帧间隔。同机同夜连续跑,数字用于相对比较而非绝对承诺。
|
||||
|
||||
| 指标 | before(main d70db8f) | after(本分支) | 变化 |
|
||||
|---|---|---|---|
|
||||
| 挂载时延(navigationStart → host attach,中位) | 376.9 ms | 272.9 ms | **-28%** |
|
||||
| 挂载+扫 tab 期间 longtask 数 / 总时长 / 最长 | 2 / 161 ms / 96 ms | 2 / 146 ms / 84 ms | 总时长 -9%,最长 -13% |
|
||||
| 核心 client.js 体积(未压缩) | 1.46 MB | 0.80 MB | **-45%** |
|
||||
| 宿主聚合 client bundle 传输量(含全部插件,gzip) | 1,436,478 B | 1,233,778 B | **-203 KB / -14%** |
|
||||
| 宿主聚合 client bundle 解码体积 | 5,136,894 B | 4,497,363 B | -640 KB(即字典总量,吻合) |
|
||||
| 侧栏 shape 的资源请求数 / 其中 settings.get | 20 / ×4 | 18 / ×2 | boot 合并生效 |
|
||||
| 底栏拖拽帧间隔 p50 / p95 / max | 8 / 10 / 11 ms | 8 / 10 / 11 ms | 持平(本就不掉帧;修复的是布局突变) |
|
||||
| 底栏拖拽 pushed width 泄漏(右面板关闭时) | **504 px**(bug) | **0 px** | 修复 |
|
||||
|
||||
### 结构性收益(不随单次测量波动)
|
||||
|
||||
- **核心 bundle**:19 个字典(源码 ~640KB)出核心包,`client.js` 1.46MB → 822KB(构建产物实测)。绝大多数会话(未装 better-locale)永远不拉 `client-locale.js`(gzip 171KB);装了的用户按需拉一次(HTTP 缓存 + ETag 复用)。
|
||||
- **boot RPC**:settings 文档从 2 次串行 → 1 次;`loadExternalDisable` 有了 2s 超时兜底。
|
||||
- **GitLens 轮询的 git 进程 spawn**:每分钟 60 个(2 个/tick × 30 tick)→ ~34 个(1 个/tick + 每 15 tick 多 1 个),-43%。
|
||||
- **ChangesTab fold**:每 2.5s poll 的 fold 次数 2 → 1(每次 fold 对至多 4000 条事件逐条处理、tool/call 还要 `JSON.parse`)。
|
||||
- **SideChat 转录**:内容未变的行不再重建对象 → 下游 React reconcile + markdown 解析 + DOMPurify 全部跳过(引用稳定性由 `sidechat-transcript.spec.ts` 契约钉住)。
|
||||
- **TextEditor**:每次按键的 O(文档长度) 字符串化 + shell 重渲染 → 0(仅 dirty 布尔)。
|
||||
|
||||
### 3.1 alpha.3 复测(rebase 到 main c199212 之后)
|
||||
|
||||
main 在本分支开发期间合入了 alpha.3 适配(peer `^0.1.2-alpha.3`,v0.18.1-alpha.0)与 sidechat 渲染升级(新增 `turnSummary` 行 kind 与 tool `card`——`rowsEqual` 已补齐两者,card 只在 tool/result 落地时变化、被 `executing`/`failed`/`resultText` 的比较覆盖,无需值比较)。rebase 后在 alpha.3 真机重测(同法 3 次中位,before 侧 = main c199212 代码 + 本分支的测量工具):
|
||||
|
||||
| 指标 | before(main c199212) | after(本分支) | 变化 |
|
||||
|---|---|---|---|
|
||||
| 挂载时延(中位) | 372.3 ms | 316.5 ms | **-15%** |
|
||||
| longtask 总时长 / 最长 | 170 / 100 ms | 165 / 96 ms | -3% / -4% |
|
||||
| 宿主聚合 bundle 传输 | 1,456,668 B | 1,246,595 B | **-210 KB / -14%** |
|
||||
| boot settings.get | ×4 | ×2 | 合并生效 |
|
||||
| 底栏拖拽 pushed width 泄漏 | **504 px**(复现) | **0 px** | 修复 |
|
||||
|
||||
全部收益在新 main 基线上成立;完整 mount lane(17 测试)在 alpha.3 真机全绿。§3 主表为 alpha.2 上的原始测量记录(两端同宿主,对比自洽),保留作历史基线。
|
||||
|
||||
## 4. 守护
|
||||
|
||||
- e2e:`drag-layout.e2e.ts` 新增底栏回归(宿主布局全程静止)与 center-col 标注断言(恰一节点、为 conversation slot 的父级);`perf.e2e.ts` 的 width-leak 断言在每次测量 lane 里复检。
|
||||
- 单测:`prefs.spec.ts`(boot 决策单次 fetch)、`sidechat-transcript.spec.ts`(行引用复用契约)、`locales.spec.ts`(chunk 内全部字典 key-set 与 zh 一致)、`layout-css.spec.ts`(锚点选择器契约,禁 `:has` 回潮)、`bundle-route.spec.ts`(chunk 清单含 locale)。
|
||||
- 完整 mount lane(17 测试)在本分支全绿(alpha.2 与 alpha.3 真机各跑一轮)。
|
||||
|
||||
## 5. 明确不做(取舍)
|
||||
|
||||
- `pinnedEntries` 的 store 版本号改造、连续 `store.reduce` 合并:侵入 store API,收益不明确,KISS 取舍。
|
||||
- FileTree / SideChat 列表虚拟化:Set 化 + 引用复用后若真实负载仍不达标再立项。
|
||||
- `e2e-mount.sh` 的 `command -v "$DSH_CMD"` 对带空格命令失效(回退未钉版本的 npx)是**宿主侧脚本的既有行为**,本地以 `DSH_CMD=/tmp/dsh-alpha2.sh` 包装脚本绕过,不改脚本(CI 全局安装钉版本,不受影响)。
|
||||
@@ -1,76 +0,0 @@
|
||||
# 2026-09-03 全方位代码防腐 / 质量优化审查报告
|
||||
|
||||
> 三路探索子代理对 src / tests / scripts / CI / 文档做全方位摸底后,经用户批准按「P0 速修 + P1 行为不变去重 + P2 深度项全做」执行,共 10 个变更流、11 个 PR,全部走分支 + PR + 三 job CI 门禁合入。本文记录审查结论、落地清单、过程中的重要发现与遗留 backlog。
|
||||
|
||||
## 1. 审查结论总览
|
||||
|
||||
底子健康,无大面积腐化:
|
||||
|
||||
- 类型纪律严格:全仓 0 `as any`、0 `@ts-ignore`、0 TODO/FIXME。
|
||||
- 模块边界零违规:懒加载 chunk 隔离、client externals 白名单、tsdown purity gate 全部干净。
|
||||
- 错误 envelope 一致:宿主侧 `wire.ts` / 客户端侧 `api.ts` 双层归一。
|
||||
- 测试规模:118 个 vitest spec(约 3300 断言)+ 6 条 Playwright e2e lane。
|
||||
|
||||
压力点集中在:少数巨型组件、若干字面重复、脚本间复制粘贴、以及缺 lint / task-runner 基础设施。
|
||||
|
||||
## 2. 已落地的变更流
|
||||
|
||||
| PR | 主题 | 要点 |
|
||||
|---|---|---|
|
||||
| #524 | feat/test-utils | 共享 `tests/test-utils.ts`(`setupReactAct` + `renderRoot`),迁移 28 个 spec 的重复样板;`chunk-artifact` / `manifest-consistency` 加 lib 缺失 skipIf 守卫 |
|
||||
| #525 | feat/dedup-core | api envelope 解析、`extOf` 双实现、lineage-walk 四份、GitLens 错误归一化六处收敛;删零引用死代码 `sidechatSeed`;console 分级统一。测试零改动 |
|
||||
| #526 | feat/dev-commands | Makefile(11 目标薄封装,`check` 聚合对齐 CI);package.json 补 `test:mount:aggregate` / `clean` / `packageManager: pnpm@11.8.0`;release.yml 补 consumer-types 并统一步骤序;.editorconfig;双语 README 补文档 |
|
||||
| #527 | fix/scripts-hardening | aggregate 脚本 tarball 改 mtime 选取(修真 bug);DSH_CMD 默认值统一;package-registry.mjs 补 mermaid chunk;抽 `scripts/e2e-common.sh` 消除两 e2e 脚本约 40% 重复(函数级等价性探针 + 双 lane 真机验证) |
|
||||
| #534 | feat/palette-single-source | One Dark/Light 语法色板单源化(`one-dark-palette.ts`),迁移前后映射机器证明逐字节相等 |
|
||||
| #535 | feat/i18n-catalog-names | 目录项 name / shell 预设 desc 词典化(20 key × 21 词典文件,`string | (() => string)` 镜像 desc 模式);兜底错误条改 `var(--dsw-alias-*, 原hex)` 皮肤令牌链 |
|
||||
| #536 | feat/eslint | ESLint 10 flat config(typescript-eslint recommended + 经典 react-hooks 规则),存量 141 错误/21 警告清零(含 24 处真实死代码删除、`_` 前缀 mock 约定入 config);`pnpm lint` / `make lint` / CI 三 job 全部接入;`make check` 同步含 lint |
|
||||
| #540 | feat/drop-rc7-fallback | 删 rc.7 宿主 `window.__DSH_MODULES__` 回退(peer 基线 rc.1 下不可达);测试 fixture 切换到与生产同路径;附带修复 main 上 #535/#536 语义合并冲突导致的 add-plugin-modal spec 断裂 |
|
||||
| #541 | feat/unify-polling | `use-polling.ts` 原语(83 行)统一四处轮询习语(SubagentView 自排程 3s / SideChatView fixed 2s / GitLens fixed 2s / ChangesTab fixed 2.5s),守护 spec 断言零改动 |
|
||||
| #542 | feat/split-sidebar | Sidebar.tsx 1831 → 1176 行,抽 `src/client/sidebar/` 五模块(TabContent 74 / use-center-column 171 / use-host-feeds 269 / use-pinned-tabs 153 / free-windows 167);面板拖拽机约 330 行因 `sidebar-layout-push.spec` 的源码文本计数断言钉住而有意保留主文件 |
|
||||
|
||||
过程中同步发生的外部事件:DSH 0.1.2-rc.1 适配(PR #537)合入后,Wave 2 三个 PR 全部 rebase 到新 main 并重跑门禁再合并。
|
||||
|
||||
## 3. 过程中的重要发现(记录在案)
|
||||
|
||||
### 3.1 Windows agent-pty PTY 超时 flaky(既有,非本轮引入)
|
||||
|
||||
`tests/agent-pty.spec.ts` 的真实 PTY spawn 用例在 ci-windows 上间歇性 5s 超时。证据链:main@`ceed6b2`(Wave 1 首个合并的 push run)挂 agent-pty ×4 + git.spec ×1;#535、#536、#541 的 ci-windows 各挂过 1-2 个同类用例,**重跑均绿**;失败组合在多次运行间漂移。建议后续对该 describe 块在 win32 上提升 testTimeout 或加有界重试。
|
||||
|
||||
### 3.2 desktop-layout.e2e.ts 间歇 flaky(上游时序敏感)
|
||||
|
||||
该用例测「右面板保持桌面 session 头部位置」,区域与上游适配提交 `ab6f960`(toggle cluster 重定位动画)重叠。本机(钉版 rc.1 CLI)稳定因宿主 onboarding 浮层 / workspace 选择对话框不可达而失败,且 main 原样产物 A/B 对照同样失败;CI 上 aa01140 的 push run 也挂过一次(同内容分支 run 与后续 main 验证 run 均绿)。属宿主时序敏感的既有 flaky,建议上游动画稳定后加等待收敛。
|
||||
|
||||
### 3.3 跨分支语义合并冲突的流程教训
|
||||
|
||||
#535 新增了 `builtinTabPlugins` 的使用(依赖既有 import),#536 的 eslint 清理在其各自基线上把当时未使用的同名 import 删除——两个各自全绿的 PR 文本合并成功但语义断裂(typecheck TS2304 + spec 加载即挂),且被并发 run 取消掩盖后带病合入 main,由 Wave 3 两个子代理独立发现并热修(#540 `8820efd`)。教训:**并行分支触碰同一文件时,每合入一个后其余分支需重验(rebase + typecheck/test),不能只信各自 PR 的绿勾**;unused-import 类清理与功能分支并行时尤其如此。
|
||||
|
||||
### 3.4 GitHub push run 丢触发一例
|
||||
|
||||
#541 合并产生的 main push run(`68839aa`)从未被创建(workflows 均 active,按 SHA 查询 0 runs),经 `workflow_dispatch` 手动补跑三 job 全绿。原因不明,遇到「merge 后 main 无 push run」时可按此法补验。
|
||||
|
||||
## 4. 基础设施变化速查
|
||||
|
||||
- **Makefile**:`help`(默认目标)/ `install` / `build` / `typecheck` / `lint` / `test` / `check`(typecheck+build+test+consumer-types+lint 聚合)/ `clean` / `pack` / `mount` / `mount-aggregate` / `registry`。package.json scripts 仍是唯一事实源,Makefile 只做薄封装与依赖链。
|
||||
- **Lint**:`pnpm lint` = `eslint .`(flat config,`reportUnusedDisableDirectives: error`);CI 三处(ci / ci-windows / release)在 Typecheck 后执行。
|
||||
- **本地工具钉版**:`packageManager: pnpm@11.8.0`(CI workflow 的显式 `version:` 输入已移除,统一由该字段供版)。
|
||||
- **e2e 本地跑法**:`make mount` / `make mount-aggregate`;本机无 rc.1 CLI 时 `npm install --prefix /tmp/dsh-rc1-bin @deepseek-ai/dsh@0.1.2-rc.1` 后置 PATH。
|
||||
|
||||
## 5. Backlog(本次仅记录)
|
||||
|
||||
1. mount.e2e 的固定 `waitForTimeout` 与英文文案选择器(`[title="Files"]` 等)——宿主文案/语言变更即碎。
|
||||
2. CI 无 Playwright 浏览器缓存(每 run 重装 Chromium)。
|
||||
3. `state.spec`(1498 行)/ `smoke.spec`(1208)/ `service.spec`(1124)三个千行 spec 拆分。
|
||||
4. FreeWindow / TreePanel / split-pane / DiffPane / DiffRows 等组件缺直接渲染 spec(现靠 state 纯函数 spec + e2e 间接守护)。
|
||||
5. coverage 工具引入评估(当前零覆盖率度量)。
|
||||
6. CHANGELOG / CONTRIBUTING 文件(README「🆕 最近更新」小节手工充当 changelog)。
|
||||
7. Sidebar.tsx 剩余拖拽机(~330 行)拆分——前置条件是先重构 `sidebar-layout-push.spec.tsx` 的源码文本计数断言为行为断言。
|
||||
8. `JobOutputPane` 的 2s 轮询未迁移 `use-polling`(独立形状,见 #541 PR body)。
|
||||
9. install.sh / install.ps1 / check-consumer-types.sh 内部的 say/warn/die 重复——install.sh 必须自包含随 npm 包分发,属有意设计,除非改分发方式否则不动。
|
||||
10. §3.1 / §3.2 两个 flaky 的根治。
|
||||
|
||||
## 6. 验证基线(全部 PR 合并后于 main 复核)
|
||||
|
||||
- `pnpm lint` / `pnpm typecheck` / `pnpm build` / `pnpm test`:119 files / 1243 passed / 9 skipped。
|
||||
- `pnpm check:consumer-types` 通过。
|
||||
- 真机挂载 lane:mount 5/5、drag-layout 7/7、float-window 2/2、toggle-layout 1/1、perf 2/2(desktop-layout 见 §3.2)。
|
||||
- aggregate 双挂载回归 lane 通过。
|
||||
@@ -1,62 +0,0 @@
|
||||
# 文件树重命名 / 删除设计(2026-09-04)
|
||||
|
||||
> 状态:已实现。本文档记录功能设计、关键取舍与已知限制。
|
||||
|
||||
## 背景
|
||||
|
||||
文件树右键菜单此前只有打开/复制/上传下载等只读+导出操作,缺少资源管理器最基本的两个变更操作。本次补全:
|
||||
|
||||
- **重命名**:行内编辑(VS Code/Finder 惯例,用户已确认)——菜单点「重命名」后行内名字变输入框(预选中、Enter/失焦提交、Esc 取消)。
|
||||
- **删除**:弹窗确认(用户指定)——复用 git lens 的 Cancel/Confirm Modal 范式,文件/目录文案区分,确认后永久删除。
|
||||
|
||||
同批修复二级菜单的视觉关系(见下节)。
|
||||
|
||||
## 服务端
|
||||
|
||||
- `src/fs-operations.ts` 新增 `renameWorkspaceEntry` / `removeWorkspaceEntry`(与 `writeWorkspaceUpload` 同层):
|
||||
- **链路感知(link-aware)**:存在性与围栏校验对**完全解析后的目标**进行(fence 开启时指向工作区外的 symlink 一律拒绝),但操作本身作用于**词法行路径**——重命名/删除一个 symlink 行只改/删**链接本身**,绝不动目标(lstat 判定,link-to-dir 不会递归进目标)。与 VS Code 语义一致:行显示什么就操作什么。
|
||||
- `renameWorkspaceEntry`:`name` 必须单段(非空、非 `.`/`..`、无分隔符——重命名不是移动);目标已存在直接 409 拒绝(POSIX rename 会静默覆盖);工作区根本身不可重命名;同名 no-op 直接成功(不触发 409)。返回**规范路径**(realpath 解析后的祖先链)供 tab 改址。
|
||||
- `removeWorkspaceEntry`:目录递归删除、文件/symlink 解链;根拒绝。无回收站(宿主无此 API;等同 git lens discard 的永久语义)。
|
||||
- `src/index.ts` 路由 `'fs.rename'`(`{path, name}`)/ `'fs.remove'`(`{path}`),走 `cwdOf` + `fenceEnabledOf(getSettings)`,与 `fs.write` 同一围栏开关。
|
||||
- `src/client/api.ts`:`fsRename` / `fsRemove`。
|
||||
|
||||
## 客户端(FileTree)
|
||||
|
||||
- 菜单尾部(复制项之后):`separator + 重命名(IconEditOutline16) + 删除(IconTrashOutline16, danger)`;**根行**(`rowMenu.path === cwd`)不渲染这两项(服务端双保险)。
|
||||
- 行内编辑:命中的行换渲染为编辑行(同缩进/图标/行高),**无 role="button"**(编辑器不是点击目标,也避免 button 嵌套 interactive 的非法结构)。裸 `<input>`(primitives `Input` 不转发 ref,而 focus+select 正是要点);ref 回调 `useCallback([])` 稳定身份——内联箭头每渲染重跑会把正在输入的缓冲区重新全选。Enter/失焦提交、Esc 取消;`isImeComposition` 守卫(IME 组合期的 Enter/Esc 属于输入法,不提交不取消——顺手把该守卫的入参 `isComposing` 放宽为可选,React 合成事件类型未声明该字段)。
|
||||
- 删除确认:primitives `Modal`(git lens 同款 outline Cancel + primary Confirm)。
|
||||
- 失败面:树顶一条可点消失的错误条(`role="alert"`),展示服务端原文(与 FenceErrorNotice 同策略);客户端单段校验失败也走这里(`renameInvalid`)。
|
||||
- **落定收尾 `pruneTree(prefix)`**:清掉旧路径前缀的所有缓存层 → `onToggle` 收起 stale 的 expanded 项 → `retryDir(parent)` 单层重载(无需整树 refresh tick)。重命名已展开目录后新目录折叠展示(expanded 不迁移,VS Code 同样)。
|
||||
|
||||
## tab 调解(tree-mutations.ts + EditorHost)
|
||||
|
||||
- `retargetPathTabs(ctx, store, old, new)`:`tab.path === old` → `service.updateTab(id, {path: new, title: baseName(new)})`(编辑内容存活,后续保存落到新路径)。
|
||||
- `closePathTabs(ctx, store, target)`:`tab.path === target || isWithinWorkspace(target, tab.path)` → `service.closeTab(id)`(目录连整个子树的打开文件一起关)。
|
||||
- tab 枚举覆盖 `state.splits` / `state.bottomSplits` / `state.floats`(浮动窗口的 tab 同样算打开)。走 service 路径(而非直改 state)使注册的生命周期回调照常触发。
|
||||
- FileTree/TreePanel 透传 `onPathRenamed?` / `onPathDeleted?`(可选,测试可不接线);EditorHost 两处 `<TreePanel>` 接线。
|
||||
|
||||
## 二级菜单视觉关系(layout.css,同批)
|
||||
|
||||
针对「二级菜单没有视觉关系」的反馈,两条纯 CSS 规则(仍以 `data-dsh-sidebar-submenu` body 属性限定作用域,宿主默认不受影响):
|
||||
|
||||
1. **父行持续高亮**:宿主 Menu 在子菜单打开期间给父行按钮标 `aria-expanded="true"`(宿主已渲染、稳定),用同一 hover token 重新着色——指针移入侧卡后父行不再熄灭。
|
||||
2. **卡片贴近**:子菜单与父卡间距 10px → 4px,`::before` 悬停走廊同步收窄;翻转规则(left/down)的间距一并 4px。规则排序:tether 基础规则在前、翻转覆盖在后(同特异性下源序决胜)。
|
||||
|
||||
`tests/menu-flip.spec.tsx` 的 layout.css 契约断言扩展覆盖这三条规则。
|
||||
|
||||
## i18n
|
||||
|
||||
新增 7 key(rename / renameInvalid / delete / deleteTitle / deleteDescFile / deleteDescDir / dismiss),zh/en/ja 手写;其余 18 个已发第三语言按 `locales.spec.ts` 的 key-set-parity 门禁同步补齐。
|
||||
|
||||
## 测试
|
||||
|
||||
- `tests/fs-operations.spec.ts`:rename(文件/目录/no-op 同名/单段违规/目标已存在 409/根拒绝/缺源/symlink 行改链接不改目标)、remove(文件/目录递归/symlink 只解链目标存活/根拒绝与缺路径)。
|
||||
- `tests/file-tree-rename-delete.spec.tsx`(jsdom,mock api):菜单项出现条件(文件/目录有、根无);重命名流(预填→Enter 提交→`fsRename`+`onPathRenamed`、Esc 取消、非法名走错误条);删除流(弹窗文案文件/目录区分、确认→`fsRemove`+`onPathDeleted`、取消、服务端失败进错误条)。
|
||||
- 真机挂载冒烟(`pnpm test:mount`)不触达这些交互,由 CI 门禁与手动验收覆盖。
|
||||
|
||||
## 已知限制
|
||||
|
||||
- 无回收站:删除即永久(fence 开关不改变这一点,只改变越界拒绝)。
|
||||
- 重命名不迁移 expanded/revealed 状态:改名已展开目录后新目录折叠、reveal 高亮不跟随(一次性视觉状态,成本/收益不划算)。
|
||||
- 并发窗口竞争:存在性检查与 rename/remove 之间有微小 TOCTOU 窗口(单用户侧边栏场景可忽略;服务端错误会进错误条)。
|
||||
- 「在其他编辑器里改名后本树 stale」既有行为不变(refresh tick / focus 刷新机制管)。
|
||||
@@ -1,61 +0,0 @@
|
||||
# 变更页操作预览增强设计(2026-09-06)
|
||||
|
||||
> 状态:已实现(PR #499)。本文档记录六个特性的设计、关键取舍与已知限制,含 2026-09-06 规范化审查后的整改记录。特性移植自 dsh-file-trace v0.2.0/v0.2.1 会话(PDF 渲染为作者后续追加)。
|
||||
|
||||
## 背景
|
||||
|
||||
变更页(Changes tab)底部预览面板此前只渲染统一 diff / 逐行读取 / 错误文本。本批扩展为:markdown 文档可切「阅读模式」、mermaid 围栏随阅读模式渲染、预览载荷做「敏感内容脱敏」、`.html` / `.pdf` 操作可切「渲染模式」,并把 diff 引擎的语法着色表扩到此前无色/错配的一批语言。全部特性共用同一预览面板与头部 toggle 交互。
|
||||
|
||||
## 1. Markdown 阅读模式
|
||||
|
||||
- 适用:`.md / .markdown / .mdx` 的 read / write / edit 操作(非错误)→ 头部「阅读 / 原文」toggle。
|
||||
- 渲染走共享 `MarkdownText`(与编辑器预览、Side Chat 同一栈,labels 契约经 `markdownTextProps()`,见 AGENTS §3.3);本地图片目的地经 `rewriteLocalImageUrls` 改写到会话域 `/sidebar/file` 媒体路由。
|
||||
- 内容来源:read → `parseReadContent()`(见下);write → 写入内容;edit → 已知 prior 时 `prior.replace(oldString, newString)` 重建结果文档,否则仅展示 newString(设计上「prior 已知才重建」)。
|
||||
- `parseReadContent`(`src/client/changes/ops.ts`):与 `parseReadLines` 共享 `contentLines()` envelope 核心(`<content>` 提取 + "(Showing lines …)" 过滤);区别在前者剥 `<n>: ` 前缀后**保留空行**(markdown 块结构依赖空行)并修剪首尾空行(渲染不可见),后者解析行号、丢弃空行。
|
||||
|
||||
## 2. Mermaid 围栏渲染
|
||||
|
||||
- 阅读模式检测到 ```mermaid 围栏时,走编辑器同款懒加载渲染器(`LazyMermaidMarkdown`,mermaid chunk 经 `/sidebar/bundle` 按需下发);无围栏文档保持单次 MarkdownText 直渲染(字节不变)。
|
||||
- `LazyMermaidMarkdown` 的定义从 `MarkdownHtml.tsx` 移到轻量模块 `src/client/mermaid-lazy.tsx`:变更页(核心 bundle)要引用这个 stub 而不把 `MarkdownHtml` 的 DOMPurify/HTML 分析机械拖进核心 bundle;`MarkdownHtml.tsx` 改为 re-export。
|
||||
- 已知边界:无引号节点标签内含被遮密钥(`[REDACTED]` 字面)时 mermaid 解析失败,回落到「错误条 + 已遮源码」——该回落是 `mermaid.tsx` 既有机制(catch → 错误 + 源码),本批未新增代码,亦未为该路径加测试(README 已注明规避方式:标签加引号)。
|
||||
|
||||
## 3. 语法着色扩展(`src/client/diff/highlight.ts`)
|
||||
|
||||
- `mjs/cjs → JS_WORDS`、`mts/cts → TS_WORDS`(此前无映射 → 纯文本);`tsx/jsx` 同用共享词表(规范化整改:原先保留内容相同的内联副本)。
|
||||
- `css/scss/less → CSS_LANG`:slash-star 块注释跨行、连字符属性词(`background-color` 整词着色)、字符串、十六进制数;此前误用 `#` 行注释配置导致不着色。
|
||||
- `html/htm/xml/svg/vue → MARKUP_LANG`:`<!-- -->` 注释 + 常见标签/属性;`graphql/gql`、`jsonc/json5` 映射。
|
||||
- 着色是启发式(词表驱动单遍扫描);CSS/MARKUP 词表混排选择器/属性/标签是刻意为之——`LangConfig` 只有一个 keywords 集,再分类属过度设计。
|
||||
|
||||
## 4. 敏感内容脱敏(`src/client/redact.ts`)
|
||||
|
||||
- 动机:追踪器把 read/write 工具结果重放进 DOM;模型读取凭据文件时明文不应上屏。**仅显示层**:会话日志与 fs 层字节不动;不是对同上下文恶意插件的安全边界。
|
||||
- 两层启发式:
|
||||
1. **路径层**:文件名含凭据形态(`.env`、`secret(s)`、`credential(s)`、`token`、`api-key`、`password(s)`、私钥/证书扩展名…)→ 整文件逐行 `[REDACTED]`。匹配带**右边界**(模式后不得继 `[a-z0-9]`),且复数只收「密钥仓库」义(credentials/secrets/passwords),不收 `tokens`(api-tokens.md 通常是文档)——避免 `tokenizer.ts` / `dev.environment.ts` / `style.keys.ts` / `api-tokens.md` 被整文件误伤。
|
||||
2. **内容层**:普通文件按行遮值——赋值形(`api_key:` / `TOKEN=` / `"password":`,**字段名保留**)、`Bearer` 头、`sk-`/`AKIA`/`ghp_`/`gho_`/`xox*`/`ya29.` 前缀 token、PEM 私钥块头。字段表**不含裸词 `key`/`auth`/`token`/`pass`**(规范化整改:这些词标注的普通内容远多于密钥——键盘键名、CSS 自定义属性、词法 token;Bearer 场景由 `BEARER_RE` 单独覆盖)。
|
||||
- 开关:默认开,头部一键切换,localStorage 记忆(key `dsh-sidebar:v1:redaction`,仓库既有前缀)。所有载荷消费方(diff 行、读取行、阅读模式源、错误文本、prior)统一从**遮蔽后**形状渲染;命中时头部显示「已脱敏」条。
|
||||
|
||||
## 5. HTML 渲染模式
|
||||
|
||||
- 适用:`.html / .htm` 操作(与编辑器 html viewer 的扩展集对齐;规范化整改:去掉越权的 `.xhtml`)→ 头部「渲染 / 原文」toggle。
|
||||
- 实现:`HtmlRenderPreview`(`DiffPane.tsx` 导出,供沙箱 spec 钉契约)以 route-src iframe 加载 `/sidebar/html`——相对资源(`./style.css`、`img/x.png`)在路由内解析;分段读取也能渲染完整文档。
|
||||
- **语义边界(刻意)**:路由服务的是**盘上已保存文件**而非操作快照——失败/未落盘的写入会渲染旧内容(README 已声明)。共享常量 `HTML_IFRAME_SANDBOX` 提到 `src/client/html-preview.ts`(编辑器与变更页共用);此面**恒定沙箱**(opaque origin + CSP 头),编辑器 tab 才持有带警告的去沙箱逃生门。
|
||||
|
||||
## 6. PDF 渲染模式(作者 2026-09-06 追加)
|
||||
|
||||
- 适用:`.pdf` 操作(非错误)→ 头部「渲染 / 原文」toggle,复用编辑器 `PdfView`(媒体路由字节流 + 显式类型 Blob → 浏览器原生 PDF 查看器内嵌,附下载入口)。规范化整改:扩展名正则补 `\.` 转义(原 `/.pdf$/i` 会误命中任何以 pdf 结尾的路径)、toggle 并入 `PaneToggle`。无独立单测(PdfView 行为由编辑器侧既有测试覆盖)。
|
||||
|
||||
## 规范化整改(2026-09-06 审查后)
|
||||
|
||||
- **皮肤契约**:`.htmlFrame` 背景硬编码 `#fff` → `var(--dsw-alias-bg-base)`(对齐 `.editorHtml`);`.redactBanner` 原用的 `--dsw-alias-state-warning-primary` 在 DSW 词表中**不存在**(实际为 `state-warn-*`),var() 恒回落到硬编码 `#b8860b` → 改用真实令牌 `var(--dsw-alias-state-warn-primary)`。
|
||||
- **约定对齐**:localStorage key 并入 `dsh-sidebar:v1:` 前缀;`tests/redact.spec.ts` 源码 import 由 `.js` 改 `.ts`(仓库惯例);DiffPane 三份同形 toggle 按钮提取 `PaneToggle`,阅读视图提取 `MdReadingView`(吸收 mermaid 分支与 codeLabels 计算);遮蔽 useMemo 单遍化(每字段一次 `redactText`,此前 hit 扫描 + mask 双遍)。
|
||||
- **去重**:`tsx/jsx` 共享 `TS_WORDS`/`JS_WORDS`;`parseReadContent`/`parseReadLines` 共享 `contentLines()`;CSS 词表去重(`hidden`/`inherit` 重复项)并去掉 CSS 没有的 `true false` 常量。
|
||||
|
||||
## 已知限制
|
||||
|
||||
- 脱敏是启发式:右边界规则放行 `mytoken.yaml`(命中遮蔽)而拒绝 `api-tokens.md`(文档);字段级遮蔽不覆盖未知命名的密钥字段——按 token 形态(`sk-` 等前缀)兜底。一键关闭可在受信任环境看原文。
|
||||
- HTML 渲染读盘上文件(见 §5);mermaid 无引号标签含遮蔽密钥时回落源码(见 §2)。
|
||||
- 阅读模式 edit 重建依赖窗口内已知 prior;窗口外旧内容不可知时仅展示新串(面板本就以「已知内容」为限)。
|
||||
|
||||
## 测试
|
||||
|
||||
`tests/redact.spec.ts`(两层遮蔽 + 边界/裸词误伤回归)、`tests/diff-highlight.spec.ts`(后缀映射、CSS 注释穿行/属性、markup 注释/标签、mjs 关键字)、`tests/changes-ops.spec.ts`(`parseReadContent` 空行保留)、`tests/changes-tab.spec.tsx`(阅读模式端到端:h1/GFM 表格/本地图片改写/切回原文)、`tests/sandbox-views.spec.tsx`(HTML 渲染 iframe 恒定沙箱 + toggle 出现条件)。
|
||||
@@ -1,217 +0,0 @@
|
||||
# Git 折叠行真实展开——修复「点击展开」无反应
|
||||
|
||||
> 日期:2026-09-07 · 状态:计划已定,待实施 · 分支(拟):`fix/diff-fold-expand`
|
||||
> 关联:上游 issue(待提)、上游 PR(待提,Closes issue)· 上游仓库 `omdsh-dev/DSH-better-sidebar`,本地 fork `LaoYueHanNi/DSH-better-sidebar`(main `6fbfeed` 与上游零差异)
|
||||
|
||||
## 1. 背景与 bug 确认(已核实)
|
||||
|
||||
**现象**:changes 组件点击 git 提交(或工作区 diff)后,diff 视图中 hunk 之间的折叠行显示「n 行…点击展开」,点击无反应(无 pointer/hover)。session(会话文件操作)lens 的折叠行可正常展开——仅 git lens 永远不可展开。
|
||||
|
||||
**证据链**:
|
||||
- `src/client/diff/rows.ts` L407-433(`unifiedSegments`):git `-U3` 不输出 hunk 之间的未改动行,gap fold 段**只有行号范围、没有 `rows` 字段**(注释明确 "rows are unknown (non-expandable)");
|
||||
- `src/client/diff/DiffRows.tsx` L193-228:`expandable = segment.rows !== undefined && segment.count >= MIN_FOLD`;`rows === undefined` 走静态分支(L197-202,**无 onClick、无 data-expandable**),但文案复用 `t('changesFold')` = zh「{count} 行…点击展开」/ en「{count} lines…click to expand」——承诺了不存在的交互;
|
||||
- `src/client/diff/diff.module.css` L50-51:仅 `[data-expandable='true']` 有 pointer/hover,静态分支视觉上也不可点,唯独文字撒谎;
|
||||
- session 路径(`diffLines` + `buildDiffSegments` → `foldOf`)的 fold 总有 `rows`,可正常 toggle 展开。
|
||||
|
||||
**上游调研结论**(2026-09-07,9 组中英文关键词 + PR 列表 + `git fetch` 源码比对):无 issue 报告过此问题(最接近的 #346 是「文件级默认折叠要多点一下」,方向相反),无已合并/进行中的修复——**全新发现**。
|
||||
|
||||
**已排除的轻量方案**:
|
||||
- 纯改文案(原方案 A):不解决「看不到被折叠内容」的本质,用户拍板不做;
|
||||
- 调大 `-U` 上下文:大 gap 依旧折叠且依旧不可展开,治标且 diff 变吵。
|
||||
|
||||
## 2. 目标与成功标准
|
||||
|
||||
点击 git 折叠行时**按需拉取两侧完整内容、按行号切片填充真实上下文行**,让「点击展开」名副其实。
|
||||
|
||||
1. git 提交/工作区 diff 折叠行点击后展开未改动上下文行(带双侧行号),再点可折叠;
|
||||
2. session lens 行为完全不变;二进制/untracked 文件不受影响;
|
||||
3. 内容拉取失败降级为无点击暗示的中性文案;
|
||||
4. `pnpm typecheck` / `pnpm test` 全绿,CI 三 job(挂载冒烟/typecheck/test)绿;
|
||||
5. 上游 issue(问题+预计方案)与 PR(实际修复,`Closes #N`)按仓库规范挂接。
|
||||
|
||||
## 3. 已确认的实现地基
|
||||
|
||||
- **服务端 `git.show` 路由已存在**(`src/index.ts` L474-480):payload `{rev, path}`(rev 可为 `:0` / `HEAD` / `<hash>^` / `<hashFull>`),返回 `{content: string | null}`,失败(null)内建于 `git.ts show()` 的 try/catch → **服务端零改动**;
|
||||
- `src/git.ts` `diff()`(L382-388):unstaged = `git diff -- path`(**index vs worktree**),staged = `git diff --cached`(**HEAD vs index**)——决定展开时两侧 rev 取法;
|
||||
- 渲染链:`DiffPane`(底部预览)/ `DiffTab`(独立 tab)→ `DiffFiles`(per-file `unifiedSegments`)→ `DiffRows`;op 路径直接 `DiffRows`(`buildDiffSegments`)。客户端 `api.ts` 尚无 `git.show` 封装(需补一行 call);
|
||||
- `SidebarDiffRef`(`src/client/state.ts` L24-26):`worktree{path, staged, untracked?, worktree?, repoRoot?}` | `commit{hash, hashFull, subject, worktree?, repoRoot?}`;
|
||||
- spawn 数组参数不经 shell,`<hash>^` 的 `^` 无需转义;`git show :0:<path>` 取 index 版本合法。
|
||||
|
||||
## 4. 实现设计(按子系统)
|
||||
|
||||
### 4.1 `src/client/api.ts` — gitShow 客户端封装
|
||||
仿 `gitCommitDiff`(L317-318):
|
||||
```ts
|
||||
gitShow: (scope: SessionScope, rev: string, path: string, worktree?: string, signal?: AbortSignal) =>
|
||||
call<{ content: string | null }>('git.show', gitPayload(scope, worktree, { rev, path }), signal)
|
||||
```
|
||||
走既有 RPC 通道,不触构建纯度门。
|
||||
|
||||
### 4.2 `src/client/diff/rows.ts` — 切片纯函数
|
||||
```ts
|
||||
export function foldRowsFromContents(fold: FoldSegment, oldContent: string, newContent: string): readonly DiffRow[]
|
||||
```
|
||||
- 以 old 侧驱动 `[oldStart..oldEnd]` 逐行生成 `{kind:'context', oldLine, newLine?, text}`,`newLine = oldLine - oldStart + newStart`(仅当落在 `[newStart..newEnd]` 内才带);再以 new 侧驱动补纯新增段(old 映射不到的行,只带 `newLine`);
|
||||
- 内容 `split('\n')` 切;空内容 = 空数组;行尾 `\r` 保留(与 git diff ctx 行行为一致)。
|
||||
|
||||
### 4.3 `src/client/diff/DiffRows.tsx` — 异步展开状态机
|
||||
- 新 prop:`resolveFold?: (segment: FoldSegment) => Promise<readonly DiffRow[]>`(不传 = 现状,op 路径不变);
|
||||
- 新 state:`foldData: Map<number, readonly DiffRow[]>`、`foldLoading: Set<number>`、`foldFailed: Set<number>`,随现有 useEffect(L119-123)在 segments 变化时整体重置;
|
||||
- expandable 判定放宽:`(segment.rows !== undefined || resolveFold !== undefined) && segment.count >= MIN_FOLD`;
|
||||
- 点击未填充的 git fold:置 loading → `resolveFold(segment)` → 成功存 `foldData`(渲染优先于 `segment.rows`)、失败标 `foldFailed`;
|
||||
- 渲染三态:加载中 `t('changesFoldLoading')`;失败 `t('changesFoldUnavailable')`(无 onClick/pointer);正常沿用 `changesFold`(此时承诺为真);已展开的 fold 渲染 rows,再点折叠回。
|
||||
|
||||
### 4.4 `src/client/diff/DiffFiles.tsx` — loader 下传 + Promise 缓存
|
||||
- 新可选 prop `resolveFold?: (file: DiffFile, segment: FoldSegment) => Promise<readonly DiffRow[]>`,转发 `DiffRows`;
|
||||
- 实例级 `useRef<Map<string, Promise<readonly DiffRow[]>>>`,key = `displayPath(newPath)|oldStart|newStart`,同文件多 fold 共享一次内容拉取、并发点击天然去重;`parsed` 变化时重置。
|
||||
|
||||
### 4.5 `src/client/changes/DiffPane.tsx` / `src/client/DiffTab.tsx` — lens loader
|
||||
按 `SidebarDiffRef` 构造传给 `DiffFiles`:
|
||||
- **commit lens**:旧侧 `gitShow(scope, '<hashFull>^', displayPath(file.oldPath))`(root commit → null → ''),新侧 `gitShow(scope, hashFull, displayPath(file.newPath))`;与 `commitDiff` 的 `-m --first-parent` 语义一致;rename 由 `oldPath/newPath` 天然覆盖;
|
||||
- **worktree lens**:`staged=true` → 旧 `HEAD` / 新 `:0`;`staged=false` → 旧 `:0` / 新侧 `api.fsRead` 工作区(`resolveSidebarPath(repoRoot ?? worktree ?? cwd, path)`,同现有 untracked fallback L196/L68 的写法);
|
||||
- **staged 侧漂移修正(关键边缘)**:DiffPane L182-188 / DiffTab L49-57 在请求侧 diff 为空时 fallback 渲染另一侧,但 `gitRef.staged` 不变——把实际生效侧记为 state(如 `effectiveStaged`),loader 依它取数,否则切片行号错位;
|
||||
- untracked 文件走 `untrackedFile` 全加渲染,无 fold,loader 不涉及。
|
||||
|
||||
### 4.6 i18n — 两个新 key,18 个语言文件全补
|
||||
- `changesFoldLoading`:zh `展开中…` / en `Expanding…`
|
||||
- `changesFoldUnavailable`:zh `上下文未加载` / en `Context unavailable`
|
||||
- `locales.ts` 为基准;`locales-ja.ts` 同步是 AGENTS 硬性底线;其余 15 个 `locales-*.ts` 照 `changesFold` 现状全量补译;纯 `t()` 通道,不涉 `markdownTextProps()`。
|
||||
|
||||
### 4.7 测试(vitest,沿用现有模式)
|
||||
- `tests/diff-rows.spec.ts` 增补 `foldRowsFromContents`:对称区间 / 纯删除 gap(old 有 new 无)/ 纯新增 gap / `\r\n` 行尾 / 空内容 / 行号越界截断;
|
||||
- 新组件测试(参照 `tests/diff-files-collapse.spec.tsx`):mock `api.gitShow` → 点击 git fold 断言展开行渲染、再点折叠、失败降级文案、session fold 路径不受影响;
|
||||
- 回归:`diff-rows` / `diff-files-collapse` / `diff-highlight` / `builtins` 全绿。
|
||||
|
||||
## 5. 边缘 case 清单(实现时逐条对照)
|
||||
|
||||
| 场景 | 处置 |
|
||||
|---|---|
|
||||
| root commit 无父 | `git.show` 返 null → 空串(旧侧视为空) |
|
||||
| rename | `parseUnifiedDiff` 保留 oldPath/newPath,两侧各取各的 |
|
||||
| 二进制文件 | 无 hunks,不渲染 DiffRows,不涉及 |
|
||||
| untracked | `untrackedFile` 全加渲染,无 fold |
|
||||
| 无换行文件 | `count` 与切片行数可差 1,展开后以实际行为准 |
|
||||
| `\r\n` 行尾 | blob 行与 diff ctx 行同样保留 `\r`,一致 |
|
||||
| staged fallback 侧漂移 | `effectiveStaged` state(§4.5) |
|
||||
| 并发/重复点击 | Promise 缓存去重 |
|
||||
| refresh / 切目标 | tick + segments 变化重置展开态 |
|
||||
| 大文件 | 按需拉取不预取,失败降级兜底 |
|
||||
|
||||
## 6. 流程步骤(issue / PR 全文草稿)
|
||||
|
||||
### 步骤 1:向上游提 issue
|
||||
|
||||
```bash
|
||||
gh issue create --repo omdsh-dev/DSH-better-sidebar \
|
||||
--title "[Bug] Git 提交/工作区 diff 折叠行显示「n 行…点击展开」,点击无反应" \
|
||||
--body-file <下方正文>
|
||||
```
|
||||
|
||||
模板字段:环境 = `DSH Web(浏览器访问)`;类别 = `🐛 Bug`;DSH 版本可留空;插件版本 = `main(6fbfeed)`。
|
||||
|
||||
**描述字段正文**:
|
||||
|
||||
> **现象**:在「变更」面板点击历史提交(或查看工作区 diff)后,diff 视图中 hunk 之间的折叠行显示「n 行…点击展开」,但点击无任何反应(无 pointer 光标、无 hover);而会话文件操作(文件追踪)lens 的折叠行可以正常展开——仅 git lens 永远不可展开。
|
||||
>
|
||||
> **期望行为**:要么点击真实展开被折叠的上下文行,要么不显示「点击展开」字样。
|
||||
>
|
||||
> **复现步骤(Bug 必填)**:打开侧边栏 → 变更 → 历史 → 点击任一含多个 hunk 的提交 → 观察文件 diff 中 hunk 之间的折叠行 → 点击。
|
||||
>
|
||||
> **根因(代码定位)**:git 的 unified diff(`-U3`)不输出 hunk 之间的未改动行,`unifiedSegments`(`src/client/diff/rows.ts`)为此产生的 fold 段只有行号范围、没有 `rows`;`DiffRows`(`src/client/diff/DiffRows.tsx`)对 `rows === undefined` 渲染静态分支(无 onClick),但复用了含「点击展开」的 `changesFold` 文案,形成「承诺了不存在的交互」。
|
||||
|
||||
**补充信息字段正文**:
|
||||
|
||||
> **预计修正方案**:不采用纯改文案或调大 `-U` 上下文(治标),按需真实展开——点击折叠行时经既有 `git.show` 服务路由拉取该文件两侧完整内容(commit:`<hash>^` 与 `<hash>`;worktree staged:`HEAD` 与 `:0`;unstaged:`:0` 与工作区文件),按 fold 已知行号范围切片为 context 行填充渲染;per 文件 Promise 缓存,失败降级为中性文案。将在 fork 修好后提 PR(Closes 本 issue)。
|
||||
>
|
||||
> 截图:<用户提供的截图,展示「11 行…点击展开」行>
|
||||
|
||||
### 步骤 2:分支与实现
|
||||
|
||||
```bash
|
||||
git checkout -b fix/diff-fold-expand # 基于与上游一致的 main(6fbfeed)
|
||||
```
|
||||
|
||||
按 §4 落地;同时新增本设计文档的正式版(本文件即底稿,文末补「实施偏差」节,惯例参照 `docs/plans/2026-09-06-changes-op-previews-design.md`)。
|
||||
|
||||
**提交拆分**(oyw-commit-style:`type(scope): 中文概括` + 无序列表 body + 背景动机段 + 决策记录伴随行;禁 AI 署名、禁 `--no-verify`):
|
||||
1. `docs(diff): git 折叠上下文按需展开设计文档`
|
||||
2. `feat(api): 暴露 gitShow 单文件版本内容读取`
|
||||
3. `feat(diff): 折叠行异步展开状态机与内容切片`(rows.ts + DiffRows + DiffFiles + 测试)
|
||||
4. `fix(diff): git lens 折叠行接入按需展开,修复点击无反应`(DiffPane/DiffTab + i18n + 组件测试)
|
||||
|
||||
每个 commit 保持 typecheck/test 可绿。
|
||||
|
||||
### 步骤 3:验证
|
||||
|
||||
```bash
|
||||
pnpm typecheck && pnpm test # 全量门禁
|
||||
pnpm build && pnpm pack && pnpm test:mount # 挂载冒烟(CI 同款)
|
||||
```
|
||||
|
||||
真机点开含多 hunk 提交,验证展开/折叠/失败降级,截图用于 PR。
|
||||
|
||||
### 步骤 4:发 PR
|
||||
|
||||
```bash
|
||||
git push -u origin fix/diff-fold-expand
|
||||
gh pr create --repo omdsh-dev/DSH-better-sidebar --head LaoYueHanNi:fix/diff-fold-expand \
|
||||
--title "fix(diff): git 折叠上下文按需展开,修复「点击展开」无反应" --body-file <下方正文>
|
||||
```
|
||||
|
||||
**PR body**:
|
||||
|
||||
> ## 问题
|
||||
> Closes #<N>。git 提交/工作区 diff 的折叠行显示「n 行…点击展开」但点击无反应:`-U3` 裁剪使 `unifiedSegments` 的 gap fold 无行文本,渲染分支复用了含点击承诺的文案。
|
||||
>
|
||||
> ## 方案
|
||||
> 点击时按需经 `git.show` 拉取两侧完整内容,按 fold 行号切片为 context 行填充;不预取(大 lockfile 场景零开销),per 文件 Promise 缓存去重并发,失败降级为「上下文未加载」中性文案。session lens 行为不变,服务端零改动(`git.show` 路由已存在)。
|
||||
>
|
||||
> ## 改动清单
|
||||
> - `src/client/api.ts`:gitShow 封装
|
||||
> - `src/client/diff/rows.ts`:`foldRowsFromContents` 纯函数
|
||||
> - `src/client/diff/DiffRows.tsx`:`resolveFold` prop + 展开/加载/失败状态机
|
||||
> - `src/client/diff/DiffFiles.tsx`:loader 下传 + Promise 缓存
|
||||
> - `src/client/changes/DiffPane.tsx` / `src/client/DiffTab.tsx`:两种 lens 的取数映射(含 staged fallback 侧修正)
|
||||
> - `src/client/locales*.ts` × 18:`changesFoldLoading` / `changesFoldUnavailable`
|
||||
> - `tests/diff-rows.spec.ts` + 新组件测试
|
||||
> - `docs/plans/…`:设计文档
|
||||
>
|
||||
> ## 测试
|
||||
> - 新增:切片纯函数单测(对称/纯删/纯增/\r\n/越界);组件测试(mock gitShow:展开、折叠、降级)
|
||||
> - 回归:`pnpm typecheck`、`pnpm test` 全绿;`pnpm test:mount` 冒烟通过
|
||||
> - 截图:<展开前后对比>
|
||||
|
||||
### 步骤 5:review 跟进
|
||||
|
||||
按上游惯例(#499 经验)预判 review 关注点:皮肤令牌(本改动无颜色,不涉)、约定对齐(prop 命名/状态机形状)、i18n 完整性;整改意见在同分支追加 commit,重跑门禁。
|
||||
|
||||
## 7. 明确不做
|
||||
|
||||
- 不做纯文案拆分的「方案 A」单独 PR(失败降级文案已含在 B 内);
|
||||
- 不调大 `-U` 上下文;
|
||||
- 不改服务端;
|
||||
- 不动 `changesFold` 现有文案(B 落地后「点击展开」对 git fold 也为真)。
|
||||
|
||||
## 8. 仓库规范备忘(实施时遵守)
|
||||
|
||||
- 代码改动必须走 PR(AGENTS §1);fork 侧开发,push 到 `LaoYueHanNi` 后向上游发 PR;
|
||||
- 风格对齐:prop 命名、状态机形状、组件拆分与 CSS 模块组织以 `src/client/diff/` 现有实现为准,不引入新依赖/新工具/新交互模式;有歧义时参照上游最近合入的同类改动;
|
||||
- i18n:zh 新 key 必须同步 ja(其余语言全补);
|
||||
- 皮肤契约:无硬编码颜色(本改动无颜色);
|
||||
- 提交信息遵循 oyw-commit-style,正文无序列表列改动点 + 背景动机段;
|
||||
- 仅在用户明确说「提交」时才 commit;PR 创建前等用户批准。
|
||||
|
||||
---
|
||||
|
||||
## 实施偏差记录
|
||||
|
||||
> 实施过程中与本文设计的任何偏离,逐条记录在此(惯例:文档以本节为准)。
|
||||
|
||||
1. **语言文件数量**:§4.6 / §6 的「18 个语言文件」计数有误——实际变更点为 **20 个文件 21 处**(`locales.ts` 内 zh/en 双块 + 19 个 `locales-*.ts`:ar/de/fr/hi/id/ko/it/ja/pl/nl/pt/ru/sv/th/tr/vi/zh-HK/zh-MO/zh-TW),已全量补齐,PR body 计数随之更正。
|
||||
2. **缓存拆为两层**:§4.4 把「同文件多 fold 共享一次内容拉取」归给 DiffFiles 的 fold 级 Promise 缓存,但 fold 级 key 含行号(`path|oldStart|newStart`),无法去重内容请求;实施时把内容拉取缓存上移到 lens 层(`DiffPane`/`DiffTab` 的 `foldContents`,key = `displayPath`),DiffFiles 的 fold 级缓存保留(价值:文件头折叠再展开导致 DiffRows 重挂载后秒开、并发点击去重,reject 时摘除)。两层各司其职。
|
||||
3. **loading 态交互与竞态防护**:§4.3 三态渲染之外,加载中的 fold 不响应点击且无 pointer(`data-expandable` 移除),防 loading 期间重复触发;另以 `foldEpoch` ref 使 segments 重置后 still-in-flight 的 resolve 回调失效,防「旧 fold 的迟到结果写进新 segments 同下标」竞态(§4.3 未展开)。
|
||||
4. **unstaged 新侧读取失败的宽容降级**:§4.5 未细化——工作区侧 `fsRead` 失败(如文件已删除)或返回二进制时降级为空串(catch → null → `''`)而非整体 failed,让旧侧(index)上下文依然可展开。
|
||||
5. **DiffPane 组件级 gitScope**:fold loader 需要 SessionScope,新增组件级 `gitScope`(useMemo);load effect 的局部 `paneScope` 保持原样不动,最小侵入。
|
||||
6. **本机测试基线**:Windows 本机全量跑 `tests/git.spec.ts` / `tests/git-worktree.spec.ts` 中 3 个真实 git 子进程集成用例会并发抖动失败(stash 后基线复现同样失败,与本次改动无关;CI Linux 绿)。diff 相关 18/18 全绿(新增 `foldRowsFromContents` 6 用例 + 组件测试 4 用例)。
|
||||
7. **本地挂载冒烟未执行**:Windows 本地 Git Bash 下 `e2e-mount.sh` 将 tarball 规范为 MSYS 风格 `/d/...`,`file:` scheme 前缀使 MSYS 自动参数转换失效,Windows node 解析为 `C:\d\...` 而 ENOENT(经 `DSH_CMD` 注入 cygpath wrapper 可绕过,再因依赖下载网络受限放弃);`pnpm build` 产物构建已绿,挂载冒烟由 CI `plugin-mount` job 同款覆盖。
|
||||
8. **服务端零改动前提被证伪(真机修正)**:§3/§5 的「git.show 路由已存在、服务端零改动」不成立——该路由此前无真实调用方,其 path 经 `resolveGitPath` 解析为绝对文件系统路径(工作区命令语义),而 `git show <rev>:<path>` 只接受仓库相对路径,恒 fatal 返 null,两侧空串切片 0 行,展开表现为「marker 消失、零行渲染」(link 真机首测发现)。修正:路由透传仓库相对路径(diff 输出剥前缀的形态,暴露面与 git.diff/git.log 一致,仅本仓库版本树),另在两 lens 加「两侧皆空 → 降级『上下文未加载』」防御,取数异常不再静默空白展开。
|
||||
@@ -1,79 +0,0 @@
|
||||
# 终端 Shell 配置加固(引号解析 + 双平台预检 + 友好报错)设计
|
||||
|
||||
日期:2026-09-08 | 状态:已确认(用户选定方案二) | 分支:`feat/terminal-shell-quoted-args-probe`
|
||||
|
||||
## 背景与现状
|
||||
|
||||
配置启动终端命令的功能已存在于 main(`fd9544f` #95 部署级 `config.shell`/`shellArgs` + pwsh 探测;`c973d6e` #125 设置页 `terminalShell`/`terminalShellArgs` 用户偏好,覆盖顺序:设置页 > yaml > 平台自动,UI 标签页与模型 `terminal_*` 工具两终端面共用)。本次不是新增功能,而是加固三处已知缺口:
|
||||
|
||||
1. **参数解析硬切**:`shellOverridesOf`(`src/index.ts:257`)用 `args.split(/\s+/)`,引号内空格被切碎(`-File "C:\my script\init.ps1"` 得到 3 个参数)。
|
||||
2. **POSIX 无预检**:`resolveShellExecutable`(`src/pty-manager.ts:351`)仅 win32 做存在性探测(PATHEXT/PATH/System32/pwsh 目录,找不到抛 `SidebarError('pty-error', 'shell executable not found: …')` → WS catch `ws.close(1011, message)`);POSIX 直接透传 node-pty,配错路径只得到 `[process exited with code N]`。
|
||||
3. **报错不可操作**:客户端 `TerminalView.tsx:247` 对任意 `1011 + reason` 显示原始英文 reason 横幅,没有本地化说明与"去设置改"指引;粘贴带引号路径(`"C:\Program Files\…\pwsh.exe"`)会因引号参与路径解析而失败;`shell.get` 返回启动时解析的 shell,设置页覆盖后新终端标签标题不跟随。
|
||||
|
||||
## 方案取舍
|
||||
|
||||
- **方案一(最小)**:只修引号参数解析。POSIX 配错仍不可诊断,引号路径仍炸——否。
|
||||
- **方案二(选定)**:解析 + 双平台预检 + 本地化友好报错 + 标题跟随。不碰声明式设置契约。
|
||||
- **方案三(全家桶)**:方案二 + 设置页实时 `shell.probe` inline 校验。校验型输入行需扩展 declarative settings 契约(`service.ts` + `docs/external-plugin-guide.md`),收益/成本比低(保存后开终端立即可见)——YAGNI,不做。
|
||||
|
||||
## 设计
|
||||
|
||||
### 1. 引号感知参数解析 `splitShellArgs`(`src/pty-manager.ts`)
|
||||
|
||||
- 空白分词;`'…'` / `"…"` 分组,组内字符**全部字面**——`\` 不是转义符(Windows 路径场景直接可用)。代价:参数值本身含引号无法表达,注释注明该取舍。
|
||||
- 未闭合引号宽容处理:剩余输入并入当前 token(不抛错——设置页输入体验优先)。
|
||||
- 空引号对 `""` 丢弃,不产生空参数。
|
||||
- `shellOverridesOf` 中 `args.split(/\s+/).filter(Boolean)` 替换为 `splitShellArgs(args)`。
|
||||
|
||||
### 2. 路径去引号 `unquotePath`(`src/pty-manager.ts`)
|
||||
|
||||
- 长度 ≥2 且首尾为成对 `"` 或 `'` 时剥掉,否则原样返回。
|
||||
- 统一入口:`shellOverridesOf` 与 `resolveShellExecutable` 首行均走它(单一实现防两处漂移)。
|
||||
|
||||
### 3. POSIX 预检(泛化 `resolveShellExecutable`)
|
||||
|
||||
- POSIX 分支:配置值含 `/` → `existsSync` 直查;裸名 → 沿 `env.PATH`(`:` 分隔,缺省回退 `/usr/bin:/bin`)逐目录探测。
|
||||
- 找不到统一抛 `SidebarError('shell-not-found', 'shell executable not found: "…"')`(新错误码;消息保持英文技术文案——模型侧 `terminal_create` 工具报错复用同一消息,模型读英文更稳)。
|
||||
- `ShellExecutableResolutionOptions` 的 `platform/env/exists` 注入口径不变,win32 逻辑不动。
|
||||
|
||||
### 4. 友好报错(host ↔ client 线协议)
|
||||
|
||||
- `SidebarError` 增加可选 `meta` 载荷,`shell-not-found` 时携带配置的 shell 原文。
|
||||
- WS 升级 handler catch(`src/index.ts` 终端 upgrade)识别 `error.code === 'shell-not-found'` → close reason 发机器标记 `shell-not-found:<shellDisplayName(shell)>`(受 WS close reason 123 字节上限约束,超长截断显示名)。
|
||||
- `TerminalView.tsx` `onclose` 在通用 1011 分支**之前**识别 `reason.startsWith('shell-not-found:')` → 本地化 fatal 横幅:`terminalShellNotFound`(「未找到配置的 Shell:<名>」)+ `terminalShellNotFoundHint`(「请检查 设置 → 侧边卡片 → 终端 的 Shell 路径」)。
|
||||
- locale 新 key 落点:`locales.ts`(zh + en 两块)+ `locales-ja.ts`(仓库规则强制同步),其余语言文件走既有回退链。
|
||||
|
||||
### 5. 标题跟随
|
||||
|
||||
- `shell.get` 路由改为 `overrides.shell ?? 启动时解析值`,`shellDisplayName` 随之;仅影响新开标签的标题,不触碰已存在 pty。
|
||||
|
||||
## 错误流总览
|
||||
|
||||
```
|
||||
设置页 terminalShell/Args(或 yaml config.shell/Args)
|
||||
→ shellOverridesOf:unquotePath(shell) + splitShellArgs(args)
|
||||
→ ptyManager.open / registry.create
|
||||
→ resolveShellExecutable:win32 既有探测 ∥ POSIX 新探测
|
||||
→ 找不到:SidebarError('shell-not-found', msg, meta:{shell})
|
||||
├─ WS 路径:catch → close(1011, 'shell-not-found:<name>') → TerminalView 本地化横幅 + 设置指引
|
||||
└─ 工具路径:terminal_create 抛英文消息 → 模型可见,自行纠正
|
||||
```
|
||||
|
||||
## 测试计划
|
||||
|
||||
- `tests/pty-helpers.spec.ts`:`splitShellArgs` 语义矩阵(普通分词 / 单双引号 / 含空格路径 / 反斜杠字面 / 未闭合宽容 / 空引号丢弃 / 连续空白);`unquotePath`(成对剥、不成对留);POSIX 探测(fake `options.env.PATH` + `options.exists`:命中解析、绝对路径缺失抛 `shell-not-found`);win32 既有用例保持绿。
|
||||
- `shellOverridesOf` 引号行为与 `shell.get` 跟随:就近扩展现有 spec,无宿主则新建。
|
||||
- 客户端横幅为纯展示逻辑,已有 TerminalView spec 则扩展,否则不强行 e2e。
|
||||
- 门禁:`pnpm typecheck` + `pnpm test`;不改 chunk 边界与 exclude 配置,mount e2e 不受影响。
|
||||
|
||||
## 实施偏差记录
|
||||
|
||||
- **报错文案合并为单 key**:设计时的 `terminalShellNotFound` + `terminalShellNotFoundHint` 两 key 合并为单个 `terminalShellNotFound`(含 `{name}` 占位符,一句话内含指引),避免跨语言的句子拼接问题。
|
||||
- **POSIX 旧契约用例被有意改写**:`tests/pty-helpers.spec.ts` 原用例 `keeps POSIX bare shell resolution delegated to execvp` 锁定「POSIX 裸名直通 execvp」,与本设计冲突,已删除并由 `POSIX: resolves bare names along PATH…` 等新用例替代。
|
||||
- **locale 落点为全部 21 词典**:设计原计划只写 zh/en/ja,实施时受 `tests/locales.spec.ts` 的「第三方词典键集必须与 zh 相等」门禁约束,`terminalShellNotFound` 实际落入 `locales.ts`(zh+en 双块)+ 19 个 `locales-*.ts`(共 21 文件 22 处),`chunks/locale.tsx` 的 `Record<CopyKey, string>` 类型检查兜底。
|
||||
- **`resolveShellExecutable` 的 JSDoc 同步**:win32 错误码从 `pty-error` 变更为 `shell-not-found` 后,函数上方文档一并更新(commit `8704994`)。
|
||||
- **本机全量测试基线**:`pnpm test` 在本机(Windows)存在 29 个**预存**环境性失败(5 个 client spec 的 jsdom `reading 'clear'` ×24、smoke 的 git 身份 ×2、git.spec 截断用例、agent-pty 的 node-pty「Signals not supported on windows」unhandled error)。经与 `origin/main` 同法全量对比确认**零新增失败**(main 30 个,feat 29 个——main 上多出的 `git-worktree` 用例为 flaky)。CI 以 CI 环境为准。
|
||||
|
||||
## 交付
|
||||
|
||||
- AGENTS 硬约束:`feat/*` 分支开发,commit 后 `gh pr create`,review 合并进 main;不直推。
|
||||
@@ -1,645 +0,0 @@
|
||||
# 终端 Shell 配置加固 实施计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 设置页/yaml 配置的终端 shell 支持引号参数与带引号路径,POSIX 与 Windows 一致地预检可执行文件,配置错误时终端标签显示本地化的可操作报错,且终端标题跟随设置覆盖。
|
||||
|
||||
**Architecture:** 全部改动在插件仓库内:`pty-manager.ts` 新增两个纯函数(`unquotePath`/`splitShellArgs`)并把 `resolveShellExecutable` 的探测泛化到 POSIX(统一抛新错误码 `shell-not-found`,`SidebarError` 加可选 `meta`);`index.ts` 的 `shellOverridesOf` 改用新解析器、`shell.get` 返回覆盖后的有效 shell、`attachTerminal` 的 catch 把错误映射为机器可读 WS close reason `shell-not-found:<名>`;`TerminalView.tsx` 识别该前缀渲染本地化横幅(全部 21 个语言词典同步新 key)。
|
||||
|
||||
**Tech Stack:** TypeScript(tsdown 构建,vitest 测试,无新依赖)
|
||||
|
||||
**Spec:** `docs/plans/2026-09-08-terminal-shell-config-hardening-design.md`(本计划与其同读;偏差回填其「实施偏差记录」节)
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 禁止修改 DSH 源码(`~/.dsh/source/current` 零写入)。
|
||||
- 代码改动走 `feat/*` 分支 + `gh pr create`,不直推 main。
|
||||
- 不新增任何依赖;不改 `dependencies`/`peerDependencies`/`scripts`。
|
||||
- **i18n 键集相等是硬门禁**:新 key 必须同时加进 `locales.ts`(zh + en 两块)与全部 19 个第三方词典(`locales-ja/-de/-fr/-pt/-ko/-ar/-hi/-id/-tr/-vi/-th/-ru/-it/-nl/-sv/-pl/-zh-HK/-zh-TW/-zh-MO.ts`);`chunks/locale.tsx` 的 `Record<CopyKey, string>` 类型检查 + `tests/locales.spec.ts` 双重拦截。
|
||||
- 不改 vitest `exclude`;不改 chunk 边界(不新增 core bundle 的静态 import)。
|
||||
- 测试命令一律 `pnpm exec vitest run <file>`(仓库用 vitest + tsconfig `allowImportingTsExtensions`)。
|
||||
- 每个任务以 commit 结束,conventional commits(`feat(terminal): …` / `test(terminal): …`),正文可中文。
|
||||
|
||||
---
|
||||
|
||||
### Task 0: 建分支 + 提交设计与计划文档
|
||||
|
||||
**Files:**
|
||||
- Create: 分支 `feat/terminal-shell-quoted-args-probe`
|
||||
- 已存在待提交: `docs/plans/2026-09-08-terminal-shell-config-hardening-design.md`、`docs/plans/2026-09-08-terminal-shell-config-hardening-plan.md`
|
||||
|
||||
- [ ] **Step 1: 确认工作树干净并建分支**
|
||||
|
||||
```bash
|
||||
git status --porcelain # 期望为空(main 已与 origin/main 同步)
|
||||
git checkout -b feat/terminal-shell-quoted-args-probe
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit 设计与实施计划**(文档随 feat 分支走 PR,保持 PR 自洽)
|
||||
|
||||
```bash
|
||||
git add docs/plans/2026-09-08-terminal-shell-config-hardening-design.md docs/plans/2026-09-08-terminal-shell-config-hardening-plan.md
|
||||
git commit -m "docs(plans): 终端 shell 配置加固设计与实施计划"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1: `unquotePath` + `splitShellArgs` 纯函数(TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/pty-manager.ts`(文件末尾 `shellSpawnArgs` 之后追加)
|
||||
- Test: `tests/pty-helpers.spec.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `unquotePath(value: string): string`、`splitShellArgs(input: string): string[]`(均导出,Task 3 的 `shellOverridesOf` 消费)
|
||||
|
||||
- [ ] **Step 1: 写失败测试** — `tests/pty-helpers.spec.ts` 顶部 import 补两个名字(第 19-25 行的现有 import 块):
|
||||
|
||||
```ts
|
||||
import {
|
||||
defaultShell,
|
||||
ensureSpawnHelper,
|
||||
resolveShellExecutable,
|
||||
shellDisplayName,
|
||||
shellSpawnArgs,
|
||||
splitShellArgs,
|
||||
unquotePath,
|
||||
} from '../src/pty-manager.ts'
|
||||
```
|
||||
|
||||
在 `describe('pty helpers', () => {` 内、`'restores the spawn-helper executable bit idempotently'` 用例之后追加:
|
||||
|
||||
```ts
|
||||
it('unquotes a paired surrounding quote from a configured shell path', () => {
|
||||
expect(unquotePath('"C:\\Program Files\\PowerShell\\7\\pwsh.exe"'))
|
||||
.toBe('C:\\Program Files\\PowerShell\\7\\pwsh.exe')
|
||||
expect(unquotePath("'/usr/bin/my shell'")).toBe('/usr/bin/my shell')
|
||||
// Unpaired or single characters stay verbatim.
|
||||
expect(unquotePath('"mismatched')).toBe('"mismatched')
|
||||
expect(unquotePath('pwsh.exe')).toBe('pwsh.exe')
|
||||
expect(unquotePath('"')).toBe('"')
|
||||
expect(unquotePath('')).toBe('')
|
||||
})
|
||||
|
||||
it('splits shell args with quote-aware grouping', () => {
|
||||
expect(splitShellArgs('-NoLogo -File "C:\\my init\\init.ps1"'))
|
||||
.toEqual(['-NoLogo', '-File', 'C:\\my init\\init.ps1'])
|
||||
// Single quotes group too, and preserve inner double quotes verbatim.
|
||||
expect(splitShellArgs("-c 'echo \"hi\"'")).toEqual(['-c', 'echo "hi"'])
|
||||
expect(splitShellArgs(' -l ')).toEqual(['-l'])
|
||||
expect(splitShellArgs('')).toEqual([])
|
||||
expect(splitShellArgs(' ')).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps backslashes literal inside quotes and tolerates an unclosed quote', () => {
|
||||
// Backslash is NOT an escape (Windows paths): "C:\a\" ends with a slash.
|
||||
expect(splitShellArgs('"C:\\a\\"')).toEqual(['C:\\a\\'])
|
||||
// An unclosed quote folds the remainder into one token instead of erroring.
|
||||
expect(splitShellArgs('"unclosed quote')).toEqual(['unclosed quote'])
|
||||
// Empty quote pairs produce no empty-string argument.
|
||||
expect(splitShellArgs('"" x')).toEqual(['x'])
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run tests/pty-helpers.spec.ts`
|
||||
Expected: FAIL — `splitShellArgs`/`unquotePath` 不是导出成员(`SyntaxError` 或 `undefined is not a function`)。
|
||||
|
||||
- [ ] **Step 3: 最小实现** — `src/pty-manager.ts` 末尾(`shellSpawnArgs` 函数之后)追加:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Strip ONE pair of surrounding quotes from a configured shell path. Users
|
||||
* paste Windows paths with spaces pre-quoted (`"C:\Program Files\…"`); the
|
||||
* quotes are shell-input syntax, not part of the path. Unpaired quotes and
|
||||
* shorter values stay verbatim.
|
||||
*/
|
||||
export function unquotePath(value: string): string {
|
||||
if (value.length >= 2) {
|
||||
const first = value[0]
|
||||
const last = value[value.length - 1]
|
||||
if ((first === '"' && last === '"') || (first === "'" && last === "'")) return value.slice(1, -1)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a settings-page shell-arguments string into argv with quote-aware
|
||||
* grouping: `'…'` / `"…"` group whitespace, and characters inside quotes are
|
||||
* LITERAL — a backslash is never an escape, so Windows paths survive intact
|
||||
* (`-File "C:\my init\init.ps1"` → three tokens, the last containing spaces).
|
||||
* The price is that an argument containing a literal quote character cannot
|
||||
* be expressed; shell startup arguments never need one. An unclosed quote
|
||||
* folds the remainder into the current token (settings input stays
|
||||
* forgiving); an empty quote pair yields no argument.
|
||||
*/
|
||||
export function splitShellArgs(input: string): string[] {
|
||||
const args: string[] = []
|
||||
let current = ''
|
||||
let quote: '"' | "'" | null = null
|
||||
let started = false
|
||||
for (const ch of input) {
|
||||
if (quote !== null) {
|
||||
if (ch === quote) quote = null
|
||||
else current += ch
|
||||
continue
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch
|
||||
started = true
|
||||
continue
|
||||
}
|
||||
if (/\s/.test(ch)) {
|
||||
if (started) {
|
||||
args.push(current)
|
||||
current = ''
|
||||
started = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
current += ch
|
||||
started = true
|
||||
}
|
||||
if (started) args.push(current)
|
||||
return args.filter(arg => arg !== '')
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `pnpm exec vitest run tests/pty-helpers.spec.ts`
|
||||
Expected: PASS(全部用例)。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/pty-manager.ts tests/pty-helpers.spec.ts
|
||||
git commit -m "feat(terminal): 引号感知的 shell 参数解析与路径去引号纯函数"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: POSIX 预检 + `shell-not-found` 错误码(TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/wire.ts:10-36`(错误码联合 + 构造器 meta)
|
||||
- Modify: `src/pty-manager.ts:351-400`(`resolveShellExecutable` POSIX 分支)
|
||||
- Test: `tests/pty-helpers.spec.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 的 `unquotePath`
|
||||
- Produces: `SidebarErrorCode` 新值 `'shell-not-found'`;`SidebarError` 构造器第 4 参 `meta?: Record<string, string>`;`resolveShellExecutable` 在 POSIX 找不到时抛 `SidebarError('shell-not-found', 'shell executable not found: "<configured>"', 400, { shell: configured })`(Task 3 的 WS catch 消费)
|
||||
|
||||
- [ ] **Step 1: 写失败测试** — `tests/pty-helpers.spec.ts` 追加(import 区补 `SidebarError`:`import { SidebarError } from '../src/wire.ts'`):
|
||||
|
||||
```ts
|
||||
it('POSIX: resolves bare names along PATH and names a missing configured shell', () => {
|
||||
const options = {
|
||||
platform: 'linux' as const,
|
||||
env: { PATH: '/usr/local/bin:/usr/bin' },
|
||||
exists: (path: string) => path === join('/usr/bin', 'zsh'),
|
||||
}
|
||||
expect(resolveShellExecutable('zsh', options)).toBe(join('/usr/bin', 'zsh'))
|
||||
const thrown = (() => {
|
||||
try {
|
||||
resolveShellExecutable('nope', options)
|
||||
return undefined
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
})()
|
||||
expect(thrown).toBeInstanceOf(SidebarError)
|
||||
expect((thrown as SidebarError).code).toBe('shell-not-found')
|
||||
expect((thrown as SidebarError).message).toBe('shell executable not found: "nope"')
|
||||
expect((thrown as SidebarError).meta?.shell).toBe('nope')
|
||||
})
|
||||
|
||||
it('POSIX: checks an absolute path exists and passes it through verbatim', () => {
|
||||
expect(resolveShellExecutable('/explicit/zsh', { platform: 'linux', env: {}, exists: () => true }))
|
||||
.toBe('/explicit/zsh')
|
||||
expect(() => resolveShellExecutable('/missing/zsh', { platform: 'linux', env: {}, exists: () => false }))
|
||||
.toThrow('shell executable not found: "/missing/zsh"')
|
||||
})
|
||||
|
||||
it('unquotes the configured shell before resolving (Windows probe included)', () => {
|
||||
const options = {
|
||||
platform: 'win32' as const,
|
||||
env: { PATH: 'C:\\Tools' },
|
||||
exists: (path: string) => path.replaceAll('\\', '/') === 'C:/Tools/pwsh.exe',
|
||||
}
|
||||
expect(resolveShellExecutable('"pwsh.exe"', options).replaceAll('\\', '/')).toBe('C:/Tools/pwsh.exe')
|
||||
})
|
||||
```
|
||||
|
||||
**同时改写旧契约用例**(第 115-117 行 `'keeps POSIX bare shell resolution delegated to execvp'`)——该用例锁定的正是本任务**有意变更**的行为,整段替换为上面第一个新用例即可(删除旧用例)。
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run tests/pty-helpers.spec.ts`
|
||||
Expected: FAIL — `'nope'` 原样返回而非抛错(旧 POSIX 直通行为),且 `SidebarError` 无 `meta` 属性。
|
||||
|
||||
- [ ] **Step 3: wire.ts 加错误码与 meta** — `src/wire.ts`:
|
||||
|
||||
```ts
|
||||
/** Machine-readable error codes of the sidebar API. */
|
||||
export type SidebarErrorCode =
|
||||
| 'bad-request'
|
||||
| 'not-found'
|
||||
| 'forbidden'
|
||||
| 'method-error'
|
||||
| 'too-large'
|
||||
| 'fs-error'
|
||||
| 'git-error'
|
||||
| 'pty-error'
|
||||
| 'pty-deps-missing'
|
||||
| 'shell-not-found'
|
||||
| 'job-error'
|
||||
| 'sidechat-error'
|
||||
| 'subagents-unavailable'
|
||||
| 'settings-rejected'
|
||||
| 'settings-conflict'
|
||||
| 'internal'
|
||||
|
||||
/** One API failure with its wire code and HTTP status. */
|
||||
export class SidebarError extends Error {
|
||||
constructor(
|
||||
readonly code: SidebarErrorCode,
|
||||
message: string,
|
||||
readonly status = 400,
|
||||
/** Optional structured context (e.g. `{ shell }` for shell-not-found). */
|
||||
readonly meta?: Record<string, string>,
|
||||
) {
|
||||
super(message)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: pty-manager.ts 泛化探测** — `resolveShellExecutable` 整体替换为:
|
||||
|
||||
```ts
|
||||
export function resolveShellExecutable(
|
||||
shell: string,
|
||||
options: ShellExecutableResolutionOptions = {},
|
||||
): string {
|
||||
const configured = unquotePath(shell.trim())
|
||||
const platform = options.platform ?? process.platform
|
||||
if (configured === '') return configured
|
||||
|
||||
const env = options.env ?? process.env
|
||||
const exists = options.exists ?? existsSync
|
||||
const notFound = (): SidebarError =>
|
||||
new SidebarError('shell-not-found', `shell executable not found: "${configured}"`, 400, { shell: configured })
|
||||
|
||||
if (platform === 'win32') {
|
||||
const rawPathext = windowsEnv(env, 'PATHEXT')
|
||||
const executableExts = (rawPathext ?? '.COM;.EXE')
|
||||
.split(';')
|
||||
.map(extension => extension.trim())
|
||||
// node-pty ultimately calls CreateProcess; batch files need an
|
||||
// intermediate cmd.exe and therefore are not valid shell executables.
|
||||
.filter(extension => /^\.(?:com|exe)$/i.test(extension))
|
||||
if (executableExts.length === 0) executableExts.push('.EXE', '.COM')
|
||||
|
||||
const hasExtension = win32Path.extname(configured) !== ''
|
||||
const names = hasExtension
|
||||
? [configured]
|
||||
: executableExts.map(extension => configured + extension.toLowerCase())
|
||||
const hasPath = win32Path.isAbsolute(configured) || /[\\/]/.test(configured)
|
||||
const candidates: string[] = []
|
||||
if (hasPath) {
|
||||
candidates.push(...names)
|
||||
} else {
|
||||
const path = windowsEnv(env, 'PATH')
|
||||
if (path !== undefined) {
|
||||
for (const dir of path.split(';').map(entry => entry.trim()).filter(Boolean)) {
|
||||
for (const name of names) candidates.push(win32Path.join(dir, name))
|
||||
}
|
||||
}
|
||||
const systemRoot = windowsEnv(env, 'SystemRoot')
|
||||
if (systemRoot !== undefined && systemRoot.trim() !== '') {
|
||||
for (const name of names) candidates.push(win32Path.join(systemRoot, 'System32', name))
|
||||
}
|
||||
if (/^pwsh(?:\.exe)?$/i.test(configured)) {
|
||||
for (const dir of windowsPwshCandidateDirs(env)) {
|
||||
candidates.push(win32Path.join(dir, 'pwsh.exe'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidate of [...new Set(candidates)]) {
|
||||
if (exists(candidate)) return candidate
|
||||
}
|
||||
throw notFound()
|
||||
}
|
||||
|
||||
// POSIX: the previous pass-through delegated a wrong name to execvp and the
|
||||
// pty died with a bare "[process exited with code N]". Probe like Windows:
|
||||
// a path with a separator must exist; a bare name is searched along PATH
|
||||
// (the colon form is fixed by the platform). A miss is a clear, actionable
|
||||
// error instead of a cryptic exit code.
|
||||
if (configured.includes('/')) {
|
||||
if (!exists(configured)) throw notFound()
|
||||
return configured
|
||||
}
|
||||
const path = env.PATH ?? '/usr/bin:/bin'
|
||||
for (const dir of path.split(':').map(entry => entry.trim()).filter(Boolean)) {
|
||||
const candidate = join(dir, configured)
|
||||
if (exists(candidate)) return candidate
|
||||
}
|
||||
throw notFound()
|
||||
}
|
||||
```
|
||||
|
||||
确认 `src/pty-manager.ts` 已有 `join`(node:path)与 `SidebarError` import(`SidebarError` 来自 `./wire.ts`——若尚无此 import 则补 `import { SidebarError } from './wire.ts'`)。
|
||||
|
||||
- [ ] **Step 5: 跑测试确认通过**
|
||||
|
||||
Run: `pnpm exec vitest run tests/pty-helpers.spec.ts`
|
||||
Expected: PASS。随后跑全量确认无连带破坏:`pnpm exec vitest run tests/smoke.spec.ts tests/tools.spec.ts`(POSIX 预检改变了裸名缺失时的行为——若 smoke/tools 有依赖直通的用例,按新契约修正其期望值并在 commit 正文注明)。
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/wire.ts src/pty-manager.ts tests/pty-helpers.spec.ts
|
||||
git commit -m "feat(terminal): POSIX 也预检 shell 可执行文件,统一 shell-not-found 错误码"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: host 接线——覆盖解析、shell.get 跟随、WS close reason(TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/index.ts:248-259`(`shellOverridesOf`)、`:562-566`(`shell.get`)、`:1306-1308`(`attachTerminal` catch)、新增导出函数 `wsCloseReasonOf`
|
||||
- Test: `tests/smoke.spec.ts:823-881`(fake settings)+ 新用例
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 `unquotePath`/`splitShellArgs`;Task 2 `SidebarError.meta` + `'shell-not-found'`
|
||||
- Produces: `shellOverridesOf` 返回 `{ shell?: string; shellArgs?: string[] }`(引号已处理);`wsCloseReasonOf(error: unknown): string`(导出,`shell-not-found` → `shell-not-found:<displayName≤100字符>`,其余 → message/String);WS close reason 线协议前缀 `shell-not-found:`(Task 4 客户端镜像)
|
||||
|
||||
- [ ] **Step 1: 写失败测试** — `tests/smoke.spec.ts`:
|
||||
|
||||
(a) fake settings 的 `register` 保留预置值(第 837-840 行)——替换为:
|
||||
|
||||
```ts
|
||||
register(ns: string, schema: unknown) {
|
||||
// Preserve a pre-seeded value: tests stage prefs through the `pre`
|
||||
// map before the plugin mounts and registers the same namespace.
|
||||
const existing = namespaces.get(ns)
|
||||
namespaces.set(ns, { schema, value: existing?.value ?? undefined, revision: 0 })
|
||||
return { get: () => ({}), watch: () => () => {}, update: async () => {}, replace: async () => {} }
|
||||
},
|
||||
```
|
||||
|
||||
(b) `'side card settings routes'` describe 内追加(`invoke`/`mountWithSettings` 复用):
|
||||
|
||||
```ts
|
||||
it('shell.get reflects the settings-page override with the quotes stripped', async () => {
|
||||
const route = mountWithSettings(createFakeSettings({
|
||||
'dsh-better-sidebar': {
|
||||
terminalShell: '"C:\\Program Files\\PowerShell\\7\\pwsh.exe"',
|
||||
terminalShellArgs: '-NoLogo',
|
||||
},
|
||||
}))
|
||||
const result = await invoke(route, 'shell.get', {})
|
||||
expect(result.ok).toBe(true)
|
||||
expect(result.value).toMatchObject({
|
||||
shell: 'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
||||
name: 'pwsh',
|
||||
})
|
||||
})
|
||||
|
||||
it('maps a shell-not-found failure to the machine-readable close reason', async () => {
|
||||
expect(wsCloseReasonOf(new SidebarError(
|
||||
'shell-not-found',
|
||||
'shell executable not found: "C:\\Program Files\\PowerShell\\7\\pwsh.exe"',
|
||||
400,
|
||||
{ shell: 'C:\\Program Files\\PowerShell\\7\\pwsh.exe' },
|
||||
))).toBe('shell-not-found:pwsh')
|
||||
expect(wsCloseReasonOf(new Error('boom'))).toBe('boom')
|
||||
expect(wsCloseReasonOf('plain')).toBe('plain')
|
||||
})
|
||||
```
|
||||
|
||||
import 区补:`wsCloseReasonOf` 取自 `'../src/index.ts'`(该文件已 import `apply`——并入同一 import 语句),`SidebarError` 取自 `'../src/wire.ts'`。
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run tests/smoke.spec.ts`
|
||||
Expected: FAIL — `shell.get` 返回启动时 shell 而非覆盖值;`wsCloseReasonOf` 未导出。
|
||||
|
||||
- [ ] **Step 3: 实现 index.ts 三处**
|
||||
|
||||
(a) `shellOverridesOf`(第 248-259 行)——解析走新函数:
|
||||
|
||||
```ts
|
||||
function shellOverridesOf(getSettings: () => SidebarSettingsFace | undefined): { shell?: string; shellArgs?: string[] } {
|
||||
const settings = getSettings()
|
||||
const value = settings?.get().value
|
||||
if (value === null || typeof value !== 'object') return {}
|
||||
const record = value as Record<string, unknown>
|
||||
const shell = typeof record.terminalShell === 'string' ? unquotePath(record.terminalShell.trim()) : ''
|
||||
const args = typeof record.terminalShellArgs === 'string' ? record.terminalShellArgs.trim() : ''
|
||||
return {
|
||||
shell: shell === '' ? undefined : shell,
|
||||
shellArgs: args === '' ? undefined : splitShellArgs(args),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
(`unquotePath`/`splitShellArgs` 并入第 48 行现存的 `'./pty-manager.ts'` import。)
|
||||
|
||||
(b) `shell.get`(第 562-566 行)——注释与返回值更新:
|
||||
|
||||
```ts
|
||||
// The effective terminal shell and its display name: the settings-page
|
||||
// override when set (quotes stripped), else the boot-time resolution.
|
||||
// The client titles terminal tabs with the name, so a changed setting is
|
||||
// visible on the next opened tab without a plugin restart.
|
||||
'shell.get': () => {
|
||||
const effective = shellOverridesOf(getSettings).shell ?? terminalShell
|
||||
return { shell: effective, name: shellDisplayName(effective) }
|
||||
},
|
||||
```
|
||||
|
||||
(c) `attachTerminal` 的 catch(第 1306-1308 行)改用映射函数,并在 `attachTerminal` 上方新增导出:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* The WS close reason for a failed terminal attach. A missing configured
|
||||
* shell gets a SHORT machine-readable marker (`shell-not-found:<name>`,
|
||||
* capped — a WS close reason allows at most 123 bytes) that the client maps
|
||||
* to a localized, actionable banner; every other failure keeps the raw
|
||||
* message (the model-side tool errors read it verbatim).
|
||||
*/
|
||||
export function wsCloseReasonOf(error: unknown): string {
|
||||
if (error instanceof SidebarError && error.code === 'shell-not-found') {
|
||||
const name = shellDisplayName(String(error.meta?.shell ?? '')).slice(0, 100)
|
||||
return `shell-not-found:${name}`
|
||||
}
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
```
|
||||
|
||||
catch 体改为:
|
||||
|
||||
```ts
|
||||
} catch (error) {
|
||||
ws.close(1011, wsCloseReasonOf(error))
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `pnpm exec vitest run tests/smoke.spec.ts`
|
||||
Expected: PASS(含既有 `'serves the effective terminal shell and its display name'`——无设置时行为不变)。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/index.ts tests/smoke.spec.ts
|
||||
git commit -m "feat(terminal): shell 覆盖去引号进解析、标题跟随覆盖、close reason 机器标记"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 客户端本地化横幅 + 全语言词典(typecheck 守门)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/client/TerminalView.tsx:62`(镜像常量)、`:224-250`(onclose 分支)
|
||||
- Modify: `src/client/locales.ts`(zh 块 `:120` 后、en 块 `:569` 后各 1 key)
|
||||
- Modify: 19 个第三方词典各 1 key:`locales-ja.ts:108`、`locales-de.ts`、`-fr`、`-pt`、`-ko`、`-ar`、`-hi`、`-id`、`-tr`、`-vi`、`-th`、`-ru`、`-it`、`-nl`、`-sv`、`-pl`、`-zh-HK`、`-zh-TW`、`-zh-MO`(均插在各自 `terminalDepsProfile` 行后)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 3 的线协议前缀 `shell-not-found:`
|
||||
- Produces: locale key `terminalShellNotFound`(占位符 `{name}`);`TerminalView` 的 fatal 横幅文本
|
||||
|
||||
- [ ] **Step 1: TerminalView 识别前缀** — `src/client/TerminalView.tsx` 第 62 行 `PTY_DEPS_MISSING` 常量后追加:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* The WS close-reason prefix the host sends when the CONFIGURED shell was
|
||||
* not found (mirror of src/index.ts wsCloseReasonOf; wire contract, keep the
|
||||
* literal in lockstep). The view renders a localized, actionable banner.
|
||||
*/
|
||||
const SHELL_NOT_FOUND_PREFIX = 'shell-not-found:'
|
||||
```
|
||||
|
||||
`socket.onclose`(第 224 行起)在 `PTY_DEPS_MISSING` 分支之后、通用 1011 分支(第 247 行)之前插入:
|
||||
|
||||
```ts
|
||||
// The configured shell could not be found (settings page or yaml):
|
||||
// a localized banner beats the raw English close reason.
|
||||
if (event.code === 1011 && event.reason.startsWith(SHELL_NOT_FOUND_PREFIX)) {
|
||||
setFatal(t('terminalShellNotFound', { name: event.reason.slice(SHELL_NOT_FOUND_PREFIX.length) || '?' }))
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 加全语言 key**(21 个词典文件,各在 `terminalDepsProfile` 行后插 1 行;键集由 `chunks/locale.tsx` 的 `Record<CopyKey, string>` 与 `tests/locales.spec.ts` 双重把关,漏一个文件 typecheck 即红):
|
||||
|
||||
```ts
|
||||
// locales.ts zh 块:
|
||||
terminalShellNotFound: '未找到配置的 Shell:{name},请到 设置 → 侧边卡片 → 终端 检查 Shell 路径',
|
||||
// locales.ts en 块:
|
||||
terminalShellNotFound: 'Configured shell not found: {name} — check the shell path under Settings → Side card → Terminal',
|
||||
// locales-ja.ts:
|
||||
terminalShellNotFound: '設定されたシェルが見つかりません:{name}(設定 → サイドカード → ターミナル のシェルパスを確認してください)',
|
||||
// locales-de.ts:
|
||||
terminalShellNotFound: 'Konfigurierte Shell nicht gefunden: {name} — Shell-Pfad unter Einstellungen → Side card → Terminal prüfen',
|
||||
// locales-fr.ts:
|
||||
terminalShellNotFound: 'Shell configuré introuvable : {name} — vérifiez le chemin du shell dans Réglages → Side card → Terminal',
|
||||
// locales-pt.ts:
|
||||
terminalShellNotFound: 'Shell configurado não encontrado: {name} — verifique o caminho do shell em Configurações → Side card → Terminal',
|
||||
// locales-ko.ts:
|
||||
terminalShellNotFound: '설정된 셸을 찾을 수 없습니다: {name} — 설정 → 사이드 카드 → 터미널에서 셸 경로를 확인하세요',
|
||||
// locales-ar.ts:
|
||||
terminalShellNotFound: 'لم يتم العثور على الصدفة المُعدَّة: {name} — تحقق من مسار الصدفة في الإعدادات → Side card → Terminal',
|
||||
// locales-hi.ts:
|
||||
terminalShellNotFound: 'कॉन्फ़िगर किया गया shell नहीं मिला: {name} — Settings → Side card → Terminal में shell पथ जाँचें',
|
||||
// locales-id.ts:
|
||||
terminalShellNotFound: 'Shell yang dikonfigurasi tidak ditemukan: {name} — periksa path shell di Pengaturan → Side card → Terminal',
|
||||
// locales-tr.ts:
|
||||
terminalShellNotFound: 'Yapılandırılan kabuk bulunamadı: {name} — Ayarlar → Side card → Terminal altındaki kabuk yolunu denetleyin',
|
||||
// locales-vi.ts:
|
||||
terminalShellNotFound: 'Không tìm thấy shell đã cấu hình: {name} — kiểm tra đường dẫn shell trong Cài đặt → Side card → Terminal',
|
||||
// locales-th.ts:
|
||||
terminalShellNotFound: 'ไม่พบ shell ที่กำหนดไว้: {name} — ตรวจสอบพาธ shell ใน การตั้งค่า → Side card → Terminal',
|
||||
// locales-ru.ts:
|
||||
terminalShellNotFound: 'Настроенная оболочка не найдена: {name} — проверьте путь к оболочке в Настройки → Side card → Terminal',
|
||||
// locales-it.ts:
|
||||
terminalShellNotFound: 'Shell configurata non trovata: {name} — verifica il percorso della shell in Impostazioni → Side card → Terminal',
|
||||
// locales-nl.ts:
|
||||
terminalShellNotFound: 'Geconfigureerde shell niet gevonden: {name} — controleer het shell-pad onder Instellingen → Side card → Terminal',
|
||||
// locales-sv.ts:
|
||||
terminalShellNotFound: 'Konfigurerat skal hittades inte: {name} — kontrollera skalsökvägen under Inställningar → Side card → Terminal',
|
||||
// locales-pl.ts:
|
||||
terminalShellNotFound: 'Nie znaleziono skonfigurowanej powłoki: {name} — sprawdź ścieżkę powłoki w Ustawienia → Side card → Terminal',
|
||||
// locales-zh-HK.ts:
|
||||
terminalShellNotFound: '未找到配置的 Shell:{name},請到 設定 → 側邊卡片 → 終端 檢查 Shell 路徑',
|
||||
// locales-zh-TW.ts:
|
||||
terminalShellNotFound: '未找到配置的 Shell:{name},請到 設定 → 側邊卡片 → 終端 檢查 Shell 路徑',
|
||||
// locales-zh-MO.ts:
|
||||
terminalShellNotFound: '未找到配置的 Shell:{name},請到 設定 → 側邊卡片 → 終端 檢查 Shell 路徑',
|
||||
```
|
||||
|
||||
- [ ] **Step 3: typecheck 守门**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
Expected: PASS —— 若漏改任何一个第三方词典,`chunks/locale.tsx` 的 `checked(dict: Record<CopyKey, string>)` 直接编译失败。
|
||||
|
||||
- [ ] **Step 4: 跑 locale 契约测试**
|
||||
|
||||
Run: `pnpm exec vitest run tests/locales.spec.ts`
|
||||
Expected: PASS(含 `keeps every shipped third-language dictionary key-set-equal to zh`)。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/client/TerminalView.tsx src/client/locales.ts src/client/locales-*.ts
|
||||
git commit -m "feat(terminal): 配置 shell 未找到时显示本地化可操作横幅(全语言)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 全量门禁 + 设计文档偏差回填 + PR
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/plans/2026-09-08-terminal-shell-config-hardening-design.md`(「实施偏差记录」节回填)
|
||||
|
||||
- [ ] **Step 1: 全量门禁**
|
||||
|
||||
Run: `pnpm typecheck && pnpm test`
|
||||
Expected: PASS。若 `tests/tools.spec.ts` / e2e 有依赖旧 POSIX 直通行为的用例失败,按新契约修期望值并单独 commit(`test(terminal): 跟随 POSIX 预检新契约`)。
|
||||
|
||||
- [ ] **Step 2: 文档核对与偏差回填**
|
||||
|
||||
Run: `rg -n "terminalShell|shellArgs" docs/ README.md`
|
||||
Expected: 仅设计/计划文档命中(`docs/external-plugin-guide.md` 不记载设置页 shell 行,无需同步;若出现命中则按 AGENTS §5 同步该文档)。回填设计文档「实施偏差记录」:① `terminalShellNotFoundHint` 并入单 key `terminalShellNotFound`(含 `{name}` 占位);② 旧用例 `keeps POSIX bare shell resolution delegated to execvp` 被有意改写;③ 新增 locale 键实际落点为全部 21 词典(不止 zh/en/ja)。Commit:
|
||||
|
||||
```bash
|
||||
git add docs/plans/2026-09-08-terminal-shell-config-hardening-design.md
|
||||
git commit -m "docs(plans): 回填终端 shell 配置加固实施偏差"
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 推送并建 PR**
|
||||
|
||||
```bash
|
||||
git push -u origin feat/terminal-shell-quoted-args-probe
|
||||
gh pr create --title "feat(terminal): 终端 shell 配置加固——引号解析、POSIX 预检、本地化报错" --body-file - <<'EOF'
|
||||
## 概要
|
||||
配置启动终端命令的功能已存在(#95/#125),本 PR 加固三处缺口:
|
||||
1. 设置页 shell 参数支持引号分组(`-File "C:\my init\init.ps1"` 正确成 3 个 argv);shell 路径自动剥成对引号
|
||||
2. POSIX 预检可执行文件(对齐 Windows 既有探测),统一 `shell-not-found` 错误码
|
||||
3. 配置错误时终端标签渲染本地化可操作横幅(21 语言),终端标题跟随设置覆盖
|
||||
|
||||
## 测试
|
||||
- `tests/pty-helpers.spec.ts`:splitShellArgs/unquotePath 语义矩阵 + POSIX 探测
|
||||
- `tests/smoke.spec.ts`:shell.get 覆盖跟随 + wsCloseReasonOf 映射
|
||||
- `pnpm typecheck` + `pnpm test` 全绿
|
||||
EOF
|
||||
```
|
||||
|
||||
Expected: PR URL 返回。
|
||||
|
||||
- [ ] **Step 4: 汇报** — 向用户回报 PR 链接、改动文件清单、行为对照(改前/改后)。
|
||||
@@ -1,90 +0,0 @@
|
||||
# DSH 0.1.5-alpha.2 适配(v0.19.0-alpha.1 基线)
|
||||
|
||||
日期:2026-09-09 分支:`feat/dsh-0.1.5-alpha.2`
|
||||
|
||||
## 背景
|
||||
|
||||
DSH `dsh-v0.1.5-alpha.1..dsh-v0.1.5-alpha.2` 的宿主契约里有一批会实际波及本插件的变更。本计划记录哪些值得处理、哪些只做兼容、以及每一处的子系统级落点。插件对外的全部行为差异以 [docs/external-plugin-guide.md](../external-plugin-guide.md) 为准;仓库级适配要点(基线、CI 钉版、宿主契约)同步到 [AGENTS.md](../AGENTS.md) §3。
|
||||
|
||||
## 上游 delta(alpha.1 → alpha.2,影响本插件的六项)
|
||||
|
||||
| # | 上游变更 | 对插件的影响 |
|
||||
|---|---|---|
|
||||
| 1 | `SidebarRightGuideEntry` 去掉 `description` 字段;指南条目变为「图标 + 标题」胶囊 | 删除 `TabDescriptor.description` 与 6 个 `guideDesc*` 词典键(连同 `nativeGuideDesc` 兜底句——不再有第二行说明) |
|
||||
| 2 | 新增**全局面板**:根级 keyed `main` 槽(预留 key `conversation`,由 ui-conversation 注册为 `main.conversation`)、根级 `sidebar.panellist` 列表槽(`SidebarPanelMetadata` / `SidebarPanelIconOwnerProps`)、`ctx.layout.selectPanel(MainPanelId|null)` / `beginNavigation()` / `dispose()`、全局标准 prop `usePanelInfo`;`rightbar` 改根级并新增会话级 `rightbar.session` 子槽 | 插件**不接入**(兼容 only) |
|
||||
| 3 | 原生 tab 体宿主 `.paneBody`(`dsh-client-ui-dockkit`)是**有确定高度的块级滚动容器**,不是 flex 容器;DSH 自己的 tab 体(`ui-sidebar-documentpreview` 的文档预览、`ui-sidebar-files` 的 FilesBody)在根上声明 `height: 100%` | 原生适配层给每个 tab 体包一层 `height: 100%` 的列 flex 宿主(`sidebar.module.css` `.nativeTabHost`,带 `data-dsh-native-tab-host`),修复 sidechat 输入框贴不到面板底的根塌陷 |
|
||||
| 4 | 文件地址语法:`fileAddressFor` 一律返回 session 作用域地址(绝对路径保留前导 `/`);`parseFileAddress` 前缀解析并去 `?`/`#`;`absoluteFileAddress` 仍在 | 镜像 alpha.2 语法(插件自实现于 `src/client/resource-address.ts`——客户端 bundle 纯度门禁禁 value-import 未列名的 `@deepseek-ai/*`,不能直接引 DSH 工具包) |
|
||||
| 5 | `ui-sidebar-textpreview` 被 `ui-sidebar-documentpreview` 取代(同 `text` kind、`fallback` 带、`dsh-resource://file/**` 模式) | 插件 `editor` 仍以 `extension` 优先级认领 `dsh-resource://file/**`,压过其 `text` 的 `fallback` 带 |
|
||||
| 6 | 基线升到 0.1.5-alpha.2(peer 下限 / manifest / CI 钉版) | 插件基线 = alpha.2 only;alpha.1 用户留在 v0.19.0-alpha.0 |
|
||||
|
||||
> 后续(2026-09-10):`SidebarRightGuideEntry.description` 在 DSH 0.1.5-rc.1 回归(可选,且宿主只在 guide 列出的条目 ≤ 4 条时渲染),插件已在 **v0.19.0** 恢复该字段——见 [2026-09-10-dsh-0.1.5-rc.1-adaptation.md](./2026-09-10-dsh-0.1.5-rc.1-adaptation.md)。
|
||||
>
|
||||
> 后续(2026-09-10):退役自绘右侧面板(#605)时,`use-host-feeds.ts` 的三处自动激活被误改成 `target: 'bottom'`,任务页因此落到插件自己的底部工作台而非 DSH 原生右侧栏。修复见 [2026-09-10-auto-activation-native-landing-fix.md](./2026-09-10-auto-activation-native-landing-fix.md)。
|
||||
|
||||
## 决策
|
||||
|
||||
- **基线 = alpha.2 only**:peer 下限 `^0.1.5-alpha.2`、manifest `>=0.1.5-alpha.2`、CI 钉 `@deepseek-ai/dsh@0.1.5-alpha.2`;不再对 alpha.1 做运行时兼容。
|
||||
- **全局面板:兼容 only,不接入**——不迁移到 `main` 槽 / `sidebar.panellist` / `ctx.layout` / `usePanelInfo` / 根级 `rightbar`,仅保证在 alpha.2 宿主上不破坏。
|
||||
- **删除 `TabDescriptor.description`**:宿主不再渲染第二行,字段留着只会在类型上撒谎;同步删 6 个 `guideDesc*` 与 `nativeGuideDesc` 兜底句。
|
||||
- **修复 tab 体填充**:native 适配层为每个 tab 体(含 orphaned 回退)包 `height:100%` 列 flex 宿主 `.nativeTabHost`;tab 组件根遵循 `flex:1` / `height:100%` + `min-height:0` 契约。
|
||||
- **镜像 alpha.2 地址语法**:`fileAddressFor` 不再产生 `absolute` 作用域(但仍解析旧地址与第三方地址),`parseFileAddress` 前缀解析并去 `?`/`#`。
|
||||
- **中心列锚点跟随 alpha.2**:`center-column.ts` 的 `CENTER_COLUMN_SELECTOR` 改为 `#root [data-slot="main.conversation"], #root [data-slot="conversation"]`,`resolveCenterColumn` 从对话槽宿主向上跳过 `display: contents` 祖先取真实列;alpha.1 的 `[data-slot="conversation"]` 依旧认可。
|
||||
|
||||
## 改动清单(子系统级)
|
||||
|
||||
| 子系统 | 文件 | 变更 |
|
||||
|---|---|---|
|
||||
| 基线 | `package.json` | 版本 → `0.19.0-alpha.1`;dsh-* peer 全部 `^0.1.5-alpha.2`;devDeps 同步钉 `0.1.5-alpha.2` |
|
||||
| 基线 | `pnpm-lock.yaml` | 锁文件对齐 alpha.2 |
|
||||
| 基线 | `dsh.plugin.json` | `version` → `0.19.0-alpha.1`;`engines.dsh` → `>=0.1.5-alpha.2` |
|
||||
| 基线 | `.github/workflows/ci.yml` | mount lane 钉 `@deepseek-ai/dsh@0.1.5-alpha.2` |
|
||||
| 基线 | `package.json` | 按 §3-9 补提 `dsh-session-persistence`(`pnpm peers check` 唯一新失配项) |
|
||||
| 基线 | `package.json` | **上游打包缺陷**:`@deepseek-ai/dsh-client-ui-primitives@0.1.5-alpha.2` 的 manifest 不再声明任何 `dependencies`(alpha.1 声明了 19 个),但其 `lib/*.js` 仍裸 import `anser` / `shiki` / `@shikijs/langs/*` / `mdast-util-*` / `micromark-*` / `katex`。宿主自身由预构建前端 bundle 满足,独立安装的消费方(本仓库的 dev/test 树)必须自备,故把 alpha.1 声明的同一组版本提升进 devDependencies(`@shikijs/langs`、`@types/mdast`、`anser`、`katex`、`mdast-util-from-markdown/gfm/math`、`micromark-core-commonmark/extension-gfm/extension-math/factory-space/util-character/util-classify-character/util-sanitize-uri/util-symbol/util-types`、`shiki`;`clsx` 已在 dependencies) |
|
||||
| 基线/文档 | `AGENTS.md` | §2/§3 基线戳、§10 补 alpha.2 行为差异(本 workstream 回填) |
|
||||
| Tab 契约 | `src/client/service.ts` | `TabDescriptor` 删除 `description` 字段;`SIDEBAR_SERVICE_VERSION` → `0.19.0-alpha.1` |
|
||||
| Tab 契约 | `src/client/builtins/tabs.tsx` | 6 个内置 tab 删除 `description` 行 |
|
||||
| Tab 契约 | `src/client/native/index.ts` | native 注册的 guide 条目去掉 `description`(删 `descriptionOf` 与通用兜底);`guide` 条目只剩 `order`/`title`/`icon` |
|
||||
| 词典 | `src/client/locales.ts` + 各 `locales-*.ts` | 删除 `nativeGuideDesc` + 6 个 `guideDesc*` 词条(zh/en + 全部第三方语言) |
|
||||
| Tab 高度 | `src/client/native/tab-adapter.tsx` | `NativeTabBody` 把 live 组件与 orphaned 回退都包进 `css.nativeTabHost`(带 `data-dsh-native-tab-host`) |
|
||||
| Tab 高度 | `src/client/sidebar.module.css` | 新增 `.nativeTabHost`(`display:flex; flex-direction:column; height:100%; min-height:0`) |
|
||||
| 地址语法 | `src/client/resource-address.ts` | 重写为镜像 alpha.2 文件地址语法(`fileAddressFor` 不再产 `absolute` 作用域、`parseFileAddress` 前缀去 `?`/`#`、保留 `absolute` 解析) |
|
||||
| 中心列锚点 | `src/client/center-column.ts` | `CENTER_COLUMN_SELECTOR` 加 `main.conversation`;新增 `columnOfSlotHost` 跳过 `display: contents` 祖先 |
|
||||
| 中心列锚点 | `src/client/sidebar/use-center-column.ts` | 宿主查找逻辑对齐 alpha.2 |
|
||||
| 中心列锚点 | `src/client/layout.css` | 选择器/注释同步 `main.conversation` 与 `display: contents` 跳过 |
|
||||
| 测试 | `tests/builtins.spec.ts` | 指南断言从 `description` 改成 `title`(图标+标题胶囊) |
|
||||
| 测试 | `tests/native-surface.spec.ts` | 注册/guide 断言去 `description`;断言 guide 条目不再携带 `description` |
|
||||
| 测试 | `tests/center-column-locator.spec.ts` | 覆盖 `main.conversation` 解析与 `display: contents` 跳过 |
|
||||
| 测试 | `tests/resource-address.spec.ts` | 覆盖 alpha.2 地址语法 |
|
||||
| 测试 | `tests/e2e/mount.e2e.ts` | guide 页断言跟随胶囊形态;新增 tab 体填充断言(可见 tab 的 `[data-dsh-native-tab-host]` 盒高 == 其带盒祖先高 ±2px,且 sidechat composer 底边距面板底 ≤12px) |
|
||||
| 文档 | `docs/external-plugin-guide.md` | §0 行为差异表、§4.1 删 `description`、§10 加 tab 体高度契约、§12 中心列锚点、`ui-sidebar-documentpreview` |
|
||||
| 文档 | `docs/plans/2026-09-09-dsh-0.1.5-alpha.2-adaptation.md` | 本计划 |
|
||||
|
||||
## 已知差距
|
||||
|
||||
- **side-chat 线程仍只携带 `subagent/descriptor`,不携带 alpha.2 的 `subagent/catalog`**:DSH 自己的直接子代理发现现在优先读 `catalog`,而我们沿用的 `subagents.listDescendants`(`src/subagent-live-route.ts`)与转录种子的 `type: 'subagent/descriptor'`(`src/sidechat-routes.ts`)路径不变。catalog 只作为新增读面出现,不构成对现有路径的破坏,本期不跟进。
|
||||
|
||||
## 验证
|
||||
|
||||
- **门禁**(本分支 HEAD):`pnpm typecheck` / `pnpm lint` / `pnpm check:consumer-types` 全绿;`pnpm vitest run --exclude '**/*.e2e.ts'` = 124 files / 1295 passed / 9 skipped;`pnpm peers check` = No peer dependency issues found。
|
||||
- **真机挂载冒烟**(`DSH_CMD` 指向 npm 安装的 DSH 0.1.5-alpha.2,`scripts/e2e-mount.sh`):7 passed。新增的 tab 体填充断言(可见 tab 的 `[data-dsh-native-tab-host]` 盒高 == 其带盒祖先 ±2px,sidechat composer 底边距面板底 ≤12px)是这次修复的回归守护——它在修复前失败(`gap -862px`,因为断言最初漏跳过 `display: contents` 槽宿主)。
|
||||
- **真机 3080(web profile 升到 alpha.2 + 插件 0.19.0-alpha.1)**:
|
||||
- 原生 guide 六行均为 48px「图标+标题」胶囊,无第二行:文件 / 文件变动 / 任务管理 / 侧边对话(beta) / 终端 / 浏览器。
|
||||
- 文件树点击 `AGENTS.md` → tab 标题 `AGENTS.md` + CodeMirror 挂载(`editor` 接管成立)。
|
||||
- **sidechat 输入框贴底**:原生右侧栏 `hostHeight 962 == paneBodyHeight 962`(fillGap 0),composer 底边距面板底 8px(即它自己的 margin);底部工作台同样 8px。
|
||||
- 底部工作台中心列锚点:`[data-dsh-bottom-panel]` 与 `[data-dsh-center-col]` 左右边完全重合(gap 0)——证明 `main.conversation` + 跳过 `display: contents` 的解析在真机成立;真机 `[data-slot]` 清单同时确认 `main` / `main.conversation` / `rightbar.session` 已存在、旧 `conversation` 槽已消失。
|
||||
- `pageerror` 0 条;控制台仅 `POST /api/dsh-worktree/attach` 500(第三方 dsh-tauri-worktree 插件,与本次改动无关)。
|
||||
- 未覆盖:**聊天里的文件链接点击**——当前可见会话没有可解析的文件提及(内联 `code` 是纯文本,不是链接),该漏斗(`ctx.sidebarRight.openResource(fileAddressFor(...))`)由挂载车道与 `tests/resource-address.spec.ts` 覆盖,本实例未实测。
|
||||
- 仓库没有守护指南文档章节的测试;`tests/builtins.spec.ts` / `tests/native-surface.spec.ts` 守护的是 guide 行形态而非文档文本。
|
||||
- 早期本分支相关测试曾整体收集失败:`@deepseek-ai/dsh-client-ui-primitives@0.1.5-alpha.2` 在模块加载时裸 `import 'anser'`,而该包 manifest 已不再声明依赖。补齐 devDependencies 后恢复(见改动清单)。
|
||||
|
||||
## 不做
|
||||
|
||||
- 不接入 alpha.2 全局面板(`main` 槽 / `sidebar.panellist` / `ctx.layout` / `usePanelInfo` / 根级 `rightbar`)。
|
||||
- 不引入 `subagent/catalog` 读取面。
|
||||
- 不改 DSH 源码(仓库硬约束 §1)。
|
||||
|
||||
## 落地
|
||||
|
||||
- #603 → #604 → #605 → #609 依序合入 `main`(各自 retarget 到 main 后以 merge commit 合并);main 的 `package.json` 为 `0.19.0-alpha.1`、`engines.dsh` 为 `>=0.1.5-alpha.2`。main 的 CI 首轮 `ci-windows` 因既有的 pty/git 5s 超时抖动失败,重跑后三 job 全绿。
|
||||
- 发布 [v0.19.0-alpha.1](https://github.com/omdsh-dev/DSH-better-sidebar/releases/tag/v0.19.0-alpha.1):`release.yml` 通过全门禁后经 npm Trusted Publishing 发布,npm `alpha` dist-tag → `0.19.0-alpha.1`(带 provenance)。发布产物核对:`lib/index.js` 与本地已验证构建逐字节一致;`lib/client.js` 仅差打包非确定性(已知);`package.json` 仅少 dev-only 字段(`packageManager` / `prepare` / `prepublishOnly`,npm 发布时剥离)。
|
||||
- 注意:新版本在 registry 上可见有约 5 分钟传播延迟(版本元数据先到、tarball 后到),期间 `npm view`/tarball 可能 404——不是发布失败。
|
||||
@@ -1,192 +0,0 @@
|
||||
# 终端等待 banner(wait banner + 跳过)设计
|
||||
|
||||
> 2026-09-09 · 目标 v0.18.2
|
||||
|
||||
## 背景
|
||||
|
||||
`terminal_wait_for`(v0.18.1,PR #595 起 needle 支持 regex)会阻塞 agent 的工具调用直到
|
||||
needle 出现 / 超时 / 终端退出。等待期间用户在侧边栏看不到任何"agent 正在等什么"的信号,
|
||||
也无法干预——只能等 agent 自己超时(默认 10s,长等待可设到分钟级)。
|
||||
|
||||
需求:
|
||||
|
||||
1. agent 开始 `wait for needle` 时,在对应终端的顶部显示 banner:
|
||||
**「Agent 正在等待 {needle}」**,右侧「跳过等待」按钮;
|
||||
2. 点击跳过后,`terminal_wait_for` 立即返回,agent 明确知道是用户跳过(而非超时);
|
||||
3. 终端 tab 未激活时也能感知等待 → tab 标题加 ⏳ 徽章。
|
||||
|
||||
## 方案对比
|
||||
|
||||
| 方案 | 思路 | 取舍 |
|
||||
|---|---|---|
|
||||
| **A. 搭现有推送车(采纳)** | 等待状态并入 `AgentTerminalSnapshot`,经现有 `/sidebar/ws/agent-terminals` 推送;跳过走新 `/sidebar/api` 方法 | 改动集中;零新 WS 生命周期;推送时机由 registry `notify()` 天然覆盖 |
|
||||
| B. 独立 waits 推送 WS | 新开 `/sidebar/ws/agent-waits` | 重连/订阅/teardown 全套翻倍,YAGNI |
|
||||
| C. 客户端轮询 | 定时拉 terminal 列表 | 延迟与开销差,违背现有推送架构 |
|
||||
|
||||
已确认的两个决策:
|
||||
|
||||
- **skip 语义 = 新增第四种返回 `kind:'skipped'`**(不复用 timeout、不走 abort——abort
|
||||
语义属于工具调用取消,用户语义只是"不等了");
|
||||
- **banner 主体在终端卡片顶部 + tab 标题 ⏳ 徽章**(复用 v0.12+ 的 `TabDescriptor.badge`
|
||||
通道,git tab 有先例)。
|
||||
|
||||
## 设计
|
||||
|
||||
### 1. Host:`AgentPtyRegistry`(src/agent-pty.ts)
|
||||
|
||||
- `AgentTerminalHandle` 增活动等待记录数组 `waits: Array<{ needle, since, skipped }>`;
|
||||
并发 `wait_for` 同 uuid 时各登记一条。
|
||||
- `waitFor()`:快速路径(已 exited / needle 已在 transcript)不登记、不推 banner;
|
||||
进入轮询前登记记录并 `notify()`(banner 出现),`finally` 移除记录并 `notify()`(banner
|
||||
消失——found/timeout/exited/skipped/工具 abort 五条出路统一走 finally)。
|
||||
- 轮询每圈检查本记录的 `skipped` → 返回 `{ kind:'skipped', needle }`;50ms 轮询
|
||||
天然承载跳过延迟,无需事件唤醒。与终端退出在同一 tick 竞争时**退出优先**(见实施
|
||||
偏差记录)。
|
||||
- 新方法 `skipWait(uuid): number`:标记该 uuid 全部活动记录并返回条数;uuid 未知 →
|
||||
`not-found` 404(与 `expect` 一致)。
|
||||
- `AgentTerminalSnapshot` 增 `waiting?: { needle: string; since: number }`(取最新登记
|
||||
的一条;并发等待显示最新 needle,跳过跳过全部)。
|
||||
- `AgentTerminalWaitResult` 增 `skipped` 变体。
|
||||
|
||||
### 2. 工具层:`terminal_wait_for`(src/tools.ts)
|
||||
|
||||
- output schema `oneOf` 增 `{ kind: 'skipped', needle }` 分支;
|
||||
- render:`Skipped by user while waiting for "<needle>" — the wait ended early; call terminal_read to inspect the transcript if needed.`;
|
||||
- 工具描述补一句:用户可从侧栏 banner 跳过等待(返回 `skipped`)。
|
||||
|
||||
### 3. API:`buildApi`(src/index.ts)
|
||||
|
||||
- 新方法 `'agent-pty.skip-wait'`(命名对齐现有 `'agent-pty.close'`):
|
||||
`requireString uuid` → `registry.skipWait(uuid)`;degraded 模式(registry 为 null)
|
||||
幂等返回 `{ ok: true, skipped: 0 }`。
|
||||
|
||||
### 4. 推送零改动
|
||||
|
||||
`attachAgentList` 发的就是 `registry.list(sessionId)`,snapshot 新增的 `waiting` 字段
|
||||
自动随车;等待开始/结束由 `notify()` 触发推送。兼容性:
|
||||
|
||||
- 旧客户端 + 新 host:多余字段被忽略;
|
||||
- 新客户端 + 旧 host:永远收不到 `waiting` → 无 banner,安全降级。
|
||||
|
||||
### 5. 客户端状态(src/client/state.ts)
|
||||
|
||||
- `SidebarState` 增 `agentWaits: Record<uuid, { needle: string; since: number }>`(默认
|
||||
`{}`;等待结束即删除键);
|
||||
- `reconcileAgentTerminals` 从同一推送同步该映射——需解除"无 tab 增删即早退"的短路
|
||||
(仅 waiting 变化也要产出新状态);
|
||||
- **不持久化**:`sanitizeState` 逐字段重建、不恢复该字段 → 刷新后自然清空;WS attach
|
||||
即发首条推送,立刻纠正。
|
||||
|
||||
### 6. Banner(src/client/TerminalView.tsx)
|
||||
|
||||
- 仅 `agent:` tab 渲染;位置在终端区顶部(现有 fatal/deps banner 之上);
|
||||
- 内容:`t('terminalWaitBanner', { needle })`(zh「Agent 正在等待 {needle}」)+ 右侧
|
||||
`t('terminalSkipWait')`(zh「跳过等待」)按钮 → `api.agentSkipWait(uuid)`;
|
||||
- needle 超 ~80 字符截断显示,`title` 悬浮显全文;
|
||||
- banner 消失完全由推送驱动(skipped/exited/abort 均经 `notify`),点击不做乐观更新。
|
||||
|
||||
### 7. Tab ⏳ 徽章(src/client/builtins/tabs.tsx)
|
||||
|
||||
- terminal 描述符增 `badge: (_ctx, scope, state) => ...`:`agent:` tab 且
|
||||
`state.agentWaits[uuid]` 存在时返回 `'⏳'`,否则 null——复用 v0.12+ 徽章通道
|
||||
(git changes tab 先例),不碰 `TabBar`。
|
||||
|
||||
### 8. i18n / 样式
|
||||
|
||||
- 词典键:`terminalWaitBanner`、`terminalSkipWait`;zh/en 同步,**ja 翻译必须同步
|
||||
`src/client/locales-ja.ts`**(仓库规则);
|
||||
- `.terminalWaitBanner` 全部消费 `--dsw-alias-*` / `--ds-*` 令牌,无硬编码颜色
|
||||
(皮肤契约,`tests/theme.spec.ts` 守护)。
|
||||
|
||||
### 9. 测试
|
||||
|
||||
- `tests/agent-pty.spec.ts`:跳过返回 `skipped`;活动等待期间 snapshot 含 `waiting`、
|
||||
结束后消失;`skipWait` 未知 uuid → 404、无活动等待 → 0;等待开始/结束触发 `notify`;
|
||||
- `tests/tools.spec.ts`:FakeRegistry 增 skipped 形状;schema/render 断言;
|
||||
- `tests/agent-terminal-reconcile.spec.ts`:`agentWaits` 设置/清除、仅 waiting 变化也
|
||||
触发状态更新;
|
||||
- badge / banner 组件测试视现有 jsdom 基建可行性补齐。
|
||||
|
||||
## 边缘情况
|
||||
|
||||
- **并发多个 `wait_for` 同一 uuid**:banner/⏳ 显示最新登记的 needle;跳过一次跳过全部;
|
||||
- **工具调用取消(abort)**:`finally` 移除记录 → banner 消失并推送;
|
||||
- **终端退出 / 用户关 tab(close frame 杀 pty)**:轮询下一圈返回 `exited`,记录移除;
|
||||
- **UI-tab 终端**:无等待概念,`agentWaits` 永不含有其条目。
|
||||
|
||||
## 不做的事
|
||||
|
||||
- 不新增独立 waits WebSocket(方案 B 否决);
|
||||
- 不改 `ctx.betterSidebar` 服务面(`external-plugin-guide.md` 无需同步);
|
||||
- 不做乐观 UI(banner 消失以服务端推送为准)。
|
||||
|
||||
## 实施偏差记录
|
||||
|
||||
- **⏳ tab 徽章不走 `TabDescriptor.badge`**:实现时核实该 API 是 type-keyed
|
||||
(`badge(ctx, scope, state)`,无 tab 参数、external 插件共享),无法定位单个
|
||||
tab。改为 Sidebar 壳层 `tabBadgeOf` 的 sidebar-internal 特例(agent: 前缀判断
|
||||
+ `state.agentWaits` 查表),`service.ts` 与 external-plugin-guide.md 零改动。
|
||||
- **banner 抽为独立组件 `TerminalWaitBanner.tsx`**:设计原文写在 TerminalView
|
||||
内;抽出后无 xterm 依赖,jsdom 单测可直接渲染(TerminalView 仍负责订阅 store
|
||||
并把 onSkip 接到 `api.agentSkipWait`)。
|
||||
- **词典同步扩到全部 21 个语言文件**:设计只写了 zh/en/ja,但
|
||||
`tests/locales.spec.ts` 断言每个第三方词典与 zh 键集相等——只同步三语会让该
|
||||
守护测试挂掉。按仓库惯例(全语言同步,如 e8761ee)补齐其余 18 语言同两键。
|
||||
- **banner 配色**:仓库 CSS 无 state-info 令牌,沿用 `.terminalBanner` 的 warn
|
||||
对(`--dsw-alias-state-warn-label/-tertiary`),符合皮肤契约。
|
||||
- **设计文档与计划随 feat 分支进 PR**:main 受分支保护,纯文档直推被 hook 拒绝
|
||||
(`4a6a77a` / `2448068` 随本分支合并)。
|
||||
- **测试断言以文本计数取代 CSS 类名**:CSS module 类名在测试变换下不稳定,
|
||||
⏳ 数量断言改从 `textContent` 计数;长 needle 截断断言改为计量 needle 自身
|
||||
贡献字符数(本地化句子包裹 needle,整体长度断言与语言相关,原计划内部矛盾)。
|
||||
- **新测试的 localStorage 兜底**:Node 26 + jsdom 29 组合下测试环境可能无全局
|
||||
`localStorage`(上游 `bottom-auto-terminal.spec.tsx` 在同环境即失败,属已知
|
||||
win32/环境非回归类);新 spec 的 afterEach 加守卫 + try/catch(jsdom opaque
|
||||
origin 下 localStorage 可以是 throwing accessor,裸 typeof 也会炸),
|
||||
上游测试一律未动。
|
||||
- **已知限制:跨会话 pinned 虚拟 tab 收不到实时 banner/⏳**(Copilot review
|
||||
指出):等待状态 feed 按 viewer 会话订阅(`/sidebar/ws/agent-terminals` 按
|
||||
当前 sessionId 连接),钉到其他会话的 pinned 虚拟 agent tab
|
||||
(`pinned:<homeSessionId>:agent:<uuid>`)在其 home 会话非当前会话时收不到
|
||||
实时等待状态。修复需跨会话 wait feed 多路复用(连 home 会话推送或聚合
|
||||
推送),超出本特性范围,留待后续设计;跳过 API 按 uuid 全局寻址,跨会话
|
||||
跳过本身可达。
|
||||
- **Copilot review 第三轮修复(合并 rc.1 后扫描)**:
|
||||
1. `truncateNeedle` 改按 Unicode 码点截断(`Array.from`),原 UTF-16 码元
|
||||
截断会在截断点劈开代理对(emoji 渲染为替换字符);
|
||||
2. banner 消息加 `role="status"` + `aria-live="polite"`,完整未截断的
|
||||
本地化消息经 `aria-label` 提供给读屏器(不新增词典键);
|
||||
3. `terminal` tab 类型禁用时推送处理从整体跳过改为 `mirrorAgentWaits`
|
||||
——仍镜像权威 `waiting` map(等在禁用窗口内 resolve 也清 banner 状态),
|
||||
只跳过 tab 增删 reconcile,否则重启用后已开终端残留 stale banner/⏳
|
||||
直到无关推送到达;
|
||||
4. badge 测试跟踪挂载的 root 并在 `afterEach` 里先 unmount 再清 DOM
|
||||
(裸 `innerHTML = ''` 不触发 React effect 清理,store 订阅/host-feed
|
||||
effect 会泄漏进后续测试);
|
||||
5. 补并发双 `waitFor` 同 uuid 测试:快照显示最新 needle(`waits.at(-1)`)、
|
||||
`skipWait` 返回 2、两个 promise 都以各自 needle `skipped` 收敛、快照
|
||||
清空且 skipWait 幂等归零。
|
||||
- **轮询优先级:终端退出优先于用户跳过**(v0.19.0 发版前 review 发现):设计原文
|
||||
写「每圈优先检查 `skipped`」,实现是 `aborted → handle.exited → record.skipped`,
|
||||
即 pty 退出与用户点跳过落进同一个 50ms tick 时返回 `exited`。**维持实现现状**,
|
||||
设计原文已同步为「退出优先」:两种顺序下用户可见结果完全相同(banner 消失、
|
||||
`skipWait` 返回 0、等待结束),只有工具结果的 `kind` 文案不同;而进程已死是
|
||||
客观事实,报 `exited` 不撒谎,工具结果本身也引导 agent 用 `terminal_read` 复核。
|
||||
若日后要让「用户意图优先」,改动点是循环内两行先后 + 一条同 tick 竞态测试。
|
||||
- **两个已知限制的补充记录**(同一次 review):
|
||||
1. **pinned tab 的 banner 与 ⏳ 不对称**:pinned 虚拟 id 是
|
||||
`pinned:<homeSessionId>:agent:<uuid>`,`isAgentTabId` 只认 `agent:` 前缀,因此
|
||||
钉住的 agent tab **不会误挂 ⏳**(比"收不到"更强的保证);但 Workbench 渲染
|
||||
pinned tab 时给 TerminalView 传的是**原 tab id + home 作用域**,所以 home 会话
|
||||
正在等待时该视图**会**显示 banner,而它的 strip 徽章仍不出现。两个信号数据源
|
||||
不同(banner 读 home store,徽章读 viewer 的 `agentWaits`)。
|
||||
2. **⏳ 到不了 DSH 原生右侧栏**:徽章经 `getTabBadge` 渲染,唯一消费点是插件
|
||||
自绘的 `TabBar`;原生栏的 `NativeTabTitle` 契约只带 title,不带 badge。agent
|
||||
终端标签页走 `openTabInBottomPane`(底部工作台),故 agent 流程可见;这是原生
|
||||
承载面契约决定的上限,非本特性缺陷。
|
||||
- **注释随实现同步**(v0.19.0 发版前 review):`use-host-feeds.ts` effect 头部原写
|
||||
「terminal tab 类型禁用时 pushes are ignored」,与上面第 3 项的 `mirrorAgentWaits`
|
||||
改法矛盾,已改为「不增删 tab,但仍镜像权威 wait map」;`src/index.ts` 的
|
||||
`agent-pty.skip-wait` 注释原文把「无活动等待 → 0」泛化成了「幂等」,已收窄为
|
||||
「无活动等待返回 `{skipped:0}`;未知 uuid 走 `expect` → 404(已退出的终端即属此
|
||||
列),客户端 `.catch` 容忍并等下一次推送收敛」。
|
||||
@@ -1,980 +0,0 @@
|
||||
# 终端等待 banner + 跳过等待 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** agent 阻塞在 `terminal_wait_for` 时,对应终端卡片顶部显示「Agent 正在等待 {needle}」banner(右侧「跳过等待」按钮),tab 标题加 ⏳ 徽章;跳过后工具返回新增的 `{kind:'skipped'}`。
|
||||
|
||||
**Architecture:** 等待状态登记在 `AgentPtyRegistry` 的 handle 上,随现有 `/sidebar/ws/agent-terminals` 推送快照(`waiting` 字段)自然下发;跳过走新增 `/sidebar/api/agent-pty.skip-wait` 路由。客户端把推送镜像进 `state.agentWaits`,banner 组件与 tab 徽章从 store 读。
|
||||
|
||||
**Tech Stack:** TypeScript / React 18 / vitest(node 环境 pty 测试 + jsdom Sidebar 壳测试)/ ws 推送 / CSS Modules(皮肤令牌)。
|
||||
|
||||
**Spec:** `docs/plans/2026-09-09-terminal-wait-banner-design.md`(与本计划同分支,执行者两份都读)。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 分支:`feat/wait-banner`(已含设计文档提交 `4a6a77a`);所有代码改动走该分支 → PR,禁止直推 main(分支保护)。
|
||||
- 皮肤契约:新增 CSS 只允许 `--dsw-alias-*` / `--dsw-font-*` / `--ds-*` 令牌;本项目复用现成 warn 对 `--dsw-alias-state-warn-label` / `--dsw-alias-state-warn-tertiary`(仓库 CSS 无 state-info 令牌,已核实)。
|
||||
- i18n:新增 zh key 必须同步 en 词典与 `src/client/locales-ja.ts` 的 ja 翻译。
|
||||
- client bundle 纯度门:新文件只准 import 本插件模块(locales / css / api),禁止 value-import `@dsh-external/*` 或非白名单 `@deepseek-ai/*`。
|
||||
- 不改 `src/client/service.ts` / `docs/external-plugin-guide.md`(⏳ 徽章走 shell 层特例,不动 TabDescriptor API)。
|
||||
- 上游测试(含 win32 已知 27 个非回归失败)一律不改。
|
||||
- 每个任务用 `pnpm exec vitest run <file>` 验证;最终任务必须 `pnpm lint` + `pnpm typecheck` + `pnpm test` + `pnpm build` 全绿才 push(CI 曾因漏 lint 挂过)。
|
||||
- 提交信息风格:`feat(terminal): ...` / `test(...)`,仓库惯例小写 scope。
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Registry 等待登记 + `skipped` 结果 + `waiting` 快照
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/agent-pty.ts`
|
||||
- Test: `tests/agent-pty.spec.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 现有 `AgentPtyRegistry.waitFor` / `notify` / `subscribe` / `snapshotOf`。
|
||||
- Produces:
|
||||
- `type AgentTerminalActiveWait = { needle: string; since: number; skipped: boolean }`
|
||||
- `AgentTerminalHandle.waits: AgentTerminalActiveWait[]`(create 时初始化为 `[]`)
|
||||
- `AgentTerminalSnapshot.waiting?: { needle: string; since: number }`(最新一条活动等待)
|
||||
- `AgentTerminalWaitResult` 新增 `{ kind: 'skipped'; needle: string }` 变体
|
||||
- `registry.skipWait(uuid: string): number`(标记全部活动等待,返回条数;未知 uuid 抛 not-found)
|
||||
|
||||
- [ ] **Step 1: 写失败测试(4 个新用例,加在 `describe('AgentPtyRegistry')` 内、现有 `waitFor` 用例之后)**
|
||||
|
||||
在 `tests/agent-pty.spec.ts` 的 import 块不需要新增(`AgentPtyRegistry` / `snapshotOf` 已导入)。追加用例:
|
||||
|
||||
```ts
|
||||
it('waitFor returns skipped when the user skips from the sidebar', async () => {
|
||||
const registry = new AgentPtyRegistry(testShell())
|
||||
try {
|
||||
const uuid = registry.create('s1', 'skip-test', 'echo skip-ready', process.cwd(), 80, 24)
|
||||
await waitForTranscript(registry, uuid, 'skip-ready')
|
||||
// waitFor registers its record synchronously (before the first poll
|
||||
// await), so the skip can fire immediately after the call.
|
||||
const waitPromise = registry.waitFor(uuid, 'NEVER_APPEARS_XYZ', 30_000)
|
||||
expect(await registry.skipWait(uuid)).toBe(1)
|
||||
const result = await waitPromise
|
||||
expect(result.kind).toBe('skipped')
|
||||
if (result.kind === 'skipped') expect(result.needle).toBe('NEVER_APPEARS_XYZ')
|
||||
// Idempotent: nothing left to skip once the wait resolved.
|
||||
expect(registry.skipWait(uuid)).toBe(0)
|
||||
} finally {
|
||||
registry.disposeAll()
|
||||
}
|
||||
})
|
||||
|
||||
it('snapshot exposes waiting while a wait is active and clears after it ends', async () => {
|
||||
const registry = new AgentPtyRegistry(testShell())
|
||||
try {
|
||||
const uuid = registry.create('s1', 'wait-snap', 'echo snap-ready', process.cwd(), 80, 24)
|
||||
await waitForTranscript(registry, uuid, 'snap-ready')
|
||||
const waitPromise = registry.waitFor(uuid, 'LATER_MARK_9', 30_000)
|
||||
// Registration happens synchronously before waitFor's first await.
|
||||
expect(registry.list('s1')[0]?.waiting?.needle).toBe('LATER_MARK_9')
|
||||
expect(typeof registry.list('s1')[0]?.waiting?.since).toBe('number')
|
||||
expect(registry.skipWait(uuid)).toBe(1)
|
||||
const result = await waitPromise
|
||||
expect(result.kind).toBe('skipped')
|
||||
expect(registry.list('s1')[0]?.waiting).toBeUndefined()
|
||||
} finally {
|
||||
registry.disposeAll()
|
||||
}
|
||||
})
|
||||
|
||||
it('fires change listeners when a wait starts and ends', async () => {
|
||||
const registry = new AgentPtyRegistry(testShell())
|
||||
try {
|
||||
const uuid = registry.create('s1', 'watched-wait', 'echo notify-ready', process.cwd(), 80, 24)
|
||||
await waitForTranscript(registry, uuid, 'notify-ready')
|
||||
let changes = 0
|
||||
const unsubscribe = registry.subscribe(() => { changes += 1 })
|
||||
const waitPromise = registry.waitFor(uuid, 'NEVER_NOTIFY_1', 30_000)
|
||||
const afterStart = changes
|
||||
expect(afterStart).toBeGreaterThanOrEqual(1)
|
||||
expect(await registry.skipWait(uuid)).toBe(1)
|
||||
expect(await waitPromise).toEqual({ kind: 'skipped', needle: 'NEVER_NOTIFY_1' })
|
||||
expect(changes).toBeGreaterThan(afterStart)
|
||||
unsubscribe()
|
||||
} finally {
|
||||
registry.disposeAll()
|
||||
}
|
||||
})
|
||||
|
||||
it('skipWait rejects an unknown uuid with not-found', () => {
|
||||
const registry = new AgentPtyRegistry(testShell())
|
||||
try {
|
||||
expect(() => registry.skipWait('missing-uuid')).toThrow(/not found/)
|
||||
} finally {
|
||||
registry.disposeAll()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run tests/agent-pty.spec.ts`
|
||||
Expected: FAIL — `waiting` 为 undefined、`skipWait` 不存在、skipped 分支不存在(TS 编译错误或断言失败)。
|
||||
|
||||
- [ ] **Step 3: 实现 `src/agent-pty.ts`**
|
||||
|
||||
(a) 在 `AgentTerminalReadResult` 之后、`AgentTerminalWaitResult` 之前新增:
|
||||
|
||||
```ts
|
||||
/** One active wait_for registration on a handle (banner + skip bookkeeping). */
|
||||
export interface AgentTerminalActiveWait {
|
||||
/** The needle being awaited (shown on the sidebar wait banner). */
|
||||
needle: string
|
||||
/** Epoch ms when the wait registered (age display / debugging). */
|
||||
since: number
|
||||
/** Flipped by `skipWait()`; the waiting poll returns `skipped` within one tick. */
|
||||
skipped: boolean
|
||||
}
|
||||
```
|
||||
|
||||
(b) `AgentTerminalWaitResult` union 末尾追加变体:
|
||||
|
||||
```ts
|
||||
| {
|
||||
/** The user skipped the wait from the sidebar banner. */
|
||||
kind: 'skipped'
|
||||
/** The needle that was awaited. */
|
||||
needle: string
|
||||
}
|
||||
```
|
||||
|
||||
(c) `AgentTerminalSnapshot` 追加字段(`exitSignal` 之后):
|
||||
|
||||
```ts
|
||||
/**
|
||||
* The model's active `terminal_wait_for` on this terminal (the sidebar
|
||||
* renders the wait banner from it). Present only while a wait is
|
||||
* registered; carries the LATEST wait when several overlap.
|
||||
*/
|
||||
waiting?: { needle: string; since: number }
|
||||
```
|
||||
|
||||
(d) `AgentTerminalHandle` 追加字段(`exitSignal` 之后):
|
||||
|
||||
```ts
|
||||
/** Active wait_for registrations (skip bookkeeping; empty while idle). */
|
||||
waits: AgentTerminalActiveWait[]
|
||||
```
|
||||
|
||||
(e) `create()` 的 handle 字面量中 `exited: false,` 之后加 `waits: [],`。
|
||||
|
||||
(f) `snapshotOf` 在 `return out` 前追加:
|
||||
|
||||
```ts
|
||||
const active = handle.waits.at(-1)
|
||||
if (active !== undefined) out.waiting = { needle: active.needle, since: active.since }
|
||||
```
|
||||
|
||||
(g) 新方法(放在 `waitFor` 之后):
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Mark every active wait on one terminal as skipped (the sidebar banner's
|
||||
* skip button). Each waiting poll loop observes its record's flag within
|
||||
* one 50ms tick and returns `{kind:'skipped'}`. Idempotent: 0 when nothing
|
||||
* is waiting (a stale banner racing a wait that already resolved).
|
||||
* @returns the number of waits that transitioned to skipped.
|
||||
*/
|
||||
skipWait(uuid: string): number {
|
||||
const handle = this.expect(uuid)
|
||||
let count = 0
|
||||
for (const record of handle.waits) {
|
||||
if (!record.skipped) {
|
||||
record.skipped = true
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
```
|
||||
|
||||
(h) 重写 `waitFor` 中段(`const firstHit = ...` 快速路径之后到方法结尾),登记记录 + 轮询检查 + `finally` 清理:
|
||||
|
||||
```ts
|
||||
const firstHit = locateNeedle(handle.transcript, needle, re)
|
||||
if (firstHit !== undefined) {
|
||||
return { kind: 'found', needle, line: firstHit.line, column: firstHit.column, match: firstHit.match, elapsedMs: Date.now() - start }
|
||||
}
|
||||
// Register the active wait so the sidebar can show the wait banner (the
|
||||
// snapshot's `waiting` field rides the agent-terminals push). Registered
|
||||
// only after the fast paths: a wait that resolves instantly never
|
||||
// flashes the banner. The notify() makes every push subscriber (the
|
||||
// sidebar view) converge on the new waiting state immediately.
|
||||
const record: AgentTerminalActiveWait = { needle, since: start, skipped: false }
|
||||
handle.waits.push(record)
|
||||
this.notify()
|
||||
try {
|
||||
while (true) {
|
||||
if (signal?.aborted) signal.throwIfAborted()
|
||||
if (handle.exited) {
|
||||
return { kind: 'exited', needle, exitCode: handle.exitCode ?? null, exitSignal: signalNameOf(handle.exitSignal) }
|
||||
}
|
||||
if (record.skipped) {
|
||||
return { kind: 'skipped', needle }
|
||||
}
|
||||
const hit = locateNeedle(handle.transcript, needle, re)
|
||||
if (hit !== undefined) {
|
||||
return { kind: 'found', needle, line: hit.line, column: hit.column, match: hit.match, elapsedMs: Date.now() - start }
|
||||
}
|
||||
if (Date.now() >= deadline) {
|
||||
return { kind: 'timeout', needle, timeoutMs: timeout, totalLines: handle.transcript.split('\n').length }
|
||||
}
|
||||
await new Promise(resolve => {
|
||||
const t = setTimeout(resolve, 50)
|
||||
// Allow the Node process to exit even if the timer is pending.
|
||||
if (typeof t === 'object' && 'unref' in t) (t as { unref: () => void }).unref()
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
const index = handle.waits.indexOf(record)
|
||||
if (index !== -1) handle.waits.splice(index, 1)
|
||||
this.notify()
|
||||
}
|
||||
```
|
||||
|
||||
(i) `waitFor` 的 JSDoc:`@returns one of `found` / `timeout` / `exited`.` 改为 `@returns one of `found` / `timeout` / `exited` / `skipped`.`;并在 `@param signal` 行后补一行 `* A wait can also be skipped by the user from the sidebar banner (`skipWait`), which resolves it with `{kind:'skipped'}`.`
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `pnpm exec vitest run tests/agent-pty.spec.ts`
|
||||
Expected: PASS(全部用例,含既有 425 行文件内的用例)。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/agent-pty.ts tests/agent-pty.spec.ts
|
||||
git commit -m "feat(terminal): registry 支持等待登记、skipWait 与 skipped 结果"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 工具层 `skipped` 分支 + 宿主 `agent-pty.skip-wait` 路由
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/tools.ts`(terminal_wait_for 定义,约 287-377 行)
|
||||
- Modify: `src/index.ts`(buildApi 表,`'agent-pty.close'` 之后,约 545 行)
|
||||
- Test: `tests/tools.spec.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 的 `AgentTerminalWaitResult` skipped 变体、`registry.skipWait(uuid): number`。
|
||||
- Produces: `/sidebar/api/agent-pty.skip-wait` 路由(payload `{uuid}` → `{ok:true, skipped:number}`);工具 output schema 第四分支;Task 4 的客户端 `api.agentSkipWait` 调它。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
|
||||
`tests/tools.spec.ts`:(a) `FakeRegistry` 增加注入口 —— 在 `readonly terminals = ...` 行后加属性,并把 `waitFor` 改为:
|
||||
|
||||
```ts
|
||||
/** When set, waitFor resolves with the skipped shape instead of the found default. */
|
||||
nextWaitResult: { kind: 'skipped' } | undefined
|
||||
|
||||
waitFor(_uuid: string, needle: string): Promise<
|
||||
{ kind: 'found'; needle: string; line: number; column: number; match: string; elapsedMs: number }
|
||||
| { kind: 'skipped'; needle: string }
|
||||
> {
|
||||
// Mirrors the registry's empty-needle rejection (the tool layer no
|
||||
// longer duplicates this check).
|
||||
if (needle === '') return Promise.reject(new Error('needle must be a non-empty string'))
|
||||
if (this.nextWaitResult !== undefined) return Promise.resolve({ kind: 'skipped', needle })
|
||||
return Promise.resolve({ kind: 'found', needle, line: 0, column: 0, match: needle, elapsedMs: 1 })
|
||||
}
|
||||
```
|
||||
|
||||
(b) 在 `describe('agent terminal tools')` 内、现有 `terminal_wait_for returns the registry result...` 用例后追加:
|
||||
|
||||
```ts
|
||||
it('terminal_wait_for returns skipped (schema-valid) when the registry reports a user skip', async () => {
|
||||
const { captured, registry } = mount()
|
||||
const tool = toolOf(captured, 'terminal_wait_for')
|
||||
registry.nextWaitResult = { kind: 'skipped' }
|
||||
const value = await tool.execute({ uuid: 'uuid-1', needle: 'BUILD_OK', timeout_ms: 1000 }, exec('s1'))
|
||||
expect(value).toEqual({ kind: 'skipped', needle: 'BUILD_OK' })
|
||||
expect(validateJsonSchemaValue(tool.output.schema, value, 'value')).toEqual([])
|
||||
})
|
||||
|
||||
it('terminal_wait_for render describes the user skip and the needle', () => {
|
||||
const { captured } = mount()
|
||||
const tool = toolOf(captured, 'terminal_wait_for')
|
||||
const blocks = tool.output.render({}, { kind: 'skipped', needle: 'BUILD_OK' }) as Array<{ type: string; text: string }>
|
||||
expect(blocks[0]!.type).toBe('text')
|
||||
expect(blocks[0]!.text).toContain('Skipped by user')
|
||||
expect(blocks[0]!.text).toContain('BUILD_OK')
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run tests/tools.spec.ts`
|
||||
Expected: FAIL(schema 拒绝 skipped / render 走到 exited 分支输出不符)。
|
||||
|
||||
- [ ] **Step 3: 实现**
|
||||
|
||||
(a) `src/tools.ts` terminal_wait_for:
|
||||
|
||||
描述末尾(`'The wait is cooperative: ...'` 句之后)追加一句:
|
||||
|
||||
```ts
|
||||
+ 'The user can skip the wait from the sidebar ( a banner on the terminal\'s tab shows the needle and a skip button ) — the tool then returns `skipped`.',
|
||||
```
|
||||
|
||||
output.schema 的 `oneOf` 数组末尾(exited 分支对象之后)追加:
|
||||
|
||||
```ts
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
kind: { type: 'string', required: true, const: 'skipped' },
|
||||
needle: { type: 'string', required: true },
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
render 的 value 类型与分支 —— `const v = value as { kind: 'found' | 'timeout' | 'exited' | 'skipped'; ... }`(原行加 `'skipped'`),并在 timeout 分支后插入:
|
||||
|
||||
```ts
|
||||
if (v.kind === 'skipped') {
|
||||
return [{ type: 'text', text: `Skipped by user while waiting for "${v.needle}" — the wait ended early. Call terminal_read to inspect the transcript and decide how to proceed.` }]
|
||||
}
|
||||
```
|
||||
|
||||
(b) `src/index.ts` buildApi 表,`'agent-pty.close'` 条目(约 541-545 行)之后追加:
|
||||
|
||||
```ts
|
||||
// The sidebar wait banner's skip button: abort every active
|
||||
// terminal_wait_for on one agent terminal. Idempotent — 0 when nothing
|
||||
// is waiting (a stale banner racing a wait that already resolved).
|
||||
// Degraded mode (node-pty unavailable) has no registry and no waits: an
|
||||
// honest ok.
|
||||
'agent-pty.skip-wait': (payload) => {
|
||||
const uuid = requireString(payload, 'uuid')
|
||||
return { ok: true, skipped: agentPtyRegistry?.skipWait(uuid) ?? 0 }
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `pnpm exec vitest run tests/tools.spec.ts`
|
||||
Expected: PASS。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/tools.ts src/index.ts tests/tools.spec.ts
|
||||
git commit -m "feat(terminal): terminal_wait_for 增 skipped 结果与 agent-pty.skip-wait 路由"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 客户端状态 `agentWaits` 镜像 + 推送类型
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/client/state.ts`(SidebarState 接口 ~121 行、makeDefaultState ~221 行、sanitizeState ~1283 行、reconcileAgentTerminals ~1017 行)
|
||||
- Modify: `src/client/sidebar/use-host-feeds.ts`(~66 行解析类型)
|
||||
- Test: `tests/agent-terminal-reconcile.spec.ts`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 宿主推送数组(Task 1 的 snapshot 含 `waiting`)。
|
||||
- Produces:
|
||||
- `SidebarState.agentWaits: Record<string, { needle: string; since: number }>`(uuid → 活动等待;瞬态,不持久化)
|
||||
- `reconcileAgentTerminals(state, list)` 的 list 元素增 `waiting?: { needle: string; since: number } | null`,并把等待状态镜像进 `agentWaits`(仅等待变化也产出新状态)。
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
|
||||
`tests/agent-terminal-reconcile.spec.ts` 追加两个用例(沿用文件现有 import,无需新增):
|
||||
|
||||
```ts
|
||||
it('mirrors the pushed waiting state into agentWaits and clears it when it ends', () => {
|
||||
let s = makeDefaultState()
|
||||
s = reconcileAgentTerminals(s, [{ uuid: 'aaa-111', title: 'busy', waiting: { needle: 'READY_1', since: 42 } }])
|
||||
expect(s.agentWaits['aaa-111']).toEqual({ needle: 'READY_1', since: 42 })
|
||||
// The next push without waiting drops the entry (the push is authoritative).
|
||||
s = reconcileAgentTerminals(s, [{ uuid: 'aaa-111', title: 'stable' }])
|
||||
expect(s.agentWaits['aaa-111']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('produces a new state when ONLY the wait state changes (no tab add/remove)', () => {
|
||||
let s = makeDefaultState()
|
||||
s = reconcileAgentTerminals(s, [{ uuid: 'aaa-111', title: 'stable' }])
|
||||
const before = s
|
||||
const next = reconcileAgentTerminals(s, [{ uuid: 'aaa-111', title: 'stable', waiting: { needle: 'N', since: 1 } }])
|
||||
expect(next).not.toBe(before)
|
||||
expect(next.agentWaits['aaa-111']).toBeDefined()
|
||||
// Idempotent: the same push again returns the same reference.
|
||||
expect(reconcileAgentTerminals(next, [{ uuid: 'aaa-111', title: 'stable', waiting: { needle: 'N', since: 1 } }])).toBe(next)
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run tests/agent-terminal-reconcile.spec.ts`
|
||||
Expected: FAIL(`agentWaits` 不存在 → TS 错误 / 断言失败)。
|
||||
|
||||
- [ ] **Step 3: 实现**
|
||||
|
||||
(a) `SidebarState` 接口 `floats: FloatWindow[]` 之后追加:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Live agent-terminal wait state (uuid → the wait the model currently
|
||||
* blocks on in `terminal_wait_for`), mirrored from the host's
|
||||
* agent-terminals push. Transient by design: sanitizeState never restores
|
||||
* it, so a reload starts clean and the next push (sent immediately on WS
|
||||
* attach) repopulates it.
|
||||
*/
|
||||
agentWaits: Record<string, { needle: string; since: number }>
|
||||
```
|
||||
|
||||
(b) `makeDefaultState` 的返回字面量 `floats: [],` 之后加 `agentWaits: {},`。
|
||||
|
||||
(c) `sanitizeState` 的返回字面量 `floats,` 之后加(带注释):
|
||||
|
||||
```ts
|
||||
// The agent wait state is TRANSIENT (like revealed): never restored from
|
||||
// storage — the host's first push after attach repopulates it.
|
||||
agentWaits: {},
|
||||
```
|
||||
|
||||
(d) `reconcileAgentTerminals`:参数类型改为
|
||||
|
||||
```ts
|
||||
agentTerminals: ReadonlyArray<{ uuid: string; title: string; waiting?: { needle: string; since: number } | null }>,
|
||||
```
|
||||
|
||||
在 `const toRemove = ...` 之后、`if (toAdd.length === 0 && toRemove.length === 0) return state` 处,替换为:
|
||||
|
||||
```ts
|
||||
// Mirror the live wait state from the push (authoritative: a vanished
|
||||
// waiting field simply drops the entry). A waits-only change must still
|
||||
// produce a new state — the tab add/remove no-change check alone would
|
||||
// swallow banner updates.
|
||||
const serverWaits: Record<string, { needle: string; since: number }> = {}
|
||||
for (const terminal of agentTerminals) {
|
||||
if (terminal.waiting !== undefined && terminal.waiting !== null) {
|
||||
serverWaits[terminal.uuid] = { needle: terminal.waiting.needle, since: terminal.waiting.since }
|
||||
}
|
||||
}
|
||||
if (toAdd.length === 0 && toRemove.length === 0 && sameAgentWaits(state.agentWaits, serverWaits)) return state
|
||||
```
|
||||
|
||||
文件末尾的 `return next` 改为 `return { ...next, agentWaits: serverWaits }`。
|
||||
|
||||
在 `reconcileAgentTerminals` 前方新增辅助函数:
|
||||
|
||||
```ts
|
||||
/** Shallow equality of two agent-wait maps (same keys, same needle+since). */
|
||||
function sameAgentWaits(
|
||||
a: SidebarState['agentWaits'] | undefined,
|
||||
b: Record<string, { needle: string; since: number }>,
|
||||
): boolean {
|
||||
if (a === undefined) return Object.keys(b).length === 0
|
||||
const aKeys = Object.keys(a)
|
||||
if (aKeys.length !== Object.keys(b).length) return false
|
||||
for (const key of aKeys) {
|
||||
const av = a[key]
|
||||
const bv = b[key]
|
||||
if (av === undefined || bv === undefined) return false
|
||||
if (av.needle !== bv.needle || av.since !== bv.since) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
(e) `src/client/sidebar/use-host-feeds.ts` 66 行的解析类型改为:
|
||||
|
||||
```ts
|
||||
const list = JSON.parse(event.data) as Array<{ uuid: string; title: string; command: string; exited: boolean; waiting?: { needle: string; since: number } | null }>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过 + 全仓 typecheck(状态字段新增会暴露遗漏的构造点)**
|
||||
|
||||
Run: `pnpm exec vitest run tests/agent-terminal-reconcile.spec.ts && pnpm typecheck`
|
||||
Expected: PASS。若 typecheck 报其他 `SidebarState` 字面量缺 `agentWaits`(tests 里的手搓状态),补 `agentWaits: {}` —— **只许改测试里测试自己构造的字面量,不许改上游断言逻辑**。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/client/state.ts src/client/sidebar/use-host-feeds.ts tests/agent-terminal-reconcile.spec.ts
|
||||
git commit -m "feat(sidebar): 镜像 agent 终端等待状态到 agentWaits(瞬态)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 等待 banner 组件 + TerminalView 接线 + api 方法 + i18n + CSS
|
||||
|
||||
**Files:**
|
||||
- Create: `src/client/TerminalWaitBanner.tsx`
|
||||
- Modify: `src/client/TerminalView.tsx`(render 区 ~387-408 行 + 组件顶部订阅)
|
||||
- Modify: `src/client/api.ts`(`agentPtyClose` 之后 ~348 行)
|
||||
- Modify: `src/client/locales.ts`(zh ~117 / en ~567,紧随 `terminalRetry`)
|
||||
- Modify: `src/client/locales-ja.ts`(~105,紧随 `terminalRetry`)
|
||||
- Modify: `src/client/sidebar.module.css`(`.terminalBanner` 块之后 ~2202 行)
|
||||
- Test: `tests/terminal-wait-banner.spec.tsx`(新建)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 2 的 `agent-pty.skip-wait` 路由;Task 3 的 `state.agentWaits`;`api` 对象、`t()`、`css` module。
|
||||
- Produces: `TerminalWaitBanner({ needle: string; onSkip: () => void })` + `truncateNeedle(needle: string): string`(80 字符截断);词典键 `terminalWaitBanner` / `terminalSkipWait`。
|
||||
|
||||
- [ ] **Step 1: 写失败测试(新文件 `tests/terminal-wait-banner.spec.tsx`)**
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* TerminalWaitBanner unit tests: localized text with the needle, the skip
|
||||
* button wiring, and the long-needle display cap (full text stays on the
|
||||
* title attribute). The component imports no xterm — renderable in plain
|
||||
* jsdom (the repo's jsdom pattern, see bottom-auto-terminal.spec.tsx head).
|
||||
*/
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { createElement } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
import { TerminalWaitBanner } from '../src/client/TerminalWaitBanner.tsx'
|
||||
|
||||
describe('TerminalWaitBanner', () => {
|
||||
const mounted: Array<{ root: Root; container: HTMLDivElement }> = []
|
||||
function mount(needle: string, onSkip: () => void): HTMLDivElement {
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const root = createRoot(container)
|
||||
act(() => { root.render(createElement(TerminalWaitBanner, { needle, onSkip })) })
|
||||
mounted.push({ root, container })
|
||||
return container
|
||||
}
|
||||
afterEach(() => {
|
||||
for (const { root, container } of mounted.splice(0)) {
|
||||
act(() => { root.unmount() })
|
||||
container.remove()
|
||||
}
|
||||
})
|
||||
|
||||
it('shows the localized waiting text with the needle and a working skip button', () => {
|
||||
let skipped = 0
|
||||
const container = mount('BUILD_OK', () => { skipped += 1 })
|
||||
// jsdom navigator.language is en-US → the en dictionary applies.
|
||||
expect(container.textContent).toContain('Agent is waiting for')
|
||||
expect(container.textContent).toContain('BUILD_OK')
|
||||
expect(container.textContent).toContain('Skip wait')
|
||||
const button = container.querySelector('button')
|
||||
expect(button).not.toBeNull()
|
||||
act(() => { button!.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
|
||||
expect(skipped).toBe(1)
|
||||
})
|
||||
|
||||
it('truncates a long needle for display but keeps the full text on the title', () => {
|
||||
const long = 'X'.repeat(200)
|
||||
const container = mount(long, () => {})
|
||||
const span = container.querySelector('span[title]')
|
||||
expect(span).not.toBeNull()
|
||||
expect(span!.getAttribute('title')).toBe(long)
|
||||
expect((span!.textContent ?? '').length).toBeLessThanOrEqual(80)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run tests/terminal-wait-banner.spec.tsx`
|
||||
Expected: FAIL(模块不存在)。
|
||||
|
||||
- [ ] **Step 3: 实现**
|
||||
|
||||
(a) 新建 `src/client/TerminalWaitBanner.tsx`:
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* The "Agent 正在等待 {needle}" wait banner: rendered at the top of an
|
||||
* agent-owned terminal's view while the model blocks in terminal_wait_for.
|
||||
* The skip button asks the host to abort every active wait on the terminal
|
||||
* (`agent-pty.skip-wait`); the banner disappears when the host's next
|
||||
* agent-terminals push drops the waiting field — no optimistic UI. Kept in
|
||||
* its own module (no xterm imports) so jsdom tests can render it directly.
|
||||
*/
|
||||
import { t } from './locales.ts'
|
||||
import css from './sidebar.module.css'
|
||||
|
||||
/** Cap the needle shown inline; the full text rides the title tooltip. */
|
||||
const NEEDLE_DISPLAY_CAP = 80
|
||||
|
||||
/** Truncate one needle for inline display (title attr carries the full text). */
|
||||
export function truncateNeedle(needle: string): string {
|
||||
return needle.length > NEEDLE_DISPLAY_CAP ? `${needle.slice(0, NEEDLE_DISPLAY_CAP - 1)}…` : needle
|
||||
}
|
||||
|
||||
export function TerminalWaitBanner(props: { needle: string; onSkip: () => void }) {
|
||||
const { needle, onSkip } = props
|
||||
return (
|
||||
<div className={css.terminalWaitBanner}>
|
||||
<span className={css.terminalWaitNeedle} title={needle}>
|
||||
{t('terminalWaitBanner', { needle: truncateNeedle(needle) })}
|
||||
</span>
|
||||
<button type="button" className={css.terminalRetry} onClick={onSkip}>
|
||||
{t('terminalSkipWait')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
(b) `src/client/api.ts`:`agentPtyClose` 条目之后追加:
|
||||
|
||||
```ts
|
||||
/** Skip every active terminal_wait_for on one agent terminal (the wait
|
||||
* banner's skip button). Idempotent: {skipped:0} when none is active. */
|
||||
agentSkipWait: (uuid: string) =>
|
||||
call<{ ok: true; skipped: number }>('agent-pty.skip-wait', { uuid }),
|
||||
```
|
||||
|
||||
(c) `src/client/locales.ts`:zh 词典 `terminalRetry: '重试',` 行后加:
|
||||
|
||||
```ts
|
||||
terminalWaitBanner: 'Agent 正在等待 {needle}',
|
||||
terminalSkipWait: '跳过等待',
|
||||
```
|
||||
|
||||
en 词典 `terminalRetry: 'Retry',` 行后加:
|
||||
|
||||
```ts
|
||||
terminalWaitBanner: 'Agent is waiting for {needle}',
|
||||
terminalSkipWait: 'Skip wait',
|
||||
```
|
||||
|
||||
(d) `src/client/locales-ja.ts`:`terminalRetry: '再試行',` 行后加(仓库规则:新 zh 键必须同步 ja):
|
||||
|
||||
```ts
|
||||
terminalWaitBanner: 'エージェントが {needle} を待機中',
|
||||
terminalSkipWait: '待機をスキップ',
|
||||
```
|
||||
|
||||
(e) `src/client/sidebar.module.css`:`.terminalBannerUrl` 块之后加:
|
||||
|
||||
```css
|
||||
/* The "Agent 正在等待 {needle}" wait banner: a warn-toned strip above the
|
||||
terminal surface while the model blocks in terminal_wait_for; the needle
|
||||
ellipsizes inline and the full text rides the title tooltip. */
|
||||
.terminalWaitBanner {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 3px 10px;
|
||||
font: var(--dsw-font-xxxs-11);
|
||||
color: var(--dsw-alias-state-warn-label);
|
||||
background: var(--dsw-alias-state-warn-tertiary);
|
||||
}
|
||||
|
||||
.terminalWaitNeedle {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
```
|
||||
|
||||
(f) `src/client/TerminalView.tsx`:
|
||||
|
||||
imports 区追加:
|
||||
|
||||
```ts
|
||||
import { TerminalWaitBanner } from './TerminalWaitBanner.tsx'
|
||||
```
|
||||
|
||||
组件体内(`const [lastUrl, setLastUrl] = ...` 之后)加等待订阅:
|
||||
|
||||
```ts
|
||||
// Agent terminals only: the model's active terminal_wait_for (mirrored
|
||||
// from the host's agent-terminals push into the store) drives the wait
|
||||
// banner. Read + subscribe like the font prefs above; the banner vanishes
|
||||
// when the host's push drops the waiting field (skip / exit / abort all
|
||||
// converge through the same push). getSnapshot() is {sessionId, state?,
|
||||
// prefs} — the state may be briefly undefined around session switches.
|
||||
const agentUuid = isAgentTabId(tabId) ? agentUuidOf(tabId) : null
|
||||
const [waiting, setWaiting] = useState<{ needle: string; since: number } | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
if (agentUuid === null) return
|
||||
const read = (): void => {
|
||||
const next = store.getSnapshot().state?.agentWaits?.[agentUuid]
|
||||
setWaiting(prev => {
|
||||
const nextValue = next === undefined ? undefined : { needle: next.needle, since: next.since }
|
||||
if (prev?.needle === nextValue?.needle && prev?.since === nextValue?.since) return prev
|
||||
return nextValue
|
||||
})
|
||||
}
|
||||
read()
|
||||
return store.subscribe(read)
|
||||
}, [agentUuid, store])
|
||||
```
|
||||
|
||||
return JSX 中 `<div className={css.terminalWrap}>` 的第一个子元素位置(`depsFatal` 条目之前)加:
|
||||
|
||||
```tsx
|
||||
{agentUuid !== null && waiting !== undefined && (
|
||||
<TerminalWaitBanner
|
||||
needle={waiting.needle}
|
||||
onSkip={() => { void api.agentSkipWait(agentUuid).catch(() => { /* 跳过失败时 banner 留存,可重试 */ }) }}
|
||||
/>
|
||||
)}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `pnpm exec vitest run tests/terminal-wait-banner.spec.tsx`
|
||||
Expected: PASS。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/client/TerminalWaitBanner.tsx src/client/TerminalView.tsx src/client/api.ts src/client/locales.ts src/client/locales-ja.ts src/client/sidebar.module.css tests/terminal-wait-banner.spec.tsx
|
||||
git commit -m "feat(sidebar): agent 终端等待 banner(needle + 跳过等待按钮)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: tab 标题 ⏳ 徽章(shell 层特例)+ jsdom 链路测试
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/client/Sidebar.tsx`(`tabBadgeOf` ~828-841 行)
|
||||
- Test: `tests/agent-wait-badge.spec.tsx`(新文件,harness 照抄 `tests/bottom-auto-terminal.spec.tsx` 1-97 行的模式)
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 3 的 `state.agentWaits`;`isAgentTabId` / `agentUuidOf`(Sidebar.tsx 已 import,第 38-39 行)。
|
||||
- Produces: agent tab 的 ⏳ 徽章 pill(复用 `css.tabBadge`);推送 → 状态 → 徽章全链路。
|
||||
|
||||
- [ ] **Step 1: 写失败测试(新文件 `tests/agent-wait-badge.spec.tsx`)**
|
||||
|
||||
完整文件(FakeWebSocket 记录 url 与实例,驱动 `/sidebar/ws/agent-terminals` 推送;`registerStubTerminal` 防止懒加载 chunk 在 jsdom 里拉真实 bundle):
|
||||
|
||||
```tsx
|
||||
/**
|
||||
* The agent-terminal wait badge chain, end to end in jsdom: the host's
|
||||
* agent-terminals push (mirroring the real /sidebar/ws/agent-terminals
|
||||
* frames) reconciles tabs AND the wait map; the shell's tabBadgeOf renders
|
||||
* the hourglass pill on the agent tab while a wait is live and drops it
|
||||
* when the push clears. Harness mirrors tests/bottom-auto-terminal.spec.tsx
|
||||
* (real Sidebar shell + fake context + stubbed WebSocket).
|
||||
*/
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createElement } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { act } from 'react-dom/test-utils'
|
||||
import { setupReactAct } from './test-utils.ts'
|
||||
setupReactAct()
|
||||
|
||||
import { Sidebar } from '../src/client/Sidebar.tsx'
|
||||
import { createSidebarStore, openTabInActivePane } from '../src/client/state.ts'
|
||||
import { createBetterSidebarService, type BetterSidebarService } from '../src/client/service.ts'
|
||||
|
||||
/** jsdom has no WebSocket; the agent-terminals push effect constructs one on mount. */
|
||||
class FakeWebSocket {
|
||||
static instances: FakeWebSocket[] = []
|
||||
onmessage: ((event: { data: unknown }) => void) | null = null
|
||||
onclose: (() => void) | null = null
|
||||
onerror: (() => void) | null = null
|
||||
close = (): void => {}
|
||||
constructor(readonly url: string) {
|
||||
FakeWebSocket.instances.push(this)
|
||||
}
|
||||
}
|
||||
|
||||
let sessionSeq = 0
|
||||
|
||||
function mountSidebar(): { container: HTMLDivElement; store: ReturnType<typeof createSidebarStore>; service: BetterSidebarService; unmount: () => void } {
|
||||
vi.stubGlobal('WebSocket', FakeWebSocket)
|
||||
FakeWebSocket.instances = []
|
||||
const container = document.createElement('div')
|
||||
document.body.append(container)
|
||||
const store = createSidebarStore()
|
||||
const service = createBetterSidebarService(store)
|
||||
// Replace the terminal descriptor with a stub: the real one lazy-loads the
|
||||
// xterm chunk, which jsdom cannot fetch — the badge under test renders in
|
||||
// the TAB STRIP, independent of the view component.
|
||||
service.registerTab({
|
||||
id: 'terminal',
|
||||
title: () => 'Terminal',
|
||||
component: () => null,
|
||||
})
|
||||
const sessionId = `s1-${++sessionSeq}`
|
||||
store.setSession(sessionId)
|
||||
// Stub the terminal descriptor: the real one lazy-loads the xterm chunk,
|
||||
// which jsdom cannot fetch (the bottom-auto-terminal harness does the same).
|
||||
service.registerTab({
|
||||
id: 'terminal',
|
||||
title: () => 'Terminal',
|
||||
component: () => null,
|
||||
})
|
||||
const localeSnapshot = { active: 'en' }
|
||||
const sessionsSnapshot = {
|
||||
current: sessionId,
|
||||
byId: { [sessionId]: { cwd: '/tmp' } },
|
||||
}
|
||||
const ctx = {
|
||||
locale: { subscribe: () => () => {}, getSnapshot: () => localeSnapshot },
|
||||
sessions: { list: { subscribe: () => () => {}, getSnapshot: () => sessionsSnapshot } },
|
||||
betterSidebar: service,
|
||||
get: (name: string) => name === 'betterSidebar' ? service : undefined,
|
||||
}
|
||||
const root: Root = createRoot(container)
|
||||
act(() => { root.render(createElement(Sidebar, { ctx: ctx as never, store })) })
|
||||
return {
|
||||
container,
|
||||
store,
|
||||
service,
|
||||
unmount: () => {
|
||||
act(() => { root.unmount() })
|
||||
container.remove()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let sessionSeq = 0
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
localStorage.clear()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
/** The agent-terminals push socket the shell opened for the current session. */
|
||||
function feedsSocket(): FakeWebSocket {
|
||||
const socket = FakeWebSocket.instances.find(candidate => candidate.url.includes('/sidebar/ws/agent-terminals'))
|
||||
if (socket === undefined) throw new Error('agent-terminals socket not connected')
|
||||
return socket
|
||||
}
|
||||
|
||||
describe('agent terminal wait badge (push → state → tab pill)', () => {
|
||||
it('shows ⏳ on the agent tab while a wait is active and drops it when it ends', () => {
|
||||
const { container } = mountSidebar()
|
||||
const terminal = { uuid: 'u1', title: 'dev server', command: '', exited: false }
|
||||
// First push: the agent tab lands in the active pane.
|
||||
act(() => { feedsSocket().onmessage?.({ data: JSON.stringify([terminal]) }) })
|
||||
expect(container.textContent).toContain('dev server')
|
||||
expect(container.textContent).not.toContain('⏳')
|
||||
// Second push carries a live wait → the hourglass pill appears.
|
||||
act(() => { feedsSocket().onmessage?.({ data: JSON.stringify([{ ...terminal, waiting: { needle: 'READY_1', since: 1 } }]) }) })
|
||||
expect(container.textContent).toContain('⏳')
|
||||
// The wait resolved (skip/exit/abort all converge to a waiting-less push).
|
||||
act(() => { feedsSocket().onmessage?.({ data: JSON.stringify([terminal]) }) })
|
||||
expect(container.textContent).not.toContain('⏳')
|
||||
})
|
||||
|
||||
it('shows ⏳ only on agent tabs, never on UI-owned terminal tabs', () => {
|
||||
const { container, store } = mountSidebar()
|
||||
// A UI-owned terminal tab (id NOT agent:) lives in the strip too; the
|
||||
// badge lookup keys on the agent uuid, so it never badges.
|
||||
act(() => { store.reduce(s => openTabInActivePane(s, { id: 'terminal:manual-1', type: 'terminal', title: 'UI terminal' })) })
|
||||
const terminal = { uuid: 'u1', title: 'dev server', command: '', exited: false }
|
||||
act(() => { feedsSocket().onmessage?.({ data: JSON.stringify([{ ...terminal, waiting: { needle: 'READY_1', since: 1 } }]) }) })
|
||||
// Exactly ONE hourglass: the agent tab. (Counted from textContent — CSS
|
||||
// module class names are not stable under the test transform.)
|
||||
expect((container.textContent.match(/⏳/g) ?? []).length).toBe(1)
|
||||
// The wait resolved → the pill disappears entirely.
|
||||
act(() => { feedsSocket().onmessage?.({ data: JSON.stringify([terminal]) }) })
|
||||
expect(container.textContent).not.toContain('⏳')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
> 实现注意:`sessionSeq` 声明放 `mountSidebar` 之前(与 bottom-auto-terminal.spec.tsx 相同的模块级写法);`createSidebarStore` 返回类型直接用 `ReturnType<typeof createSidebarStore>`,勿手写接口。
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
Run: `pnpm exec vitest run tests/agent-wait-badge.spec.tsx`
|
||||
Expected: FAIL(⏳ 从不出现)。
|
||||
|
||||
- [ ] **Step 3: 实现 Sidebar.tsx 的 `tabBadgeOf` 特例**
|
||||
|
||||
`src/client/Sidebar.tsx` 828 行 `const tabBadgeOf = (tab: SidebarTab): ReactNode => {` 函数体开头(descriptor 查找之前)插入:
|
||||
|
||||
```ts
|
||||
// Agent-terminal wait indicator (sidebar-internal, deliberately NOT a
|
||||
// TabDescriptor.badge — that API is type-keyed and shared with external
|
||||
// plugins, and cannot address one tab): the agent-terminals push mirrors
|
||||
// the model's live terminal_wait_for into state.agentWaits; an agent tab
|
||||
// whose uuid is waiting shows the hourglass pill.
|
||||
if (isAgentTabId(tab.id)) {
|
||||
const wait = state.agentWaits?.[agentUuidOf(tab.id)]
|
||||
if (wait !== undefined) return <span className={css.tabBadge}>{'⏳'}</span>
|
||||
}
|
||||
```
|
||||
|
||||
(`isAgentTabId` / `agentUuidOf` 已在 38-39 行 import;`css` / `state` 均在作用域内。)
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
|
||||
Run: `pnpm exec vitest run tests/agent-wait-badge.spec.tsx`
|
||||
Expected: PASS(2 个用例)。
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/client/Sidebar.tsx tests/agent-wait-badge.spec.tsx
|
||||
git commit -m "feat(sidebar): agent 终端 tab 标题在等待时显示 ⏳ 徽章"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 设计文档偏差记录 + 全量验证 + 推送 + PR
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/plans/2026-09-09-terminal-wait-banner-design.md`(实施偏差记录节)
|
||||
- 无代码改动。
|
||||
|
||||
- [ ] **Step 1: 更新设计文档**
|
||||
|
||||
在 `docs/plans/2026-09-09-terminal-wait-banner-design.md` 末尾追加:
|
||||
|
||||
```markdown
|
||||
|
||||
## 实施偏差记录
|
||||
|
||||
- **⏳ tab 徽章不走 `TabDescriptor.badge`**:实现时核实该 API 是 type-keyed
|
||||
(`badge(ctx, scope, state)`,无 tab 参数,external 插件共享),无法定位单个
|
||||
tab。改为 Sidebar 壳层 `tabBadgeOf` 的 sidebar-internal 特例(agent: tab 前缀
|
||||
判断 + `state.agentWaits` 查表),`service.ts` 与 external-plugin-guide.md
|
||||
零改动。
|
||||
- **banner 抽为独立组件 `TerminalWaitBanner.tsx`**:设计原文写在
|
||||
TerminalView.tsx 内;抽出后无 xterm 依赖,可直接被 jsdom 单测渲染
|
||||
(TerminalView 仍负责订阅 store 并接线 onSkip → api.agentSkipWait)。
|
||||
- **设计文档随 feat 分支进 PR**:main 受分支保护,纯文档直推被 hook 拒绝
|
||||
(`4a6a77a` 随本分支合并)。
|
||||
- **banner 配色**:仓库 CSS 无 state-info 令牌,沿用 `.terminalBanner` 的
|
||||
warn 对(`--dsw-alias-state-warn-label/-tertiary`),符合皮肤契约。
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 全量验证**
|
||||
|
||||
Run(依次,全绿才继续):
|
||||
`pnpm lint` → `pnpm typecheck` → `pnpm test` → `pnpm build`
|
||||
|
||||
Expected:
|
||||
- lint: 0 error(历史上 CI 因 no-useless-assignment 挂过——新代码避免「先声明后必然覆盖」的写法);
|
||||
- test: 除已知的 win32 非回归失败集(bottom-auto-terminal / free-window / sidebar-auto-activation / sidebar-crash 等既有失败,对照 main 基线)外全绿,新增 spec 全部 PASS;
|
||||
- build: lib/ 产物含新 chunk `client.js` 变化(TerminalWaitBanner 打进 client bundle)。
|
||||
|
||||
- [ ] **Step 3: 提交并推送**
|
||||
|
||||
```bash
|
||||
git add docs/plans/2026-09-09-terminal-wait-banner-design.md
|
||||
git commit -m "docs(plans): 终端等待 banner 实施偏差记录"
|
||||
git push -u origin feat/wait-banner
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 建 PR**
|
||||
|
||||
```bash
|
||||
gh pr create --repo omdsh-dev/DSH-better-sidebar --base main --head feat/wait-banner \
|
||||
--title "feat(terminal): 等待 banner + 跳过等待(terminal_wait_for skipped)" \
|
||||
--body "设计:docs/plans/2026-09-09-terminal-wait-banner-design.md(随本 PR)。agent 阻塞在 terminal_wait_for 时:对应终端卡片顶部显示「Agent 正在等待 {needle}」banner,右侧跳过按钮走 agent-pty.skip-wait;tab 标题 ⏳ 徽章;工具新增 {kind:'skipped'} 返回。"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 等待 CI(用 terminal_wait_for 盯 `(ci|ci-windows|plugin-mount).*\((pass|fail)\)` 之类的多结局 needle,勿 sleep)**
|
||||
|
||||
Expected: 三个 job 全 pass。
|
||||
@@ -1,60 +0,0 @@
|
||||
# 自动激活落点修复:任务页回到 DSH 原生右侧栏
|
||||
|
||||
日期:2026-09-10 分支:`fix/auto-activation-native-landing`
|
||||
|
||||
## 背景
|
||||
|
||||
用户报告:**自动打开的侧边栏是底栏,而不是右侧边栏**。两个自动激活开关(`autoOpenSubagent` / `autoOpenJobs`)与子代理拓扑跳回在 v0.19.0 上把任务页(`subagent` tab)开进了插件自绘的**底部工作台**,底栏随之展开;而产品文案描述的是侧边栏落点——README 特性表「新子代理 / 新任务可自动激活任务页,**宽屏同时展开侧边栏**,窄屏不强制展开全屏抽屉」,`settingsSubagentDesc` / `settingsJobsDesc` 及全部 20 份词典同样措辞,README 的 #314 条目也写着「宽屏展开侧边栏,窄屏只准备 Tab」。
|
||||
|
||||
## 根因
|
||||
|
||||
`223b254`(v0.19.0-alpha.0「退役插件自绘右侧面板与自由窗口」)改写了 `src/client/sidebar/use-host-feeds.ts` 的三处触发:
|
||||
|
||||
| | 改动前(v0.18.x) | 改动后(v0.19.0) |
|
||||
|---|---|---|
|
||||
| 展开 | `store.reduce(s => s.panelOpen ? s : togglePanel(s))`(宽屏才展开) | 无(由落点决定) |
|
||||
| 落点 | `activePane = firstLeaf(s.splits).id` + `openTab({ type:'subagent', title })` —— **无 `target`** | `openTab({ type:'subagent', title, target:'bottom' })` |
|
||||
|
||||
`service.ts:683` 的判据是「装了原生面 + 非 `bottom` → 落 DSH 原生右侧栏」,`target: 'bottom'` 直接绕过它走 `land = openTabInBottomPane`(`service.ts:744` → `state.ts:453`,置 `bottomOpen: true`)。也就是说:退役右侧面板时,`splits`(插件自己的右面板)消失,落点被改成了插件自己的**底**面板,而正解是当时同文件其它打开早已默认落上的新宿主——DSH 原生右侧栏。
|
||||
|
||||
## 决策
|
||||
|
||||
| # | 决策 | 理由 |
|
||||
|---|---|---|
|
||||
| 1 | 三处触发改回默认落点(`'right'`) | 宿主 `openContent` 内部 `planSetExpanded(state, true)`(`dsh-client-ui-sidebar-right`),打开即在同一步展开原生栏;这才是文案里的「侧边栏」 |
|
||||
| 2 | 后台活动(子代理 / 新任务)在窄屏**停放**,不展开 | 宿主 `RightbarSeat` 有 `autoFullscreen = viewportWidth < 768`,该宽度下原生栏以 `position: fixed; inset: 0` 全屏绘制——展开即整屏抢占,正是 #373 修掉的行为。宿主没有「只放 tab、不展开」的公开选项,故开前读 `ctx.sidebarRight.isExpanded()`、开后 `toggleExpanded()` 收回;两次提交落在同一 React 批内,中间态不会渲染 |
|
||||
| 3 | 停放仅在目标会话在屏时执行 | `isExpanded` / `toggleExpanded` 作用于**已挂载会话**(`mounted()` = `binding.surfaces[binding.sessionId]`);目标不在屏时执行会误动用户当前那一栏 |
|
||||
| 4 | 拓扑跳回(用户点击)一律展开 | 它是显式手势,不是后台活动;对齐 v0.18.x 跳回无窄屏门禁的语义 |
|
||||
| 5 | 底部工作台自有流程保持 `target: 'bottom'` | 底栏 `+` 菜单(`Sidebar.tsx:628`)与首次展开自动终端(`:336`)本就该落底栏;`OpenTabSeed.target` API 不变 |
|
||||
|
||||
阈值一致性:插件既有 `NARROW_MAX_WIDTH = 768`(`width < 768`,`src/client/breakpoints.ts`)与宿主 `viewportWidth < 768` 的全屏规则逐字对齐,不引入第二个断点。
|
||||
|
||||
窄屏语义与 v0.18.x 等价:**tab 已就位、抽屉不展开**(用户点开会话头右侧的展开控件即可看到任务页)。
|
||||
|
||||
## 改动清单(子系统级)
|
||||
|
||||
| 子系统 | 文件 | 变更 |
|
||||
|---|---|---|
|
||||
| 触发 | `src/client/sidebar/use-host-feeds.ts` | 新增模块内 `activateTasksPage(ctx, sessionId, { background })`(落点 + 停放判定集中在此);三处调用改为它(子代理 / 后台任务 `background: true`,跳回 `false`);重新引入 `isNarrowWidth`;三段 docblock 与文件头同步 |
|
||||
| 测试 | `tests/sidebar-auto-activation.spec.tsx` | 断言原生面收到 `openTab { kind:'subagent', revealIfOpened:true }`、底部工作台保持原状(未展开、未新增 tab)、窄屏停放次数(1 / 0)、宽屏与已展开栏不停放、去抖触发时读视口、跳回不停放 |
|
||||
|
||||
无 API / 持久化 / i18n / README 行为文案改动:文案本就在描述修复后的行为。
|
||||
|
||||
## 验证
|
||||
|
||||
- **回归守护(确定性)**:新 spec 10 passed;把 `src/client/sidebar/use-host-feeds.ts` 回退到 `HEAD` 后同 10 条**全红**(10 failed),证明它测的正是这次修复。
|
||||
- **门槛**:`pnpm typecheck` / `pnpm lint` / `pnpm check:consumer-types` 干净;`pnpm test` = **124 files / 1299 passed / 9 skipped**(较基线 +3 条,即本 spec 的净增)。
|
||||
- **打包挂载**:`pnpm build && pnpm pack && pnpm test:mount` = **7 passed**(真实 DSH 0.1.5-rc.1,scratch profile,不触碰用户 `~/.dsh`)。
|
||||
- **环境事实(踩过一次)**:本机 PATH 上的 `dsh` 是 **0.1.2-rc.1**,用它跑挂载车道会在 `dsh plugin add` 后报 `dsh-better-sidebar 未出现在 dsh.profile.bundles 中——挂载未注册`(该版本不会把 `file:` tarball 注册进 profile bundles,且插件 peer 要求 `^0.1.5-rc.1`)。CI 装的是钉版 0.1.5-rc.1;本地需用同版本的 shim / `DSH_CMD` 才能复现车道结果。
|
||||
- **真机**:见 PR 描述(3080 实例,web profile 换装新 tarball 后人工触发子代理 / 后台任务确认)。
|
||||
|
||||
## 未覆盖(诚实记录)
|
||||
|
||||
- 挂载车道是 keyless 的(无模型),无法真派生子代理或后台任务,因此**该行为只有单测是确定性证据**;宿主 `session/create` 的请求体不接受 `origin` / `parentId`,也无法用 RPC 造出 `origin: 'subagent'` 的子会话,所以真机断言只能依赖人工触发或非确定性的 prompt。
|
||||
- 宿主若在窄屏自行折叠(`shown && !fullscreen && !canShow` 的 room rule),停放不会与之冲突(两者都指向「不展开」)。
|
||||
|
||||
## 不做
|
||||
|
||||
- 不新增设置项、不改变 `OpenTabSeed.target` 的语义、不把任务页改到别处承载。
|
||||
- 不动底栏 `+` 菜单 / 首次展开自动终端(它们是底部工作台自己的流程)。
|
||||
- 不改 DSH 源码(仓库硬约束 §1)。
|
||||
@@ -1,86 +0,0 @@
|
||||
# DSH 0.1.5-rc.1 适配(v0.19.0 正式版基线)
|
||||
|
||||
日期:2026-09-10 分支:`feat/dsh-0.1.5-rc.1`
|
||||
|
||||
## 背景
|
||||
|
||||
DSH `dsh-v0.1.5-alpha.2..dsh-v0.1.5-rc.1`(上游 commit `183f08e9c`,npm 发布 2026-09-10T03:12Z)的 delta 很小:373 个变更文件,绝大多数是各包 `package.json` 版本号、原生右侧栏预览 UI 打磨与测试快照。触及本插件的只有一处契约回归——`SidebarRightGuideEntry.description` 回来了。本计划记录该 delta、插件对它的取舍、以及 v0.19.0 正式版的基线决策。上一版(alpha.2)的适配记录见 [2026-09-09-dsh-0.1.5-alpha.2-adaptation.md](./2026-09-09-dsh-0.1.5-alpha.2-adaptation.md);插件对外的全部行为差异以 [docs/external-plugin-guide.md](../external-plugin-guide.md) 为准;仓库级适配要点(基线、CI 钉版、宿主契约)同步到 [AGENTS.md](../AGENTS.md) §2/§3。
|
||||
|
||||
## 上游 delta(alpha.2 → rc.1,影响本插件的三项)
|
||||
|
||||
| # | 上游变更 | 对插件的影响 |
|
||||
|---|---|---|
|
||||
| 1 | `SidebarRightGuideEntry.description?: () => string` 回归(可选):原生指南**只在列出条目 ≤ 4 条时**渲染它(上游 `MAX_DESCRIBED_ENTRIES = 4`),更长的列表整列丢弃说明;没有 `icon` 的条目改由宿主补方块占位 | 恢复 `TabDescriptor.description`(六个内置类型声明它,`guideDesc*` 词条一并回来);**不恢复**旧的通用兜底句 `nativeGuideDesc`——宿主自己没有兜底,通用句在所有页面上长得一样、纯噪音;未声明该字段的 descriptor 就不发 `description` 字段 |
|
||||
| 2 | 内置 `files` 类型重新声明指南说明,且改用彩色的 `FileTypeIcon kind="folder"` 作指南字形 | 仅背景信息:`files` 页面 kind 由插件接管,该行现在随插件渲染(`editor` descriptor 的图标与说明) |
|
||||
| 3 | `@deepseek-ai/dsh-client-ui-primitives@0.1.5-rc.1` **仍不声明 `dependencies`**,而其 bundle 继续裸 import `anser` / `shiki` / `@shikijs/langs/*` / `mdast-util-*` / `micromark-*` / `katex` | 上一版提升进 devDependencies 的那组第三方包**不得回退**(回退即 vitest 一加载 primitives 就 `Cannot find package 'anser'`);`ui-primitives` 的导出面未变,只有 CodeBlock 渲染变化 |
|
||||
|
||||
**明确未变**(不必再核):
|
||||
|
||||
- 全局主面板模型——根级 keyed `main` 槽、`sidebar.panellist`、`ctx.layout.selectPanel` / `beginNavigation`、根级 `rightbar` + `rightbar.session`;插件仍不接入。
|
||||
- 文件地址语法——`packages/util/workspace-path` 只动了 `package.json` 的版本号。
|
||||
- 原生 tab 体宿主契约——`dsh-client-ui-dockkit` 的 `.paneBody` 仍是「有确定高度的块级滚动容器」,只加了两行 z-index。
|
||||
- core / agent / session / subagent 宿主 API——仅版本号变化。
|
||||
|
||||
## 决策
|
||||
|
||||
- **插件版本 0.19.0(无 prerelease 后缀)**:`release.yml` 按「版本号是否含 `-`」自动选 dist-tag,因此本版发 npm **`latest`**;npm 的 `alpha` 标签保持指向 `0.19.0-alpha.1`(上游 rc.1 自己的 GitHub Release 仍标 pre-release,与本插件的通道无关)。
|
||||
- **基线 = DSH 0.1.5-rc.1+ only**:peer 下限 `^0.1.5-rc.1`、manifest `engines.dsh >=0.1.5-rc.1`、CI 钉 `@deepseek-ai/dsh@0.1.5-rc.1`。DSH **0.1.5-alpha.2 用户留在 v0.19.0-alpha.1**,DSH **0.1.2-rc.1 用户留在 v0.18.1**。
|
||||
- **恢复 `description`,不恢复通用兜底句**:宿主渲染第二行时读 descriptor 自己的说明;`nativeGuideDesc` 兜底句不再存在——没写说明的条目就是单行「图标 + 标题」。
|
||||
- **接受 ≤4 渲染规则,不做额外工程**:插件默认贡献 6 个 guide 条目 > 4,说明在默认组合下不渲染(见下节)。不为此裁剪内置类型、不改条目数、不把关键信息只放进说明里。
|
||||
- **升级本地 3080 实例**到 DSH 0.1.5-rc.1 + 插件 0.19.0,作为真机观察点。
|
||||
|
||||
## ≤4 渲染规则的后果(重要)
|
||||
|
||||
上游只在 guide 列出的条目 **≤ 4 条**时渲染 `description`(`MAX_DESCRIBED_ENTRIES = 4`),更长的列表整列丢掉所有说明——注意是「整列丢弃」,不是截断到前四条。插件默认贡献 6 条 guide 条目(文件 / 文件变动 / 任务管理 / 侧边对话 / 终端 / 浏览器),**因此在默认组合下,插件与内置类型的说明都不会出现**,六条仍是「图标 + 标题」。
|
||||
|
||||
说明只在读者于插件设置页关掉足够多的 tab 类型、把 guide 压到 ≤ 4 条时才出现(逐类型开关;关掉 `editor` 会同时移除接管出去的「文件」行)。这是上游行为而非插件缺陷:插件恢复字段是为了让「关到 ≤ 4 条」的读者拿到真实说明,而不是为了在默认组合里显示什么。指南 §0 与 §4.1 都写明了这条规则,消费插件不应把关键信息只放在 `description` 里。
|
||||
|
||||
## 改动清单(子系统级)
|
||||
|
||||
| 子系统 | 文件 | 变更 |
|
||||
|---|---|---|
|
||||
| 基线 | `package.json` | 版本 → `0.19.0`(无后缀);dsh-* peer 全部 `^0.1.5-rc.1`;devDeps 同步钉 `0.1.5-rc.1`;`dsh-client-locale` 跟随基线到 `0.1.5-rc.1`(该包已回到与 DSH 同 tuple 发版,§3-9 的落后例外保持消除) |
|
||||
| 基线 | `pnpm-lock.yaml` | 锁文件对齐 rc.1 |
|
||||
| 基线 | `dsh.plugin.json` | `version` → `0.19.0`;`engines.dsh` → `>=0.1.5-rc.1` |
|
||||
| 基线 | `.github/workflows/ci.yml` | mount lane 钉 `@deepseek-ai/dsh@0.1.5-rc.1`(rc.1 在 npm 上同时是 `latest` 与 `next`) |
|
||||
| 基线 | `package.json` | 上一版提升的第三方 devDependencies(anser / shiki / `@shikijs/langs` / `mdast-util-*` / `micromark-*` / katex)保留不回退——`ui-primitives@0.1.5-rc.1` 仍无 `dependencies` |
|
||||
| Tab 契约 | `src/client/service.ts` | `TabDescriptor` 恢复可选 `description`;`SIDEBAR_SERVICE_VERSION` 跟随插件版本(`0.19.0`) |
|
||||
| Tab 契约 | `src/client/builtins/tabs.tsx` | 六个内置类型(editor / git / subagent / sidechat / terminal / browser)重新声明 `description` |
|
||||
| Tab 契约 | `src/client/native/index.ts` | guide 条目在 descriptor 声明了说明时带上 `description`;未声明就不发该字段(不补通用句) |
|
||||
| 词典 | `src/client/locales.ts` + 19 份 `locales-*.ts` | 恢复 6 个 `guideDesc*` 词条(zh / en / ja 与其余第三方语言) |
|
||||
| 测试 | `tests/builtins.spec.ts` | 指南断言从「仅 `title`」改回「`title` + `description`」 |
|
||||
| 测试 | `tests/native-surface.spec.ts` | 断言 guide 条目携带 `description`;`editor` 类型仍不单独占行、接管行承载其图标与说明 |
|
||||
| 文档 | `docs/external-plugin-guide.md` | 顶部基线行(v0.19.0 / DSH 0.1.5-rc.1+ / 两个钉版旧版本)、§0 「新建标签页列表」行、§4.1 恢复 `description` 字段与 ≤4 规则 |
|
||||
| 文档 | `docs/plans/2026-09-09-dsh-0.1.5-alpha.2-adaptation.md` | 加一行前向链接(该版删掉的 `description` 在 rc.1 回归) |
|
||||
| 文档 | `docs/plans/2026-09-10-dsh-0.1.5-rc.1-adaptation.md` | 本计划 |
|
||||
| 文档 | `AGENTS.md` | §2 CI 基线戳、§3 标题与基线 prose、§3 第 9/10 条(devDeps 不回退 / `description` 回归) |
|
||||
| 文档 | `README.md` / `README_EN.md` | 顶部与安装节的「支持的 DSH 版本」徽章、安装节正式版说明、新增 `### v0.19.0` 更新段落 |
|
||||
|
||||
## 验证
|
||||
|
||||
- **本工作流(文档)未执行任何门禁或真机验证**,只做了与工作树现状的交叉核对:`package.json` 为 `0.19.0`(dsh-* peer 与 devDep 均 `0.1.5-rc.1`)、`dsh.plugin.json` 的 `engines.dsh` 为 `>=0.1.5-rc.1`、`.github/workflows/ci.yml` 钉 `@deepseek-ai/dsh@0.1.5-rc.1`、`SIDEBAR_SERVICE_VERSION` 为 `0.19.0`、`src/client/builtins/tabs.tsx` 六个类型与 `src/client/native/index.ts` 的 guide 条目(含 `files` 接管行)重新带上 `description`、`src/client/locales*.ts` 恢复 6 个 `guideDesc*` 键。
|
||||
- 仓库**没有守护指南文档章节的测试**——`tests/builtins.spec.ts` / `tests/native-surface.spec.ts` 守护的是 guide 行的形态,不是文档文本,因此文档改动没有对应的测试命令可跑。
|
||||
- **待补(由主代理在实施完成后回填,本文件不预填未经执行的结论)**:门禁数字(`pnpm typecheck` / `pnpm lint` / `pnpm vitest run --exclude '**/*.e2e.ts'` / `pnpm peers check`)、`scripts/e2e-mount.sh` 真机挂载车道的通过数,以及本地 3080 实例(DSH 0.1.5-rc.1 + 插件 0.19.0)上的实测结果——默认组合下 6 条 guide 行仍是「图标 + 标题」、关掉 tab 类型至 ≤ 4 条后说明出现、以及 `pageerror` / 控制台状况。
|
||||
|
||||
### 回填(主代理实测)
|
||||
|
||||
- **门禁**:`pnpm typecheck` / `pnpm lint` / `pnpm check:consumer-types` 全绿;`pnpm vitest run --exclude '**/*.e2e.ts'` = **124 files / 1296 passed / 9 skipped**;`pnpm peers check` = No peer dependency issues found(rc.1 未新增失配的传递 peer,因此没有再提升任何 devDep)。
|
||||
- **挂载冒烟**(`DSH_CMD` 指向 npm 安装的 0.1.5-rc.1,`scripts/e2e-mount.sh`):**7 passed**,含 tab 体填充断言与新增的 ≤4 条描述断言。
|
||||
- **≤4 断言的真实形态**:默认 6 条下永远拿不到说明文本,所以该用例先用插件自己的 `settings.get` / `settings.update` 把启用集合压到 3 个类型(files / sidechat / browser),断言 `files` 胶囊文本含英文说明片段 `workspace tree`,并断言被禁用的三个条目离开 guide。
|
||||
- **踩坑(已修)**:恢复设置时最初只把原始 `tabsEnabled`(scratch profile 里是 `{}`)原样发回——而该路由的 patch 是**逐键合并**,空对象清不掉刚写下的三个 `false`,于是同车道的 `perf.e2e.ts` 扫 guide 时找不到 `git` 条目而失败。改为对本次动过的每个键显式恢复(原值缺省即 `true`)后全绿;这条经验对任何"临时改设置再恢复"的 e2e 都适用。
|
||||
- **真机 3080**(DSH 0.1.5-rc.1 + 插件 0.19.0):guide 六行胶囊均为「图标 + 标题」、**无说明**(6 > 4,符合上游规则);胶囊高度 56px(上游把 `min-height` 从 alpha.2 的 48px 调到 56px);文件树点击 `AGENTS.md` → tab 标题 `AGENTS.md` + CodeMirror 挂载;sidechat 宿主盒 962px == `.paneBody` 962px(fillGap 0),composer 底边距 8px;底部工作台与 `[data-dsh-center-col]` 左右边完全重合(gap 0);`pageerror` 0;控制台仅第三方 `dsh-tauri-worktree` 的 `/api/dsh-worktree/attach` 500(alpha.2 期同样存在,与本次无关)。rc.1 真机的 `[data-slot]` 清单与 alpha.2 完全一致(`main` / `main.conversation` / `rightbar` / `rightbar.session`),印证"全局面板模型未变"。
|
||||
|
||||
## 不做
|
||||
|
||||
- 不为 ≤4 渲染规则裁剪内置 tab 类型、调整条目数或改写图标。
|
||||
- 不恢复 `nativeGuideDesc` 通用兜底句。
|
||||
- 不接入 alpha.2 的全局面板(`main` 槽 / `sidebar.panellist` / `ctx.layout` / `usePanelInfo` / 根级 `rightbar`)——沿用上一版结论,rc.1 上这些面同样只是兼容保留。
|
||||
- 不回退提升进 devDependencies 的第三方包。
|
||||
- 不改 DSH 源码(仓库硬约束 §1)。
|
||||
|
||||
## 落地
|
||||
|
||||
- [#613](https://github.com/omdsh-dev/DSH-better-sidebar/pull/613) 合入 `main`(merge commit `754974a`);main 的 `package.json` 为 `0.19.0`、`engines.dsh` 为 `>=0.1.5-rc.1`。PR 的 `ci` / `ci-windows` / `plugin-mount` 三个 check 全绿。
|
||||
- 发布 [v0.19.0](https://github.com/omdsh-dev/DSH-better-sidebar/releases/tag/v0.19.0):`release.yml` 通过全门禁后经 npm Trusted Publishing 发布到 **`latest`**(带 provenance);实测 dist-tags:`latest = 0.19.0`、`alpha = 0.19.0-alpha.1`(不变)、`beta` 不变。
|
||||
- 产物核对:`lib/index.js` 与本地已验证构建**逐字节一致**,且与 `v0.19.0-alpha.1` 的同名文件同哈希(宿主侧代码在这两版之间确实没动);`lib/client.js` 仅差已知的打包非确定性;发布包的 client bundle 内含恢复后的说明文案。
|
||||
- **npm 传播延迟仍是 ~4–5 分钟**:06:49:10 工作流报 `Published package dsh-better-sidebar@0.19.0`,06:53:53 dist-tags 才可见 `latest = 0.19.0`(版本元数据与 tarball 依次到达,期间 `npm view` / tarball 会 404)。这不是发布失败,别重发。
|
||||
@@ -1,107 +0,0 @@
|
||||
# DSH 0.1.5-rc.2 适配(v0.19.1 基线)+ CI 三类失败修复
|
||||
|
||||
日期:2026-09-10(复核 2026-09-11) 分支:`feat/dsh-0.1.5-rc.2`
|
||||
|
||||
## 背景
|
||||
|
||||
DSH `dsh-v0.1.5-rc.1..dsh-v0.1.5-rc.2`(上游 4 个 commit / 300 个变更文件,npm 发布 2026-09-10T14:57Z,dist-tag **`next`**;`latest` 仍是 rc.1)是 0.1.5 线的第二个候选版。本计划记录该 delta、插件对它的结论(**无需任何运行时/类型层适配**,只推进钉版),以及同一时间窗内 `ci-windows` / `plugin-mount` 两个车道真实存在的三类 CI 失败与修法。上一版(rc.1)记录见 [2026-09-10-dsh-0.1.5-rc.1-adaptation.md](./2026-09-10-dsh-0.1.5-rc.1-adaptation.md);仓库级适配要点同步到 [AGENTS.md](../../AGENTS.md) §2/§3。
|
||||
|
||||
## 上游 delta(rc.1 → rc.2,影响本插件的部分:无)
|
||||
|
||||
300 个文件里绝大多数是各包 `package.json` 的 `version` 单行改动(`1+1-`)。逐文件过完后的真实代码改动只有四处:
|
||||
|
||||
| # | 上游变更 | 文件 | 对插件的影响 |
|
||||
|---|---|---|---|
|
||||
| 1 | 消息反馈改为对称提交(弹窗、动作按钮、控制器、槽位、i18n) | `packages/client/ui-message-feedback/src/client/*` | 无——插件不注册反馈槽、不读该控制器 |
|
||||
| 2 | 产物卡片内边距与呈现微调 | `packages/client/ui-deliverables/src/client/*`、`ui-chat/src/client/chat/TurnTailNodeView.module.css` | 无——插件不消费这两个包的组件,也不做会话尾迹布局 |
|
||||
| 3 | `CodeFileIcon` 的 477 行内联 SVG 拆进新增的 `code-file-icon-artwork.ts`(另有 `.manifest.json` 记录设计导出摘要) | `packages/client/ui-primitives/src/CodeFileIcon.tsx` | 无——`CodeFileIcon` 的导出面与 props 未变,且插件**本来就不用它**(插件文件树用 `IconCodeOutline16`;`#611` 的文件图标主题还在 PR #594,未进 main) |
|
||||
| 4 | 其余全部为版本号 | 各 `package.json` | 无 |
|
||||
|
||||
**明确未变(不必再核)**:
|
||||
|
||||
- `packages/api/*`(session-controller / workspace-controller / gateway / remotes)、`packages/host/*`、`packages/session/*`、`packages/agent/*`——**零**变更;除 `packages/feedback/message-feedback/src/types.ts`(1 行)外**零**类型文件变更。
|
||||
- 斜杠 RPC 方言、一次性 token 鉴权、会话事件模型、`sidebarRight` / `sidebarRightTabs` 承载面、`SidebarRightGuideEntry.description` 的 ≤4 渲染规则:全部沿用 rc.1 结论。
|
||||
- `@deepseek-ai/dsh-client-ui-primitives` 仍不声明 `dependencies`(bundle 仍裸 import `anser` / `shiki` / `@shikijs/langs/*` / `mdast-util-*` / `micromark-*` / `katex`)——提升进 devDependencies 的那组包继续不得回退。
|
||||
|
||||
## 决策
|
||||
|
||||
- **peer 下限保持 `^0.1.5-rc.1` 不动**:rc.2 无契约变化,`^0.1.5-rc.1` 天然容纳它,rc.1 用户无需升级即可用新版插件。因此本版是**纯 patch 的 0.19.1**,只推进 devDependency 钉版、CI 挂载钉版与 `SIDEBAR_SERVICE_VERSION`。
|
||||
- **`engines.dsh` 保持 `>=0.1.5-rc.1`**:与 peer 下限同源(同一个支持范围,两处不能分叉)。
|
||||
- **CI 挂载车道钉 rc.2**:真机证据要落在最新版上;`latest` 仍是 rc.1 只是上游的发布节奏,与本插件支持范围无关。
|
||||
- **不为此版写运行时兼容层**:上游 delta 里没有本插件触达的面,写兼容层就是无据的复杂度。
|
||||
|
||||
## CI 三类失败(2026-09-09/10 窗口,60 次已完成 run 的取证)
|
||||
|
||||
窗口内 `ci-windows|Test` 失败 8 次、`plugin-mount|Install dsh CLI (pinned)` 失败 4 次。逐条读日志后归为三类,其中两类是**真实缺陷**、一类是**运维噪音**:
|
||||
|
||||
### A. Windows 真实超时(6/8 次 `ci-windows` 失败,主因)
|
||||
|
||||
- 证据:run `34494370187`、`34490215638`、`34459499945`(**3 次 attempt 全红**)、`34372999975`、`34322271074`、`34319666611` 均为 `Error: Test timed out in 5000ms`,位置 `tests/agent-pty.spec.ts:71:3` / `:106:3`;`tests/install-powershell.spec.ts:105:41` 的 `powershell.exe` 单测自身耗时 **12151ms** 后才超时。
|
||||
- 根因:`vitest.config.ts` 没有任何 `testTimeout` 配置(默认 5000ms),而这些 spec 每个用例都要真起一个 `PowerShell + ConPTY` 对;`tests/agent-pty.spec.ts` 的 `waitForTranscript` 默认预算**也**是 5000ms——内层轮询吃掉整个对外预算,结构上必然超时。
|
||||
- 修法:**全局** `testTimeout: 15_000`(`vitest.config.ts`)+ 两个最重的 describe 显式 `{ timeout: 30_000 }`(vitest 4 的 describe 级选项,已实测生效)+ `waitForTranscript` 默认预算降到 15s。**为什么是全局而不是逐文件**:本 PR 第一次跑真实 Windows 时,同一个 5000ms 默认又在**第三个文件**上翻车——`tests/git.spec.ts:85`「caps status entries at the truncation limit」耗时 **9607ms**(构造病态 untracked 集合 + 真实 git 调用)。三个不同文件、同一个根因,逐文件加超时是打地鼠;15s 是默认值的 3 倍、也是 Windows 上实测最大单测耗时的 ~1.5 倍,而 pty / powershell 两类最重的用例各有自己的 30s 预算。
|
||||
|
||||
### B. Windows worker 静默死亡(2 次,非确定性)
|
||||
|
||||
- 证据:run `34500290681`(295615abb)、`34448911025`(bbf953b8a) 报 `[vitest-pool]: Worker forks emitted error / Caused by: Error: Worker exited unexpectedly`(`Errors 1 error`),同一份日志里 `0 FAIL`、125 个文件全过;后者静默吞掉一个文件的 10 条结果。同一 commit `8ae527170` 的另一次 run 里 `ci-windows` 是**绿的** → 该步骤可证伪地非确定。
|
||||
- 归因(**推断,非确证**):真起进程的 spec 有 5 个(`agent-pty` / `pty-deps` / `pty-helpers` / `install-powershell` / `smoke`),node-pty 的 ConPTY 辅助进程与冷启 `powershell.exe` 都绑定控制台,在默认**无界 fork 池**下并行抢占;本地 macOS 无法复现该模式,因此不做无证据的猜测性改动(不加 `retry`,不改默认 pool)。
|
||||
- 修法(两次迭代,第一次被真实结果证否):先上 `test:windows`(`vitest run --maxWorkers=2`)——**真实 Windows 上仍复现**(run `34574847774`:126/127 文件全过、`0 FAIL`、`Unhandled Error: Worker exited unexpectedly`,丢结果的那个 worker 正是跑 `tests/native-surface.spec.ts` 的)。该次日志里 `native-surface.spec.ts` **零结果**,与历史两次崩溃同形(`34500290681` 125/126、`34448911025` 123/124——都是「N-1 个文件有结果 + teardown 期 worker 静默退出」)。
|
||||
- 迭代后的修法:`--maxWorkers=1`(整个套件跑在一个 fork 里,**零 worker 回收**)。理由:跨 4 次崩溃的一致形态是「worker 回收/退出竞态」,而不是某个测试失败;单 worker 从根上取消这个竞态,代价只是墙钟时间(本地 14s → 41s,Windows 步骤仍在 30 分钟 job 预算内)。**仍不加 retry**:retry 会把真实回归一起吞掉。
|
||||
|
||||
### C. `plugin-mount` 的 npm 安装失败(4 次:2× ETARGET + 2× OOM)
|
||||
|
||||
- 证据:run `34492160123` / `34491256626` 报 `npm error notarget No matching version found for @deepseek-ai/dsh-tool-bash-persistent@^0.1.5-rc.2` / `@deepseek-ai/dsh-sandbox-windows-acl@^0.1.5-rc.2`;run `34490912649` 先刷 **3062 条 ERESOLVE**、9 分钟后 `FATAL ERROR: Ineffective mark-compacts near heap limit` exit 134。同一机制在 2026-09-09 的 alpha 线也复现过一次(run `34363627184`,1808 条 ERESOLVE → OOM);同期成功的安装是 `added 521 packages in 19s`、**0 条 ERESOLVE**。
|
||||
- 根因:**钉版并没有真的钉住**。`@deepseek-ai/dsh@0.1.5-rc.1` 的传递依赖声明为浮动 `^0.1.5-rc.1`,会被上游**分阶段发布**中的 rc.2 满足(rc.2 的 14:43–14:57 逐个包上线),于是 npm 组出 rc.1/rc.2 混合 peer 图 → 解析器爆炸(OOM),或对尚未发布的兄弟包硬报 ETARGET(两个 ETARGET 分别在该包上线前 5.5 分钟与 25 秒)。
|
||||
- 修法(`.github/workflows/ci.yml`):钉一个**已完整发布**的 rc.2(修 ETARGET),并加 `NODE_OPTIONS=--max-old-space-size=4096`(修 OOM)。
|
||||
- **一次被真实结果否掉的中间方案(值得记)**:第一版修复还加了 `--legacy-peer-deps`(想法是「跳过 peer 协商即与发布窗口解耦」)。真实 Windows 结果表明它只是把安装**弄坏**而不是弄稳:它跳过的正是全局安装必须提供的 peer,而 `@deepseek-ai/dsh-app-boot` 在 boot 时 require 的 `@deepseek-ai/cordis-plugin-group` **是 peer 而不是 dependency**,于是 CLI 在冒烟开始前就以 `ERR_MODULE_NOT_FOUND` 死掉(run 34560785101 的 `plugin-mount`)。本地复现并验证:同一命令去掉该 flag 后安装树完整(`dsh/node_modules/@deepseek-ai/cordis-plugin-group` 存在),且 `DSH_CMD` 指向这个 classic 安装的 `test:mount` **7 passed**——宿主自带依赖,插件侧看不到差别。
|
||||
- 顺带修掉一处过时注释(原文断言「rc.1 同时是 `latest` 与 `next`」——现在 `latest=rc.1`、`next=rc.2`);这条注释正是浮动范围隐患长期不可见的来源。
|
||||
|
||||
## 改动清单(子系统级)
|
||||
|
||||
| 子系统 | 文件 | 变更 |
|
||||
|---|---|---|
|
||||
| 基线 | `package.json` | `version` → `0.19.1`;dsh-* **devDependencies** 全部钉 `0.1.5-rc.2`(含 `dsh-client-locale`,上游仍与 DSH 同 tuple 发版);**peerDependencies 一律保持 `^0.1.5-rc.1`** |
|
||||
| 基线 | `pnpm-lock.yaml` | 锁文件对齐 rc.2 |
|
||||
| 基线 | `dsh.plugin.json` | `version` → `0.19.1`;`engines.dsh` 保持 `>=0.1.5-rc.1`(与 peer 下限同源) |
|
||||
| 基线 | `src/client/service.ts` | `SIDEBAR_SERVICE_VERSION` → `0.19.1`(`tests/service.spec.ts` 断言它 === `package.json` 的 version) |
|
||||
| CI | `.github/workflows/ci.yml` | mount 车道钉 `@deepseek-ai/dsh@0.1.5-rc.2` + `NODE_OPTIONS=--max-old-space-size=4096`(C 类;**不用** `--legacy-peer-deps`,理由见 C 节);`ci-windows` 的 `Test` 改跑 `pnpm test:windows`(B 类);注释同步事实 |
|
||||
| CI | `package.json` | 新增 `scripts.test:windows`(`vitest run --maxWorkers=2`) |
|
||||
| CI | `vitest.config.ts` | 全局 `testTimeout: 15_000`(A 类;覆盖 git / powershell / pty 三类真起进程的 spec,逐文件打地鼠的做法在真实 Windows 上被证否) |
|
||||
| CI | `tests/agent-pty.spec.ts` | `AgentPtyRegistry` describe 显式 `{ timeout: 30_000 }`;`waitForTranscript` 默认预算 5000 → 15_000(A 类) |
|
||||
| CI | `tests/install-powershell.spec.ts` | `PowerShell installer entry points` describe 显式 `{ timeout: 30_000 }`(A 类) |
|
||||
| 文档 | `docs/plans/2026-09-10-dsh-0.1.5-rc.2-adaptation.md` | 本计划 |
|
||||
| 图标 | `src/client/file-icons.tsx` / `src/client/builtins/tab-icons.*` / `src/client/native/tab-adapter.tsx` | 文件图标改用宿主官方 `FileTypeIcon`;内置 tab 与原生芯片图标彩色——独立记录见 [2026-09-11-file-icons-and-tab-glyphs.md](./2026-09-11-file-icons-and-tab-glyphs.md) |
|
||||
| 文档 | `AGENTS.md` | §2 CI 基线戳与 mount 车道说明、§3 标题/基线段(rc.2 仍无契约变化,只推进钉版) |
|
||||
| 文档 | `README.md` / `README_EN.md` | 支持版本徽章与说明、新增 `### v0.19.1` 段 |
|
||||
| 文档 | `docs/external-plugin-guide.md` | 顶部基线行 → v0.19.1 / DSH 0.1.5-rc.1+(已在 rc.2 上验证) |
|
||||
| 文档 | `tests/native-surface.spec.ts`、`tests/builtins.spec.ts`、`tests/e2e/mount.e2e.ts`、`src/client/service.ts`、`src/client/native/index.ts` | 注释里的 `DSH 0.1.5-rc.1` → `0.1.5-rc.1+`(同一行为在 rc.2 未变) |
|
||||
|
||||
## 验证
|
||||
|
||||
- **未执行**的门禁与真机步骤都留空,由主代理实施后回填(本文件不预填未经执行的结论)。
|
||||
|
||||
### 回填(主代理实测)
|
||||
|
||||
- **门禁(rc.2 钉版,macOS arm64)**:`pnpm install` 成功且锁文件对齐 rc.2;`pnpm peers check` = `No peer dependency issues found`;`pnpm typecheck` / `pnpm lint` / `pnpm check:consumer-types` 全绿;`pnpm test` = **126 files / 1312 passed / 9 skipped**(与 rc.1 基线同数,无新增 skip)。
|
||||
- **真机挂载冒烟(rc.2)**:`DSH_CMD` 指向 `npx -y --package @deepseek-ai/dsh@0.1.5-rc.2 dsh`;见"落地"节的实测计数。
|
||||
- **真实 Windows 收敛**:`ci-windows` **连续两次全绿**(同一 commit `5451f5c`)——`pull_request` 自动触发的 run [34561189320](https://github.com/omdsh-dev/DSH-better-sidebar/actions/runs/34561189320) 与随后 `workflow_dispatch` 的 run [34561457067](https://github.com/omdsh-dev/DSH-better-sidebar/actions/runs/34561457067),两次都是 **126 files / 1318 passed / 3 skipped / 0 errors**(`Unhandled Errors` 与 `Worker exited unexpectedly` 出现次数均为 **0**;同环境的 2026-09-09/10 窗口里这两行出现过 2 次)。同一分支的 `plugin-mount` 两次都绿:`added 521 packages in 24s`(**0 条 ERESOLVE**,与历史 OOM run 的 3062 条对比)、`test:mount` **7 passed**、聚合双挂载通过。
|
||||
- **首次真实结果的证伪(已修正)**:本 PR 的第一版修复在同一 commit 上仍红两处,`.github/workflows/ci.yml` 的改动因此被真实证据改过一轮——见 C 节的「一次被真实结果否掉的中间方案」与 A 节第三个文件(`tests/git.spec.ts:85`,9607ms)。
|
||||
- **B 类的实测结论(已按"升级而非掩盖"走完两轮)**:`--maxWorkers=2` 下仍崩溃(run `34574847774`),故升级为 `--maxWorkers=1`(单 fork、零回收)。这**不等于证明了根因**(4 次崩溃的一致形态是「N-1 文件有结果 + teardown 期静默退出」,只能说明是 worker 回收竞态而非测试失败);收敛判据是单 worker 下连续两次 `ci-windows` 全绿——**已达成**:run [34575721734](https://github.com/omdsh-dev/DSH-better-sidebar/actions/runs/34575721734)(PR 触发)与 run [34576146970](https://github.com/omdsh-dev/DSH-better-sidebar/actions/runs/34576146970)(同 commit `4fe67e3` 的 `workflow_dispatch`)都是 **127 files / 0 errors**,`Unhandled Errors` 与 `Worker exited unexpectedly` 出现 0 次。
|
||||
- **本地 3080 真机**(DSH 0.1.5-rc.2 + 插件 0.19.1):部署与路由探针已完成(见"落地"节),GUI 人工验证由用户执行。
|
||||
|
||||
## 不做
|
||||
|
||||
- 不为 rc.2 写任何运行时/类型层兼容层(上游 delta 未触及本插件的面)。
|
||||
- 不把 peer 下限抬到 `^0.1.5-rc.2`(会无谓地把 rc.1 用户挡在上一版)。
|
||||
- 不为 B 类做无证据的猜测性改动:不加 vitest `retry`、不改默认 pool、不关掉真实 pty 测试。
|
||||
- 不改 DSH 源码(仓库硬约束 §1);不改 `release.yml`(0.19.1 无 prerelease 后缀 → 自动发 `latest`,符合 patch 语义)。
|
||||
|
||||
## 落地
|
||||
|
||||
- PR:[#626](https://github.com/omdsh-dev/DSH-better-sidebar/pull/626)(`feat/dsh-0.1.5-rc.2`)。
|
||||
- 本地真机挂载冒烟(rc.2,macOS arm64):`pnpm test:mount` **7 passed**(含 tab 体填充断言、指南 ≤4 条说明渲染断言、mermaid / README 预览与 sidechat 路由烟测);`pnpm test:mount:aggregate` 通过(聚合双挂载下插件自动退让,`/sidebar/api` 未知方法 404 与 `terminal.deps` 契约均符合)。
|
||||
- 本地 3080 实例部署(`~/.dsh/profiles/web` + `~/.dsh/dsh-web.sh`):profile 依赖从 `dsh-better-sidebar-0.19.0.tgz` 改指 0.19.1 tarball 并 `pnpm install`(`node_modules/dsh-better-sidebar/package.json` = **0.19.1**);`dsh-web.sh` 的 npx 钉版改 `@deepseek-ai/dsh@0.1.5-rc.2`,旧进程(PID 99521/99509/99507)定向 kill 后由 LaunchAgent `com.pm2.dshweb.resurrect` 自动拉起(新 PID 36025,npx 缓存 hash 因版本变更从 `1da1392061ab1944` 变为 `c40503fdf38a82ea`)。**已核**:新缓存里的 `@deepseek-ai/dsh` = **0.1.5-rc.2**,`/sidebar/api/shell.get` → `{"ok":true,"value":{"shell":"zsh","name":"zsh"}}`、`settings.get` 回默认设置、`terminal.deps` → `{"ok":true}`(三条插件路由在生产实例上工作);GUI 人工验证由用户执行,结论回填到本节的下一步。
|
||||
- PR:[#626](https://github.com/omdsh-dev/DSH-better-sidebar/pull/626)(`feat/dsh-0.1.5-rc.2`)。PR 三个 check(`ci` / `ci-windows` / `plugin-mount`)在 run 34561189320 与 run 34561457067 上均绿。
|
||||
- 3080 人工验证:通过(用户确认)。文件树/编辑器 tab 的官方彩色图标、指南六行彩色、原生右侧栏芯片图标、任务(层叠)与终端(缩小、无描边)两处返工后的观感均确认无问题。
|
||||
- **合并**:[#626](https://github.com/omdsh-dev/DSH-better-sidebar/pull/626) 合入 main(merge commit `817ee8f`)。main 的首轮 CI 变红——不是本 PR 的改动,而是 `tests/agent-pty.spec.ts` 自身的隐患(skip 类用例的命令里含等待 needle,交互式 shell 回显后 needle 提前进 transcript,`waitFor` 走快路径):修复见 [#628](https://github.com/omdsh-dev/DSH-better-sidebar/pull/628)(bare shell + `waitForShellReady`,merge commit `8753096`),main 随后三个 check 全绿。
|
||||
- **发版**:[v0.19.1](https://github.com/omdsh-dev/DSH-better-sidebar/releases/tag/v0.19.1)(GitHub Release → `release.yml` → npm Trusted Publishing)。`latest = 0.19.1`、`alpha = 0.19.0-alpha.1`(不变);带 provenance;发布元数据核对:peer 下限仍 `^0.1.5-rc.1`、`dependencies` 无 `cordis`、无 install 生命周期脚本。npm 传播延迟约 5 分钟(08:49:47 报 Published,08:56 前后 dist-tag 才可见 0.19.1)——这是既有现象,不是发布失败。
|
||||
- **图标 PR 收尾**:#531 已被 GitHub 标为 MERGED(其合并提交 `1d05e39` 在 main 上);#594 的 base 分支被删除后无法自动标记,已附说明(哪些保留、哪些被宿主官方图形替换)后关闭。
|
||||
@@ -1,92 +0,0 @@
|
||||
# 文件图标与彩色 Tab 图标(v0.19.1,#531 + #594 合并后的重做)
|
||||
|
||||
日期:2026-09-11 分支:`feat/dsh-0.1.5-rc.2`(并入 PR [#626](https://github.com/omdsh-dev/DSH-better-sidebar/pull/626))
|
||||
|
||||
## 背景与决策
|
||||
|
||||
[#611](https://github.com/omdsh-dev/DSH-better-sidebar/issues/611) 希望文件树按类型显示图标(「仿照官方做法」),[#429](https://github.com/omdsh-dev/DSH-better-sidebar/pull/429)(@fenter)为此做了 563 条彩色图标数据,落成两个叠着的 PR:
|
||||
|
||||
- [#531](https://github.com/omdsh-dev/DSH-better-sidebar/pull/531)(`feat/file-icons-api`,base=main):文件树/文件 tab 按扩展名分图标 + 对外 `registerFileIcon` API;
|
||||
- [#594](https://github.com/omdsh-dev/DSH-better-sidebar/pull/594)(`feat/file-icon-theme`,base=#531 分支):在其上叠「内置单色 / 彩色品牌图标」可选主题,彩色数据做成懒加载 chunk(255 kB)+ 设置页开关。
|
||||
|
||||
两者在 2026-09-08 都绿,但都基于 v0.18.1、落后 main 55 个 commit,且与 DSH 0.1.5-rc.2 的一个新事实冲突:**rc.2 的 `ui-primitives` 已经导出官方文件类型图形** `FileTypeIcon` / `CodeFileIcon`(48 类全彩代码/配置图形 + 12 类类目色板图形 + `classifyFileType` 分类器)。也就是 #611 想要的东西上游自己做了。
|
||||
|
||||
用户决策(2026-09-11):**两个 PR 都合、并进 v0.19.1 一个 PR;文件图标改用宿主官方 `FileTypeIcon`;内置 tab 图标彩色 + 原生右侧栏芯片也加图标;删掉主题开关,彩色即唯一行为。**
|
||||
|
||||
## 上游事实(实测,非推断)
|
||||
|
||||
| 事实 | 证据 |
|
||||
|---|---|
|
||||
| 宿主导出官方图形 | `@deepseek-ai/dsh-client-ui-primitives@0.1.5-rc.2` 的 `lib/types/index.d.ts` 导出 `FileTypeIcon`、`classifyFileType`、`fileExtension`、`CodeFileIcon`、`isCodeFileType` 与 `CodeFileType` 类型;`FileTypeIcon` 声明 `size` 默认 28 并透传给内部 `FileGlyph`/`CodeFileIcon`(14px 树行可用) |
|
||||
| 全彩图形保留自己的调色板 | `CodeFileIcon.d.ts` 的文档原话「its identifying palette intact」;`kind:'folder'` 落宿主的 `IconFolderClose16`(单色,随 `currentColor`) |
|
||||
| 分类器不返回文件夹 | `classifyFileType` 只走 `classifyCodeFileType ?? NAME_TYPES ?? EXTENSION_TYPES ?? 'other'`,`'folder'` 只能由调用方以 `kind` 显式指定 |
|
||||
| 官方图形在 rc.1 就有 | `npm pack @deepseek-ai/dsh-client-ui-primitives@0.1.5-rc.1` 的 tarball 内含 `lib/FileTypeIcon.module.css` 与 `lib/types/{FileTypeIcon,CodeFileIcon,code-file-types}.d.ts`(rc.2 只是把 SVG 数据拆进 `code-file-icon-artwork.ts`) |
|
||||
| 原生 tab 定义没有 icon 字段 | `dsh-client-ui-sidebar-right` 的 `SidebarRightTabDefinition` 只有 `id/kind/patterns/priority/canOpen/title/guide`;`icon` 只在 `SidebarRightGuideEntry` 上 |
|
||||
| 但 title 槽就是芯片内容 | 宿主 `contract/slots.d.ts`:`'sidebar.right.pane.tab.title'` —— 「A tab's title as its chip (and a floating panel's header) shows it」 |
|
||||
|
||||
## 合并(3 处冲突,均为注释/列表并集)
|
||||
|
||||
| 文件 | 冲突 | 解法 |
|
||||
|---|---|---|
|
||||
| `src/client/service.ts` | ① import 区(main 保留 `extOf`,PR 侧已改成 `baseName, extOf` + 自己的 `isNarrowWidth`)② `SIDEBAR_FEATURES` 注释与列表(main 写「v0.19.0 删除了 floatWindows」,PR 侧在旧清单上加 `'fileIcons'`) | ① 取 `baseName, extOf`:`isNarrowWidth` 属于 PR 自己的「新建会话按视口折叠」逻辑,main 已把那套 seeding 重写掉,合并后该 import 是死代码(实测 grep 确认后删除)。② 并集:保留「已移除 floatWindows」措辞 + 追加 `'fileIcons'` |
|
||||
| `src/client/Sidebar.tsx` | `tabIconOf` 上方注释(两侧各改一句) | 并集:保留 main 的「The tab icon from the tab-type registry.」并补 PR 侧「带 path 的编辑器 tab 显示文件图标」的说明 |
|
||||
| `docs/external-plugin-guide.md` | `features` 清单注释 | 并集:main 的新措辞(只增不删,唯一例外 v0.19.0 删除 floatWindows)+ `'fileIcons'` |
|
||||
|
||||
#594 的其余冲突(`tests/plugin-shape.spec.ts`、`tests/prefs.spec.ts`)**取本线一侧**:main 已删除这两个文件里的 store-seeding 用例并重写了 schema 断言(`SIDEBAR_PREFS_DEFAULTS` 解构),PR 侧重新加回的块描述的是 main 不再存在的行为——把旧块带回来只会测到死代码。
|
||||
|
||||
## 重做(把 #594 的彩色实现换成宿主官方图形)
|
||||
|
||||
### 删除
|
||||
|
||||
- `src/client/chunks/file-icons.tsx`(563 条数据)、`src/client/file-icon-theme.ts`(chunk loader)、`tests/file-icon-theme.spec.tsx`;
|
||||
- `fileIconTheme` pref 三处(`prefs-shared.ts` / `src/config.ts` / `src/client/prefs.ts`)、设置页「文件图标」select 行、20 份词典里的 6 个 `settingsFileIconTheme*` key(共 126 行);
|
||||
- chunk 注册三处(`src/bundle-route.ts` 的 `CHUNK_NAMES`、`src/client/chunk-loader.ts` 的 `ChunkName`、`tsdown.config.ts` 的 `CHUNKS` 与 react-icons alias)与对应测试期望(`bundle-route.spec.ts` / `manifest-consistency.spec.ts`);
|
||||
- `src/client/index.tsx` 里驱动主题 loader 的 effect。
|
||||
|
||||
### 新增/改写
|
||||
|
||||
| 位置 | 内容 |
|
||||
|---|---|
|
||||
| `src/client/file-icons.tsx` | `builtinFileIcon(path, size)` → `<FileTypeIcon path size />`;`builtinFolderIcon` → `<FileTypeIcon kind="folder" size />`(宿主只有一个文件夹图形,开/合两态不另画);`fallbackFileIcon(size)` → `<FileTypeIcon kind="other" size />` |
|
||||
| `src/client/service.ts` 的 `fileIcon` 链 | 具体注册 → **catch-all 全局默认 → 宿主图形**(原先 built-in map 排在 catch-all 之前) |
|
||||
| `src/client/builtins/tab-icons.tsx` + `.module.css` | 六个内置类型 + diff 视图的彩色 glyph;颜色全部 `var(--dsw-alias-*)`(files=brand、changes=success、tasks=warn、sidechat/browser=business、terminal=label-primary,各带 fallback 链) |
|
||||
| `src/client/native/tab-adapter.tsx` 的 `NativeTabTitle` | 渲染 `[glyph][title]`:编辑器 tab 带 path 时用 `service.fileIcon(path, 14)`,其余用 `descriptor.icon(14)`;glyph `aria-hidden`;`NativeTitleInjected` 增加 `service` 与 `descriptorId`,`registerSlots` 的 title 注入同步 |
|
||||
| `src/client/sidebar.module.css` | 新增 `.chipIcon`(芯片 glyph 的排版,颜色继承) |
|
||||
| `tests/theme.spec.ts` | 豁免段整体删除,改为三条守护:图标模块零颜色字面量、`tab-icons.module.css` 里每条 `color` 都解析到 `var(--dsw-`、`src/client/chunks/*` 不含 6 位颜色字面量(图标数据不得再做回 chunk) |
|
||||
| `tests/file-icons.spec.tsx` | 内置断言从 codicon 身份改为宿主 `FileTypeIcon`;新增「path/size 透传」「文件夹用 `kind:'folder'`」;catch-all 用例改写为「注册了 catch-all 就接管全部未具体命中的行」 |
|
||||
| `tests/native-surface.spec.ts` | 新增 3 例芯片 glyph:descriptor glyph 在标题前、编辑器 tab 用文件图标而非类型图标、类型注销后只剩标题 |
|
||||
|
||||
## 语义后果(必须写进文档)
|
||||
|
||||
**宿主分类器覆盖任意路径**,因此「插件自己已经能画这个扩展名」不再是拦住 catch-all 的理由:注册 `exts: []` 的插件现在会接管所有未具体命中的文件行(原先会被内置 map 拦下)。旧版彩色主题那种「整表注册、靠内置 map 兜底」的形态不再成立——只想补几个类型的插件用 `exts`/`names`。已写进 [指南 §7](../external-plugin-guide.md) 与 [AGENTS.md](../../AGENTS.md) §5。
|
||||
|
||||
## 验证
|
||||
|
||||
**已执行(macOS arm64,DSH 0.1.5-rc.2 钉版)**
|
||||
|
||||
- `pnpm typecheck` / `pnpm lint` / `pnpm check:consumer-types` 全绿;
|
||||
- `pnpm test` = **127 files / 1343 passed / 9 skipped**(v0.19.1 基线 126/1312 + 合并与新增的 31 例,失败数 0;返工后 +1 例 glyph 身份断言);
|
||||
- `pnpm build`:`lib/client.js` **0.85 → 0.86 MB**(+10 kB),**没有** `lib/client-file-icons.js`;核心 bundle 内不含图标数据(只有 6 个被引用的 glyph 组件);
|
||||
- 真机挂载冒烟(`DSH_CMD` 指向 `@deepseek-ai/dsh@0.1.5-rc.2`):`pnpm test:mount` **7 passed**(含 guide 深扫、tab 体填充断言、mermaid/README 预览、sidechat 路由),`pnpm test:mount:aggregate` 通过。
|
||||
|
||||
### 真机反馈的两处返工(用户人工验证后)
|
||||
|
||||
| 反馈 | 结论与改动 |
|
||||
|---|---|
|
||||
| 「任务管理」图标不对——该页是 subagent + 后台 jobs,不是 todo | `VscTasklist`(清单)→ **`VscLayers`**(多层堆叠 = 后台在跑的工作)。`tests/builtins.spec.ts` 用 glyph 身份把这条读法钉住 |
|
||||
| 终端图标「非常怪」 | 根因是**观感权重**:`VscTerminal` 是整套里最宽的实心矩形,在 14px 下比邻座重。**只缩小一档**(`0.85 × size`,下限 10px)——形状与绘制方式保持图标集原样 |
|
||||
| 追加反馈:上一版给全部 tab glyph 加的 `currentColor` 发丝描边(`strokeWidth: 1` + `non-scaling-stroke`)「太粗」 | **整体回退**:不描边,glyph 只用包裹类的令牌着色。“描边”是我对「怪」的过度解读,反馈的本意只是终端尺寸。`tests/builtins.spec.ts` 现在同时钉住「终端尺寸更小」与「glyph 不带 style 覆盖」,防止描边再溜回来 |
|
||||
|
||||
### 真实 CI(已收敛)
|
||||
|
||||
`ci` / `plugin-mount` 每轮都绿;`ci-windows` 的 worker 回收竞态在 `--maxWorkers=1`(单 fork、零回收)下**连续三次全绿**:run [34575721734](https://github.com/omdsh-dev/DSH-better-sidebar/actions/runs/34575721734)、[34576146970](https://github.com/omdsh-dev/DSH-better-sidebar/actions/runs/34576146970)、[34576493202](https://github.com/omdsh-dev/DSH-better-sidebar/actions/runs/34576493202),均 **127 files / 0 errors**。两轮迭代(`--maxWorkers=2` 被真实结果证否)的取证见 [rc.2 计划 §B](./2026-09-10-dsh-0.1.5-rc.2-adaptation.md)。
|
||||
|
||||
**待验证**
|
||||
|
||||
- 3080 人工验证(返工后第二轮):树行与编辑器 tab 的彩色图标、指南六行彩色、原生右侧栏芯片图标、14px 下官方 48 类图形的可读性、以及这次返工后的任务/终端两个图标(用户此前明确选择用官方图形;若细节折损不可接受,回退方案是「树行用官方图形、编辑器 tab 保留插件 glyph 着色」,改动只在 `file-icons.tsx` 一处)。
|
||||
|
||||
## 不做
|
||||
|
||||
- 不改 DSH 源码;不把 #429 的 563 条数据搬回仓库(要走按需加载也应按懒加载 chunk 走,而不是常驻数据);
|
||||
- 不为彩色图标新增皮肤契约豁免(插件侧零硬编码颜色);
|
||||
- 不恢复 `floatWindows`;不动已合入的 rc.2 基线结论与 CI 修复。
|
||||
@@ -1,216 +0,0 @@
|
||||
# 任务管理页重构:工作流图 + Agent Teams + 后台任务抽屉(v0.20 设计)
|
||||
|
||||
日期:2026-09-14 分支:`feat/tasks-graph-workflow-teams`
|
||||
|
||||
## 背景与目标
|
||||
|
||||
DSH 0.1.5-rc.2 引入两个新的可观测面:**workflow**(`dsh-tool-workflow` 的 run/agent 生命周期事件)与 **Agent Teams**(实验层,`ctx.agentTeams` 的共享任务板)。任务管理页(原「子代理拓扑」)系统性地接纳两者,并按用户确认的「简洁后现代主义」方向重构为**工作流图**:分层节点 + 贝塞尔连线、拖拽平移、滚轮缩放、右下角控制条、已完成节点折叠聚合、点击浮窗取代页内 dock。
|
||||
|
||||
用户确认的关键交互决策(问答摘要):
|
||||
|
||||
1. 数据范围 = 当前会话树(主代理 + 全部后代)。
|
||||
2. 节点点击 = 跳转转录;ⓘ = 详情浮窗。
|
||||
3. 后台任务输出 = 锚定浮窗(替换页内底部 dock)。
|
||||
4. 自动折叠**只**作用于后台任务抽屉,阈值 8 个代理;图节点的折叠是「已完成」语义的折叠聚合,点击展开、控制条可再折叠。
|
||||
5. 图/树切换按钮在**两种模式下都可用**(早期设计稿把 toggle 藏在画布容器内、树模式下无法切回——真实实现把控制条移出滚动面)。
|
||||
6. 默认视图由设置 select 决定(默认工作流图),页内切换为临时态。
|
||||
7. 任务板 v1 只做基础操作(完成/重开/删除/改派/新建/编辑,CAS)。
|
||||
|
||||
## 数据源事实(侦察结论)
|
||||
|
||||
### Workflow
|
||||
|
||||
- **没有**服务注册表 / 持久化 / HTTP 接口;只有调用方会话日志里的 4 个会话事件:`tool-workflow/run-start | agent-start | agent-end | run-end`。
|
||||
- 只有**顶层 run** 落事件(`exec.parent === undefined`),嵌套 run 不可见——与官方 `dsh-client-ui-workflow-run` 面板折叠的恰好是同 4 类事件,因此插件折叠结果与官方面板一致。
|
||||
- run 的 member agent 是普通子会话(catalog 里有行),`childId` 即子会话 id——图模式把它们**重挂**到 run 节点下;catalog 里没有的 member 用 run 自带数据合成占位节点。
|
||||
|
||||
### Agent Teams(实验层)
|
||||
|
||||
- 只在 `~/.dsh-web` 类 profile 的实验 bundle 里存在;`ctx.get('agentTeams')` 缺席时返回 `undefined`(不抛)——**结构性降级**的支点。
|
||||
- TeamId ≡ lead SessionId;`tryMembership(agent)` 命中后 `remoteView(agent) → {members, tasks}`;`remoteCreateTask` / `remoteUpdateTask`(CAS,`expectedRevision`,冲突走 `team-task-conflict` union 返回而非抛错)。
|
||||
- 成员上限 8、任务上限 256;teammate = lead 的可续接子会话(`member.id` = 子会话 id,与拓扑树天然关联)。
|
||||
- `~/.dsh/task-board/ledger-v2.json` 与 Agent Teams **无关**(名字误导,已核证)。
|
||||
- 官方团队面板是 read-mostly + 手动刷新——客户端 5s 轮询(仅页面可见时)不是过度设计。
|
||||
|
||||
### Jobs
|
||||
|
||||
沿用既有:`jobsBySession` push 镜像 + `jobs.output` 事件回放(不碰模型游标)+ 两击终止。
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
host 半(src/)
|
||||
workflow-runs.ts 纯折叠 foldWorkflowRuns(events, originSessionId) → WorkflowRunView[]
|
||||
workflow-routes.ts workflows.list:树枚举 + 存储日志与 live 镜像按 seq 去重合并
|
||||
team-routes.ts teams.view / taskCreate / taskUpdate:结构镜像 Remote 词汇,三分支降级
|
||||
client 半(src/client/)
|
||||
tasks-model.ts 统一视图模型(图/树共用):catalog 走树、run 重挂、team 富化、fold 聚合
|
||||
tasks-graph-layout.ts 免依赖 tidy-tree 分层布局(<100 节点无需虚拟化;bundle 纯度禁图库)
|
||||
TasksGraph.tsx 画布:pan / wheel-zoom-to-cursor / 相位虚线框 / 控制条(图树切换+折叠+缩放)
|
||||
TasksTree.tsx 树模式:缩进行 + 键盘导航(官方 catalog 配方)
|
||||
TasksPopovers.tsx 节点详情 / run 详情 / 团队任务板(CAS 操作)
|
||||
JobsDrawer.tsx 底部抽屉(≥8 代理自动折叠,手动优先)+ 输出浮窗(回放,尾钉)
|
||||
AnchoredPopover.tsx 锚定浮窗:geometry + 关闭契约(沿用 selection-popup 的 #425 修复)
|
||||
tool-icons.tsx 工具字形映射(宿主 primitives 图标,currentColor)
|
||||
```
|
||||
|
||||
### 降级矩阵(可证伪)
|
||||
|
||||
| 环境 | workflows.list | teams.view | 页面表现 |
|
||||
|---|---|---|---|
|
||||
| 无 workflow/无 team(3384 桌面 profile r) | `{runs: []}`(200,空而非错) | `{available:false}` | 图/树照常,无 run 节点、无团队 chip |
|
||||
| 有 team 无 workflow(3080) | `{runs: []}` | `{available:true, team:{...}}` | roster 富化节点 + chip + 任务板 |
|
||||
| 有 workflow | 折叠结果 | — | run 节点 + 相位框 + member 重挂 |
|
||||
| subagents 服务缺席 | 仅折叠 root | 不影响 | 旧快照退化行为 |
|
||||
|
||||
## 关键取舍
|
||||
|
||||
- **零实验包 import**:`context-types.ts` 结构镜像 `TeamService` 的 Remote 词汇;`ctx.get('agentTeams')` 探活在路由内。实验层缺席时 mutation 抛 503 `team-error`(与 `subagents-unavailable` 同形)。
|
||||
- **workflow 无事件 = 空列表不是错误**:旧会话/无 workflow 会话静默为空(jobs 镜像同款语义)。
|
||||
- **fold 规则**:per-parent 的「已完成/出错的**叶子** agent」折叠为一个聚合节点;当前会话、teammate、有子节点的、run 节点**永不折叠**(折叠父节点会藏住活跃分支)。
|
||||
- **树/图同一模型**:fold 状态、团队富化、run 重挂两种模式共享——不会视觉漂移。
|
||||
- **控制条移出滚动面**:图模式的缩放按钮与图/树切换都在视图容器级(绝对定位右下),树模式下缩放按钮不渲染但切换在(早期实现把它放进滚动面、树模式下切不回去——有测试守护)。
|
||||
- **页内视图切换是临时态**,默认视图走 `tasksViewMode` pref(schemastery `z.union([z.const('graph'), z.const('tree')])`——schemastery 无 `z.enum`)。
|
||||
- **AnchoredPopover 不复用 primitives 的 HoverCard**:浮窗需要持久交互(任务板表单、输出滚动),HoverCard 是悬停语义;关闭契约(外部 mousedown / Escape / anchor 离屏的 IntersectionObserver)沿用 selection-popup 已验证的模式。
|
||||
- **主题硬阴影用 `color-mix(in srgb, var(--dsw-alias-border-l1) 55%, transparent)`**:后现代硬投影但颜色全部出自令牌(theme.spec 只守 `color:`,皮肤契约的精神照旧满足)。
|
||||
|
||||
## 实施偏差记录
|
||||
|
||||
1. **诊断行(diagnostic catalog entries)从模型中省略**:旧树逐 parent 渲染 corrupt/unavailable 行;新模型跳过它们,目录加载失败改由页头横幅「N 个分支加载失败 + 重试(逐个 refresh)」统一承载。理由:诊断行罕见(通常是 side-chat 遗产),逐 parent 内联会破坏图的 tidy 布局;横幅保留可达性与重试。
|
||||
2. **catalog `state:'error'` 不再逐层内联**,同上进横幅。
|
||||
3. **loadin rows**:图模式无「加载中占位卡」;树模式保留 `summaryBackedLoading` 的 loading 行。图模式靠 5s 轮询自然收敛。
|
||||
4. **子代理 keyboard 导航保留在树模式**;图模式节点 `tabIndex=-1`(平移/缩放下 tab 序无意义,树模式是可达性面)。
|
||||
5. **本地 `pnpm test:mount` 需要绕过桌面 shim**:桌面版 `~/.local/bin/dsh` shim 回退到桌面捆绑 CLI 且**不尊重 `DSH_HOME`**(`dsh plugin add` 会写到真实 `~/.dsh` profile——本次亲历,把真实 profile web 的 better-sidebar 指到了本分支 tarball)。正确姿势与 CI 一致:`DSH_CMD="npx -y --package @deepseek-ai/dsh@0.1.5-rc.2 dsh" pnpm test:mount`。
|
||||
6. **测试断言从 `container.textContent` 迁到 `document.body`**:输出浮窗 portal 到 body(#425 契约),jobs-view 套件相应更新。
|
||||
7. **fold 聚合节点的 aria-label 带计数文本**:与控制条的 fold 切换按钮消歧(两者同文案会导致 a11y 选择器歧义)。
|
||||
|
||||
## 复审返工(2026-09-14 晚,真机截图反馈)
|
||||
|
||||
用户在 3384(`DSH_HOME=~/.dsh`,profile web)看到首版实现后给出四条反馈,逐条定位并修复:
|
||||
|
||||
| 反馈 | 根因 | 修法 |
|
||||
|---|---|---|
|
||||
| 「按钮和文本的颜色不对,不够后现代」 | 新 CSS **17 处裸用 `var(--dsw-alias-accent)`——该令牌在 DSH 主题里不存在**(旧文件的用法都带 fallback 才没暴露),声明被浏览器整条丢弃;容器边框用的 `border-l1` 只有 4% 黑,几乎不可见 | accent → `--dsw-alias-state-business-primary`;容器边 → `--dsw-alias-border-l4`(16%);整体回到设计稿语言:ink 边框、3px 硬投影、mono 微型字 + 字距大写标签、点阵画布、横向缩放条 |
|
||||
| 「点 ⓘ 没反应 / 图里点不动(树能点)」 | 画布 `pointerdown` 里对容器 `setPointerCapture`,捕获把派生 click 重定向到容器,节点永远收不到点击(jsdom 不模拟捕获语义,所以单测全绿——**只有真实浏览器能暴露**) | 去掉 capture,pan 仅从背景起手(`closest('[data-graph-node]'/'[data-graph-controls]')` 直接返回),监听挂 window |
|
||||
| 「图很小 / 不在中间 / 任务板没显示」 | fit 在容器为 0 尺寸时静默放弃且不再重试;只居中横轴、上限 1.0;窄面板里 5 个兄弟节点挤成 770px 宽 → 缩到 35% | ResizeObserver + 首次非零尺寸补 fit、双轴居中、`FIT_MIN_SCALE=0.78` 可读性下限;**按容器宽度求解排布**(`layoutTasksGraphForWidth`:先取仍满足可读性预算的最宽排布,只有窄到 ≤1 列才允许 20% 横向溢出);任务板改为**常驻可见条**(不再藏在 chip 后) |
|
||||
| 「卡片信息过多过杂,都被省略看不见」 | 卡片同时塞 displayTitle + 模式 + 状态 + 模型 + live 文本,132px 宽(窄面板的目标宽度)下全部省略号 | 卡片只留三层:标题(1 行)/ mono 元信息(模式或模型 · 状态)/ live 行(仅运行中);其余(会话标题、team 角色、模型全名、最新文本、跳转)进 ⓘ 浮窗 |
|
||||
| 「非常窄,非常挤」 | 设计按宽画布做,未以原生右侧栏窄宽为目标 | 度量全部改按窄栏基准(卡片 132×46、行距 112);行高/间距/字号显式声明(宿主 body 行高曾把行撑高);`user-select: none` 防拖拽选中文本 |
|
||||
|
||||
回归面:`tests/tasks-page.spec.tsx` 增 3 例(背景 pointerdown 后节点仍可激活、节点上的手势绝不启动 pan、团队任务板无需点击即常驻可见);`tests/tasks-graph-layout.spec.ts` 重写 9 例(band 换行、换行 band 不得压到兄弟子树行、宽度求解两段式、运行时预留 live 行)。
|
||||
|
||||
### 本地可视化自检 harness(未入库,`tmp-visual/`,git-excluded)
|
||||
|
||||
真实浏览器里的组件级回归无法靠 jsdom 覆盖(点击捕获、fit/居中、主题令牌解析都是浏览器行为)。harness 用 esbuild 把**真实组件** + 与 `buildTasksModel` 同形状的 fixture(走同一个模型,因此折叠/重挂/富化都真实生效)打包进一个页面,注入从 `dsh-client-ui-theme` 抽出的令牌 CSS,`@deepseek-ai/dsh-client-ui-primitives` 别名到轻量桩(避免把 katex/shiki 资源拖进截图包),再由 Playwright 在 360px / 720px 两档宽度截图并断言:节点点击回调、ⓘ 回调、背景拖拽平移量。本次返工的四条反馈里有三条正是它先复现、修完再确认的。
|
||||
|
||||
## 第二轮返工(2026-09-14 深夜,13 条反馈)
|
||||
|
||||
用户第二轮反馈三条,逐条落地:
|
||||
|
||||
| 反馈 | 做法 |
|
||||
|---|---|
|
||||
| 任务板「太窄太小」,且任务应显示到**对应节点**上 | 任务按 `ownerName → 成员名 → 成员会话 id` 映射进模型(`TasksNodeTask`),节点第 4 行渲染「☑ 主题 · 状态 +N」;任务板改为:成员 Pill 行**兼当负责人筛选**、任务行 = 状态点 + 主题 + 负责人 + 状态 Tag + **一个**溢出菜单;新建/编辑改为 `Modal` + `Input`(真正的宽表单),负责人用 Pill 选择 |
|
||||
| 所有组件用自建、不用原生 HTML | 任务板全量换成宿主 primitives:`Menu`(含子菜单的改派)/`Modal`/`Input`/`Button`/`Pill`/`Tag`/`Switch`/`StateDot`;后台任务行的 kind/状态用 `Tag`、终止用 `Button`、输出浮窗的复制/跟随/终止同理。页面仅剩结构性 div(卡片、树行)不是表单控件 |
|
||||
| Agent 用图标而非自绘 SVG;优化工具展示 | `◉/◇/▶/✓/⇪/⇩/i/☰/⌗/⌂` 等字形全部删除,改用宿主图标:`IconAgentPresetOutline16`(代理)、`IconUserOutline16`(teammate)、`IconBranchOutline16`(workflow run)、`IconChecklistOutline14`(折叠聚合 / 任务行 / 折叠按钮)、`IconTreeCorner8x10`(树视图)、`IconFullscreenOutline16`(适应)、`IconEllipsisOutline16`(节点详情)、`IconCopyOutline16`/`IconStopFill16`(作业)。live 行改为工具自身图标 + 工具名 + 参数(去掉手绘方框) |
|
||||
| 后台任务弹窗要能拖动 + 体验 | `AnchoredPopover` 增加 `draggable`(拖动整体、按钮/输入/`pre` 不拦截、双击复位、视口内钳制)与 `width` 参数;输出浮窗宽 380 且可拖,新增「复制输出」「跟随最新」开关(关闭后不再尾钉)、kind/状态 Tag、拖动提示;抽屉行高亮当前打开的作业 |
|
||||
|
||||
i18n:本轮新增 19 条文案,zh/en/ja + 18 份第三语言词典同步(`tests/locales.spec.ts` 16/16)。
|
||||
|
||||
## 第三轮返工(2026-09-14 深夜,交互统一)
|
||||
|
||||
用户第三轮反馈两点,都指向「减少控件、统一入口」:
|
||||
|
||||
| 反馈 | 做法 |
|
||||
|---|---|
|
||||
| 图卡片去掉详情按钮,「统一改为弹出详细」 | 删除每张卡片右上角的 `…` 按钮(树行同理):**卡片/行本身就是详情入口**,点击即弹详情窗口;跳转转录改为详情窗口内的主按钮。折叠聚合节点仍保留「点击=展开/再折叠」的语义 |
|
||||
| 任务所有入口统一为「点击弹出非全屏可拖动编辑(兼查看)窗口」,含 编辑owner + 编辑 + 重开 + 删除;默认多行 markdown 预览,点编辑才进入多行编辑 | 新增单一组件 `TaskWindow.tsx`:`TaskWindow`(内容)+ `TaskPopover`(可拖动外壳,宽 430)+ `MultilineField`(自建多行输入)+ `OwnerPicker`(Pill 即点即改派)+ `TaskCreateButton`。三处入口全部复用它——任务板行、节点任务行、节点详情里的任务清单;「新建」也走同一个窗口(create 模式直接进入编辑态)。任务板由此**删掉了溢出菜单与 Modal**,只留筛选 Pills、任务行与新建按钮 |
|
||||
|
||||
新文案 3 条(任务详情 / 暂无描述 / 被阻塞),zh/en/ja + 18 份第三语言同步。
|
||||
|
||||
组件复用清单(本轮):
|
||||
|
||||
| 组件 | 复用点 |
|
||||
|---|---|
|
||||
| `TaskPopover` | 任务板行、节点任务行、节点详情任务清单、新建按钮(4 处) |
|
||||
| `MultilineField` | 任务描述编辑(宿主 primitives 无多行输入,故自建) |
|
||||
| `AgentGlyph` / `WorkflowGlyph` / `FoldGlyph` / `TaskLine` | 图与树两种模式共用 |
|
||||
|
||||
> **回退后修订**:上表与上一行提到的 `TaskCreateButton` 已删除——它唯一的调用方就是任务板,包一层没有收益;任务板的「新建」现在直接是宿主 outline `Button` + `IconPlusOutline16`(同一文案键 `teamTaskCreate`)。`TaskWindow` / `TaskPopover` / `MultilineField` / `OwnerPicker` 不变。
|
||||
|
||||
## 验证结果(2026-09-14)
|
||||
|
||||
PR:[#680](https://github.com/omdsh-dev/DSH-better-sidebar/pull/680)(分支 `feat/tasks-graph-workflow-teams`)。
|
||||
|
||||
- `pnpm typecheck` ✅;`pnpm vitest run` **133 文件 / 1400 用例通过**(新增:host 路由 29、模型 7、布局 9、页面交互 9);`pnpm build` ✅(皮肤契约 / 市场清单 / chunk 纯度守卫全绿)。
|
||||
- `pnpm test:mount`(`DSH_CMD="npx -y --package @deepseek-ai/dsh@0.1.5-rc.2 dsh"`,本地必须绕开桌面 shim,见实施偏差 5)**7/7 通过**:真实挂载 + 无头 tab 全扫。其 PERF_JSON 里可读到 scratch profile(**无实验层**,等价 3384 的 profile)上 `/sidebar/api/workflows.list → {runs:[]}`、`/sidebar/api/teams.view → {available:false}` —— 即降级矩阵左列的实测证据。
|
||||
- 3080(`~/.dsh-web`,含实验层)实景协议验证:`teams.view → {available:true, team:null}`、`workflows.list → {runs:[]}`、`teams.taskCreate → 404 team-error "the tree root leads no team"`(三分支里「层在、无团队」这条)。
|
||||
- 本地可视化自检 harness(真实组件 + 真实主题令牌 + Playwright,360px/720px 两档):节点点击、ⓘ 浮窗、背景平移(Δ+60/+40)、折叠聚合、任务板常驻全部通过;本轮四条真机反馈中的三条由它先复现。
|
||||
- 复现步骤与陷阱(令牌提取、primitives 桩、shim 与 tarball 重装坑)已沉淀到 `.workspace-docs/notes/dsh/09-14-dsh-better-sidebar任务管理页重构(工作流图与Teams任务板).md`。
|
||||
|
||||
未在本机自动化验证、留给用户实机确认的一项:3384 桌面应用**重启后**的 UI 级复看(新 bundle 已装进 `~/.dsh/profiles/web`,与仓库构建产物 SHA-256 一致)。
|
||||
|
||||
---
|
||||
|
||||
## 回退记录与结论(2026-09-21:任务页回到插件自有体系)
|
||||
|
||||
> 本节是 2026-09-17 – 09-21 那次视觉基座尝试的**唯一**记录:本文档曾用 250 余行详述它的依赖版本、Tailwind 接入方式、令牌映射表、vendored 组件清单、体积与真机排障,那套基座已整体回退,正文随之删除,只留「为什么回退」与「回退后的结论」。**本节出现的 shadcn / Tailwind / radix 字样都是对已废弃方案的引用**,不代表本仓库或消费插件的建议做法。
|
||||
|
||||
任务页 2026-09-17 换成 shadcn/ui + Tailwind v4 视觉基座(组件源码 vendoring 进 `src/client/ui/`,样式由工具类产出,颜色经 shadcn 语义令牌桥接到 `--dsw-*`),09-20 又按「对齐上游 stock」回调一轮;09-21 判定该基座与本插件的体系不可调和,整体回退。
|
||||
|
||||
**改动面**:8 个页面文件(`TasksGraph` / `TasksTree` / `TaskWindow` / `TeamBoard` / `JobsDrawer` / `TasksPopovers` / `SubagentView` / `tasks-shared`)以 `12b4716` 的原生实现(CSS Modules + 宿主 `@deepseek-ai/dsh-client-ui-primitives`)为基线重建,`src/client/ui/**`(23 个文件)、`components.json`、`scripts/ui-css.mjs` 与其 `ui:css` 脚本删除;`radix-ui` / `class-variance-authority` / `tailwind-merge` / `tailwindcss` / `@tailwindcss/postcss` / `postcss` 六个依赖下线;`tasks` 懒加载 chunk 与 `CHUNK_NAMES` 的 `tasks` 项一并取消(回到 `terminal` / `editor` / `mermaid` / `locale` 四项)。**回退只换视觉基座:09-17–09-21 期间的行为修复逐条重新落地**(见下「保留的行为修复」)。
|
||||
|
||||
### 回退根因
|
||||
|
||||
1. **默认尺度与插件体系冲突(最直接的一条)**:stock shadcn 的正文是 `text-sm`(14px)、meta `text-xs`(12px)、控件按 `h-9`(36px) 起;本插件的语言是正文 12px / meta 11px / 微标 10px、控件 28px(`sidebar.module.css` 的图标按钮即为 28px 方)。要贴合就得把 stock 的每个尺寸逐处改回去——那等于不用 shadcn,只是多背一层工具类。09-20 的「stock 回调」正是这条冲突的产物:字号一放开,`GRAPH_NODE_W` 132px 的图节点标题只剩约 7 个汉字(“图形重布重写”被截成“图…”),两轮下来只能把宽度 132→150→176 并改两行标题救场。
|
||||
2. **层级语义冲突**:shadcn 给静态组件默认带投影(card `shadow-sm`,input / textarea / toggle / button-outline `shadow-xs`);本插件的语言是**静态面板无阴影**(层级 = 1px `--dsw-alias-border-l2` + hairline `--dsw-alias-border-l1` + 表面阶梯 + 墨色三档),只有浮层用 `--dsw-shadow-lv2/3`。第一轮把静态阴影全删了,stock 回调又把它们恢复成 stock——两次改的都是同一份 vendored 源码,每次跟上游 `--diff` 都要重新施加。
|
||||
3. **彩色语义是第二套词汇表**:shadcn 的 `primary` / `secondary` / `muted` / `accent` / `destructive` 与 DSH 的 `--dsw-alias-state-*` / `--dsw-alias-interactive-bg-hover` 并非一一对应,任务状态(进行中 / 阻塞 / 完成 / 错误)还要再自造 `--success` / `--warning` 两个非 stock 令牌,整张映射表 22 条必须逐条维护:`--border` 先桥到 `--dsw-alias-border-l4`、09-20 又改成 `l2`,只因为 shadcn 的 1px 边框语义变了——而 `l2`(1px 边框)/ `l1`(hairline)的分工本来就是本插件自己的契约,不必经过第三层命名。
|
||||
4. **不引 preflight 就得自己补一套**:插件注入的是全局 `<style data-plugin>`(无 shadow DOM),引 Tailwind 完整入口会重置整张宿主页面,所以只能引 `theme` + `utilities`;表单控件因此保留 UA 外观,真机反馈「任务页看起来完全没有 CSS,边框非常模糊」的根因即在此。补救是在 `theme.css` 写限定 `.dsw-tasks` 作用域的 preflight 子集 + 显式 `@layer theme, base, components, utilities;` 层序声明 + 页面根类。**这整套补偿只为承载 Tailwind 而存在**,原生 CSS Modules 不需要。
|
||||
5. **架构代价**:radix 浮层栈 + `tailwind-merge` 把核心包撑到 1452623 bytes(+457.6 KiB,超 +250 KiB 预算),处置是把任务页整体下沉为懒加载 chunk(`lib/client-tasks.js` 706.7 KiB,`CHUNK_NAMES` 加 `tasks`、`package.json#files` 补 glob)。原生实现下这些负载不存在,`tasks` chunk 随回退下线。
|
||||
6. **vendored 源码的持续负担**:每个 vendored 组件的本地适配(`cn` 改 `./utils`、图标换宿主 `IconXxx`、删 `dark:` 覆写、`text-white` → 令牌、React 18 的 `forwardRef`)都要跟着上游逐次重放;`popover.tsx` / `skeleton.tsx` 自始至终没有消费者。
|
||||
|
||||
### 结论(写给消费插件)
|
||||
|
||||
**嵌入式插件沿用宿主体系**:样式用 CSS Modules + `--dsw-*` 令牌(参考 `src/client/sidebar.module.css` / `src/client/changes/changes.module.css` / `src/client/SideChatView.module.css`),控件与图标用宿主 `@deepseek-ai/dsh-client-ui-primitives` 的 `Button` / `Menu` / `Modal` / `Pill` / `Tag` / `Switch` / `Input` / `Tooltip` / `StateDot` / `MarkdownText` / `DisclosureRow` 与 `IconXxx`;不引 Tailwind / radix / CVA / lucide,不自绘 svg(连线几何除外)。插件样式表是**全局作用域**的,任何自带 reset 或工具类的方案都要额外处理与宿主无层级样式表的优先级关系——这是本插件选择自有体系的直接原因。
|
||||
|
||||
### 保留的行为修复(与视觉基座无关,已在原生实现上重新落地)
|
||||
|
||||
| 修复 | 位置 | 守护 |
|
||||
|---|---|---|
|
||||
| 节点 id 去重:workflow member 的 `childId` 不在 run 发起者 catalog 里、但存在于树中别处时不再合成同 id 的第二张卡片;返回前兜底去重 | `src/client/tasks-model.ts` | `tests/tasks-model.spec.ts` |
|
||||
| 任务窗口 CAS:编辑 / 改派携带 `expectedRevision`,冲突不吞 | `src/client/TaskWindow.tsx` | `tests/tasks-page.spec.tsx` |
|
||||
| 后台任务终止两击确认(首击 arm、二击才发 `jobs.kill`) | `src/client/JobsDrawer.tsx` | `tests/subagent-jobs-view.spec.tsx` |
|
||||
| 后台任务抽屉 ≥8 代理自动折叠,条形按钮 `aria-expanded` 可展开 | `src/client/JobsDrawer.tsx` | `tests/tasks-page.spec.tsx` |
|
||||
| 输出浮窗可拖动;稳定 `data-*` 钩子(`data-graph-node` / `data-graph-controls`)不变 | `AnchoredPopover.tsx` / `TasksGraph.tsx` | `tests/tasks-page.spec.tsx` |
|
||||
|
||||
### 测试与文档清扫
|
||||
|
||||
- 删除三个守 shadcn 层的 spec:`tests/ui-foundation.spec.ts`(Tailwind 入口 / 令牌桥 / 层序 / 作用域 reset)、`tests/ui-bundle.spec.ts`(核心包与 chunk 的体积与产物断言)、`tests/ui-shadows.spec.ts`(静态面板阴影白名单)。
|
||||
- `tests/theme.spec.ts` 去掉指向 `src/client/ui/**` 与令牌桥的一节,保留「图标模块零颜色字面量」与「每条 `color:` 解析到主题令牌」,后者从 tab 图标单表扩展到 `src/client/**/*.module.css` 全量(`color-mix()` / `var()` 链同样按令牌逐个校验,`inherit` / `currentcolor` / `transparent` 视为「不自己上色」)。
|
||||
- `tests/bundle-route.spec.ts` 的 `CHUNK_NAMES` 期望回到 4 项;`tests/manifest-consistency.spec.ts` 的「每个 chunk 都被 `package.json#files` 覆盖」断言保留,列表继续从 `CHUNK_NAMES` 派生(不手抄)。
|
||||
- `README.md` 两处「视觉基座为 shadcn/ui + Tailwind v4」描述删除;`docs/external-plugin-guide.md` 删除 §12.3「在插件里用 Tailwind / shadcn/ui」整节(含 preflight 规避、令牌桥、静态面板零阴影三条)。
|
||||
|
||||
## 本轮打磨(2026-09-27,`feat/tasks-graph-polish`)
|
||||
|
||||
图模式(主显示模式)按用户 brief 换成两段式卡片:上段是类型徽章 + 相位徽标 + 名称 + 元信息,
|
||||
下段小条是状态点 + 状态词 + **主 Agent 同款合并活动行** + 已完成节点的折叠 chevron;
|
||||
运行中小条从左到右扫光;层级/分支与 workflow 相位各用一套令牌色阶分组;后台任务从插件自建的
|
||||
三条路由改成宿主客户端 `ctx.jobs`(推送 roster + 非消费观察流 + kill,`src/jobs-routes.ts` 已删除);
|
||||
窄栏可读性按 a–f 六项逐一处理(卡片高度预算重算、内容包围盒居中、缩放手感、平移夹取、连线 stub 加粗、
|
||||
相位框配色)。**树模式保持原样**(只随共享数据类型做最小适配)。
|
||||
|
||||
完整设计与偏差记录见 [2026-09-27-tasks-graph-polish.md](2026-09-27-tasks-graph-polish.md)。
|
||||
|
||||
## ⚠️ 0.1.7 复审(2026-09-27):本文的团队一半已成历史
|
||||
|
||||
本文档的数据源侦察与路由设计写于 **DSH 0.1.5-rc.2 / 0.1.6-alpha.2** 时代。团队那一半——`remoteView` /
|
||||
`remoteCreateTask` / `remoteUpdateTask` 三个 Remote 方法、`teams.view` 路由、`team-task-conflict`
|
||||
冲突联合、以及只有 `listMembers` 才带的成员 `status` / `model`——**在 0.1.7 上全部失效**:上游删掉了
|
||||
那三个方法,把团队的读路径改到 Lead Session 的 `agentTeam` Session projection(写路径保留
|
||||
`createTask` / `updateTask`,但拒绝语义从「返回联合」改成「抛 `TeamError`」,冲突码
|
||||
`TEAM_TASK_STALE_REVISION`)。结果是 0.1.7 上团队条从来不渲染。
|
||||
|
||||
改写、真机证据与实施偏差见 [2026-09-27-agent-teams-dsh-0.1.7-adaptation.md](2026-09-27-agent-teams-dsh-0.1.7-adaptation.md);
|
||||
本文档其余章节(workflow 折叠、图/树布局、fold 规则、jobs 数据源)仍然有效。
|
||||
|
||||
## 验收(回退后,2026-09-21)
|
||||
|
||||
`pnpm typecheck` ✅ 0 错误;`pnpm lint` ✅ 0 错误;`pnpm vitest run` **133 文件通过 / 1410 用例通过 / 9 skipped / 0 失败**(回退前:4 个文件失败,其中 `theme.spec.ts`、`ui-foundation.spec.ts` 在收集期就因 `src/client/ui/theme.css` 缺失报 ENOENT,`ui-shadows.spec.ts` 2 例、`bundle-route.spec.ts` 1 例断言失败)。
|
||||
@@ -1,65 +0,0 @@
|
||||
# DSH 0.1.6-alpha.2 适配(devDeps/CI 钉版推进,peer 下限不动)
|
||||
|
||||
日期:2026-09-17 分支:`feat/tasks-graph-workflow-teams`(PR #680)
|
||||
|
||||
## 背景
|
||||
|
||||
用户实机(桌面 3384)已在 **DSH 0.1.6-alpha.1** 上运行,而插件此前钉的是 `0.1.5-rc.2`(`latest`/`next` 仍是 rc.2,`alpha` 线已到 **0.1.6-alpha.2**)。本次把插件的 **devDependencies 与 CI 挂载钉版**推到 `0.1.6-alpha.2`,**peer 下限保持 `^0.1.5-rc.1` 不动**——rc.2 用户不受影响,插件同时兼容两条线。
|
||||
|
||||
同时按要求核对了「rebase 上游」:插件仓库 `origin/main` tip `1fcf43ccbedd`(2026-09-11)**是当前分支的祖先**(13 ahead / 0 behind),git 层面无可 rebase 的内容;本文件记录的是 DSH 上游适配。
|
||||
|
||||
## 上游 delta(rc.2 → 0.1.6-alpha.2,只列触达本插件的面)
|
||||
|
||||
`@deepseek-ai/dsh-client-ui-primitives` 逐模块 `diff`(.d.ts)+ 全量 `lib/**/*.js` 裸 import 扫描的结论:
|
||||
|
||||
| # | 变更 | 影响 | 处置 |
|
||||
|---|---|---|---|
|
||||
| 1 | **移除** `IconSendOutline16`(该版本唯一被移除的图标;新增 7 个) | `SideChatView` 编译失败 | 换成 `IconSendOutline14`(编译器建议的继任者) |
|
||||
| 2 | `TerminalBlockLabels` **新增必需字段 `noExitCode: string`**(语义:命令结束时拿不到退出码——被未知信号终止或从未启动) | 标签对象类型不符 | 补 `noExitCode` 并新增词条 `sideChatBlockNoExitCode`(20 份词典同步) |
|
||||
| 3 | `ConnectionIndicator` **移除 `reconnectLabel`**(停用态文案本身已命名重连动作) | 传了不存在的 prop | **跨线兼容 shim**:rc 线里该 prop 是必需且是重连按钮的 hover/焦点动作文案,peer 范围仍覆盖 rc,因此把组件类型加宽一次(`ConnectionIndicatorCompat`)并继续传该 prop——rc 渲染它、alpha.2 忽略多余键(`src/client/SideChatView.tsx` 有注释说明何时删掉这个 shim) |
|
||||
| 4 | `Menu` / `Modal`:**仅文档 + 键盘语义细化**(Tab 在列表内 settle、Escape 归属、菜单打开时 Escape 先给菜单) | 无类型破坏 | 不改代码;e2e/harness 覆盖既有交互 |
|
||||
| 5 | `index.d.ts` 新增导出:`Checkbox`、`isDarwinDesktop`、`MarkdownDelegateProvider`/`MarkdownDelegate`/`MarkdownExternalLinkHandler`、`UserTextReferences`;模块新增 `SiteGlyph` | 纯增量 | 不使用,不改代码 |
|
||||
| 6 | `dsh-tools` 在本线**不再 peer `dsh-code-runtime`** | 旧 hoist 失去理由,且上游从未发布该包 alpha.2 | **从 devDependencies 删除 `dsh-code-runtime`**(保留会造出「22 个 alpha.2 + 1 个 rc.2」的混合树) |
|
||||
| 7 | primitives **仍声明零 `dependencies`**,但 `lib/index.js` **新增两个裸 import**:`diff`(`DiffBlock` 的 `structuredPatch`)与 `simple-icons`(品牌字形) | 缺失会让**所有** import primitives 的 spec 在 vite 解析期直接 `Failed to resolve import`(6 个 jsdom 套件实测挂掉) | `diff@^9.0.0`(对齐桌面实机 9.0.0)与 `simple-icons@^15.0.0` 进 `dependencies`;并在 AGENTS.md §3.9 记录「先扫裸 import 与 node_modules 的差集」 |
|
||||
|
||||
上游发布完整性核查:**23 个 `@deepseek-ai/*` devDeps 中 22 个有 alpha.2**,唯一缺失的 `dsh-code-runtime` 正是第 6 条里被上游放弃 peer 的那个;`dsh-tool-bash-persistent` / `dsh-sandbox-windows-acl` / `dsh-app-boot` 等历史上出过 ETARGET 的包**都已发布**,因此把 CI 钉到 alpha.2 没有踩到「分阶段发布」的坑(仍保留 `NODE_OPTIONS=--max-old-space-size=4096`)。
|
||||
|
||||
## 改动清单
|
||||
|
||||
| 文件 | 变更 |
|
||||
|---|---|
|
||||
| `package.json` | 22 个 `@deepseek-ai/*` devDeps `0.1.5-rc.2` → `0.1.6-alpha.2`;删除 `@deepseek-ai/dsh-code-runtime`;新增 `diff@^9.0.0`、`simple-icons@^15.0.0` |
|
||||
| `pnpm-lock.yaml` | 随装更新 |
|
||||
| `src/client/SideChatView.tsx` | `IconSendOutline16` → `IconSendOutline14`;标签对象补 `noExitCode`;删 `reconnectLabel` |
|
||||
| `src/client/locales.ts` / `locales-ja.ts` + 18 份第三语言词典 | 新增 `sideChatBlockNoExitCode`(20 份 key 集同步) |
|
||||
| `.github/workflows/ci.yml` | 挂载车道**保持 `0.1.5-rc.2`**(见下「CI 钉版为什么留在 rc.2」),注释里记入探针证据 |
|
||||
| `AGENTS.md` | §2 钉版说明、§3 新增 #680 适配段、§3.9 追加 `diff`/`simple-icons` 两个 hoist |
|
||||
| 本文件 | 适配记录 |
|
||||
|
||||
peer 范围、`dsh.client.inject`、chunk externals 白名单、`SIDEBAR_SERVICE_VERSION`(`0.19.1`)**均未改动**——alpha.2 对宿主契约只做了第 4/5 条的文档级与增量变更。
|
||||
|
||||
## 验证(真实输出)
|
||||
|
||||
- `pnpm install`:`+ diff 9.0.0`、`+ simple-icons 15.0.0`,`Done`;`pnpm peers check` → **No peer dependency issues found**。
|
||||
- `pnpm typecheck` → 0 错误(修完上述 3 处后)。
|
||||
- `pnpm lint` → 0。
|
||||
- `pnpm vitest run` → **136 文件 / 1503 用例通过 / 9 skipped**(其中 `locales.spec` 16/16)。
|
||||
- `pnpm build` → 成功;`ui-bundle` 守卫仍绿(核心包 835.6 KiB、chunk 709.8 KiB、无 preflight/oklch/lucide)。
|
||||
- `pnpm test:mount`(`DSH_CMD="npx -y --package @deepseek-ai/dsh@0.1.6-alpha.2 dsh"`,真实挂载 + 无头 tab 全扫)→ **7/7**;`PERF_SUMMARY mount=480ms longtasks=2/221ms`(预算 ≤8);资源清单里 `/sidebar/bundle/tasks.js` **真实下载**(`decodedBodySize 726905`),懒加载路径在 alpha 线上同样成立。
|
||||
|
||||
> **注(2026-09-21 回退后)**:上面两条引用的 `ui-bundle` 守卫与 `tasks` 懒加载 chunk(`/sidebar/bundle/tasks.js`)都是已废弃的 shadcn/ui 视觉基座时期的产物,该基座已整体回退(见 [2026-09-14 任务页设计文档](./2026-09-14-tasks-graph-workflow-teams-design.md) 的「回退记录与结论」):`src/client/ui/**`、`ui-bundle` spec 与 `tasks` chunk 均已删除,任务页回到核心包 + 原生 CSS Modules。以上数字是当时的实测记录,原样保留。
|
||||
|
||||
## CI 钉版为什么留在 rc.2(含证伪过程)
|
||||
|
||||
第一版把挂载车道也钉到 `0.1.6-alpha.2`,CI 的 `plugin-mount` **确定性失败**(重跑同样红):`mount.e2e.ts:216` 等 `[data-dsh-better-sidebar] [title]`(插件底部工作台 tab 条)90s 不出现,`perf.e2e.ts:182` 同样卡在底部拖拽条。失败截图显示外壳正常但**没有活动会话**(会话面板停在空态占位),按 e2e 自己的注释「没有 session scope → 工作台没有 pane」推断:alpha 线在 CI 上不再自动激活 scratch profile 的种子会话。
|
||||
|
||||
关键一步是**证伪「这是本分支引入的」**:从 `origin/main` 拉探针分支 `probe/dsh-alpha2-ci-pin`,**只改这一行钉版**(零插件改动),用 `gh workflow_dispatch --ref` 触发(ci.yml 的 push 只对 main 生效,探针分支没有 PR 就不会自动跑)——`plugin-mount` **同样红**、同一断言、同一处(run `35484961070`)。本地(macOS)同一钉版 `pnpm test:mount` 却 7/7,说明这是 **alpha 线 + Linux CI 车道**的组合问题,与插件代码无关。
|
||||
|
||||
按仓库硬约束(不改 DSH 源码)与 KISS,处置是:**devDeps 继续跟 alpha.2(对齐维护者实机),车道回到已发布的 rc.2**,并在 ci.yml 注释与 AGENTS.md §2/§3 写明证据与「车道学会在 alpha 线激活种子会话之前,alpha 只做本地验证」。把车道适配 alpha 线(例如断言前显式点击种子会话,或等外壳自动选中)留作**独立事项**:那是 e2e 基础设施改动,不该混进依赖钉版推进。
|
||||
|
||||
## 不做 / 留待发版决定
|
||||
|
||||
- **不改 peer 下限**(用户确认):`^0.1.5-rc.1` 继续容纳 rc.1/rc.2 用户;本适配对他们是无感的。
|
||||
- **不改 README 的版本徽章与安装策略文案**:它们描述的是**已发布**的 v0.19.1("已在 0.1.5-rc.2 上验证"),把分支上的 alpha.2 验证写进去会让已发布版声称未做过的验证。发版时(若该分支进入某个 alpha 版本)再一并更新。
|
||||
- **不清理 `sideChatConnReconnect`**:跨线 shim 仍在用它(rc 线必需),删它要动 20 份词典且会让 rc 用户少文案。
|
||||
- **源码编译需要 alpha.2 的类型**:标签对象里 `noExitCode` 在 rc 线类型上不存在(多余属性会被 TS 拒绝),所以本分支的 `pnpm typecheck` 以 alpha.2 为准;运行时两线都安全(多余键被忽略)。
|
||||
@@ -1,130 +0,0 @@
|
||||
# DSH 0.1.6-alpha.2 适配(v0.20.0 基线)
|
||||
|
||||
日期:2026-09-21 分支:`feat/dsh-0.1.6-alpha.2`
|
||||
|
||||
## 背景
|
||||
|
||||
DSH `dsh-v0.1.5-rc.2..dsh-v0.1.6-alpha.2`(上游 **1687 个 commit**,中间还有 `dsh-v0.1.6-alpha.1`;`git diff --shortstat` = 5420 个变更文件 / +878 484 / −63 637)是 0.1.6 线的第二个预发布版,也是本插件自 rc.1 以来第一次遇到**真实破坏**的 delta。两件关键的事实构成这版全部工作:
|
||||
|
||||
1. **两条插件面向的硬契约收紧,且都是静默失败**(§上游 delta 1/2);三条 `ui-primitives` 破坏是编译期可见的。
|
||||
2. **宿主内置了插件原本自研的两个面**(右列终端、右列浏览器)。继续保留就是同一面存在两套实现,并且遮蔽产品正在开发中的宿主类型。
|
||||
|
||||
本计划记录该 delta、逐项结论与子系统级落点;上一版记录见 [2026-09-10-dsh-0.1.5-rc.2-adaptation.md](./2026-09-10-dsh-0.1.5-rc.2-adaptation.md),仓库级适配要点同步到 [AGENTS.md](../../AGENTS.md) §3(本版整体重写,是该节的权威摘要)。
|
||||
|
||||
本版由三条 commit 组成(`git log --oneline -4`,`git show --stat <sha>`):
|
||||
|
||||
| # | commit | 主题 | 规模 |
|
||||
|---|---|---|---|
|
||||
| 1 | `58ba17d` | `fix(native): meet DSH 0.1.6-alpha.2 guide-id and turn-tail contracts` | 17 files,+135 / −611 |
|
||||
| 2 | `d608998` | `chore(baseline): target DSH 0.1.6-alpha.2 and drop 0.1.5-rc support` | 27 files,+448 / −350 |
|
||||
| 3 | `5912274` | `feat: yield the terminal and browser to DSH 0.1.6's own sidebar types` | 79 files,+381 / −9 464 |
|
||||
|
||||
## 上游 delta(rc.2 → alpha.2,影响本插件的部分)
|
||||
|
||||
所有 alpha.2 结论都用 tag 取证(`git -C /tmp/dsh-0162 diff dsh-v0.1.5-rc.2 dsh-v0.1.6-alpha.2 -- <path>` / `git show dsh-v0.1.6-alpha.2:<path>`),不引用工作树、不引用记忆。
|
||||
|
||||
| # | 上游变更 | 上游文件(alpha.2) | 对插件的影响 |
|
||||
|---|---|---|---|
|
||||
| 1 | `SidebarRightGuideEntry.id` 变**必填**且同一 provider 内唯一;注册时 `new Set(entries.map(e => e.id)).size !== entries.length` 抛 `duplicate guide entry id` | `packages/client/ui-sidebar-right/src/client/tab-registry.ts`(抛点 `:251`,接口定义 `:61` 起;单测 `tests/tab-registry.client.spec.ts:302`) | **破坏(静默)**:插件两个 guide 条目都在 `undefined` 上相撞 → `register` 抛错 → 抛点在本插件 `ctx.inject(['sidebarRightTabs'], cb)` 的**回调体内**被 cordis 吞掉 → 整个原生承载面空掉(指南无条目、`openTab` 全拒)。→ §决策 6 |
|
||||
| 2 | `conversation.chat.turnTail` 由 `chain` 改 `list`;list 槽要求 `options.id`,缺则抛 `list slot "…" requires options.id` | `packages/client/ui-chat/src/client/contract/slots.ts:213`;抛点在 `packages/client/ui-slots/src/index.ts:1230` | **破坏(静默)**:插件传 `select` 直接抛(同样被吞)。语义也变了——chain 的 `select` 让一个条目**替换**宿主产物行,list 只能**追加**。→ §决策 5 |
|
||||
| 3 | `ui-primitives` 三处导出面变化:`IconSendOutline16` 不再导出(只剩 `IconSendOutline14`);`TerminalBlockLabels` 新增**必填** `noExitCode`;`ConnectionIndicator` 删掉 `reconnectLabel` | `packages/client/ui-primitives/src/icons/index.tsx`(`IconSendOutline16` 在 rc.2 有、alpha.2 无)、`src/**`(`git diff … -- packages/client/ui-primitives/src` 里 `−reconnectLabel` / `+noExitCode`) | **破坏(编译期)**:适配见 §改动清单「基线」 |
|
||||
| 4 | 宿主自带右列终端:`ui-sidebar-terminal` + `api-terminal-controller`;模型侧等价物 `packages/terminal/tool-terminal`(包名 `@deepseek-ai/dsh-tool-terminal`)**未被任何 shipped bundle 挂载** | `packages/client/ui-sidebar-terminal/**`、`packages/api/terminal-controller/**`、`packages/terminal/tool-terminal/**` | **让位** → 插件整套 PTY 栈删除。证据:`git grep -rn tool-terminal -- packages/bundle/` 在 alpha.2 只有 **1 处命中且是注释**(`packages/bundle/web-app/cordis.patch.yml:404`,讨论后台 job 注册表的生产者),没有任何 bundle 行挂载该包 |
|
||||
| 5 | 宿主自带 `browser` kind,且 `ui-chat` 已内置把正文 http(s) 链接委派给它 | `packages/client/ui-sidebar-browser/**`;`packages/client/ui-chat/src/client/apply.ts:147`(`ctx.get('sidebarRightTabs')?.get('browser')` 守卫后 `ctx.sidebarRight.openTab('browser', { params: { url } })`) | **让位** → 插件 `browser` tab 类型与宿主 `browser.probe` 路由删除;DOM 层链接接管保留(宿主不做按协议分流) |
|
||||
| 6 | 原生右列布局**现在持久化**:`localStorage` 的 `dsh.sidebar-right.v1.<sessionId>`(zod 校验,tab 体渲染前恢复) | `packages/client/ui-sidebar-right/src/client/persistence.ts`(`sidebarPersistence = 'dsh.sidebar-right.v1'`)、`README.zh.md:72` | §3.10 里「原生布局只在内存,不做持久化」的旧说法对 0.1.6 起**已失效**;插件不接入、只用现成面 |
|
||||
| 7 | `sidebar-right` 新增面:`SidebarRightTabDefinition.multiple?`、`SidebarRightPlacement.preferNewPane?`、class 上的 `openTabs` / `tabsIn(sessionId)` / `registerCloseHandler(kind, handler)`(**不在** `ISidebarRight` 接口里)、keyed 槽 `sidebar.right.tab.guide.entry` | `tab-registry.ts:103`、`contract/slots.ts:117`、`service.ts:218/228/261/387` | 插件只用到 `multiple`;其余仅保证不破坏 |
|
||||
| 8 | 官方管控面:新增 `packages/boot/plugin-manager`(`ctx.pluginManager` + `plugin_manager` 工具)与 `packages/client/ui-plugin-manager`(Web Plugins 页);`dsh.profile.patchReload` 在 alpha.2 已从**代码与文档整体消失**(rc.2 由 `apps/cli/src/plugin.ts` / `profile-boot.ts` 读取 + 4 份 `apps/cli/**` 文档 10 处说明;alpha.2 在 `packages/` + `apps/` 上 **0 命中**,只剩 `.agents/notes` 的陈旧文本);HMR 归 `packages/boot/hmr` | 同上 | **零额外声明**:两者都会接管任何已声明 `dsh.bundle.patch` 的包(本插件 `package.json` 的 `dsh.bundle.patch` = `./cordis.patch.yml`),本插件无需适配。`patchReload` 本插件**从未消费**(`grep -rn patchReload src tests scripts` 零命中),因此删除对本插件零影响 |
|
||||
| 9 | 依赖形状:`ui-primitives` 仍**不声明** `dependencies`(`peerDependencies` 只有 `@deepseek-ai/cordis`),bundle 新增裸 import `diff`(`src/DiffBlock.tsx:3`)与 `simple-icons`(`src/SiteGlyph.tsx:44`,全新 3 万余品牌图标集);`packages/code-runtime` **整包消失**(rc.2 还在,npm 上停在 0.1.5-alpha.2) | `packages/client/ui-primitives/package.json`、`packages/client/ui-primitives/src/*` | devDependencies 提升 `diff` / `simple-icons`;移除 `@deepseek-ai/dsh-code-runtime` |
|
||||
|
||||
**明确未变(不必再核)**:
|
||||
|
||||
- 指南「列出的条目 ≤ 4 条才渲染说明」的规则仍在:`MAX_DESCRIBED_ENTRIES = 4`(`packages/client/ui-sidebar-right/src/client/tabs/guide/GuideBody.tsx:40`)。
|
||||
- 一次性 token 鉴权、斜杠 RPC 方言、`MarkdownText` labels 嵌套契约、会话事件模型(`assistant/assistant-stream` 瞬时帧):全部沿用 0.1.5 线结论,本版零改动。
|
||||
- 插件的承载面用法不变:仍由 `ctx.inject(['sidebarRightTabs'], cb)` 驱动类型注册(**不能**等槽声明),仍只经 `ctx.sidebarRight.openTab/openResource/close` 与具体类上的 `openTabIn/…` 操作。
|
||||
- 宿主仍有 chain 槽(如 `conversation.composer`、`sidebar.right.tab.guide`),但本插件**从未**注册它们——插件的槽面只有三处:`conversation.session.header.utilities`(底部工作台开关)、`settings.section`、原生 `sidebar.right.pane.tab` / `.title`;唯一用过的「整行接管」型贡献(轮尾产物行)随 `turnTail` 改 list 而删除(#2)。
|
||||
- `dsh.plugin.json` **不是** DSH 契约:`git grep -n 'dsh\.plugin\.json'` 在 `dsh-v0.1.5-rc.2` 与 `dsh-v0.1.6-alpha.2` 上**均 0 命中**。它只服务第三方 registry(社区市场)通道,`lib/client-registry.js` 同理。
|
||||
|
||||
## 决策
|
||||
|
||||
1. **peer 下限必须动:`^0.1.5-rc.1` → `^0.1.6-alpha.2`**。semver 的预发布规则要求比较子与候选版本同 `[major,minor,patch]` 元组,`^0.1.5-rc.1` 在数学上**永远容纳不下任何 `0.1.6-*` 预发布版**。实测(用仓库树内的真实 `semver@7.8.5`):
|
||||
|
||||
| range \ candidate | `0.1.5-rc.2` | `0.1.6-alpha.1` | `0.1.6-alpha.2` | `0.1.6-alpha.3` | `0.1.6-rc.1` |
|
||||
|---|---|---|---|---|---|
|
||||
| `^0.1.5-rc.1` | true | false | false | false | false |
|
||||
| `^0.1.5-rc.2` | true | false | false | false | false |
|
||||
| `*`(裸) | false | false | false | false | false |
|
||||
| `^0.1.6-alpha.2` | false | false | **true** | **true** | **true** |
|
||||
|
||||
即:0.1.5 的下限不是「保守」而是**结构上不可能**容纳 0.1.6 预发布;`^0.1.6-alpha.2` 天然容纳 0.1.6 的 alpha/rc 全线并排除 0.1.5。
|
||||
2. **支持线取舍:0.1.5-rc.\* 用户停留在 v0.19.1**。rc.2 → alpha.2 有真实破坏(#1/#2),按本仓库先例(v0.19.0 抬到 0.1.5-rc.1 时直接桥掉 0.1.2 线)**不写运行时兼容层**;0.1.5-alpha.2 及更早同样停留在旧版。`engines.dsh` 与 peer 下限同源(同一个支持范围,两处不能分叉)。
|
||||
3. **终端整体让位**:删除整套 PTY 栈(见改动清单),宿主 `ui-sidebar-terminal` 成为唯一提供者。**接受模型侧代价并写进 README**:插件原有的 8 个 `terminal_*` 工具(`terminal_create/list/send/read/wait/resize/signal/close`,默认关)是模型唯一的**跨调用持久**终端,而上游等价物 `@deepseek-ai/dsh-tool-terminal` 未被任何 shipped bundle 默认挂载,删除后模型只剩一次性 `bash` / `pwsh`——这是「不维护第二套 PTY」的既定代价,不是意外,所以不补自家的替代实现。
|
||||
4. **浏览器让位,但保留 DOM 层链接接管**。宿主不做按协议分流(https 默认交系统浏览器),那是插件唯一还能提供的能力,因此 `src/client/link-intercept.ts` 与已发布的 `urlTarget` 机制(v0.13.0+,第三方消费插件按它声明)不动;接管目标改指**宿主的** `browser` kind(`ctx.sidebarRight.openTab('browser', { params: { url } })`),并保留 `window.open` 兜底——宿主没装该包时不至于点了没反应。
|
||||
5. **轮尾接管整体删除**。list 只能追加,插件再注册一行只会与宿主的产物卡**重复**;上游是有意选 list(「An additive list lets both plugins contribute without either knowing the other's data or rendering」,`.agents/notes/implemented/feature/2026-09-17-persistent-plan-cards.md:27`)。产物行交回宿主,它开文件走宿主 `openFile` → 本插件的 editor,**行为不变**。文件打开的那一半(`sidebar-file.ts`)与路径助手(`paths.ts` 的 `resolveSidebarPath`)保留,因为文件变动 tab 与编辑器宿主仍在用。
|
||||
6. **guide 条目 id 作为类型契约落进插件侧**:两个条目各自带 `id`(descriptor id / `'files'`),并加测试守护「每个 guide 条目 id 非空且互不重复」。**不做运行时兜底**(自动补 id 会把契约破坏变成永久沉默,正是这次最难查的形态)。
|
||||
7. **加一条可见的失败接缝**:`registerNativeSurface` 新增可选 `reportFailure(phase, error)`,接到 `src/client/index.tsx` 的 `fail()` 可见诊断条;单个 descriptor 注册失败不再带走其余注册。理由就是这次的症状——整个原生承载面静默空掉,除了一行缺失的指南之外没有任何提示。
|
||||
8. **卡片采用宿主胶囊配方,但字重走令牌阶梯**:几何/描边/填充/圆角照抄宿主 `GuideBody.module.css` 的 `.entry`(0.5px `--dsw-alias-border-l4` / 24px 圆角 / `--dsw-alias-bg-layer-1` / `min-height: 56px` / `padding: 14px 20px`),标签字号**不抄**宿主那里的裸 `font-size: 15px`(它背后没有令牌),改用阶梯最近一档 `--dsw-font-s-strong-14`(14px medium)。皮肤契约要求插件侧零豁免面(AGENTS.md §5)。
|
||||
9. **不接入本版新增的宿主面**:`openTabs` / `tabsIn` / `registerCloseHandler` / `preferNewPane` / keyed `guide.entry` 槽 / 全局面板——插件只用到 `multiple` 与既有面。
|
||||
|
||||
## 改动清单(子系统级)
|
||||
|
||||
| 子系统 | 文件 | 变更 |
|
||||
|---|---|---|
|
||||
| 基线 | `package.json` | `version` → `0.20.0`;15 个 `@deepseek-ai/*` peer 下限 `^0.1.5-rc.1` → `^0.1.6-alpha.2`;devDependencies 同步钉 `0.1.6-alpha.2`;移除 `@deepseek-ai/dsh-code-runtime`(上游整包消失);新增 `diff` `^9.0.0`、`simple-icons` `^16.32.0`(`ui-primitives` 裸 import,不走 devDependencies 就 `Cannot find package`) |
|
||||
| 基线 | `dsh.plugin.json` | `version` → `0.20.0`;`engines.dsh` → `>=0.1.6-alpha.2` |
|
||||
| 基线 | `pnpm-lock.yaml` | 锁文件对齐 alpha.2(646 行变动) |
|
||||
| 基线 | `.github/workflows/ci.yml` | mount 车道钉 `@deepseek-ai/dsh@0.1.6-alpha.2`;注释改为「alpha.2 是 npm 的 `alpha` dist-tag,`latest` 仍是 0.1.5 线」 |
|
||||
| 基线 | `scripts/e2e-common.sh` | 回退 npx **显式钉版**:新增 `DSH_NPX_SPEC`(缺省 `@deepseek-ai/dsh@0.1.6-alpha.2`)——不钉就会解析 `latest`,而 `latest` 仍是插件已不支持的 0.1.5 线 |
|
||||
| 基线 | `src/client/service.ts` | `SIDEBAR_SERVICE_VERSION` → `0.20.0`(`tests/service.spec.ts` 断言它 === `package.json` 的 version) |
|
||||
| 基线 | `src/client/SideChatView.tsx` | `IconSendOutline16` → `IconSendOutline14 size={16}`(保住原 16px 视觉尺寸);`TerminalBlockLabels` 补必填 `noExitCode: t('sideChatBlockNoExitCode')`;`ConnectionIndicator` 去掉 `reconnectLabel` |
|
||||
| 基线 | `src/client/locales.ts` + 19 份 `locales-*.ts`(共 20 份词典) | 新增 `sideChatBlockNoExitCode`(zh「已结束(无退出码)」/ en「Ended without an exit code」+ 各语言);`sideChatConnDisconnected` 改口径为「连接已断开,点击立即重连」——断线标签现在**自身**命名重试动作,跟随宿主文案 |
|
||||
| Tab 契约 | `src/client/native/index.ts` | 两个 guide 条目各带 `id`(descriptor id / 接管条目 `'files'`);`NativeSurfaceDeps` 新增可选 `reportFailure?: (phase: string, error: unknown) => void`,wrapper 内 try/catch 上报(单个 descriptor 失败不再带走其余注册) |
|
||||
| Tab 契约 | `src/client/index.tsx` | `fail()` 诊断条上移到 `apply()` 前部,并以 `reportFailure: (phase, error) => fail(\`native ${phase}\`, error)` 注入 `registerNativeSurface` |
|
||||
| Tab 契约 | `tests/native-surface.spec.ts` | +15 行:遍历注册到的全部 guide 条目,断言每个 `id` 是 string、总数 > 0、`new Set(ids).size === ids.length` |
|
||||
| 轮尾 | `src/client/intercept.tsx`(−131)、`src/client/produced-files.ts`(−120)、`tests/turn-tail-intercept.spec.ts`(−200)、`tests/produced-files.spec.ts`(−56) | 轮尾接管整体删除;`src/client/sidebar.module.css` 的接管样式随之删(−46) |
|
||||
| 轮尾 | `src/client/sidebar-file.ts`(新增 30)、`src/client/paths.ts`(+13) | 文件打开的那一半存活(文件变动 tab 与编辑器宿主仍用);`resolveSidebarPath` 移进同胞路径助手模块(绝对路径判定镜像宿主:POSIX 根 / 盘符 / UNC 都不拼 cwd) |
|
||||
| 终端 | 删除 `src/pty-manager.ts`(−548)、`src/agent-pty.ts`(−722)、`src/pty-deps.ts`(−240)、`src/tools.ts`(−500)、`src/client/TerminalView.tsx`(−477)、`src/client/TerminalWaitBanner.tsx`、`src/client/terminal-font.ts`(−255)、`src/client/terminal-links.ts`(−194)、`src/client/chunks/terminal.tsx`、`src/client/pinned.ts`(−177)、`src/client/sidebar/use-pinned-tabs.ts`(−147) | 整套 PTY 栈、xterm 视图、字体/链接助手、chunk 入口与跨会话固定终端全部下线 |
|
||||
| 终端 | `src/index.ts`(+14 / −560)、`src/config.ts`(−48)、`src/prefs-shared.ts`、`src/client/prefs.ts`(−26)、`src/client/api.ts`(−40)、`src/client/state.ts`(+11 / −244)、`src/client/TabBar.tsx`(+20 / −76)、`src/client/Sidebar.tsx`(+16 / −91)、`src/client/icons.tsx`(−11) | 终端设置项(`terminalShell` / `terminalShellArgs` / `terminalFontFamily` / `terminalFontSize` / `agentTerminalTools` / `bottomPanelAutoTerminal`)、宿主路由 `terminal.deps`、`clampTerminalFontSize`,以及被删特性留下的死状态(`SidebarTab.pin` / `nextTerminal` / `agentWaits` / `bottomOpenedOnce` / agent-tab helpers / pinned-tab memo key)一并清除 |
|
||||
| 终端 | `package.json` | `node-pty` `^1.1.0` 离开 `dependencies`;`@xterm/xterm`、`@xterm/addon-fit` 离开 devDependencies;`files` 去掉 `lib/client-terminal.js` |
|
||||
| 终端 | `pnpm-workspace.yaml` | 原 `allowBuilds: {node-pty: true, protobufjs: true}` 收缩(**块未整块删除**,见「收尾偏差」2) |
|
||||
| 终端 | `tsdown.config.ts`、`src/bundle-route.ts`、`src/client/chunk-loader.ts`、`scripts/package-registry.mjs`、`package.json` 的 `files`、`tests/{bundle-route,chunk-loader,chunk-artifact,manifest-consistency,lazy-chunk}.spec.*` | chunk 名由 `['editor','mermaid','locale','terminal']` 收敛为 `['editor','mermaid','locale']`(AGENTS.md §5 列出的镜像处全部同步) |
|
||||
| 终端 | 删除 13 个 spec | `agent-pty` / `agent-terminal-reconcile` / `agent-wait-badge` / `bottom-auto-terminal` / `open-when-sized` / `pinned` / `pty-deps` / `pty-helpers` / `terminal-deps-banner` / `terminal-font` / `terminal-links` / `terminal-wait-banner` / `tools` |
|
||||
| 浏览器 | 删除 `src/client/BrowserView.tsx`(−289)、`src/client/browser.ts`(−167)、`src/browser-probe.ts`(−26)、`tests/browser.spec.ts`、`tests/browser-probe.spec.ts`;`src/client/builtins/tabs.tsx`(+13 / −175)删 `browser` descriptor | 插件 `browser` tab 类型下线,宿主 `browser` kind 独占嵌入页(两条 commit 合计删 15 个 spec:终端 13 + 浏览器 2) |
|
||||
| 浏览器 | `src/index.ts`(删宿主 `browser.probe` 路由)、`src/prefs-shared.ts` / `src/client/prefs.ts`(删 `browserNoSandbox` / `browserAllowedLoopback`)、`src/config.ts` | 只配置插件自家 iframe 的两个设置项随它们喂养的宿主路由一并删除(宿主 `browser` kind 不进插件注册表) |
|
||||
| 浏览器 | `src/client/link-intercept.ts`、`src/client/sidebar/use-host-feeds.ts:128` | 保留 DOM 层 http(s) 接管,目标改宿主 `browser` kind(`ctx.sidebarRight.openTab('browser', { params: { url } })` + `window.open` 兜底);`urlTarget` 机制与匹配顺序(`matchUrlTarget`)不变 |
|
||||
| 卡片 | `src/client/sidebar.module.css` `.paneCard` | 采用宿主 guide 胶囊配方:`min-height: 56px` / `padding: 14px 20px` / `border: 0.5px solid var(--dsw-alias-border-l4)` / `border-radius: 24px` / `background: var(--dsw-alias-bg-layer-1)` / `font: var(--dsw-font-s-strong-14)` / hover 用 `--dsw-alias-interactive-bg-hover` |
|
||||
| 卡片 | `tests/theme.spec.ts`(+23) | 守护 `.paneCard` 的几何与「每条 paint 都解析到 `var(--dsw-`」,并断言**不得**出现上游专有拼写 `--dsw-alias-bg-l1` / `--dsw-alias-bg-l2`(证据:alpha.2 全树 4 处声明、**0 处定义**,全在 `packages/client/ui-sidebar-terminal/src/client/*.module.css`,故上游那 4 条 `background` 实际被丢弃) |
|
||||
| 挂载冒烟 | `tests/e2e/mount.e2e.ts` | 指南 sweep 拆成「插件自有 4 条(files / git / subagent / sidechat)+ 宿主自有 2 条(terminal / browser,各**恰好 1 条**——多 1 条即插件又在遮蔽宿主)」;说明表断言从「关 3 个 tab 类型」改「关 2 个」(宿主终端条目已不在插件注册表内) |
|
||||
| 聚合冒烟 | `scripts/e2e-aggregate-mount.sh` | 真方法哨兵从已删的 `terminal.deps` 改为 `settings.get`(理由写进脚本注释:`/sidebar/api` 是插件自己 fence 的 prefix 路由,未注册方法统统 404,`{"ok":true}` 只可能来自真实注册的 handler) |
|
||||
| 文档 | `AGENTS.md` §3 | 重写为「DSH 0.1.6-alpha.2 适配要点(0.1.6-alpha.2+ 基线)」:支持线 + 两条硬契约 + 两处让位 + 「本版还需知道的」9 条 |
|
||||
| 文档 | `README.md` | v0.20.0 段落、终端/浏览器「交还宿主」说明、支持版本徽章;终端卡片内写明模型侧代价与自行插入 `tool-terminal` 行的指引 |
|
||||
| 文档 | `docs/plans/2026-09-21-dsh-0.1.6-alpha.2-adaptation.md` | 本计划 |
|
||||
|
||||
### 收尾偏差(核对实际文件后修正的三处口径)
|
||||
|
||||
这三处不是计划项,而是**核对文件时发现的「提交信息与产物不符」**;写在这里以免被当作已完成,也给下一版留证据:
|
||||
|
||||
1. **安装脚本的 `-Repair` 模式**:`5912274` 的提交信息与 `README.md` 都称它已随终端删除,但三条 commit **都没有触碰这两个文件**——`git diff --stat HEAD~3..HEAD -- scripts/` 只有 `e2e-aggregate-mount.sh` / `e2e-common.sh` / `package-registry.mjs` 三个。`scripts/install.sh`(`--repair`,改动前行号 15/19/54/57/75/82/180)与 `scripts/install.ps1`(`-Repair`,改动前行号 22/26/51/175)当时**完整保留**了 node-pty 修复流程与 issue #140 文案。**该偏差已在本分支工作树修掉**(两个安装脚本 + `e2e-common.sh` 里 scratch profile 的 `allowBuilds` 块 + `package-registry.mjs` 的 node-pty 注释),截至本文档写作时**尚未提交**(见「落地」)。
|
||||
2. **`pnpm-workspace.yaml` 的 `allowBuilds` 块仍未收干净**:现状是 `node-pty: set this to true or false`(pnpm 自己写的待决占位行);原条目 `node-pty: true` 被换成该占位、`protobufjs: true` 被移除。块本身仍在,且仍以**已删除的依赖名**出现——**本版未处理**。
|
||||
3. **`browserNoSandboxWarning` 成了死键**:20 份 `src/client/locales*.ts`(词典 `locales.ts` + 19 份第三方语言)里仍在,而它描述的 `browserNoSandbox` 设置项已删,代码侧除词典外零消费——**本版未处理**。
|
||||
|
||||
## 验证
|
||||
|
||||
- **门禁(本版分支 HEAD `5912274`,macOS arm64)**:`pnpm install --registry=https://registry.npmjs.org`(**必须显式指定 registry**——机器默认的 npmmirror 源滞后,对 `@deepseek-ai/dsh-code-runtime` 报 404);`pnpm peers check` = `No peer dependency issues found`;`pnpm typecheck` 干净;`pnpm lint` 干净;`pnpm check:consumer-types` OK;`pnpm test` = **110 files / 1085 passed / 9 skipped**;`pnpm build` 恰好产出三个 chunk(`client-editor.js` / `client-mermaid.js` / `client-locale.js`)。
|
||||
- **写本文档时复核过的两条**:`pnpm peers check` = `No peer dependency issues found`、`pnpm test` = **110 files / 1085 passed / 9 skipped**,与上条一致(复核时工作树只有文档改动,测试文件与 HEAD 一致,因此数字对应 HEAD 的测试面;此后工作树又改动了 `tests/theme.spec.ts` 等收尾文件,**未重跑**)。`typecheck` / `lint` / `check:consumer-types` / `build` **未重跑**(本节记录的是门禁那一次的结果)。
|
||||
- **真机挂载冒烟(DSH 0.1.6-alpha.2):未执行**。`pnpm test:mount` 与 `pnpm test:mount:aggregate` 在本版分支上**没有跑过**——这是本版最大的未验证面:指南「插件 4 条 + 宿主 terminal/browser 各恰好 1 条」、聚合哨兵改 `settings.get`、卡片新几何,目前只有单测与 e2e 代码,没有真机结论。
|
||||
- **3080 实例 GUI 人工验证:未执行**。
|
||||
- **上游事实的取证方式**:本文每条 alpha.2 结论都来自 tag(`git -C /tmp/dsh-0162 diff dsh-v0.1.5-rc.2 dsh-v0.1.6-alpha.2 -- <path>` / `git show dsh-v0.1.6-alpha.2:<path>`),关键抛点原文为 `sidebarRight: duplicate guide entry id in "<id>"`(`tab-registry.ts:251`)与 `list slot "<name>" requires options.id`(`ui-slots/src/index.ts:1230`)。semver 表用仓库树内的 `semver@7.8.5` 实测。
|
||||
- **未验证的推断**(明确标注,不作为结论):「模型只剩一次性 `bash` / `pwsh`」是按两条**已验证**事实推出的——① `@deepseek-ai/dsh-tool-terminal` 未被任何 shipped bundle 挂载(`packages/bundle/` 零挂载行);② 插件删除的 `src/tools.ts` 确实注册了 8 个 `terminal_*` 工具。**没有**在真机上对模型可见工具清单做过实测。
|
||||
|
||||
## 落地
|
||||
|
||||
- 分支:`feat/dsh-0.1.6-alpha.2`,三条 commit(HEAD `5912274`)之上还有一批**未提交**的收尾改动:`AGENTS.md` §3 重写、`README.md` / `README_EN.md`、`docs/external-plugin-guide.md`、`scripts/install.sh` / `install.ps1`(删 `-Repair` / `--repair`)、`scripts/e2e-common.sh` / `e2e-aggregate-mount.sh` / `package-registry.mjs`(node-pty 残迹)、`tests/theme.spec.ts`(卡片守护加强,见「收尾偏差」1)。
|
||||
- 其余(PR、真机挂载冒烟、3080 GUI 验证、发版结论):**待回填**——本节不预填未经执行的结论。
|
||||
|
||||
## 不做
|
||||
|
||||
- 不为 0.1.5-rc.\* 写运行时/类型层兼容层(本版是**有意**的支持线断裂,与 v0.19.0 桥掉 0.1.2 线同一先例)。
|
||||
- 不把 peer 下限留在 `^0.1.5-rc.1` 或写成「宽松范围」——它在数学上容不下任何 0.1.6 预发布版(§决策 1 的表),留着只会让消费方装上一个必然崩的宿主。
|
||||
- **不补自家的终端替代实现**(不上第二套 PTY、不为模型侧持久终端另造工具);上游 `tool-terminal` 未被 bundle 挂载是上游的取舍,README 给指引即可。
|
||||
- 不恢复被删的轮尾产物行:list 槽只能追加,重新注册只会与宿主产物卡重复。
|
||||
- 不接入本版新增宿主面(`openTabs` / `tabsIn` / `registerCloseHandler` / `preferNewPane` / keyed `guide.entry` 槽 / 全局面板)。
|
||||
- 不改 DSH 源码(仓库硬约束 §1);不改 `release.yml`(0.20.0 无 prerelease 后缀 → 自动发 `latest`)。
|
||||
@@ -1,144 +0,0 @@
|
||||
# DSH 0.1.7-alpha.1 适配记录
|
||||
|
||||
> 适配对象:`dsh-better-sidebar` v0.20.0 开发线(支持 DSH 0.1.6-alpha.2)→ **v0.21.0-alpha.1(仅支持 DSH 0.1.7-alpha.1+)**。
|
||||
> 基线标签:`dsh-v0.1.6-alpha.2`(`ddefc45fbc`)→ `dsh-v0.1.7-alpha.1`(`c36a83ff6b`),相隔 **1299 个 commit / 4754 个文件变更**。
|
||||
> 本文记录**为什么这么改**与**改完的证据**;规则性结论沉淀在仓库根 `AGENTS.md` §3。
|
||||
>
|
||||
> ⚠️ **后续**:`v0.21.0-alpha.1` **从未发布到 npm**——这条开发线随后又适配到 **0.1.7-rc.1**,peer 下限收到 `^0.1.7-rc.1`,整个 0.21 线的变更一并随 **v0.21.1** 出货。本文保留为 alpha.1 阶段的原始记录(结论仍然有效),最新状态见 [2026-09-23-dsh-0.1.7-rc.1-adaptation.md](2026-09-23-dsh-0.1.7-rc.1-adaptation.md)。
|
||||
|
||||
## 1. 怎么做的(方法与边界)
|
||||
|
||||
先用一次 8 路 workflow 侦察把 1299 个 commit 收敛到「与本插件相交的 surface」:sidebar-right 契约、ui-chat 槽、ui-primitives 导出面、boot/plugin-manager、宿主内置侧栏能力盘点、peer 依赖图、会话格式与 sidechat 依赖面、插件触点全量清单;随后一个独立的交叉验证子代理逐条复核,**证伪了 8 条**(例如「ui-primitives 只需补 2 个 devDep」被推翻为 4 个,「聊天正文里的 http 链接什么都不开」被推翻为会回落 `window.open`)。
|
||||
|
||||
三条纪律贯穿全程:
|
||||
|
||||
1. **结论必须带证据**:每条破坏都给出 `git show <tag>:<path>` / 实测命令与行号,不采信记忆。
|
||||
2. **区分「硬报错」与「静默失效」**:后者的代价高得多——本版一共 6 处静默失效(见 §3)。
|
||||
3. **不采信子代理报告**:所有关键结论由主代理独立复跑命令核对后才进入实现。
|
||||
|
||||
**没有做的事**:不改 DSH 源码;不写 0.1.6/0.1.7 双基线兼容层;不接入 `ctx.documentPreviews`;不恢复终端。
|
||||
|
||||
## 2. 硬破坏与适配(不修就崩)
|
||||
|
||||
### 2.1 `dsh-settings` 整体重写 —— 本版最大的一处
|
||||
|
||||
0.1.6 的 `SettingsProvider.register(ns, schema)` 给出 `{get, watch, update, replace}`;0.1.7 只剩 `SettingsForms` 的 `describe/update/replace/mutate/configure`,且**表单按插件 Loader 行的 `entry.options.id` 寻址**,schema 取自 `entry.fiber.runtime.Config`。
|
||||
|
||||
后果链条(全部实测):`register` 不存在 → 插件 `ctx.inject(['settings'])` 回调体内 `TypeError` → **被 cordis 吞掉** → `settingsFace` 恒 `undefined` → 侧边卡片设置页回落到 schema 默认值、`agentOpenTools` 永远打不开、`aionui-panel` 互斥失效。
|
||||
|
||||
适配:
|
||||
|
||||
- `src/config.ts` 把用户偏好**并入导出的 `Config`**(原先只是 4 个部署限额),每个偏好字段标为 volatile,并且 **schemastery 必须换成 DSH 自己的 `@deepseek-ai/schemastery`**。这一点是计划里标记为「未验证」的风险,真机上被证实为**必须**:`meta.volatile` 只是标记,DSH 的构建额外在解析期把 volatile 字段包成 cosmokit `Volatile` 引用,而 Loader 的 `_commitVolatile` 正是提交那些引用。用公开包时 `settings.update` 返回 200、`revision` 也递增,**但有效值永远不变**——设置写入被静默丢弃(`--dsh-e2e-marker` 为空、title-bar 方案不生效就是它的症状)。另有第二个坑:`.volatile()` 经 `extra()` **返回副本而非原地修改**,写成 `for (const f of ...) f.volatile()` 会把标记丢掉,宿主随后报 `Plugin entry "better-sidebar" has no volatile fields`。
|
||||
- `src/index.ts` 新增 `ownEntryId(ctx)` **运行时自发现**行 id:按 `options.name === 'dsh-better-sidebar'` + `entry.fiber === ctx.fiber` 匹配,回退到第一个启用的同名行。硬编码 `better-sidebar` 会在聚合包(`web-ui-better-sidebar` 之类)下静默失配。
|
||||
- `viewOf()` = `describe({redactSecrets:true}).find(d => d.ns === ns)`;写入 `update(ns, patch, expectedRevision)`(`SettingsConflictError` 仍导出、构造签名未变,409 分支保留)。
|
||||
- `configure({ auto: false }, ctx.fiber)` 关掉 DSH 的自动表单,避免同一批字段渲染两遍。
|
||||
- **一次性 legacy 导入**(见 §2.2)。
|
||||
- **放弃 `settings/document-updated` 监听**:cordis 的事件只向上冒泡到该 ctx 的祖先,而 settings 服务是在它自己的 context 上 `emit`,插件的 fiber 是兄弟,监听必然不触发。改用「客户端每次重新读取表单时重新求值」覆盖同样的触发点。
|
||||
|
||||
### 2.2 用户设置会静默丢失 —— 已定位并修复
|
||||
|
||||
0.1.7 删除了 file-backed settings provider。迁移时它把 `$DSH_HOME/settings.yaml` **先改名成 `.imported`**,再逐段导入,导入键是「同名 section → 同名 entry id」。插件的 section 键是**包名** `dsh-better-sidebar`,而它的行 id 是 `better-sidebar`,`LEGACY_SECTION_ENTRIES` 里也没有这个例外——**导入必然 warn 失败,section 只留在被改名的文件里**。
|
||||
|
||||
这不是理论问题:本机 `~/.dsh/settings.yaml`(63 KB)里就有一份真实的 `dsh-better-sidebar` 段,含 `tabsEnabled`(关掉了三个第三方 tab)、`viewersEnabled`、`titleBarScheme`、`titleBarStripPx`、`editorExplorer` 等。
|
||||
|
||||
修复:插件自己在 `ctx.inject(['settings','profileContext'])` 里读 `join(profileContext.home, 'settings.yaml.imported')`(退化再读 `settings.yaml`),取出该段,**按当前 `Config` 声明的字段过滤**(段里还留着 `terminalFontSize` / `browserInterceptHttp` / `defaultWidthPercent` 等已删字段,原样转发会让整个 patch 校验失败),再 `update(entryId, section)`。**只在行的用户层为空时执行**,所以幂等,且永不覆盖升级后的新值。
|
||||
|
||||
### 2.3 `ui-primitives` 图标整族改名
|
||||
|
||||
`Icon<Name><14|16>` → `Icon<Name>Regular`(1px)/ `Icon<Name>Medium`(1.3px)。具名导出 81 → 184,**旧尺寸后缀名一个不剩**。插件 26 个具名导入消失、17 个文件受影响、107 处替换。
|
||||
|
||||
两处容易踩空的地方:
|
||||
|
||||
1. **不能全局替换**:`src/client/icons.tsx` 里有 13 个**插件自绘**的同风格 glyph(`IconImageOutline16` / `IconVscode16` …)。改名集合是从「`import ... from '@deepseek-ai/dsh-client-ui-primitives'` 的具名列表 ∩ 已不存在于该包」算出来的,机械替换 26 个符号,13 个自绘 glyph 原样保留。
|
||||
2. **失败模式是 render 期**:插件 client bundle 是 CJS + external,产物是裸属性读,缺导出 = `undefined` → React 抛 `Element type is invalid`,不是 ESM link error;而 `pnpm typecheck` 会直接报 `no exported member`,所以**类型检查是这一层唯一的守护**(此前测试对图标名零断言,本次补了 `tests/primitives-exports.spec.ts`)。
|
||||
|
||||
**静默尺寸回归**:0.1.6 的 `IconXxx14` 默认 `size = 14`,0.1.7 的 `XxxRegular` 一律默认 16。5 处原先依赖默认值的调用点(`TabBar.tsx` / `Sidebar.tsx` / `FileTree.tsx` 的 `IconCloseFillRegular`、`SubagentView.tsx` 两处 `IconRefreshOutlineRegular`)已显式传 `size={14}`,否则会静默变大 2px。另有 `StateDot` 的 ongoing 态被整图重画(10px 像素矩阵 → 14px 圆环 spinner,默认尺寸 10 → 14),插件选择跟随宿主观感。
|
||||
|
||||
### 2.4 会话格式 v3 → v4
|
||||
|
||||
**`source.kind === 'plugin'` 被硬拒**(`format v4 message requires a producer-owned source kind`),而插件的 sidechat 边界注入正是那个形状。改用 **`plugin:dsh-better-sidebar`**——这不是随手取的字符串:DSH 自己的 v3→v4 迁移对未知插件名产出的正是 `` `plugin:${plugin}` ``,用同一个值意味着**新写的数据与迁移后的历史数据形状统一**,`assertV4MessageSources` 通过,历史 sidechat 会话也不会丢。类型层面另需一条 `declare module '@deepseek-ai/dsh-llm'` 的 `MessageSourceMap` 增强(0.1.7 把它改成合并可扩展的封闭联合,没有共享 catch-all)。
|
||||
|
||||
**tool 结果消息重写**:`role: 'user'` + 嵌套 `type: 'tool-result'` → `role: 'tool'` + **顶层** `content` / `isError` / `toolCallId`。插件 4 个解析器原先靠 `candidate.type === 'tool-result'` 命中,在新形状下会 continue 到底、恒返回空串——**工具输出、jobs 回放、转录、文件变动四处一起静默空白**。现在四处都同时接受新旧形状(旧分支必须保留:历史日志里就是旧形状)。
|
||||
|
||||
### 2.5 客户端会话快照换字段
|
||||
|
||||
`SessionListState.subagentsByParent` / `jobsBySession` → `projectionsBySession`。并且 **`setSubagentCatalogOpen` 这个按需观察 API 被整体删除**——0.1.7 在连接建立时一次性加载所有 Session 的 projection,所以那套 observe/unobserve 机制应当**删除**而不是改名;这是本版唯一一处「适配反而简化了代码」。
|
||||
|
||||
- 子代理目录改读 `projectionsBySession[sid]?.values.subagentCatalog`(`{id, createdAt, mode: 'one-shot'|'continuable'|'unknown', label?}`)。旧 catalog 条目带的 `activity` / `hasChildren` 不在新投影里:`hasChildren` 由「该子会话自己的投影是否有非空 catalog」推导,`activity` 由已有的 live 通道(`subagents.live`)提供。`mode: 'unknown'` 是新值,凡穷举 mode 处都要容纳。
|
||||
- 后台任务列表**没有替代字段**,新增插件路由 `jobs.list`(读 `ctx.jobs.list(sessionId)`)。这比旧的推送镜像更权威:镜像是 last-wins 的帧折叠,注册表是状态本身。
|
||||
- 同一版里 `ctx.jobs.list/get/read/kill` 的 caller 从 `Agent` 变成 `SessionId`,插件因此可以**彻底不碰 `ctx.agents`** 来调注册表(`src/jobs-routes.ts` 删掉了整个 `agents` 查找)。
|
||||
|
||||
### 2.6 依赖与测试 harness
|
||||
|
||||
- peer 下限 `^0.1.6-alpha.2` → `^0.1.7-alpha.1`(semver 预发布规则:前者数学上不含任何 `0.1.7-*`);`@deepseek-ai/cordis` peer `^4.0.2` → `^4.0.3`。
|
||||
- devDependencies 补 **4 个**:`@deepseek-ai/dsh-client-store`、`@deepseek-ai/dsh-util-workspace-path`(`ui-primitives@0.1.7` 产物新裸 import 的两个包)、以及**这两个包自己**裸 import 的 `zustand@~4.4.7` 与 `immer@^10.1.1`(它们同样不声明 `dependencies`)。只补前两个,vitest 一加载就 `Cannot find package 'zustand'`。反过来**没有**预加 `dsh-workspace` / `dsh-agent-preset-registry`:实测 `dsh-subagent@0.1.7` 的 28 个 `lib/*.js` 从不 import 它们。
|
||||
- `tests/browser-globals.ts` 的假 `window` 补 `addEventListener` / `removeEventListener` / `dispatchEvent` / `matchMedia`。0.1.7 的 `ui-primitives` 在**模块求值期**就 `window.addEventListener('scroll'|'resize'|'blur')`,缺它会让**任何 import `service.ts` 的 spec 一个用例都跑不起来**(`TypeError: window.addEventListener is not a function`)——用 HEAD 干净 worktree 复现过,确认不是本次删除造成的。
|
||||
- `package.json.files` 补 `lib/client-locale.js`(chunk 名在 8 处镜像,`files` 一直漏这一个,locale chunk 从来没进过 npm tarball)。
|
||||
|
||||
## 3. 静默失效清单(本版的真实风险面)
|
||||
|
||||
| # | 症状 | 根因 | 守护 |
|
||||
|---|---|---|---|
|
||||
| 1 | 侧边卡片设置页只剩默认值、`agentOpenTools` 打不开 | `settings.register` 抛错被 cordis 吞 | `smoke.spec.ts` 的 settings 段 + `configure({auto:false})` |
|
||||
| 2 | 用户既有设置全部丢失 | legacy 段键名与行 id 不一致,导入静默 warn 失败 | 新增两条 legacy 迁移用例(含「已有用户值时不动」) |
|
||||
| 3 | 工具输出 / jobs 回放 / 转录 / 文件变动四处空白 | `type === 'tool-result'` 在新形状下不再命中 | 4 个解析器各有真实生产者 round-trip 用例 |
|
||||
| 4 | 任务管理页空列表 | `subagentsByParent` / `jobsBySession` 被删且读处全带 `?.` | 新 spec 断言从投影推导拓扑 + jobs 基线 |
|
||||
| 5 | 5 处图标静默变大 2px | 图标默认尺寸 14 → 16 | 显式 `size={14}` |
|
||||
| 6 | 插件自绘 markdown 里 http 链接点了没反应 | 上一版交还浏览器时兜底仍指向已不存在的 `'browser'` 类型 | `link-intercept.spec.ts` 新增 12 例 |
|
||||
| 7 | 文件树一直陈旧 | 插件接管 `files` kind,宿主自己的 watch 覆盖不到 | `fs-watch.spec.ts` 5 例 |
|
||||
|
||||
第 6 条是**上一轮自己引入的回归**,本次一并修掉:`openInSidebar` 只认领声明了 `urlTarget` 的链接,其余一律放行给宿主(DSH 0.1.7 起目的地由用户设置 `linkOpening` + 宿主决定),认领成功但目标不可用时 `window.open(..., 'noopener,noreferrer')` 兜底。
|
||||
|
||||
## 4. 取舍(用户拍板,逐条记录)
|
||||
|
||||
| 议题 | 选择 | 代价 |
|
||||
|---|---|---|
|
||||
| 支持线 | 只支持 `^0.1.7-alpha.1`,不写兼容层 | 0.1.6 及更早用户停在 v0.19.1 |
|
||||
| 发版 | `0.21.0-alpha.1`,dist-tag `alpha` | npm `latest` 仍是 0.19.1,新用户默认装不到 |
|
||||
| 设置架构 | 迁到 entry-config 模型 **+** 自己做 legacy 导入 | 约 60 行新增,换回「用户设置不丢」 |
|
||||
| 只读预览 | 让出 image / pdf / 电子表格 / Office 给内置 | 插件设置页少 3 行 viewer 开关;换来宿主的 Office→PDF、表格 worker、缩放 |
|
||||
| 文件树 | 继续接管 **+** 本次实现目录 watch | 新增宿主 WS 路由 + 客户端订阅(约 300 行) |
|
||||
| 外链 | 收敛为「只认领 urlTarget」 | 删掉 3 个偏好键(含 20 份词典 key) |
|
||||
| 视觉 | 尺寸钉回 14,`StateDot` 跟随宿主新观感 | 与宿主其他进行中状态一致,少一处硬编码 |
|
||||
|
||||
**保留在插件侧的三件**(内置做不了的):Markdown 自带渲染器、HTML 沙箱预览(`htmlViewerNoSandbox` / `htmlViewerDefaultUnsafe` 宿主没有)、以及 `code` catch-all 的**可编辑 + 保存**能力。未知二进制(`.zip` / `.wasm`)仍走 `code` 认领 → 宿主 fsRead 判 binary → head 重匹配无 sniffer → `EditorHost.tsx` 的 `binary` 兜底渲染下载面板,**功能不回归**(`builtins.spec.ts` 用 `planFsReadOutcome` 把这条链钉住)。
|
||||
|
||||
## 5. 落地偏差(与计划的差异,逐条说明)
|
||||
|
||||
1. **`MessageSourceMap` 模块增强是计划外的必需品**。计划只写了「改 kind 值」,实测仅改值会留下 `TS2322`:0.1.7 把消息 source 改成合并可扩展的封闭联合,没有共享 catch-all。按上游 `dsh-time-context` / `dsh-tmux-context` 的同款接缝在 `src/sidechat-routes.ts` 里加了 `declare module '@deepseek-ai/dsh-llm'`。
|
||||
2. **图标改名范围比侦察报告小**:报告说 39 个值符号里 26 个消失,实际要做改名的是 26 个(`IconProps` 是类型,13 个是插件自绘 glyph)。
|
||||
3. **`ui-primitives` 的裸 import 闭包比报告深一层**:报告给的是 2 个 `@deepseek-ai` 包,实际还要补 `zustand` / `immer`(第二层)。这是交叉验证阶段证伪的 8 条之一。
|
||||
4. **`setSubagentCatalogOpen` 是「删除」而不是「替代」**:计划预期有替代 API,实际 0.1.7 改成全量加载投影,那套机制整体不需要了。
|
||||
5. **`jobs.list` 是计划外的新路由**:计划里只写了「改数据源」,实际旧的 jobs 镜像在 0.1.7 没有任何替代,只能自己开路由。
|
||||
6. **`exchangeLaunchCookie` 换成 Playwright request context**:计划里列为「已知 e2e 阻塞」,落地时确认宿主静态 fallback 对纯 Node 的 GET 回 **空体 405 且无 `Set-Cookie`**(0.1.5-rc.2 与 0.1.7-alpha.1 都复现),改用 `request.newContext()` + `maxRedirects: 0`。
|
||||
7. **schemastery 必须换成 DSH 的构建(计划里标为「未验证」的风险,真机证实为必须)**:原计划认为「宿主只鸭子判定 `meta.volatile`,不必换依赖」。真机上 `settings.update` 返回 200、`revision` 递增,但 `describe().value` 仍是旧值——写入被静默丢弃。根因在 DSH 的 schemastery 构建:它在解析期把 volatile 字段包成 cosmokit `Volatile` 引用,Loader 的 `_commitVolatile` 提交的正是那些引用;公开的 `schemastery` 包只设标记、产普通值,于是「提交」无事可做。附带第二个坑:`.volatile()` 经 `extra()` 返回副本,写成 `for (const f of ...) f.volatile()` 会丢掉标记,宿主随即报 `Plugin entry "better-sidebar" has no volatile fields`。
|
||||
8. **`tests/browser-globals.ts` 的修复不在原计划里**:这是「跑起来才发现」的阻塞,由执行子代理用 HEAD 干净 worktree 复现归因后修的。
|
||||
9. **README/AGENTS 里「v0.20.0 已是 npm latest」的表述是错的**:实测 npm `latest` 是 `0.19.1`,0.20.0 从未发布。文档已按事实改写。
|
||||
|
||||
## 6. 验证证据
|
||||
|
||||
**门禁**(见本仓库 CI:`plugin-mount` 用真实 0.1.7-alpha.1 scratch profile 跑 Playwright 无头渲染):
|
||||
|
||||
- `pnpm peers check` 干净;`pnpm typecheck` / `pnpm lint` / `pnpm check:consumer-types` 通过。
|
||||
- `pnpm test` 全绿(含本次新增的 `fs-watch.spec.ts`、`primitives-exports.spec.ts`,以及 `smoke.spec.ts` 的两条 legacy 迁移用例)。
|
||||
- `pnpm build` 只产出 `client-editor.js` / `client-mermaid.js` / `client-locale.js`。
|
||||
- `pnpm test:mount` 在真实 `@deepseek-ai/dsh@0.1.7-alpha.1` 上通过。
|
||||
|
||||
**子代理的三处反证**(比正向通过更有说服力):
|
||||
|
||||
1. tool 结果解析:把新形状分支改回旧行为 → **16 failed / 88 passed**,4 条真实生产者 round-trip 全挂、jobs 回放 5 条挂;恢复后 104 全绿。
|
||||
2. 让出的预览格式:`builtins.spec.ts` 用 26 个扩展名逐条断言 `editor` **不**认领,同时断言 `md` / `html` / 代码扩展名**仍然**认领。
|
||||
3. 链接接管:新增用例断言「未命中 `urlTarget` 时 `defaultPrevented === false` 且不调用任何打开函数」,以及「命中但被禁用时走 `window.open` 兜底」。
|
||||
|
||||
## 7. 真机升级经验(把 3080 的 web 实例从 0.1.5-rc.2 升到 0.1.7-alpha.1)
|
||||
|
||||
这段是本仓库「下一次升 DSH 时先读这里」的部分——四条都不是插件代码的问题,但都会让升级看起来像插件坏了。
|
||||
|
||||
1. **profile 的解析层会留着上一代的痕迹**。`$DSH_HOME/profiles/node_modules/@deepseek-ai/` 下曾有 **240 个指向旧 npx 缓存**(`~/.npm/_npx/c40503fdf38a82ea`,即 0.1.5-rc.2)的符号链接——那是 0.1.5 的 fallback 机制,而 0.1.7 已把它换成进程内 runtime interception,**既不更新也不清理**这些链接。结果是 0.1.5 的包在世界里占着名字,`settings` 插件直接 `TypeError: this.load is not a function` 起不来。清掉该目录即可(已留 `profiles/node_modules.bak-<stamp>-legacy-links.txt` 记录每条链接的原目标)。
|
||||
2. **profile 里会有一个陈旧的 `@deepseek-ai/dsh-settings@0.0.1-rc.1` 影子包**:一个早已被禁用的第三方插件声明了 `^0.0.1-rc.1` peer,pnpm 就把整张图解析到那个远古版本,而它会**盖住宿主的 settings 服务**。修法是在 `profiles/web/pnpm-workspace.yaml` 加 `overrides:` 把它钉到宿主版本(pnpm 11 已不再读 package.json 的 `pnpm.overrides`,写了会**静默忽略**并只打一行 WARN)。
|
||||
3. **`dsh plugin --profile web install` 会短路**:即使 `node_modules` 已被删掉、tarball 内容已变,它仍报 "Already up to date" 而什么都不装(`--force` 也一样)。改依赖后请直接在 profile 目录跑 `pnpm install`。
|
||||
4. **`disabled-plugins.json` 在 0.1.7 是死文件**(全树 `git grep disabled-plugins` 零命中)。禁用 bundle 现在只能改 `dsh.profile.bundles`;保留 `dependencies` 里的声明,重新启用就是一行的事。
|
||||
|
||||
顺带修掉一个**真实的升级阻塞**:`pm2` 的 `dsh-web` 曾经重启 **22321** 次——一个上一轮会话遗留的游离进程占着 3080,pm2 的实例每次启动都 `EADDRINUSE`(错误日志涨到 121 MB)。定向 kill 掉占端口那个进程、改完版本再 `pm2 restart` 即恢复。
|
||||
|
||||
**插件侧在真机上暴露的一个真 bug**(已修,见 `99892b2`):一次性 legacy 设置迁移从没跑过。它在 `ctx.inject(['settings'], …)` 回调里启动,而那个回调在 settings **服务**出现的瞬间就触发——此时 Loader 还在挂载各行,`describe()` 里还没有本插件那一行,于是查找返回 undefined、迁移以「no form」静默收场。上游自己的迁移会等 `loader.await()`,现在照做。**这也是为什么所有早退分支都必须可观测**:迁移现在返回具名 outcome 并由调用方记录,否则这类「什么都不发生」永远查不出来。
|
||||
@@ -1,115 +0,0 @@
|
||||
# DSH 0.1.7-rc.1 适配记录
|
||||
|
||||
> 适配对象:`dsh-better-sidebar` v0.21.0-alpha.1(支持 DSH 0.1.7-alpha.1)→ **v0.21.1 正式版(仅支持 DSH 0.1.7-rc.1+)**(中途曾在 `alpha` 通道切过一版 `0.21.0-rc.1`,见 §8)。
|
||||
> 基线标签:`dsh-v0.1.7-alpha.1`(`c36a83ff6b`)→ `dsh-v0.1.7-rc.1`(`46a7f68b09`),相隔 **318 个 commit / 1361 个文件变更**(中间还跨过一个没适配的 `0.1.7-alpha.2`)。
|
||||
> 本文记录**为什么这么改**与**改完的证据**;规则性结论沉淀在仓库根 `AGENTS.md` §3(新增 §3.4 第 12 条)。上一阶段见 [2026-09-22-dsh-0.1.7-alpha.1-adaptation.md](2026-09-22-dsh-0.1.7-alpha.1-adaptation.md)。
|
||||
|
||||
## 1. 怎么做的(方法与边界)
|
||||
|
||||
先 `git fetch` 把 `dsh-v0.1.7-alpha.2` / `dsh-v0.1.7-rc.1` 两个 tag 取进本地 DSH 克隆,然后按「与本插件相交的 surface」分五路并行侦察:启动预检与展示元数据、`ui-primitives` 导出面、会话/会话快照/流式契约、平台契约(file-address / settings / webserver / gateway / 客户端 bundle 装载)、宿主文档预览能力盘点。**五路的结论一致指向「本版是发行候选收敛版,不是新一轮重写」**:绝大多数「变更文件」是 `package.json` 的版本号与 `workspace:^` → `workspace:*|~` 改写,`src/` 逐字未变。
|
||||
|
||||
侦察子的报告一律当线索处理,本轮**亲自复核**了四件事:`DiffBlockLabels` 的字段增删(读两个 tag 的 `DiffBlock.tsx`)、宿主渲染器覆盖的扩展名集合(读 `documentpreview/src/client/*/index.ts` 与 `document/unviewable.ts`)、`MAX_DESCRIBED_ENTRIES` 的真实归属(三 tag 逐字比对 `GuideBody.tsx`)、以及 icon 具名导入的真实条数(脚本提取导入面,得 25 而不是文档里记的 26)。
|
||||
|
||||
## 2. 本版到底改了什么
|
||||
|
||||
**零 `src/` 变化**(只有版本号与依赖面改写):`ui-sidebar-right`、`ui-slots`、`ui-settings`、`dsh-settings` 与 loader 的 volatile 机制、`webserver` + `client/connection` 的鉴权、`packages/session` 全线(**会话格式仍是 v4**,没有 v4→v5)、`llm` 的 `MessageSourceMap`、`subagent`(descriptor 仍是 v3)、`ui-conversation` 的槽锚点与 `turnTail`、客户端 externals 白名单(9 个 specifier 逐字不变)、`util/workspace-path` 的 `fileAddressFor`/`parseFileAddress`、`plugin-artwork.tsx` / `package-meta.ts` / `scripts/verify-package-meta.ts`(后两者逐字相同,所以图标与 locale 的三条硬规则、以及「没有任何尺寸校验」都仍然成立)。
|
||||
|
||||
真实变化只有两类:
|
||||
|
||||
1. **`ui-primitives` 的代码卡**(唯一会编译失败的一处):新增 `CodeToolbar.tsx`(alpha.1 上不存在),其 `CodeToolbarLabels` 的 `codeLabel` / `wrapLabel` / `unwrapLabel` **三个必填**;`DiffBlockLabels` / `ReadBlockLabels` 改为继承它,且**删除了 `DiffBlockLabels.files`**。icon 模块只**新增**了 `IconUsersOutline{Artwork,Regular,Medium}`,无删除、无改名、既有 icon 默认尺寸也没动。
|
||||
2. **启动期 peer 预检**(rc.1 新文件 `packages/boot/app-boot/src/plugin-compatibility.ts`):见 §4。
|
||||
|
||||
另外 rc.1 把 **vendor 补丁**一起抬了:`@deepseek-ai/cordis` 4.0.3 → 4.0.4、`schemastery` 3.18.3 → 3.18.4、`cordis-plugin-group` 1.0.3 → 1.0.4、`cordis-plugin-include` 1.0.8 → 1.0.9(loader 已是 1.0.5)。这是上游 workspace-release-ranges 政策的直接后果——包内 DSH 引用发布成**精确版本**,vendor 用 `~`。
|
||||
|
||||
## 3. 插件侧改了什么
|
||||
|
||||
### 3.1 版本与依赖面
|
||||
`package.json` / `dsh.plugin.json` / `src/client/service.ts` 的 `SIDEBAR_SERVICE_VERSION` 同步到 `0.21.0-rc.1`;14 个 DSH peer 从 `^0.1.7-alpha.1` 收到 `^0.1.7-rc.1`;28 个 devDependency 钉 `0.1.7-rc.1`。
|
||||
|
||||
**只改这两处不够**:`pnpm peers check` 一次报出四类 unmet peer——`cordis-plugin-group` / `-include` 是 `dsh-app-boot` / `dsh-config-editor` 的**传递 peer**,pnpm 会继续沿用 lockfile 里的旧副本。按 AGENTS.md §3.4 第 8 条的既有做法,把 `@deepseek-ai/cordis@4.0.4`、`@deepseek-ai/cordis-plugin-group@1.0.4`、`@deepseek-ai/cordis-plugin-include@1.0.9` 三个**显式钉进 devDependencies**(并抬 `schemastery` 到 `^3.18.4`)后收敛。
|
||||
|
||||
### 3.2 代码卡标签(唯一的编译破坏)
|
||||
`src/client/SideChatView.tsx` 的 diff / read 两个字典补上三个新键、删掉 `files`;`sideChatBlockFiles` 词条从 20 份词典中删除。**不放进 `shared`**:`TerminalBlockLabels` 不继承 `CodeToolbarLabels`,而 `terminal: { ...shared }` 是新鲜对象字面量,会触发多余属性检查。
|
||||
|
||||
**红证据**:切换 devDeps 到 rc.1 后 `pnpm typecheck` 报
|
||||
`Type '{ files: …; copy: string; … }' is missing the following properties from type 'DiffBlockLabels': codeLabel, wrapLabel, unwrapLabel`(`src/client/SideChatView.tsx(337,40)`);改完转绿。
|
||||
|
||||
### 3.3 全面采用宿主的新代码卡
|
||||
`src/client/markdown-labels.tsx` 的 `labels.code` 补 `toolbarLabels`,措辞照抄宿主(`codeBlock.title|wrap|unwrap`),四个 markdown 渲染点一并从旧的 `data-code-block-banner` info banner 切到新的 code card。`MarkdownCopyLabels` 由 2 字段扩到 5 字段,三个构造点(`TextEditor.tsx` / `DiffPane.tsx` / `SideChatView.tsx`)与两个测试 fixture 同步。
|
||||
|
||||
**这个改动带出一个真实回归,且当场被测试抓到**:新 card 对没有语法高亮的语言显示 `codeLabel`,而 mermaid 正是没有 shiki grammar 的语言——**info string 不再进 DOM**,于是 `src/client/mermaid.tsx` 里那条「读 banner 第一个元素的文本是不是 `mermaid`」的探测恒假,三个 `tests/mermaid-markdown.spec.tsx` 用例立刻红(`the mermaid fence must be swapped for a diagram: expected null not to be null`)。改为按**围栏正文**匹配:`splitMermaidBlocks(text)` 取出 mermaid 正文集合,与 `block.querySelector('code')` 的文本两侧 `trimEnd()` 后比对;`language-mermaid` 类名作为第一探针保留。
|
||||
|
||||
### 3.4 收回 9 个过度让出的扩展名
|
||||
上一版把 27 个扩展名整体让给宿主,但其中 9 个宿主**根本没有渲染器**:`avif` / `ods` 落在 `document/unviewable.ts` 的「已知二进制、无渲染器」表里直接走空状态,`xlsb` / `xlt` / `xltx` / `xltm` / `ots` / `dot` / `dotx` 落回 text 兜底后按二进制判定失败、同样是空状态。**这些文件在让出之前是走插件兜底显示下载面板的**,所以这是上一版自己引入的用户可见回归(rc.1 没有修复它)。现从 `HOST_OWNED_EXTS` 移除这 9 个,剩 18 个;`fods` 继续让出(宿主用纯文本显示这段扁平 XML,比下载面板有用)。
|
||||
|
||||
`canOpen` 此前**零测试覆盖**,这正是回归没被发现的原因;补了双向守护(`tests/native-surface.spec.ts`)。**红证据**:把旧的 27 项清单临时还原后,新用例报 `AssertionError: macro.xlsb: expected false to be true`。
|
||||
|
||||
### 3.5 peer 范围守护
|
||||
`tests/market-manifest.spec.ts` 增加一条:每个 `@deepseek-ai/dsh*` peer 必须是 `^0.1.7-<预发布>` 形式的浮动范围。理由见 §4——**失败是静默的**,而 `pnpm peers check` 看不到它(它校验解析出来的 dev 树,不校验宿主会读的范围形状)。
|
||||
|
||||
### 3.6 钉版与文档
|
||||
CI 的 `npm install -g` 与 `scripts/e2e-common.sh` 的 `DSH_NPX_SPEC` / `DSH_EXPECT_VERSION` 都钉到 `0.1.7-rc.1`,并在注释里写明**必须写精确版本号**:rc.1 上线时 npm `alpha` 指的是 **0.1.7-alpha.2**,rc.1 走的是 `next`。README / README_EN 的 6 处 shields badge、支持线段落、让出扩展名清单与「基线」段同步;指南头部版本行同步;AGENTS.md 见 §6。
|
||||
|
||||
## 4. rc.1 新增的启动期 peer 预检(本版最值得记住的一处)
|
||||
|
||||
`plugin-compatibility.ts` 遍历 profile 行的 `peerDependencies`,对名字为 `@deepseek-ai/dsh` 或以 `@deepseek-ai/dsh-` 开头的条目跑 `semver.satisfies(宿主版本, range, { includePrerelease: true })`;不满足就把整行 `row.disabled = true`,只在 stderr 留一行。四个要点:
|
||||
|
||||
- **只读 peer**,`engines.dsh` 与 `dsh.manifestVersion` 仍然零读者(`package-manifest` 的 README 在 rc.1 逐字未变,它自己的措辞已经落后于代码);
|
||||
- `peerDependenciesMeta.optional` **没有任何豁免**——我们那条 optional 的 `@deepseek-ai/dsh-client-ui-sidebar-right` 照样参与判定;
|
||||
- `includePrerelease` 被强制打开,判定比 npm **宽松**(`^0.1.6-alpha.2` 也能满足 `0.1.7-rc.1`),但**精确版本号或没有预发布子句的范围(如 `^0.1.7`)一定不过**;
|
||||
- 豁免只能写 profile 自己的 `compatibility.json`,没有全局开关也没有环境变量。
|
||||
|
||||
**因此本版的 peer 下限是刻意收紧的**:`0.21.0-alpha.1` 从未发布到 npm(真实用户为零),而修掉 `DiffBlockLabels` 之后同一份代码在 alpha.1 上会崩(alpha.1 的 `DiffBlock` 仍读已被上游删除的 `labels.files`)。声明与真机验证过的宿主一致,比声明一条跑不通的支持线更安全;rc.1 起「声明过宽」的代价还从「可能报错」变成了「静默不加载」。
|
||||
|
||||
## 5. 真机升级(把 3080 的 web 实例从 0.1.7-alpha.1 升到 0.1.7-rc.1)
|
||||
|
||||
改动四处,全部带 `.bak-20260923-141956-pre-rc1` 备份:启动器 `~/.dsh/dsh-web.sh` 的 npx 钉版、profile 的 `dsh-better-sidebar` tarball 指向、`@deepseek-ai/dsh-experimental-agent-team-profile` 0.1.7-alpha.1 → 0.1.7-rc.1、profile 的 `pnpm-workspace.yaml` override(`@deepseek-ai/dsh-settings` → 0.1.7-rc.1)。随后在 profile 目录跑**纯 `pnpm install`**(不能走 `dsh plugin install`,它对内容变化会报「Already up to date」短路),再重启 pm2 app `dsh-web`。
|
||||
|
||||
**`@deepseek-ai/dsh-experimental-agent-team-web-profile` 保持 0.1.6-alpha.2 不动**——计划阶段曾打算移除它,核对发布元数据后推翻:它唯一的 peer 是 `@deepseek-ai/cordis ^4.0.2`,被预检的**名称过滤器直接跳过**,不会被 disable;其 `dependencies` 的 `^0.1.6-alpha.2` 今天就已经在解析一份独立的 0.1.6-alpha.2 副本,与 alpha.1 时的状态同构。实测 boot 日志确认它没有被跳过。
|
||||
|
||||
**boot 日志的实测结论**:只有一行 stderr,且不是我们的插件——
|
||||
|
||||
```
|
||||
dsh: skipping profile bundle "@huanlin/dsh-plugin-better-sidebar-plugin-office": Error: Plugin
|
||||
@huanlin/dsh-plugin-better-sidebar-plugin-office@0.1.2 is incompatible with dsh 0.1.7-rc.1:
|
||||
peerDependencies {"@deepseek-ai/dsh-client-runtime":"^0.0.1-rc.1"}. …
|
||||
```
|
||||
|
||||
`@deepseek-ai/dsh-client-runtime` 是 0.1.7 早就删掉的包,所以这个三方 Office 预览插件在 alpha.1 上本来就是「加载了但跑不对」(上一轮真机排查时那个拖进旧 `dsh-settings@0.0.1-rc.1` 的影子副本也来自它)。rc.1 现在把它**干净地跳过**而不是带着坏 peer 加载,属于状态改善;**它没有 `allow-version` 豁免之外的自愈路径**。npm 上已有 `0.2.0`,其 peer 面(`react` / `dsh-better-sidebar ^0.17.0` / `@deepseek-ai/cordis ^4.0.1`)不含死包,升级即可恢复——**这一条留待用户拍板,本轮未动**。
|
||||
|
||||
重启计数 22334 → 22335(只 +1,无崩溃循环)。
|
||||
|
||||
## 6. 验证证据
|
||||
|
||||
**仓库门禁**(全部退出码 0):`pnpm peers check`(No peer dependency issues found)、`pnpm typecheck`、`pnpm lint`、`pnpm build`、`pnpm check:consumer-types`、`pnpm test` = **1132 passed | 9 skipped / 113 文件**。
|
||||
|
||||
`tests/fs-watch.spec.ts` 的 `treats a repeat add as already-watched` 在并行跑全量时超时过一次;单跑与该 spec 连跑三次都是 ~1.2s 通过(预算 15s),判为 FSEvents 在满负载下的既有抖动,随后重跑全量即绿。
|
||||
|
||||
**挂载冒烟**:`DSH_CMD='npx -y --package @deepseek-ai/dsh@0.1.7-rc.1 dsh' pnpm test:mount` → 真实 rc.1 宿主上 **7/7 通过**(含「挂载 + 内建 tab 全扫」「无错误条 / pageerror / console 错误」「经原生指南逐个打开插件 tab」「经文件树打开 seed 文件强制加载 editor chunk」「mermaid / README 预览」「sidechat 宿主路由」以及两条 perf 度量)。
|
||||
|
||||
⚠️ **这一条差点变成假证据**:`scripts/e2e-common.sh` 的 `e2e_resolve_tarball` 是从仓库根 glob 里**选 mtime 最新的 tarball**、而不是自己打包;第一次跑时根目录还留着上一版的 `dsh-better-sidebar-0.21.0-alpha.1.tgz`,于是那一轮其实是**拿旧产物做的冒烟**(输出里 `tarball:` 一行会写出版本号,现在知道要看它)。删掉旧包、`pnpm build && pnpm pack` 出 `0.21.0-rc.1` 的包后重跑,才是上面这份证据。
|
||||
|
||||
**CI**:`ci` / `ci-windows` / `plugin-mount` 三个 job 全绿(`35874792197`)。这是本分支**第一次** CI 全绿——上一轮那两份提交(09-22 与 09-23)在 `Test` 步红,而本地 `pnpm test` 一直是绿的。根因是上一版新增的 `tests/primitives-exports.spec.ts` 用 `execFileSync('rg', …)` 取文件清单:**ripgrep 在开发机上装了、CI runner 上没有**,于是整个 spec 在**收集阶段**就 `spawnSync rg ENOENT`。已换成 `readdirSync` 走一遍 `src/` + `tests/`(几百个小文件,无外部二进制依赖)。
|
||||
|
||||
**真机(3080)**:宿主跑 DSH 0.1.7-rc.1、插件 0.21.0-rc.1;一次性 token 交换 303 + 签名 cookie 正常;`[data-dsh-better-sidebar]` 挂载;**零 pageerror**;浏览器实际加载的 bundle 里含新的 `codeBlockUnwrap` 且**不含**已删除的 `sideChatBlockFiles`(证明是真机在跑新产物,不是旧缓存)。
|
||||
|
||||
**用户设置的 volatile 提交在 rc.1 上仍然成立**(这是上一轮最难的一处,而本轮抬了 schemastery / cordis):贴边设置表单读到 15 个偏好键、9 键 `tabsEnabled`、用户自己的 `pluginSettings.editor.openWith.pinned = ["vscode"]` 全部保留;把 `autoOpenJobs` 由 `false` 翻成 `true` → `settings.update` 200、revision 0 → 1、重新 `settings.get` 读到新值,再写回 `false` 复原。**一个真机观察(未改)**:表单里仍有 `viewersEnabled.{pdf,binary-download}` 这两个已被删除的 viewer 的键,属于历史遗留的配置面。
|
||||
|
||||
## 7. 落地偏差与未做
|
||||
|
||||
- **回收扩展名的条数**:侦察报告一处写「9 个」、列举却是 10 项;亲自比对宿主渲染器表后确认是 **9 个**(`fods` 会被宿主当纯文本显示,不算死路),最终按 9 个回收、`fods` 保留让出。
|
||||
- **`@deepseek-ai/dsh-experimental-agent-team-web-profile` 未移除**(计划里曾打算移除,核对发布元数据后推翻,理由见 §5)。
|
||||
- **未采用** rc.1 新增的 markdown `fileImages` 委托 / `ImageLightbox` / 本地图片悬浮预览:插件自绘的图片 URL 一律带 `?sessionId=`,而 `parseFileLink` 拒绝含 `?` 的目标,宿主这条新分支拦不到我们的 URL;要用就是新做 UI,不属于适配。
|
||||
- **未把宿主 primitives 抄进插件**:rc.1 新增的官方插件实践文档(`packages/preset/agent-preset/skills/cordis-plugin-development/references/practices.md`)明确建议插件不要 import 宿主 client 包、而是「抄一份并只依赖设计令牌」。我们**有意保留 import**(25 个 icon + 13 个组件)并在 AGENTS.md §3 记下这条分歧:该建议的理由之一是「纯 JS 插件没有类型检查、组件抛错会静默清空槽位」,而本仓库是 TS + `pnpm typecheck` + `check:consumer-types`,并且 `tests/primitives-exports.spec.ts` 已经把「符号消失」从静默失败变成红测试。
|
||||
- **未按同一份实践文档把 `jobs` 路由重构成 `ctx.sessionProjections` 单元**:那是行为等价重构,超出本轮范围。
|
||||
- **`src/client/conversation-draft.ts` 的锚点缺陷未动**:它只认 `[data-slot="conversation"]`(`center-column.ts` 已经是 `main.conversation` 优先的双臂选择器)。经核对这是 **0.1.7-alpha.1 起就存在**的既有缺陷、不是 rc.1 引入,且与本轮变更不同源;记在这里待单独处理。
|
||||
- **未重跑真机 Plugins 页的图标/文案断言**:那条路径(`readPluginMeta` + `ui-plugin-manager` 渲染)在本轮逐字未变,且上一轮已真机验证过;本轮的真机证据集中在「升级后插件仍挂载、无错误、跑的是新产物、设置 volatile 提交仍成立」。
|
||||
|
||||
## 8. 正式版 v0.21.1
|
||||
|
||||
rc.1 适配完成后先切过一版 `0.21.0-rc.1`(含 `-` → 只进 npm `alpha` 通道,GitHub Release 标为 Pre-release)。但 **DSH 0.1.7-rc.1 本身就是上游的正式发布批次**,适配它的插件版本不该停留在预发布通道上,因此重切 **`v0.21.1`**(不含 `-` → npm dist-tag `latest`):
|
||||
|
||||
- 版本号只改字面量:`package.json` / `dsh.plugin.json` / `SIDEBAR_SERVICE_VERSION`;**代码零改动**(`tests/service.spec.ts` 仍然守着 `SIDEBAR_SERVICE_VERSION === pkg.version`)。
|
||||
- peer 下限仍是 `^0.1.7-rc.1`(不变),支持线仍是 **DSH 0.1.7-rc.1 及之后的 0.1.7 线**。
|
||||
- npm `latest` **从 `0.19.1` 前移到 `0.21.1`**;`0.21.0-rc.1` 继续留在 `alpha` 通道上作为历史版本,未撤回。
|
||||
- 因此 README 与 Release 的「按 DSH 版本选插件版本」对照表里,**旧 DSH 一律改成精确钉版**(`dsh plugin --profile web add dsh-better-sidebar@0.19.1` 之类)——`@latest` 从此只服务 0.1.7-rc.1+ 这条线。
|
||||
@@ -1,121 +0,0 @@
|
||||
# Agent Teams 适配 DSH 0.1.7:数据源改 projection、写路由对接真实服务、任务窗复用 FloatingWindow
|
||||
|
||||
日期:2026-09-27 分支:`feat/tasks-graph-polish`(承接 PR #680 的打磨线)
|
||||
|
||||
## 背景(用户原话)
|
||||
|
||||
> 目前是不是没有适配智能体团队功能?包括其任务等功能。注意之前我们另一个分支是有适配的,注意复用我们之前的那个弹窗组件
|
||||
> 保持 KISS 原则,如有任意不清楚的地方,都向我详细确认
|
||||
|
||||
用户确认的三个决策:
|
||||
|
||||
| # | 问题 | 结论 |
|
||||
|---|---|---|
|
||||
| 1 | 团队数据源 | 改读宿主 **Session projection `agentTeam`**,读路径只剩 1 条路由(写) |
|
||||
| 2 | 任务弹窗 | 复用上一轮的 **`FloatingWindow`**(可拖拽 + 可拉伸,只在关闭/Esc 时消失) |
|
||||
| 3 | 验证深度 | **真机跑到有团队为止**:临时 DSH 环境里造真团队会话,看板/任务 CRUD 全验 |
|
||||
|
||||
## 诊断:为什么「没有适配」
|
||||
|
||||
不是没做,是**按 0.1.6 的服务 API 接的,而 0.1.7 把那套 API 删了**。真机(3080)curl 打我们自己的路由:
|
||||
|
||||
```
|
||||
POST /sidebar/api/teams.view {"rootSessionId":"212f7157-…"} → 200 {"available":true,"team":null} 无团队,正常降级
|
||||
POST /sidebar/api/teams.view {"rootSessionId":"session-b98c…"} → 400 team-error "membership.svc.remoteView is not a function"
|
||||
```
|
||||
|
||||
`src/team-routes.ts` 当初镜像的是 0.1.6 的 **Remote 词汇**(`npm pack @deepseek-ai/dsh-experimental-agent-team@0.1.6-alpha.2` 解包核对过:`remoteView` / `remoteCreateTask` / `remoteUpdateTask` 三个方法都在,README 明说它们服务于 `./remote` 导出):
|
||||
|
||||
| | 0.1.6-alpha.2 | 0.1.7-rc.1(实际安装的包) |
|
||||
|---|---|---|
|
||||
| 读 | `remoteView(agent): TeamView` | `listMembers(agent)` / `listTasks(caller)`;官方 Web UI 改读 **Session projection `agentTeam`** |
|
||||
| 写 | `remoteCreateTask` / `remoteUpdateTask` → 返回 `TeamTaskMutationResult` 联合 | `createTask` / `updateTask` → 返回已提交的 `TeamTaskView`,**拒绝改为抛 `TeamError`** |
|
||||
| 冲突 | `{ok:false, error:{code:'team-task-conflict'}}` | 抛 `TeamError`,`code = 'TEAM_TASK_STALE_REVISION'` |
|
||||
|
||||
官方 README(0.1.7)原文:
|
||||
|
||||
> The Web UI reads the shared Session projections and overlays activity from Session status. Task creation and updates belong to Team agents through the service and model tools.
|
||||
|
||||
**后果**:`TeamBoard` 整条团队条从不渲染(`useTeamView` 落进错误分支,页面静默无提示),团队节点富化、任务行、任务弹窗全部连带失效;客户端判的冲突码也是 0.1.6 的 `team-task-conflict`,所以「别人改过、已刷新」永远不会出现。
|
||||
|
||||
## 架构(改后)
|
||||
|
||||
```
|
||||
host(src/)
|
||||
team-routes.ts 只留两条写路由:taskCreate / taskUpdate
|
||||
├ ctx.get('agentTeams') 缺席 → 503 team-error
|
||||
├ ctx.agents.get(root) 未命中 → 404 team-error(团队按定义是 live-led)
|
||||
├ tryMembership 未命中 → 404 team-error
|
||||
├ TEAM_TASK_STALE_REVISION → 409 team-conflict
|
||||
└ 其余 TeamError → 400 team-error
|
||||
wire.ts SidebarErrorCode += 'team-conflict'
|
||||
context-types.ts agentTeams 只镜像写半边;新增 SidebarTeamProjection /
|
||||
SidebarTeamMemberProjection 镜像;projection snapshot 加 agentTeam
|
||||
|
||||
client(src/client/)
|
||||
team-projection.ts 新:读 projectionsBySession[root].values.agentTeam + 派生成员运行状态
|
||||
SubagentView.tsx 删 useTeamView 及其 5s 轮询;team/teamMembers 改为 useMemo 派生
|
||||
tasks-model.ts 节点富化改吃 roster 行(role/name/phase/status/diagnostics)
|
||||
TeamBoard.tsx 成员点、任务 Tag 走 tasks-shared 的统一状态规则;根加 data-team-board
|
||||
TaskWindow.tsx 外壳换 FloatingWindow;动作行下沉到 footer;状态机跟随宿主
|
||||
tasks-shared.tsx taskBlocked / taskStatusLabel / taskTone / taskDotState(任务状态唯一规则)
|
||||
api.ts 删 teamsView;写路由经 writeTask 把失败信封还原成 {ok:false, code, message}
|
||||
```
|
||||
|
||||
### 为什么删掉 `teams.view` 而不是修它
|
||||
|
||||
官方 0.1.7 把「读团队」明确交给 projection,而插件**已经在读同一个快照**(`subagentCatalog` 就是同一个 `projectionsBySession`)。再留一条轮询路由等于把同一份数据读两遍、并继续维护两套降级语义(`{available:false}` / `{team:null}`)。改为一次查表后:
|
||||
|
||||
- `available:false` 与 `team:null` 合并成**一个**状态(projection 缺席 = 这里没有团队),页面行为不变(团队条不渲染、无横幅、无 console error);
|
||||
- 团队数据**跟着 snapshot 推送自动更新**,不需要手动刷新(刷新按钮上的 `team.refresh()` 一并删除);
|
||||
- `subagents.live` 本来就在给每个 child 报 `running`,成员运行状态直接复用,不新增任何请求。
|
||||
|
||||
## 真机验证(可复现)
|
||||
|
||||
### 造一个真团队会话
|
||||
|
||||
host 侧的 Agent Teams 实验层是 profile 层的(`dsh-experimental-agent-team-profile`),headless 通道默认不挂,用 `--patch` 注入同一份 patch 即可,不动 profile:
|
||||
|
||||
```bash
|
||||
DSH_HOME=/Users/menghuan/.dsh-web npx -y --package @deepseek-ai/dsh@0.1.7-rc.1 \
|
||||
dsh headless --patch /tmp/dsh-selfcheck/agent-team-layer.yml --json "<spawn 一个 teammate + 建一条共享任务>"
|
||||
```
|
||||
|
||||
`/tmp/dsh-selfcheck/agent-team-layer.yml` 是 `@deepseek-ai/dsh-experimental-agent-team-profile/cordis.patch.yml` 的逐字复制(三条 insert + 四条 disable)。
|
||||
|
||||
### 证据链(本轮真实产出)
|
||||
|
||||
1. **写路由直连真实服务**(3080,新建构建):
|
||||
- 未 live 的 root → `404 team-error "the tree root is not live in this process"`(**不再是 `remoteView is not a function`**)
|
||||
- `teams.view` → `404 not-found`(路由已删)
|
||||
2. **看板从 projection 渲染**(Playwright,真实浏览器):`[data-team-board]` 出现,文本 `团队任务板团队 2 成员 · 1 任务全部leadprobe-writerlive board probe待办新建任务`。
|
||||
3. **任务窗 = FloatingWindow**:`aria-label=任务详情`、`[data-window-footer]` = `编辑 完成 删除`、**外部点击不关**、Escape 关。
|
||||
4. **claim → complete 全链路**(浏览器点击 → 宿主路由 → 真实 `TeamService`):
|
||||
- 队列中任务点「认领」→ 窗口与看板同时变 `负责人 lead` / `进行中`(Tag 不再是 阻塞)
|
||||
- 再点「完成」→ `已完成`,动作行变 `编辑 重开 删除`
|
||||
- 截图:`/tmp/dsh-selfcheck/22-live-turn.png`、`23-team-board.png`、`24-task-window.png`、`25-after-write.png`
|
||||
5. **单元/集成**:`pnpm typecheck` / `lint` 通过;`pnpm test` **1276 passed / 9 skipped**(131 个 spec,新增 9 例:team-routes 重写 12 例、team-projection 9 例、任务窗状态机 2 例、状态规则 4 例)。
|
||||
|
||||
### 真机抓到的两个单测抓不到的问题(已修)
|
||||
|
||||
| # | 现象 | 根因 | 修法 |
|
||||
|---|---|---|---|
|
||||
| 1 | 队列中任务点「完成」→ 窗口报 `only an in-progress task can complete` | 宿主的任务是状态机(`pending → claim → in_progress → complete → completed → reopen`,`release`/`reassign` 回退为 pending),而窗口只提供「完成」 | 动作行跟随状态:pending 出「认领」(`ready:false` 时禁用,理由是标签已写`阻塞`)、in_progress 出「完成」、completed 出「重开」;新增 `teamTaskClaim` 词条 ×20 |
|
||||
| 2 | 认领之后 Tag 变「阻塞」 | `ready` 的语义只是「pending 且所有 blocker 已完成 → 可以认领」,**对所有非 pending 任务都是 false**,用 `!ready` 判「阻塞」把正在做的任务标成了阻塞 | 抽出唯一规则 `taskBlocked(status, ready) = status === 'pending' && !ready`,`taskStatusLabel` / `taskTone` / `taskDotState` 共用;四处展示点(看板行、任务窗、节点任务行、图卡片)全部改走它 |
|
||||
|
||||
第 2 条是「修源头」而不是补丁:三处各自写了一遍 `ready ? … : 'teamTaskBlocked'`,所以把规则收敛成一个函数,未来只有一个地方会错。
|
||||
|
||||
## 实施偏差记录
|
||||
|
||||
1. **成员 `model` 与 `diagnostics` 从此缺失**:`agentTeam` projection 只带 `{id,name,role,phase,error?}`(已核对 `lib/types/types.d.ts`),0.1.6 的 `listMembers` 才带 `status/model/description/provider/context`。不做额外 host 路由去补(官方面板同样不显示模型):节点详情的「模型」行删除;卡片 meta 行在成员没有 catalog mode 时回退到 `成员` / `部署中` / `出错`。
|
||||
2. **运行状态改为派生**:projection 的 `phase` 是**持久生命周期**(`provisioning | active | failed`),turn 活动由 `subagents.live` 的 `running` 覆盖(官方 README 的 “overlays activity from Session status” 就是这个意思),Lead 行回退到 session list 的 `running`。规则写在 `teamMembersOf` 一处。
|
||||
3. **不做 0.1.6 回退**:不保留 `remote*` 探测。插件这一版本来就要求 0.1.7+,且 0.1.6 的 Web UI 自己在这台机器上已经是重复插入(`ui-agent-team` 被 `-profile` 与 `-web-profile` 两个 bundle 各插一次,见 `dsh --profile web --dump-config` 第 1779/1782 行)——这是既有 profile 事实,本轮不动。
|
||||
4. **`api.teamsTaskUpdate` 返回结果联合而不是抛异常**:`readEnvelope` 对失败信封是**抛** `SidebarApiError`,所以任务窗原本永远走不到 `{ok:false}` 分支(这是本轮唯一一个先写错、被真机证据纠正的地方)。现在 `writeTask()` 只在这一处把失败信封还原成 `{ok:false, code, message}`,让窗口能对 `team-conflict` 说自己的话。
|
||||
5. **动作行从窗口体内挪到 shell 的 footer**:FloatingWindow 的 body 是滚动容器,长描述会把动作推出视野;footer 是固定行。`jobPopActions` 因此去掉自己的上边框(footer 已经画了分隔)。
|
||||
6. **看板开关未改**:团队条的折叠箭头仍是旋转的 chevron(上一轮「折叠/展开分向」的结论只应用于图卡片簇),本轮不动以免扩大改动面。
|
||||
|
||||
## 已知限制
|
||||
|
||||
- **写路由需要 live 的 lead**:冷会话 / 另一个进程持有的会话 → 404。团队按定义是 live-led 的(服务要求把 live Agent 当权限凭证),这是结构性事实而非缺陷;页面在这种情况下仍能显示看板(projection 是持久的),只是动作会被拒。
|
||||
- projection 对「本客户端从未加载过的会话」是否可用:真机上验证过**新会话 + 重启后**两条路径都可用(浏览器是从 `projectionsBySession` 读的,与 `subagentCatalog` 同源);但如果将来宿主改成按需加载 projection,这块会退化成空看板而不是报错。
|
||||
- 每个成员一行、每个任务一行都在 DOM 里(上限 8 成员 / 256 任务),未做虚拟化。
|
||||
@@ -1,496 +0,0 @@
|
||||
# 文件页 / 文件变动页 UIUX 重构(2026-09-27)
|
||||
|
||||
用户需求(原文要点):
|
||||
|
||||
- 文件页:Ctrl 多选、Shift 多选;复用 DSH 自带的"使用应用打开";像 VS Code 一样给 Git 变更上色;优化性能;优化拖拽上传体验/样式;新增新建文件夹。
|
||||
- 文件变动页:彻底优化全部 UIUX 交互与功能,更简洁易用;保留 Git 视角与 Agent 视角。
|
||||
- 抽取出可复用的全局组件;设计风格与样式参照新的任务管理页;保持 KISS。
|
||||
|
||||
本文件是本次重构的**冻结契约**:所有并行实现必须遵守下面的接口与命名,不得自行改动。
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计基线:任务管理页的原生尺度
|
||||
|
||||
`src/client/tasks-graph.module.css:1-15` 已声明插件原生尺度,本次重构把文件页与文件变动页对齐到它:
|
||||
|
||||
| 维度 | 值 |
|
||||
|---|---|
|
||||
| 正文 | `--dsw-font-xxs-12`(12px);强调 `--dsw-font-xxs-strong-12` |
|
||||
| 元信息 | `--dsw-font-xxxs-11`;微标签 10px |
|
||||
| 行高 | 列表行 **28px**(原 34px);密集行 24px |
|
||||
| 控件 | 28px(宿主 `Button sm` 同高) |
|
||||
| 边 | 结构边 `--dsw-alias-border-l2` 1px;分隔线 `--dsw-alias-border-l1` |
|
||||
| 悬停 | `--dsw-alias-interactive-bg-hover` |
|
||||
| 选中 | `--dsw-alias-interactive-bg-active` + 2px 内嵌强调条(`::before`,上下内缩 4px) |
|
||||
| 圆角阶梯 | 2px 徽标/内联 chip,3px 控件簇,4px 行,8px 卡片 |
|
||||
| 唯一强调色 | `--dsw-alias-state-business-primary`(当前项、关系、live) |
|
||||
| 状态色 | 成功 `--dsw-alias-state-success-primary`、错误 `--dsw-alias-state-error-primary`、警告 `--dsw-alias-state-warn-primary` |
|
||||
| 页面留白 | 12px 横向 gutter;竖直节拍 4 / 6 / 8px |
|
||||
| 头带 | 36px 高,`padding: 0 8px 0 12px`,底 `1px solid --dsw-alias-border-l1` |
|
||||
| 阴影 | 流内面板**无阴影**;仅浮层(popover/window)用 `--dsw-shadow-lv2/3` |
|
||||
| 皮肤契约 | `tests/theme.spec.ts` 逐条扫描 `src/client/**/*.module.css`:任何 `color:` 必须至少含一个 `var(--dsw-*|--ds-*)`,禁止字面色 |
|
||||
|
||||
不可触碰的既有断言(改了会红):
|
||||
|
||||
- `tests/theme.spec.ts` 硬编码 `src/client/tasks-graph.module.css` 里的 `.cardBar[data-running='true']::after`、`@keyframes dsh-tasks-bar-sweep`、`.barActivity`;以及 `src/client/sidebar.module.css` 里的 `.paneCard`。
|
||||
- `tests/file-tree-reference-tail.spec.ts` 要求 `.explorerRef, .explorerCopied { margin-left: auto }` 留在 `sidebar.module.css`。
|
||||
- `tests/panel-host-css.spec.ts` 要求 `sidebar.module.css` 的 `:global([data-dsh-panel-host])` 规则顺序不变。
|
||||
- `tests/locales.spec.ts:109,118` 要求 **20 份词典 key 集合完全相等**:新增 key 必须同时进入全部词典。
|
||||
|
||||
---
|
||||
|
||||
## 2. 全局组件(`src/client/ui/`,由 Lead 冻结)
|
||||
|
||||
新目录 `src/client/ui/`:
|
||||
|
||||
```
|
||||
src/client/ui/index.ts 桶导出(唯一对外入口)
|
||||
src/client/ui/kit.tsx 原子组件
|
||||
src/client/ui/kit.module.css 样式(token-only)
|
||||
src/client/ui/git-status.ts 共享 git 状态存储 + hook
|
||||
```
|
||||
|
||||
### 2.1 `kit.tsx` 导出面
|
||||
|
||||
```ts
|
||||
/** 28/24px 方形图标按钮(toolbar / 行尾操作统一用它)。 */
|
||||
export function IconButton(props: {
|
||||
icon: ReactNode
|
||||
label: string // aria-label + title 默认值
|
||||
title?: string
|
||||
onClick(event: MouseEvent<HTMLButtonElement>): void
|
||||
size?: 'md' | 'sm' // md=28px(默认),sm=24px
|
||||
active?: boolean
|
||||
disabled?: boolean
|
||||
danger?: boolean
|
||||
className?: string
|
||||
}): ReactNode
|
||||
|
||||
/** 过滤/切换 chip(带可选计数)。 */
|
||||
export function Chip(props: {
|
||||
children: ReactNode
|
||||
active?: boolean
|
||||
count?: number
|
||||
title?: string
|
||||
disabled?: boolean
|
||||
onClick(): void
|
||||
className?: string
|
||||
}): ReactNode
|
||||
|
||||
/** 28px 节标题带(左侧标签 + 可选计数 + 右侧动作)。 */
|
||||
export function SectionHeader(props: {
|
||||
label: ReactNode
|
||||
count?: number
|
||||
action?: ReactNode
|
||||
className?: string
|
||||
children?: ReactNode
|
||||
}): ReactNode
|
||||
|
||||
/** 空/加载/错误/警告/提示 单行态(替代 14 个重复类)。 */
|
||||
export function Notice(props: {
|
||||
kind: 'empty' | 'loading' | 'error' | 'warn' | 'hint'
|
||||
tone?: 'page' | 'well' | 'inline' // page=16px 居中,well=4px 12px 8px(默认),inline=10px 微文案
|
||||
role?: string
|
||||
className?: string
|
||||
children: ReactNode
|
||||
}): ReactNode
|
||||
|
||||
/** 危险操作确认弹窗(宿主 Modal + 安静描述段)。 */
|
||||
export function ConfirmDialog(props: {
|
||||
open: boolean
|
||||
title: string
|
||||
description: string
|
||||
confirmLabel: string
|
||||
cancelLabel: string
|
||||
danger?: boolean
|
||||
busy?: boolean
|
||||
onConfirm(): void
|
||||
onClose(): void
|
||||
}): ReactNode
|
||||
|
||||
/** 状态字母/词徽标(Git 变更、op 类型、diff 类型共用一套 tone)。 */
|
||||
export type StatusTone =
|
||||
| 'modified' | 'added' | 'deleted' | 'untracked' | 'renamed'
|
||||
| 'conflict' | 'read' | 'write' | 'edit' | 'neutral'
|
||||
export function StatusBadge(props: {
|
||||
tone: StatusTone
|
||||
title?: string
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}): ReactNode
|
||||
```
|
||||
|
||||
实现约束:
|
||||
|
||||
- 颜色/尺寸全部来自 `kit.module.css` 的 token;`IconButton` 的 `size` 只切 class,不写内联样式。
|
||||
- `ConfirmDialog` 用宿主 `Modal`(`@deepseek-ai/dsh-client-ui-primitives`),footer = 取消(`Button variant="outline"`)+ 主按钮(`variant="primary"`,`disabled={busy}`),描述段用 `.confirmDesc`。
|
||||
- 现有页面的等价类(`.explorerConfirmDesc` / `.gitConfirmDesc` / `.gitEmpty` / `.gitPlaceholder` / `.gitError` / `.filterChip` / `.gitBadge` / `.opKind` …)在各自页面重构时迁移到 kit;迁移期两者可并存,但**不得修改被测试钉住的类**(见 §1)。
|
||||
|
||||
### 2.2 `git-status.ts` 导出面
|
||||
|
||||
共享的 git 状态存储:一个 `sessionId + cwd + worktree` 键一份快照、一个轮询器,供文件树(着色)与文件变动页(列表)同时消费,避免重复 `git status`。
|
||||
|
||||
```ts
|
||||
export type GitTone =
|
||||
| 'modified' | 'added' | 'deleted' | 'untracked' | 'renamed' | 'copied' | 'conflict'
|
||||
|
||||
/** 一行 porcelain XY 的语义(纯函数,可单测)。 */
|
||||
export interface GitFileStatus {
|
||||
letter: string // 展示字母:X 侧优先,其次 Y,'??' → 'U'
|
||||
tone: GitTone
|
||||
staged: boolean
|
||||
unstaged: boolean
|
||||
}
|
||||
export function statusOfXY(xy: string): GitFileStatus | undefined
|
||||
|
||||
export interface GitStatusView {
|
||||
/** null = 尚未拿到首个快照。 */
|
||||
snapshot: GitStatusResult | null
|
||||
loading: boolean
|
||||
error: boolean
|
||||
/** 强制重新拉取(在飞时排队一次,不丢请求)。 */
|
||||
refresh(): void
|
||||
/** 绝对路径 → 状态;干净/未知 → undefined。 */
|
||||
statusOf(absolutePath: string): GitFileStatus | undefined
|
||||
/** 该目录(含其下任意层级)是否有变更。 */
|
||||
dirHasChanges(absoluteDir: string): boolean
|
||||
}
|
||||
|
||||
export function useGitStatus(
|
||||
scope: { sessionId: string; cwd?: string },
|
||||
options?: { worktree?: string; visible?: boolean; pollMs?: number },
|
||||
): GitStatusView
|
||||
```
|
||||
|
||||
实现要点:模块级 `Map<key, Slot>`;`useSyncExternalStore` 订阅;仅当存在 `visible` 订阅者时轮询(默认 2500ms,`mode:'self-scheduling'` 式单飞);`statusOf` 首次调用时按快照惰性构建 `Map<abs, GitFileStatus>` 与"含变更目录" `Set`(O(n·depth) 一次);路径拼接复用 `resolveSidebarPath`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 文件页(`src/client/FileTree.tsx` / `TreePanel.tsx` / `sidebar.module.css`)
|
||||
|
||||
### 3.1 多选(Ctrl/Cmd 多选、Shift 连选)
|
||||
|
||||
- 状态:`selected: Set<string>`(绝对路径)+ `anchor: string | null`(Shift 起点),二者都是 FileTree 内部 state(不持久化)。
|
||||
- 交互(**必须保持现有单击语义不变**,`tests/file-tree-rename-delete.spec.tsx`、`tests/file-tree-drop.spec.tsx` 不得改断言):
|
||||
- 无修饰键单击:文件行 → 打开(并清空选择);目录行 → 展开/收起(并清空选择)。
|
||||
- Ctrl/Cmd + 单击:切换该行选中态,`anchor` 设为该行。
|
||||
- Shift + 单击:以 `anchor` 为起点,在**当前可见行序**(深度优先、展开态决定)上连选区间;`anchor` 不变。
|
||||
- 点击树空白处 / Esc:清空选择。
|
||||
- 右键:目标行未选中时先把选择收敛为该行(VS Code 语义),已选中则保留整个选区。
|
||||
- 视觉:选中行 `--dsw-alias-interactive-bg-active` + 2px 内嵌强调条;与"reveal 高亮"(`--dsw-alias-state-business-tertiary`)区分。
|
||||
- 批量操作条:选区非空时贴在树体顶部(不遮行):`已选择 N 项` + 复制路径 / 删除 / 取消选择。
|
||||
- 删除走 `ConfirmDialog`(`deleteSelectedTitle` / `deleteSelectedDesc`),逐个 `api.fsRemove`(顺序执行,失败停下并进错误条),成功后 `pruneTree` + `onPathDeleted`。
|
||||
- 复制路径写全部选中行的绝对路径(换行分隔)。
|
||||
- 键盘:行保持 `role="button" tabIndex={0}`;`ArrowUp/Down` 可移动焦点(可选,若实现须不破坏现有 Enter/Space)。
|
||||
|
||||
### 3.2 Git 变更着色(VS Code 风格)
|
||||
|
||||
- 数据:`useGitStatus(scope)`(§2.2);不做第二次 `git.status` 拉取。
|
||||
- 文件行:名字按 tone 上色(modified=warn、added/untracked/copied=success、deleted/conflict=error、renamed=business),行尾加 `StatusBadge` 字母(M/A/D/U/R)与 `title`=状态词。**实施更正**:初稿把 conflict 写成 business,落地时按语义改为 error(冲突是需要先处理掉的阻塞态;`StatusBadge` 的 conflict 同样落 error 家族)。
|
||||
- 目录行:其下任意层级有变更时,目录名同样上色(`dirHasChanges`),不显示字母。
|
||||
- Git 不可用/非仓库:完全不上色、不加徽标(静默降级)。
|
||||
- 新增 i18n:`gitStatusModified|Added|Deleted|Untracked|Renamed|Conflict`。
|
||||
|
||||
### 3.3 新建文件夹
|
||||
|
||||
- 宿主新增路由 `fs.mkdir`(`src/index.ts`)+ `mkdirWorkspaceEntry`(`src/fs-operations.ts`);客户端 `api.fsMkdir(scope, path, name)` → `{ path }`。
|
||||
- UI:目录行与根行右键菜单新增"新建文件夹";触发后在**该目录层级顶部**插入内联输入行(与 rename 同一套交互:Enter 提交、Esc 取消、blur 提交、IME 守卫、非法名 `newFolderInvalid`),提交成功即 `retryDir(dir)` 并打开该目录。
|
||||
- 名称规则与 rename 一致:非空、非 `.`/`..`、不含路径分隔符;服务端二次校验;目标已存在 → 报错进错误条。
|
||||
|
||||
### 3.4 性能
|
||||
|
||||
必须落地的四条(有证据的痛点):
|
||||
|
||||
1. **行组件 memo 化**:抽出 `FileRow` / `DirRow`(`React.memo`),把每行的回调与数据做成稳定引用(`useCallback` + ref 读 `dataRef`),使"复制态/拖拽目标/选区/菜单"变化只重渲染受影响的行,而不是整棵树。
|
||||
2. **搜索不再卸载树**:`TreePanel` 用 `hidden` 属性/样式切换结果面板与树(原来 `needle !== ''` 直接卸载 `FileTree`,丢失 level 缓存并在清空时全量重拉,见 `TreePanel.tsx:245`)。
|
||||
3. **level 拉取加世代守卫 + AbortController**:`loadDir` 丢弃过期响应;`refreshTick` 清缓存后并发重拉(不再串行等待);tick 触发时只重拉"根 + 已展开"级别。
|
||||
4. **`fs.tree` 的 `truncated` 不再被丢弃**:该级别底部渲染 `Notice kind="hint"`(新 key `filesTruncated`)。
|
||||
|
||||
另外:`useDirectoryWatch` 每次渲染重建 key/数组的问题一并修掉(入参用 memo 化数组)。
|
||||
|
||||
### 3.5 拖拽上传体验
|
||||
|
||||
- 目标:与任务页同一套视觉(token-only、无硬编码色)。当前 `FileTree.tsx:83-110` 的 SVG 用 `#3964FE` / `#9CE5ED`,改为 `currentColor` + CSS class 上色。
|
||||
- 结构保持(`tests/file-tree-drop.spec.tsx` 断言 `uploadDropZone`、pill 文案、`busy` 抑制、内存拖拽穿透):仍是"树区高亮框 + 左侧聊天邀请卡",但:
|
||||
- 高亮框:2px 虚线 `--dsw-alias-border-l2` + 8px 圆角 + 极轻强调色内衬,去掉大范围遮罩阴影(改为 `--dsw-alias-bg-base` 半透明蒙层);
|
||||
- pill:28px 高、`--dsw-alias-bg-layer-1`、1px `-l2` 边、`--dsw-font-xxxs-11`;
|
||||
- 悬浮到目录行时,目录行整体高亮(现有 `explorerRowDropTarget` 换成新风格)。
|
||||
- 上传进度卡(`UploadOverlay.tsx` / `.uploadOverlay*`):同样对齐新尺度(28px 行、12px 文案、token 色)。
|
||||
|
||||
### 3.6 使用应用打开:复用 DSH 自带能力
|
||||
|
||||
- **删除**插件自研的 open-with 全链路:配置(`sshHost` / `customEditors` / `pinned`)、`src/client/open-with.ts`、`src/client/open-with-settings.tsx`、宿主 `src/open-external.ts` 与 `open.external` 路由、`api.openExternal`、EditorHost 的 openWith props、`builtins/tabs.tsx` 的 `settings.render`(OpenWithSettings),以及对应 20 份词典里的 `openWith*` key 与相关测试。
|
||||
- **改为**消费 DSH 自带的 open-in-app(见 §4):文件行显示宿主探测到的系统关联应用;目录行显示宿主应用目录(VS Code / Cursor / Zed / Finder…)+ 文件管理器显示;宿主不可用时整段隐藏(与宿主自身行为一致)。
|
||||
|
||||
新增/改用的 i18n:`openInApp`(打开方式)、`openInAppDefault`(用默认应用打开)、`revealInFileManager`(在文件管理器中显示)、`openInAppFailed`、`openInAppEmpty`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 宿主接入层(由 Lead 冻结并先行落地)
|
||||
|
||||
### 4.1 `src/client/open-in-app.ts`(新)
|
||||
|
||||
```ts
|
||||
/** 一个可打开当前路径的应用(文件=系统关联;目录=宿主应用目录)。 */
|
||||
export interface OpenInAppEntry {
|
||||
id: string
|
||||
name: string
|
||||
icon: string | null // 文件:PNG/SVG data URL;目录:宿主图标路由
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
/** 宿主 open-in-app 能力的客户端封装(不可用时全部降级为 no-op / null)。 */
|
||||
export interface OpenInApp {
|
||||
/** 宿主是否能把路径交给原生桌面;null = 尚在探测。 */
|
||||
available(): boolean | null
|
||||
/** 目录可用的应用目录(宿主 menu 顺序)。 */
|
||||
directoryApps(): Promise<readonly OpenInAppEntry[]>
|
||||
/** 某个文件当前注册的系统应用;失败 → null。 */
|
||||
fileApps(path: string): Promise<readonly OpenInAppEntry[] | null>
|
||||
/** 用默认应用或指定应用打开(文件或目录)。 */
|
||||
open(path: string, application?: string): Promise<boolean>
|
||||
/** 在系统文件管理器中显示。 */
|
||||
reveal(path: string): Promise<boolean>
|
||||
}
|
||||
|
||||
/** 从客户端 ctx 构造(内部使用 ctx.remote.session.* 与宿主 /open-in-app/* 路由)。 */
|
||||
export function createOpenInApp(ctx: { get(name: string): unknown }): OpenInApp
|
||||
```
|
||||
|
||||
宿主 API(DSH 0.1.7-rc.x 自带,**不得自研替代**):
|
||||
|
||||
- Remote:`ctx.get('remote').session.canOpenWorkspacePath()` / `workspacePathApplications({path})` / `openWorkspacePath({path, action?:'reveal', application?})`(返回值 `{ok, value}`)。
|
||||
- HTTP(同源、带 cookie):`GET <prefix>/open-in-app/apps` → `{apps: string[]}`;`GET <prefix>/open-in-app/icon/<id>` 图标;`POST <prefix>/open-in-app/open` `{app, path}`(**仅目录**)。
|
||||
- 应用名字/图标:宿主 `open-in-app` 命名空间(`app.vscode` 等)经 locale bind 读取,失败回退到 id 本身;图标走宿主 icon 路由。
|
||||
|
||||
### 4.2 FileTree 的接线(冻结 prop)
|
||||
|
||||
`FileTree` 新增可选 prop:
|
||||
|
||||
```ts
|
||||
/** 宿主 open-in-app 句柄(EditorHost 构造并注入;缺省时不渲染该段)。 */
|
||||
openInApp?: OpenInApp
|
||||
```
|
||||
|
||||
右键菜单"打开方式"段的新语义:
|
||||
|
||||
- 文件行:`fileApps(path)` 异步拉取 → 默认应用(`openInAppDefault`,点击 `open(path)`)+ 其余应用各一行(`open(path, id)`)+ 分隔线 + `revealInFileManager`;无应用 → `openInAppEmpty`(禁用行);宿主不可用 → 整段隐藏。
|
||||
- 目录行:`directoryApps()` → 每应用一行(`open(path, id)`)+ `revealInFileManager`。
|
||||
- 失败 → 树顶部错误条(`openInAppFailed`,带路径)。
|
||||
|
||||
### 4.3 `fs.mkdir`
|
||||
|
||||
- 宿主:`src/fs-operations.ts` 新增 `mkdirWorkspaceEntry({cwd, path, name, fence})`(fence 与存在性校验同 rename),`src/index.ts` 注册 `'fs.mkdir'`。
|
||||
- 客户端:`api.fsMkdir(scope, path, name)`。
|
||||
- smoke 测试的路由清单(`tests/smoke.spec.ts:84-90`)**不含方法索引**,无需改;但要为 `mkdirWorkspaceEntry` 增单测。
|
||||
|
||||
---
|
||||
|
||||
## 5. 文件变动页(`src/client/changes/*`)
|
||||
|
||||
目标:**更简洁易用**,保留 Git 与 Agent 双视角。重构后结构:
|
||||
|
||||
```
|
||||
ChangesTab
|
||||
├─ header (36px): 宿主 SegmentedControl[Git | 本轮文件] + 右侧:当前视角的刷新按钮
|
||||
├─ Git 视角 (GitLens)
|
||||
│ ├─ 顶部一行:分支/仓库/工作树选择(仅在有多个候选时出现)+ 状态摘要
|
||||
│ ├─ 变更列表:更改(n) / 已暂存(n) 两组(SectionHeader + 单一 ChangeRow 渲染器)
|
||||
│ └─ 提交条(吸底):Input + 提交按钮 + 单条状态/错误行
|
||||
│ └─ 历史:SectionHeader + log 行 + 加载更多(空态明确)
|
||||
└─ Agent 视角 (SessionLens):Chip 过滤 + 按文件分组行 + 预览
|
||||
└─ DiffPane(共享预览,保留现有能力:redact / md 阅读 / html 渲染 / pdf / 展开为 diff 页签 / 拖拽高度)
|
||||
```
|
||||
|
||||
必须修掉的既有缺陷(来自 §6 证据,逐条给通过/拒绝检查):
|
||||
|
||||
1. 干净的仓库只出现**一处**空态(不再两组各印一次 `noChanges`);历史区有自己的空态与失败态。
|
||||
2. 单一错误通道:暂存/取消暂存失败必须被捕获并显示(不再 unhandled rejection);分支切换错误不再显示在提交框下面,而是统一状态行。
|
||||
3. 手动刷新不再静默丢弃(在飞时排队一次);仓库切换只触发**一次**完整刷新;工作树清单带上 `repoRoot`。
|
||||
4. 工作树不再在用户未操作时被自动切走(仅在首次挂载且用户未选择时自动选一次)。
|
||||
5. 预览按 `callId` 从最新 ops 重新派生(running → 结算后自动更新);恢复高度时同时应用上限 clamp。
|
||||
6. 轮询与共享:git 状态改由 `useGitStatus` 提供(同 session 的文件树共用一份),session ops 仍由本页轮询,但**仅在本视角可见时**轮询。
|
||||
7. `DiffPane` 与 `DiffTab` 的重复加载逻辑抽成一个共享 hook(`useGitDiffTarget`),行为不变(两者都要继续工作)。
|
||||
8. 视觉:全部对齐 §1 的尺度(行 28px、12px 正文、4px 行圆角、token-only),图标按钮/徽标/空态/确认弹窗改用 `src/client/ui/kit`。
|
||||
|
||||
保留能力(不得回归):Git 的暂存/取消暂存/全部操作、提交、分支切换、历史分页、放弃更改、revert、cherry-pick、复制哈希/路径、在工作树/子仓库间切换;Agent 视角的 read/write/edit 过滤、按文件分组、相对时间、字节数、运行中/出错标记;预览面板的全部渲染开关。
|
||||
|
||||
---
|
||||
|
||||
## 6. 验收(每条都要有实际证据)
|
||||
|
||||
| # | 检查 | 通过判据 |
|
||||
|---|---|---|
|
||||
| 1 | 类型/规范 | `pnpm typecheck` + `pnpm lint` 通过 |
|
||||
| 2 | 单元/组件测试 | `pnpm test` 全绿(新增测试覆盖:多选、mkdir、git 着色映射、kit 组件、重构后的 changes 页) |
|
||||
| 3 | 构建 | `pnpm build` 通过(含 CSS module 编译) |
|
||||
| 4 | 皮肤契约 | `tests/theme.spec.ts` 绿;新增 CSS module 无字面色 |
|
||||
| 5 | i18n | `tests/locales.spec.ts` 绿(20 份词典 key 集合相等) |
|
||||
| 6 | 多选 | 组件测试:Ctrl 单击切换、Shift 连选区间、Esc 清空;单击语义不变(既有测试不动仍绿) |
|
||||
| 7 | 着色 | 组件测试:mock `git.status` 后行名带 tone class、目录带 `dirHasChanges` 着色、非仓库无着色 |
|
||||
| 8 | 新建文件夹 | host 单测(fence/已存在/非法名)+ 客户端组件测试(菜单 → 输入 → `api.fsMkdir` 调用参数) |
|
||||
| 9 | open-in-app | 组件测试:宿主可用时渲染应用行、点击调 `open(path, id)`、不可用段隐藏 |
|
||||
| 10 | 性能 | 组件测试或结构性验证:搜索不再卸载树(缓存保留)、level 过期响应被丢弃、`truncated` 有提示 |
|
||||
| 11 | 文件变动 | 组件测试:空仓库只一处空态、暂存失败显示错误、预览随结算更新、视角切换仍保留预览 |
|
||||
| 12 | 真实挂载 | `pnpm build && pnpm pack && pnpm test:mount`(e2e lane)通过 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 写入范围(并行实现不得越界)
|
||||
|
||||
| 范围 | 归属 |
|
||||
|---|---|
|
||||
| `src/client/ui/**`、`docs/plans/2026-09-27-files-changes-uiux.md`、`src/client/locales.ts`(zh/en 新 key) | Lead |
|
||||
| `src/client/FileTree.tsx`、`src/client/TreePanel.tsx`、`src/client/sidebar.module.css`、`src/client/UploadOverlay.tsx`、`src/client/use-dir-watch.ts`、`tests/file-tree-*.spec.tsx`、`tests/editor-host.spec.tsx` | 文件页实现者 |
|
||||
| `src/client/changes/**`、`src/client/diff/**`、`tests/changes-*.spec.*`、`tests/git-view-worktree.spec.tsx`、`tests/sandbox-views.spec.tsx` | 文件变动页实现者 |
|
||||
| `src/index.ts`、`src/fs-operations.ts`、`src/client/api.ts`、`src/client/open-in-app.ts`、`src/client/EditorHost.tsx`、`src/client/builtins/tabs.tsx`、`src/open-external.ts`(删)、`src/client/open-with*.ts(x)`(删) | Lead(宿主接入层,先行落地) |
|
||||
| `src/client/locales-*.ts`(19 份第三方词典) | 词典传播实现者(单一写者) |
|
||||
|
||||
`src/client/locales.ts` 的 zh/en 由 Lead 追加;实现者若确需新 key,必须回报 Lead,不得自行添加(避免 20 份词典冲突)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 实施记录(落地后的偏差与事实)
|
||||
|
||||
### 8.1 交付面
|
||||
|
||||
- **全局组件**:新增 `src/client/ui/`(`kit.tsx` + `kit.module.css` + `git-status.ts` + `index.ts`)与 `tests/ui-kit.spec.tsx`;文件页与文件变动页均已真实复用(IconButton / Chip / SectionHeader / Notice / ConfirmDialog / StatusBadge / useGitStatus)。
|
||||
- **文件页**(`FileTree.tsx` / `TreePanel.tsx` / `sidebar.module.css` / `UploadOverlay.tsx` / `use-dir-watch.ts`):多选(Ctrl/Cmd 切换、Shift 按可见行序连选、Esc/空白/右键收敛)、批量条(复制路径 / 删除所选 / 取消选择)、git 着色(文件行名字上色 + M/A/D/U 徽标,目录行仅着色,非仓库静默降级)、新建文件夹(菜单 + 内联输入 + `api.fsMkdir`)、`FileRow`/`DirRow` memo、level 拉取世代守卫 + AbortController、`fs.tree` truncated 提示、搜索改为 `hidden` 停车不再卸载树、拖拽上传视觉 token 化(原 `#3964FE`/`#9CE5ED` 清零)。
|
||||
- **宿主接入**:`fs.mkdir` 路由 + `mkdirWorkspaceEntry`(`tests/fs-mkdir.spec.ts` 6 例);新增 `src/client/open-in-app.ts`(宿主 Remote `canOpenWorkspacePath`/`workspacePathApplications`/`openWorkspacePath` + 宿主路由 `/open-in-app/apps|icon|open` 的客户端适配,`tests/open-in-app.spec.ts` 10 例)。
|
||||
- **文件变动页**(`changes/**`、`diff/**`、`DiffTab.tsx`):36px 头(宿主 `SegmentedControl` + 刷新 IconButton)、单一 `ChangeRow`、SectionHeader 分组、吸底提交条 + 单一状态行、历史区独立空态、`useGitStatus` 共享状态、刷新排队、工作树只自动选一次、预览按 callId 重派生、高度恢复 clamp、session ops 轮询按视角门控、`DiffPane`/`DiffTab` 共用新 hook `src/client/diff/use-git-diff.ts`(并修掉 DiffPane 的 untracked 预览从不渲染的既有缺陷)。
|
||||
|
||||
### 8.2 相对冻结契约的偏差(均有理由)
|
||||
|
||||
1. **open-with 全链路删除**(而非并存回退):`src/open-external.ts`、`src/client/open-with.ts`、`src/client/open-with-settings.tsx`、`src/client/plugin-settings.ts`(随之成为死代码)、宿主 `open.external` 路由、`api.openExternal`、editor descriptor 的 `settings.render` 面板与 20 个 `openWith*` 词条全部移除;外部打开改为 DSH 自带 open-in-app。**取舍**:原「SSH 远端 + 自定义编辑器 URL 模板」能力随之消失——它服务的场景是 DSH 宿主在远端、浏览器在本地,此时宿主 open-in-app 本就不可用(`canOpenWorkspacePath()` 为 false,整段隐藏,与宿主自身行为一致)。若需要该能力应回到 DSH 侧或单独插件实现。
|
||||
2. **`useSubmenuFlip` 一并删除**:FileTree 的「打开方式」子菜单消失后,`src/client/menu-flip.ts` + `layout.css` 的子菜单翻转规则 + `tests/menu-flip.spec.tsx` 已无消费者,按死代码清除。
|
||||
3. **词典同步为 25 个新键**(非 26):原清单最后一项是插入锚点说明;zh/en 与 19 份词典最终同为 486 键。
|
||||
4. **`visible` 逐层下传**:文件树的 git 轮询必须随页签可见性暂停(工作台会保留所有 tab body 挂载),因此 `EditorHost → TreePanel → FileTree` 新增可选 `visible`,`useGitStatus(..., { visible })` 在页签隐藏时退订。这是实施期补的性能修正(不在原 §3 清单里)。
|
||||
5. **计划 §5 的「状态摘要」行**未单独实现:干净时用 `changesClean` 空态 + 两个 SectionHeader 计数 + 分支标签表达,避免第三处重复计数文案(KISS)。
|
||||
6. **未做浏览器级视觉验证**:拖拽/多选的观感只有 token 与尺度层面的代码证据 + 行为测试;真实挂载 lane(`pnpm test:mount`)见 §8.3。
|
||||
|
||||
### 8.3 验证证据
|
||||
|
||||
- `pnpm typecheck` / `pnpm lint` / `pnpm test`(126 文件、1318 通过、9 跳过)/ `pnpm build` 全绿(§8.6 是修正后的最终数字:1322 通过)。
|
||||
- 新增测试:`tests/ui-kit.spec.tsx`(14)、`tests/fs-mkdir.spec.ts`(6)、`tests/open-in-app.spec.ts`(10)、`tests/file-tree-multiselect.spec.tsx`(8)、`tests/file-tree-git-status.spec.tsx`(4)、`tests/file-tree-new-folder.spec.tsx`(7)、`tests/file-tree-open-in-app.spec.tsx`(8)、`tests/file-tree-row-memo.spec.tsx`(2)、`tests/file-tree-search-persist.spec.tsx`(3)、`tests/file-tree-dir-watch.spec.tsx`(5)、`tests/diff-git-target.spec.tsx`(4),以及 changes-tab / git-view-worktree 的大幅扩充。
|
||||
- 真实挂载 lane:`DSH_CMD='npx -y --package @deepseek-ai/dsh@0.1.7-rc.1 dsh' pnpm test:mount`(本机 PATH 上的 dsh 是 rc.2,脚本按基线拒绝直接运行)→ **7/7 通过**。
|
||||
|
||||
### 8.4 真实挂载 lane 抓到的缺陷(单测全绿也漏掉的)
|
||||
|
||||
`sidbar` 客户端 bundle 缺少 `remote` / `remote.session` 的 cordis inject 声明时,`ctx.get('remote').session` 会抛
|
||||
`dsh-better-sidebar: cannot get property "remote.session" without inject`——**这一抛发生在渲染路径上,会把整个「文件」面板替换成错误条 + Retry**(首轮挂载 lane 的截图与 trace 均记录在案,整个 tab sweep 因此失败)。
|
||||
|
||||
修法两条,缺一不可:
|
||||
|
||||
1. `src/client/index.tsx` 的 `inject` 补上 `'remote'` 与 `'remote.session'`(嵌套属性访问也要单独声明);
|
||||
2. `src/client/open-in-app.ts` 的 `remoteOf()` 加 try/catch:没有该服务组合的部署降级为「无打开方式菜单」,而不是把面板整块打崩(`tests/open-in-app.spec.ts` 新增一条「ctx 抛错时全部降级」用例钉子)。
|
||||
|
||||
这条正好说明为什么 §6 的验收表里 `pnpm test:mount` 不能省:单元/组件测试全部是绿的,只有真实宿主挂载才暴露了这个 inject 契约。
|
||||
|
||||
### 8.5 独立验证后的修正(verifier 报告 → 已全部落地)
|
||||
|
||||
| 编号 | 问题(verifier 证据) | 处置 |
|
||||
|---|---|---|
|
||||
| P1(高) | `open-in-app` 的 availability 是 tri-state(初始 `null`),但生产路径**没有任何地方调用 `probe()`**,于是 `available() !== true` 恒成立、整段菜单永不渲染——「复用 DSH 自带打开方式」实际未交付 | `FileTree` 挂载时按 handle 探测一次:已知答案(`true`/`false`)直接采用,`null` 才 `probe()`,结果进 state 驱动菜单重渲染。新增两条钉死该生命周期的测试(含**真实 adapter + 假 Host Remote** 的端到端用例)。真实挂载截图确认:菜单出现「Open with default app / Visual Studio Code / QuickTime Player / Reveal in File Manager」(宿主探测到的本机关联应用与真实图标) |
|
||||
| P2(中) | `EditorHost` 新增的 `visible` 从未被传入(`builtins/tabs.tsx` 的文件 tab 未解构 `visible`)→ 隐藏页签仍在轮询 git | 文件 tab 的 component 解构并传入 `visible` |
|
||||
| P3(中) | 文件树与文件变动页没有共用同一把 slot 键(GitLens 总把自动选中的 **primary** checkout 当 `worktree` 传)→ 同仓库两条 2.5s 轮询 | GitLens 只在选中 **linked** checkout 时才传 `worktree`(primary 的答案是同一份);`tests/ui-kit.spec.tsx` 增补「同键两个消费者只发一次请求」用例 |
|
||||
| P4(中低) | session 视角每个 tick 重折 ops → 新数组击穿下游 memo → 每次都重新 Blob 编码每行正文 | `pull()` 在「无新事件且游标未前进」时**保留上一次 fold 的引用**,直接返回 |
|
||||
| P5(低) | `noChanges` 成为死键(20 份词典各一条) | 全部删除(`tests/locales.spec.ts` 仍绿) |
|
||||
| P6(低) | 冻结文档写 conflict=business,实现为 error | 以实现为准修正文档(冲突是阻塞态,归 error 家族更合理) |
|
||||
| P7(低) | 新增 `fs.mkdir` 未进接入文档的 fence 路由清单 | 指南 §路由安全边界补上 `fs.mkdir`(并补齐原先漏列的 `fs.rename`/`fs.remove`) |
|
||||
| P8(低) | 行有 `data-dsh-selected` 但无 `aria-pressed` | 文件/目录行按选中态输出 `aria-pressed` |
|
||||
| P9(低) | 手动刷新与定时 tick 可重叠、重复追加同一 delta | `pull()` 单飞(在飞时新请求是 no-op,在飞的那次已覆盖到当下) |
|
||||
|
||||
### 8.6 最终验证(修正之后重跑)
|
||||
|
||||
- `pnpm typecheck` / `pnpm lint` / `pnpm test`:126 文件、**1322 通过**、9 跳过;`pnpm build` 通过。
|
||||
- 真实挂载 lane:`DSH_CMD='npx -y --package @deepseek-ai/dsh@0.1.7-rc.1 dsh' pnpm test:mount` → **7/7 通过**(含 tab sweep、perf lane)。
|
||||
- 截图级证据(临时 Playwright 取证脚本,已删除,不留在仓库):真实宿主上文件树行显示 **M/U 字母与状态色**、右键菜单出现宿主探测到的应用列表、文件变动页显示 36px `Git | Session` 头 + 未暂存/已暂存分组 + 吸底提交条 + 历史。
|
||||
- 仍属未验证:拖拽上传的视觉观感与手动多选手感(只有 token/尺度证据与行为测试)、Windows 宿主差异。
|
||||
|
||||
---
|
||||
|
||||
## 9. 第二轮:open-with 与宿主能力并存 + 打包下载 + 文件变动树(用户追加需求)
|
||||
|
||||
用户追加三条:
|
||||
|
||||
1. **打开方式 = 宿主探测到的本机关联应用 + 插件自研 open-with**(两者并存,不是二选一);
|
||||
2. **文件页多选时右键新增「压缩打包并下载」**;
|
||||
3. **文件变动页再次重构:更美观 + 以层级(树)方式显示更改**。
|
||||
|
||||
### 9.1 冻结接口
|
||||
|
||||
#### A. open-with 恢复并与 open-in-app 并存
|
||||
|
||||
- 恢复 `src/client/open-with.ts`(`OpenWithConfig` / `OpenWithTarget` / `parseOpenWithConfig` / `resolveOpenWithTargets` / `openWithUrl` / `openWithSshActive` / `newCustomEditorId` / `isValidCustomEditor`,与 main 逐字一致)、`src/client/open-with-settings.tsx`、`src/client/plugin-settings.ts`、宿主 `src/open-external.ts` 与 `open.external` 路由、`api.openExternal`、editor descriptor 的 `settings.render`,以及 20 个 `openWith*` 词条。
|
||||
- `FileTree` / `TreePanel` 恢复 main 的可选 props:`openWithTargets` / `openWithPinned` / `openWithSsh` / `onOpenWith` / `onToggleOpenWithPin`,与既有 `openInApp` **并存**。
|
||||
- 菜单形状(`打开方式` 段):
|
||||
1. 宿主行(`openInApp` 可用时):`用默认应用打开` + 各系统关联应用;
|
||||
2. 插件行:pinned 目标直达 + `在应用中打开 >` 子菜单(含图钉,main 原样);**当宿主可用时隐藏插件的 `explorer` 目标**(宿主已有「在文件管理器中显示」,避免重复);
|
||||
3. 分隔线 + 宿主 `在文件管理器中显示`(宿主不可用时该行不出现,插件的 `explorer` 目标保留,保证仍能 reveal)。
|
||||
- 宿主完全不可用(`available() === false` / 无 handle)时:宿主行整段隐藏,插件行照旧可用(SSH 远端场景因此保持可用)。
|
||||
|
||||
#### B. 压缩打包并下载
|
||||
|
||||
- 新增 `src/zip.ts`:`buildZip(entries, opts)` 产出 ZIP(store + `zlib.deflateRaw` 两种方法按压缩收益选择;UTF-8 名称标志位;目录条目;CRC32;无第三方依赖)。
|
||||
- 新增宿主 GET 路由 `/sidebar/archive?sessionId=&cwd=&name=<archive.zip>&path=<p>&path=<p>…`:每个 `path` 经既有 workspace fence 校验,文件/目录(递归)都接受;上限(条目数 / 总字节)超限返回 JSON 错误;成功时 `content-type: application/zip` + `content-disposition: attachment`。
|
||||
- 客户端 `api.archiveUrl(scope, paths, name)`(与 `downloadUrl` 同形的 GET URL)。
|
||||
- 交互:多选(≥2)时右键菜单出现 `压缩并下载({count} 项)`;单选且为目录时同样出现(单文件不出,避免与「下载」重复)。
|
||||
- 新词条:`zipDownload` / `zipDownloadCount` / `zipFailed`。
|
||||
|
||||
#### C. 文件变动页层级显示
|
||||
|
||||
- Git 视角的两个分组(未暂存 / 已暂存)各自渲染为**目录树**:单子目录压缩(`a/b/c` 一行显示为 `a/b`),目录行有 disclosure chevron + 文件夹图标,文件行有文件图标 + 状态字母 + 名称 + 弱化目录;行尾仍可暂存/取消暂存;默认展开。
|
||||
- 纯函数 `buildChangeTree(entries)`(`src/client/changes/change-tree.ts`)产出树 + 压缩规则,独立单测。
|
||||
- 视觉打磨:分组头吸顶、计数胶囊、hover 与选中态、缩进参考线、提交条与历史行间距统一到 §1 尺度。
|
||||
|
||||
### 9.2 分工与写入范围
|
||||
|
||||
| 范围 | 归属 |
|
||||
|---|---|
|
||||
| 计划文档、验证裁决、集成、最终门禁 | Lead |
|
||||
| `src/index.ts`、`src/zip.ts`、`src/client/api.ts`、`src/client/open-with.ts`、`src/client/open-with-settings.tsx`、`src/client/plugin-settings.ts`、`src/open-external.ts`、`src/client/EditorHost.tsx`、`src/client/builtins/tabs.tsx`、相关测试 | 宿主接入实现者 |
|
||||
| `src/client/FileTree.tsx`、`src/client/TreePanel.tsx`、`src/client/sidebar.module.css`、文件页测试 | 文件页实现者 |
|
||||
| `src/client/changes/**`、`src/client/diff/**`、变动页测试 | 文件变动页实现者 |
|
||||
| `src/client/locales-*.ts`(19 份) | 词典实现者 |
|
||||
| 独立验证 | 验证者 |
|
||||
|
||||
### 9.3 实施记录(第二轮)
|
||||
|
||||
- **打开方式并存**(落地形状):菜单段无标题行 —— 宿主行(`用默认应用打开` + 系统关联应用,仅宿主可用)→ 分隔线 → 插件 pinned 直达行 + `在应用中打开 >` 子菜单(图钉 / SSH 后缀原样)→ 分隔线 → 宿主 `在文件管理器中显示`。宿主可用时过滤插件的 `explorer` 目标;宿主不可用/无 handle 时只留插件行(含 `explorer`,reveal 不丢)。
|
||||
- 自研链路逐字恢复:`src/client/open-with.ts`、`open-with-settings.tsx`、`plugin-settings.ts`、`src/open-external.ts`、`open.external` 路由、`api.openExternal`、editor descriptor 的 `settings.render`,以及 20 个 `openWith*` 词条(19 份词典从 `git show main:` 逐字取回)。
|
||||
- 因菜单去掉标题行,`openInApp` 词条成为死键 → 已从 21 份词典删除(zh 507 键)。
|
||||
- 子菜单需要 `useSubmenuFlip`:第一轮删除的 `src/client/menu-flip.ts` + `layout.css` 翻转规则 + `tests/menu-flip.spec.tsx` 一并恢复。
|
||||
- **压缩打包下载**:新增 `src/zip.ts`(自研 ZIP 写入器,无第三方依赖)+ `src/archive-route.ts`(下载名净化 + 递归收集)+ `GET /sidebar/archive` + `api.archiveUrl`;`FileTree` 多选右键「压缩并下载(N 项)」(单选目录亦可,单文件不出),下载走 `fetch → blob → objectURL`,**路由错误(超限 / fence 拒绝 / 非 GET)现在会以 `zipFailed` 显示在错误条**。
|
||||
- 上限:10 000 条目 / 256 MiB 未压缩负载(收集期即中止)。
|
||||
- 实施中由系统 `unzip` 抓到的真实缺陷:central directory 只写 MS-DOS 位而 version-made-by 声称 Unix → Info-ZIP 把每个文件解成 `mode 000`;改为 Unix 语义(`0o100644 * 0x10000`,注意 `<< 16` 溢出 int32)。复验:mode 644/755、UTF-8 名、`unzip -t` 无错。
|
||||
- **文件变动层级树**:新增 `src/client/changes/change-tree.ts`(`buildChangeTree`:'/' 分段、单子目录链压缩成一行、目录先于文件、大小写不敏感排序、文件带 `GitFileStatus`、目录带下级变更数),两个分组各自渲染树;折叠状态按「组 + 路径」独立保存、默认展开;分组头吸顶 + 计数胶囊;`Stage all`/`Unstage all` 改为带 aria-label 的 IconButton;选中 = `interactive-bg-active` + 2px 内嵌强调条;12px/级缩进 + `-l1` 参考线;历史行 hash 徽标 / ref 胶囊 / 作者·时间右对齐。
|
||||
- 契约偏差(已批准):文件行不再重复渲染目录文本(树已用父行表达层级,重复是噪声);`ChangeDir` 额外带 `changes` 计数以免每行重扫子树。
|
||||
|
||||
### 9.4 第二轮验证证据
|
||||
|
||||
- `pnpm typecheck` / `pnpm lint` / `pnpm test`(**134 文件、1422 通过、9 跳过**)/ `pnpm build` 全绿。
|
||||
- 真实挂载 lane:`DSH_CMD='npx -y --package @deepseek-ai/dsh@0.1.7-rc.1 dsh' pnpm test:mount` → **7/7 通过**。
|
||||
- 截图取证(临时 Playwright 脚本,已删除):真实宿主上确认
|
||||
① 文件树多选 5 项 + 右键菜单同时含 **宿主探测应用**(Open with default app / Visual Studio Code / QuickTime Player)与 **插件 `Open with >` 子菜单** + **Zip and download (5 items)**;
|
||||
② 变动页 Git 视角以树显示:`src/client/changes`(单子目录链压缩成一行,带计数胶囊 1)→ `GitLens.tsx`(M 徽标),已暂存组 `src` → `app.ts`。
|
||||
- 单文件页回归:14 个 spec / 91 用例绿(含新增 `file-tree-archive.spec.tsx` 9 例:出现条件、成功链、三条失败路径、in-flight 守卫)。
|
||||
- 仍未验证:真实浏览器里 ZIP 的落盘行为(jsdom spy 证据)、拖拽/多选手感、Windows 宿主差异。
|
||||
|
||||
### 9.5 第三轮(verifier 二轮报告后的修复,全部完成)
|
||||
|
||||
| 编号 | 问题 | 修法 | 判别性验证 |
|
||||
|---|---|---|---|
|
||||
| F1【高】 | 中文等非 latin1 下载名 → 真实宿主 **500**(`content-disposition` 直接塞原始名,Node `writeHead` 拒绝 >U+00FF) | `archive-route.ts` 新增 `contentDispositionOf`:ASCII 回退 + `filename*=UTF-8''…`(无可用 ASCII stem 时回退 `download.zip`) | 临时回退 → 真 `node:http` 用例 500;对照断言钉住 `validateHeaderValue` 必抛 |
|
||||
| F2【中】 | 多选同名不同目录 → 归档条目重名(解压互相覆盖) | `disambiguateArchiveNames`:只对冲突项逐级上溯父目录,必要时回退完整路径 | 临时回退 → 条目重名断言变红 |
|
||||
| F3【中低】 | `maxBytes` 在读完源文件后才判定(与注释相反);且 `budget===0` 时 0 字节 FIFO 仍会 `open()` 永久阻塞 | `prepareFile` 先 `stat`,`budget <= 0 || size > budget` 立即抛,再读 | FIFO 探针:回退后 5s 超时(阻塞),修复后 44ms 通过(Windows 跳过该探针) |
|
||||
| F4【中低】 | SSH 模式下宿主不可用时 reveal 整条消失 | SSH 过滤保留 `kind === 'reveal'` 的目标,并注明理由 | 更新 open-with.spec 断言(`['explorer','vscode','cursor','custom:e2']`,reveal 恰 1 条) |
|
||||
| F5【低】 | 图钉是 `<span role="button" tabIndex={-1}>` 嵌在 menuitem 按钮内(语义非法 + 键盘不可达) | 去掉 role/tabIndex,改成诚实的鼠标热区;注释写明键盘入口是设置面板 | 临时加回 role → 用例变红 |
|
||||
| F6【低】 | 变更树行无 memo,空闲 tick 全量重渲染 | ① 行 `memo` + 恒定 props(图标解析搬进行内当探针)② 内容键 `entriesKey` 派生 tree ③ (Lead)共享 store 的 `sameStatus` 保住快照身份 | 三处各自临时回退 → 探针变红 |
|
||||
| F7【低】 | 排序依赖运行时 locale collation | `localeCompare(other, 'en', { sensitivity: 'base' })` + 码点兜底 | 反向输入顺序不变 + 瑞典 collation 对照断言 |
|
||||
| F8【低】 | POSIX 文件名里的 `\` 被静默转成层级 | `archiveName` 不再替换 `\` | 实探条目 `a\b.txt` 保持单条成员 |
|
||||
| F9【低】 | 打包中重复点击静默吞掉、大选择无反馈 | `archiveBusy` state + 树顶 `Notice kind="loading" role="status"` | deferred fetch 断言忙态行出现/消失、重复点选 fetch 仍 1 次 |
|
||||
| — | 新增交互:目录行支持「暂存/取消暂存该目录」 | `DirRow` hover IconButton(`gitStage`/`gitUnstage` 均接受目录 pathspec) | 断言 `gitStage(scope,'src/deep',MAIN)` 且点击不触发折叠 |
|
||||
|
||||
最终门禁(修复后重跑):`pnpm typecheck` / `pnpm lint` / `pnpm test`(**134 文件 / 1441 通过 / 9 跳过**)/ `pnpm build` 全绿;
|
||||
`DSH_CMD='npx -y --package @deepseek-ai/dsh@0.1.7-rc.1 dsh' pnpm test:mount` → **7/7 通过**(mount=372ms,p95 帧间隔 16ms,无宽度泄漏)。
|
||||
@@ -1,122 +0,0 @@
|
||||
# 任务输出常驻浮动窗 + 节点卡去竖线 + 手机设置
|
||||
|
||||
日期:2026-09-27 分支:`feat/tasks-graph-polish`(承接 PR #680 的打磨线)
|
||||
|
||||
## 背景(用户原话)
|
||||
|
||||
> 1. 后台任务弹窗没有高度限制,输出很长会导致看不到。同时让其变为常驻的全屏弹窗(带关闭/拉伸,建议抽取为全局组件方便后续复用)
|
||||
> 2. 优化节点UI,加上圆角,同时取消左侧装饰条,让整个节点看起来更为统一
|
||||
> 3. 设置中新增手机设置,里面放各种和手机端的适配设置,目前加上这么几项:检测到处于手机状态时,有新的后台任务或Agent任务时不会自动弹出,任务管理的默认状态切换为树状图
|
||||
|
||||
## 用户确认的决策(问答摘要)
|
||||
|
||||
| # | 问题 | 结论 |
|
||||
|---|---|---|
|
||||
| 1 | 弹窗形态 | 可拖拽 + 可拉伸的**常驻浮动窗**(默认约视口 70%×60%,最小 320×240) |
|
||||
| 2 | 复用范围 | 抽成**全局组件**,本轮只把后台任务输出换过去(节点/工作流详情、任务窗保持现状) |
|
||||
| 3 | 常驻边界 | 只靠**关闭按钮 / Escape**;外部点击、失焦、锚点离屏都不关;切会话/关标签、任务结算后都保留 |
|
||||
| 4 | 左侧竖条 | 「就是左侧的竖条,这个去掉」 |
|
||||
| 5 | 层级组色 | 「这个可以留着,只是那个竖线不要了」 |
|
||||
| 6 | 当前会话标记 | 「边框加粗高亮即可」 |
|
||||
| 7 | 圆角 | **8px**,并**保留类型描边**(成员 accent 描边、工作流 accent 描边 + 底色) |
|
||||
| 8 | 手机设置 | **自动检测(沿用 768px 窄屏判定)+ 两个独立开关(默认都开)**,分组放在设置页 |
|
||||
|
||||
## 架构与文件
|
||||
|
||||
```
|
||||
新增
|
||||
src/client/FloatingWindow.tsx 常驻浮动窗(拖拽 + 拉伸 + 关闭,内容区自滚动)
|
||||
src/client/FloatingWindow.module.css 窗框/标题栏/内容区/底部行/三个拉伸柄样式
|
||||
tests/floating-window.spec.tsx 9 例:结构、尺寸与定位、关闭契约、两个手势
|
||||
改写
|
||||
src/client/JobsDrawer.tsx JobOutputPopoverContent → JobOutputWindow
|
||||
src/client/SubagentView.tsx 任务分支改渲染窗口;默认视图加窄屏覆盖
|
||||
src/client/tasks-graph.module.css 卡片:去两条左侧竖条、圆角 8px、当前会话 1.5px 描边
|
||||
src/client/sidebar/use-host-feeds.ts 两个自动弹出 gate 加手机压制
|
||||
src/client/SideCardSection.tsx 新增「手机」分组(两行开关)
|
||||
src/prefs-shared.ts / src/config.ts / src/client/prefs.ts
|
||||
两个新偏好 mobileNoAutoOpen / mobileDefaultTree
|
||||
src/client/locales*.ts(20 份) 5 条手机设置词条
|
||||
```
|
||||
|
||||
## 关键设计
|
||||
|
||||
### 1. 常驻浮动窗(`FloatingWindow.tsx`)
|
||||
|
||||
- **为什么不是复用 `AnchoredPopover`**:那是 popover —— 锚定元素、按内容定尺寸、外部 mousedown / window blur / 锚点离开视口都会关它。被**持续观察**的面(一个跑几分钟的任务输出)需要的恰好相反的生命周期:只有关闭按钮与 Escape 结束它,而且必须能挪开、能拉伸。
|
||||
- **内容区就是高度上限**:`data-window-body` 自己 `overflow: auto`,所以「输出很长看不到」这条被结构性修掉(旧 popover 完全没有高度约束,`.popPre` 那个 160px 滚动井在改用 `TerminalBlock` 之后已成死规则 —— 本轮一并删掉)。
|
||||
- **手势复用既有范式**:移动走 window 级 `pointermove`(同 `AnchoredPopover` 的拖拽,容器捕获会重定向 click、杀掉窗内按钮),拉伸走 `setPointerCapture` + `createFrameBatcher()`(同 `EditorHost` 的面板拉伸;逐事件 setState 是 #315 的拖拽卡顿)。
|
||||
- **几何**:`position: fixed`;尺寸在**挂载时量一次**(之后每条流式输出帧都不会挪动读者摆好的窗);拖拽保证标题栏至少 48px 留在视口内;拉伸夹取到 `minSize` 与视口;window `resize` 时把窗夹回视口。
|
||||
- **常驻**:不注册外部 mousedown / blur / IntersectionObserver;只注册 Escape(捕获阶段)。
|
||||
- **皮肤契约**:令牌全量、8px 圆角与节点卡一致、浮动层保留 `--dsw-shadow-lv3`。
|
||||
|
||||
### 2. 任务输出接入
|
||||
|
||||
- `JobOutputWindow`:标题栏(任务 label + 复制按钮 + 关闭)/内容区(状态行 + `TerminalBlock`,`maxLines: Infinity` 保留「看完整输出」)/底部行(跟随最新 + 两击终止)。
|
||||
- **顺带修掉的 bug**:「跟随最新」原先滚的是包在 `TerminalBlock` 外面的普通 `div`(永不滚动)——现在滚窗口内容区。
|
||||
- 页面侧:`popover.kind === 'job'` 不再走 `AnchoredPopover`;节点/工作流详情仍是 280px 锚定小卡,任务窗不变。
|
||||
- 任务从宿主 roster 消失时窗口关闭(无处再观察);**结算不掉行,窗口继续**。
|
||||
|
||||
### 3. 节点卡(`tasks-graph.module.css`)
|
||||
|
||||
| 变更 | 说明 |
|
||||
|---|---|
|
||||
| 删除 `.cardTop::before` | 层级竖条;层级改由上段极淡底色承担,并把色调从 7/5/3.5/2.5% 抬到 **9/7/5/3.5%** 补偿对比度损失 |
|
||||
| 删除 `.nodeCurrent::before` | 当前会话竖条;`.nodeCurrent` 改为 **2px accent 描边**(「边框加粗高亮」) |
|
||||
| `.node` 圆角 4px → **8px** | 与新的浮动窗同一圆角,卡与窗成为一个家族 |
|
||||
| `.cardTop` 左内边距 10px → 8px | 竖条消失后的回正 |
|
||||
| `.nodeTeam` / `.nodeWorkflow` / `.nodeFold` / `.nodeError` | 按用户选择**保留**类型区分;只继承新圆角 |
|
||||
| 删除 `.popPre` / `.jobPopPre` | 随旧弹窗一起死掉的规则 |
|
||||
|
||||
**几何安全性**:`.node` 是 `box-sizing: border-box` + 布局给的 `min-height`,加粗描边被吸收在盒内,卡片外框尺寸不变 —— `tasks-graph-layout.ts` 的高度/行距预算**不需要改**,`tests/tasks-graph-layout.spec.ts` 保持全绿。
|
||||
|
||||
### 4. 手机设置
|
||||
|
||||
| 键 | 默认 | 语义 |
|
||||
|---|---|---|
|
||||
| `mobileNoAutoOpen` | true | 窄屏时**同时**压制子代理与后台任务两个自动弹出(既有两个开关的值不被改写;宽屏行为不变) |
|
||||
| `mobileDefaultTree` | true | 窄屏时任务页默认树状图(页内切换仍是临时覆盖,优先级最高) |
|
||||
|
||||
- 判定沿用插件既有的 `isNarrowWidth`(768px 断点,与侧边栏手机布局、park 逻辑同源),在**触发时**读视口。
|
||||
- 设置 UI:DSH 设置页「侧边卡片」分区新增「手机」分组(两行 title/desc + 宿主 `Switch`,走既有 `applyPref` 写入)。
|
||||
|
||||
## 实施偏差记录(写给复审)
|
||||
|
||||
1. **窗口尺寸只在挂载时量一次**(`useEffect(..., [])` + 一条 eslint-disable):若跟着 props/内容重算,每条流式输出帧都会把读者拖好的窗口弹回去。视口变化仍会夹取。
|
||||
2. **`aria-label` 用 `<title>` 而不是 `<h1>` 之类**:与既有 `AnchoredPopover`/`Modal` 的可访问名做法一致(`role="dialog"` + 可访问名)。
|
||||
3. **拉伸只给三个柄**(右、下、右下):实际用法是「日志变长 → 往右下长」,两轴各自还可单独拉;八个柄属于没被要求的能力。
|
||||
4. **没有做焦点陷阱**:与插件现有的两个浮层(`AnchoredPopover` / 宿主 `Modal` 之外的弹窗)一致;Escape 是唯一键盘出口。
|
||||
5. **窄屏自动激活的既有两个 spec 显式关掉了手机开关**:它们钉的是「窄屏也激活 + 收回列(park)」这条更早的承诺,而压制行为有自己的新用例;把 harness 的默认值改成「关」会让 spec 脱离真实默认,所以选择在用例里显式声明。
|
||||
6. **`mobileDefaultTree` 只在有 store 时生效**:`SubagentView` 的 prefs 来自 `store` prop,spec harness 里多数用例不传 store(读作 `graph`);视图默认的新用例显式传入 `createSidebarStore()`。
|
||||
7. **当前会话的加粗描边用 2px 而不是 1.5px**:真机自测发现 Chromium 在 device-pixel ratio 1 下**向下取整**亚像素边框宽度 —— 1.5px 的计算值直接是 1px,"加粗"完全看不出来(recon 实测 `borderTopWidth: "1px"`,而 class 与 accent 颜色都在)。改成整数 2px 后在 DPR 1 与 2 上都成立。
|
||||
8. **`.popPre` / `.jobPopPre` 被删除**:旧 `jobs` 弹窗改用 `TerminalBlock` 之后它们就没有消费者了,本轮顺手清掉(与上次「drop the dead rules」同类)。
|
||||
|
||||
## 验证证据(2026-09-27)
|
||||
|
||||
| 项 | 结果 |
|
||||
|---|---|
|
||||
| `pnpm typecheck` / `pnpm lint` | ✅ 0 错误 |
|
||||
| `pnpm test` | ✅ **1254 passed / 9 skipped / 0 failed**(120 文件;新增 14 例:窗口 9 / prefs 1 / 设置分组 1 / 自动激活压制 2 / 视图默认 1) |
|
||||
| 真机挂载 lane | ✅ **7 passed**(真实 DSH 0.1.7-rc.2,scratch profile + 无头渲染 + 全 tab 扫描 + 性能测量;日志 `/tmp/e2e-win.log`) |
|
||||
| 3080 真机自测 | ✅ 见下表(真实工作:长输出后台任务 + 子代理;截图 `/tmp/dsh-selfcheck/10-13-*.png`) |
|
||||
|
||||
### 3080 真机证据(逐项)
|
||||
|
||||
| 项 | 实测 |
|
||||
|---|---|
|
||||
| 卡片去竖条 + 圆角 + 加粗描边 | `radius: 8px`;`::before` 的 `content: none`(层级条与当前会话条都不存在了);当前会话 `borderTopWidth: 2px` + `rgb(65,118,230)`(accent);卡片外框 **247×134** 与布局预算逐像素一致(去掉竖条/加粗描边都不改几何) |
|
||||
| 窗口限高(原始 bug) | 240 行输出的任务:窗口 1008×540 全在 1440×900 视口内,内容区 `scrollHeight 5459 > clientHeight 449` —— **真的在窗内滚动**;滚到底能看到末行 `THE-END` |
|
||||
| 窗口结构 | 标题栏(任务名 + 复制 + 关闭)、内容区、底部行(跟随最新 + 终止)与三个拉伸柄(x / y / both)都在 |
|
||||
| 拖拽与拉伸 | 拖标题栏:(424,352) → (304,352);拖右下角:1008×540 → **918×480** |
|
||||
| 常驻语义 | 外部点击(窗口上方面板区、左侧会话列)后窗口仍在(count 仍为 1);Escape 关闭 |
|
||||
| 手机适配 | 窄屏(700×900)起后台任务 + 子代理后,右侧栏**未被打开**(`data-sidebar-right-panel` 不存在);手动打开任务页 → `tree: 2 / graph: 0`(默认树状图) |
|
||||
|
||||
### 真机自测发现并修掉的两处
|
||||
|
||||
1. **1.5px 的「加粗」边框在 DPR 1 下等于 1px**:Chromium 向下取整亚像素边框宽度,真机上完全看不出加粗(recon 实测 `borderTopWidth: "1px"`,而 class 与 accent 颜色都在)。改成整数 **2px**(DPR 1 与 2 都成立)。
|
||||
2. **拖拽只保证标题栏可达,会把窗口的右下角推出视口**:第一次真机拖拽后窗口底部落到 y=932(视口 900),拉伸柄随之点不到。改为「放得下就整窗留在视口内,放不下才退化成保留 48px 可达」——两个轴都套用,视口变化时同样处理。
|
||||
|
||||
### 未在真机复现 / 已知限制
|
||||
|
||||
- 关掉两个手机开关后的真机行为(真机只验了默认开启的一侧;关闭一侧由单测覆盖,且既有窄屏 park 用例显式关掉该开关后仍然全绿)。
|
||||
- **切会话会关掉窗口**:窗口状态属于该会话的 Tasks 页组件,宿主在切走时卸载该会话视图(实测点击左侧会话列表即窗口消失)。设计文档已列为限制:若要真正跨会话常驻,需要把开启状态提到 `SidebarStore`(本轮范围外)。
|
||||
@@ -1,222 +0,0 @@
|
||||
# 任务管理页「工作图」打磨:双段式卡片 + 主 Agent 同款合并活动 + 折叠聚合 + 窄栏可读性
|
||||
|
||||
日期:2026-09-27 分支:`feat/tasks-graph-polish`(基于 `feat/tasks-graph-workflow-teams`,接 PR #680)
|
||||
|
||||
## 背景:为什么不是「再做一版卡片列表」
|
||||
|
||||
上一轮(`feat/tasks-page-card-redesign`)把任务页当成「树 + 卡片」重做了一遍,方向被用户否掉:**主显示模式仍然是工作流图**(`feat/tasks-graph-workflow-teams` 里那一版图),树模式保留即可;图模式才是与官方做出差异化的地方。用户随后给出这一轮的 brief(原文):
|
||||
|
||||
> 参考图分支,将其 UI 等合并到之前我说的现代美观干净的风格:
|
||||
> - 改为组合的双段式的结构,即上方为显示节点类型和节点详细内容,而下方是一个小条,用于显示当前的状态、执行的及执行的工具,已完成、折叠展开等操作。
|
||||
> - 执行时有一个从左到右扫光的特效,同时执行的工具逻辑和主 Agent 中合并显示的逻辑一样。
|
||||
> - 当为已完成的状态时,要有一个可点击的折叠按钮,在下方的小横条中。
|
||||
> - 优化排布逻辑,考虑有很多节点/阶段的情况,考虑窄屏的特点。
|
||||
|
||||
因此这一轮 = **图分支的骨架 + 上一轮验证过的视觉/交互语言**。上一轮的卡片 UI 与树改造整体丢弃,只把钱花在已验证的数据层上(见「移植与丢弃」)。
|
||||
|
||||
## 用户确认的决策(问答摘要)
|
||||
|
||||
| # | 问题 | 结论 |
|
||||
|---|---|---|
|
||||
| 1 | 打磨范围 | ② 运行中节点的扫光/执行态视觉;③ 后台任务改读 `ctx.jobs`;④ 图布局与窄栏可读性;⑤ 折叠聚合打磨 |
|
||||
| 2 | 「阶段」要不要做成可展示列表 | **不要**:「阶段做到卡片徽标 + 颜色标明分组即可」 |
|
||||
| 3 | 阶段徽标针对哪种分组 | **两者都要**:层级/分支分组 + workflow 相位分组 |
|
||||
| 4 | 图节点双段式怎么落地 | 图上也是**真两段**(宽度保持现有量级,高度按新结构重算) |
|
||||
| 5 | 树模式要不要一起换新语言 | **不要**:树模式保持现状 |
|
||||
| 6 | 图节点小条上的折叠按钮 | **收进「已完成聚合」**(不是收起上段) |
|
||||
| 7 | 窄栏具体痛点 | a 卡片太窄文字被截 / b 偏小、不居中、缩放手感 / c 节点内信息太密 / d 连线与相位框 / e 拖拽与缩放手感 / f 折叠聚合 —— 全选 |
|
||||
| 8 | 代码落点 | 本工作区从图分支开新分支;上一轮版本丢弃 |
|
||||
|
||||
## 架构与文件
|
||||
|
||||
```
|
||||
host 半(src/)
|
||||
process-activity.ts 新增:宿主 ui-chat 的 process group 算法移植(分类 / live 详情 /
|
||||
范围切割),裁剪掉逐阶段列表(阶段改由徽标表达)
|
||||
subagent-live-route.ts 改为:树内每个子节点(含已结算)与拓扑根都回一帧
|
||||
SidebarChildLiveView { running?, text?, summary?, lastEventTime? }
|
||||
context-types.ts SidebarChildLiveView / SidebarClientJobsService / SidebarJobsSnapshot /
|
||||
SidebarObservedJob;SidebarJobView 补 owner、progress
|
||||
删 jobs-routes.ts 自建三条路由退出历史(见下)
|
||||
client 半(src/client/)
|
||||
process-labels.ts 新增:宿主 chat 命名空间措辞(liveActivityLabel / doneActivityTitle)
|
||||
tasks-card.tsx 新增:卡片两段(CardTop / CardBar)+ 类型徽章 + 深度/相位色阶
|
||||
jobs-client.ts 新增:ctx.jobs 读取层(watchRows 引用计数 / observe / collectRows)
|
||||
block-labels.ts 新增:TerminalBlock 的 labels(零新增词条)
|
||||
TasksGraph.tsx 三个节点渲染器改双段式;fit / 缩放 / 平移 / 连线重做
|
||||
tasks-graph.module.css 卡片两段 / 徽标 / 小条 / 扫光 / 相位框配色
|
||||
tasks-graph-layout.ts 卡片高度预算按新行重算,宽度 176 → 190
|
||||
SubagentView.tsx foldedIds(手动折叠集)+ 树内 jobs 改读客户端服务
|
||||
sidebar/use-host-feeds.ts 任务自动激活改骑推送 roster(detectNewJob 加观测起点时钟)
|
||||
```
|
||||
|
||||
## 关键设计
|
||||
|
||||
### 1. 卡片两段(图模式)
|
||||
|
||||
```
|
||||
┌────────────────────────────────┐
|
||||
│▎[类型徽章] [相位徽标] │ ← 上段:身份 + 组色左描边(2px)+ 极淡底色
|
||||
│ 名称(最多两行 clamp) │
|
||||
│ mode/model · 角色(mono) │
|
||||
│ ☑ 任务主题 · 状态 +N(团队成员) │
|
||||
├────────────────────────────────┤
|
||||
│ ● 运行中 · 正在运行命令 · npm… ⌄ │ ← 小条 22px:状态点 + 状态词 + 合并活动 + 折叠
|
||||
└────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 类型徽章用页面自己的词汇:`subagentMainAgent`(主代理,复用)+ 新增 `tasksKindSubagent` / `tasksKindTeammate` / `tasksKindWorkflow`;聚合节点用 `tasksFoldCompleted`(`✓N 已完成`)。
|
||||
- **组色 = 树深度**:4 档令牌色阶(`color-mix(in srgb, var(--dsw-alias-state-business-primary) 100/72/48/28%, transparent)`),作用于左描边与上段底色;更深层级钳到最后一档。
|
||||
- **相位徽标 = workflow 相位**:走 ink 色族(`--dsw-alias-label-primary` 的 100/78/58/42% 色阶),与组色族分开,避免两套分组撞色;虚线相位框保留(空间分组),改用同色极淡描边。
|
||||
- **皮肤契约约束**:宿主不提供图表调色板,插件也不允许硬编码颜色,所以「颜色分组」只能做成令牌色阶 —— 这是本轮唯一被契约限死的设计点,不是取舍。
|
||||
|
||||
### 2. 合并活动(主 Agent 同款)
|
||||
|
||||
`foldProcess(events, { live })` 把子会话日志折成宿主 process group 的形状:分类计数(`activityOf` 表逐条照抄)、最新未结算调用(含 `preparing` = 模型宣布但未派发)、`runningDetail`(宿主键优先级 + 160 字 grapheme 截断)、范围边界(`user/message` 与带文本的 `assistant/message`)。客户端 `liveActivityLabel` / `doneActivityTitle` 用宿主 `chat` 命名空间的 `message.stepProcess.*` 词条合成措辞(`chatT` 失败即回退插件文案),所以卡片上的字与主对话完全一致。
|
||||
|
||||
**一条刻意偏差**:最新范围为空(子代理刚回复完)时回退到上一个「做过事」的范围 —— 否则已结算卡片的小条只会显示「已完成分析」。回退范围永不报告运行中。
|
||||
|
||||
### 3. 扫光(执行态)
|
||||
|
||||
小条 `data-running="true"` 时叠加一条 45% 宽的令牌渐变带,`background-position 0% → 100%`,1.5s `cubic-bezier(0.33,0,0.67,1)` 循环、末 1/3 停顿;`prefers-reduced-motion: reduce` 下关闭动画(状态词与活动文本已承载同样信息)。方向从左到右(用户确认;宿主的文本 shimmer 是反向,这里刻意不同)。
|
||||
|
||||
### 4. 折叠聚合
|
||||
|
||||
- 页面持有 `foldedIds`(读者从卡片小条逐个收起)与 `folded`(控制条的全局规则),模型里两者走**同一组守卫**:只折已结算叶子、永不折当前会话、永不折有子节点的、永不折团队成员。
|
||||
- 聚合节点(`fold:<parentId>`)点击 = **全部展开**(清空手动集 + 关闭全局折叠);控制条按钮只切全局规则。
|
||||
- 折叠/展开不重置读者当前的缩放与平移(自动 fit 只在读者未触碰视图时生效)。
|
||||
|
||||
### 5. 后台任务改读宿主客户端服务
|
||||
|
||||
0.1.7 的 web profile 挂载 `ctx.jobs`(`@deepseek-ai/dsh-api-job-controller/client`):`state` 是整份快照(按被观察会话的 roster + 按任务 id 的保留输出),`watchRows` 是按会话引用计数的**推送** roster,`observe` 是**非消费**输出流(返回释放函数),`kill` 是注册表准入。插件因此删掉自建的三条路由与事件回放:
|
||||
|
||||
- 删除 `src/jobs-routes.ts`、`index.ts` 的 `jobs.list/output/kill`、`api.jobsList/jobOutput/jobKill`、`SidebarJobsService`、页面的 `jobs.list` 轮询与 `collectTreeJobs`;
|
||||
- 抽屉的行来自推送 roster(状态/时长/owner/进度)、输出面换宿主 `TerminalBlock` 显示观察流(`gapBefore` → 截断提示、`error` → 失败提示)、kill 走 `jobs.kill(owner, jobId)`(两击确认不变);
|
||||
- 无 `ctx.jobs` 的部署**整段不渲染**(结构性降级),插件的其它面不受影响。
|
||||
|
||||
**旧路由为什么必须死**:它读的是「模型已读到的输出」(事件回放),因为注册表当时只有模型的**消费**游标可读;宿主服务把这条约束变成了能力(观察流不碰游标),继续维护回放既多余又永远落后于流。
|
||||
|
||||
### 6. 布局与窄栏(a–f 全项)
|
||||
|
||||
- 卡片高度预算重算:徽章 16 + 两行标题 36 + 元信息 16 + 小条 22 + 内边距 11 + 边框 2 = **103px**(团队成员 +18 任务行);宽度 176 → **190px**(360px 侧栏仍两列,缩放 ~0.86)。**live 行不再占高度** —— 合并活动在小条里,任何状态的行高一致。
|
||||
- fit 按**内容包围盒**(而非含 padding 的整张画布)双轴居中,`FIT_MAX_SCALE` 1.15 → 1.30;空画布**双击**重新适配并恢复自动 fit。
|
||||
- 缩放范围 0.3–1.5 → **0.4–2.0**,并按 `deltaMode` 归一化滚轮刻度(行模式鼠标不再比触控板慢 30 倍)。
|
||||
- 平移加**边界夹取**(至少 72px 内容留在视口内),拖不出视野。
|
||||
- 连线改「垂直出线 stub(8px)+ 贝塞尔」,线宽 1 → **1.5px**(0.78–0.86 的 fit 缩放下 1px 会落到一个设备像素以下,整棵层级会糊掉)。
|
||||
|
||||
## 移植与丢弃
|
||||
|
||||
| 上一轮产物 | 处置 |
|
||||
|---|---|
|
||||
| `src/process-activity.ts` + 44 例 | **移植并裁剪**(删逐阶段机制,保留分类/详情/范围切割);新增「空范围回退」一条并补测 |
|
||||
| `src/client/process-labels.ts` + 10 例 | 原样移植 |
|
||||
| `chatT` + 4 例回归(含「不许解绑 bind」) | 原样移植 |
|
||||
| `src/client/block-labels.ts` | 原样移植(jobs 输出面复用) |
|
||||
| `ctx.jobs` 读取层(`clientJobs` / `useJobsSnapshot` / `collectRows`) | 移植数据层;展示组件丢弃(本分支有自己的抽屉) |
|
||||
| `subagents.live` 新载荷 + `SidebarChildLiveView` | 移植(去掉 `expand`/`stages`) |
|
||||
| 卡片 UI(`subagent-card.tsx` / `subagent-runs.ts` / 树重写 / 5 个词条) | **丢弃** |
|
||||
|
||||
## 后续变更(同日,`2026-09-27-floating-window-mobile-settings.md`)
|
||||
|
||||
本文描述的卡片语言随后被两处调整:**左侧竖条(层级条 + 当前会话条)删除**,层级改由上段极淡底色承担、
|
||||
当前会话改用加粗 accent 描边;**圆角 4px → 8px**。任务输出的锚定浮窗也换成了常驻浮动窗。
|
||||
以新文档为准。
|
||||
|
||||
## 后续修正(2026-09-27,用户反馈)
|
||||
|
||||
用户反馈两条,都在真机上定位并修掉:
|
||||
|
||||
### 1. 扫光只有「部分」→ 改成整条完整扫过
|
||||
|
||||
第一版用 `background-size: 45%` 的窄带 + `background-position: 0% → 100%`:亮带确实在移动,
|
||||
但它始终只覆盖小条的一部分(峰值从 22.5% 走到 77.5%,两端永远扫不到),看起来像中间游走的
|
||||
一小块光斑。现改为**与小条等宽的渐变带 + `transform: translateX(-100%) → translateX(100%)`**:
|
||||
光带从左边外侧进入、中途覆盖整条、再从右边外侧离开,行程就是用条宽本身表达的(任意条宽都成立),
|
||||
末 1/3 周期停在右侧外侧(两轮之间留白)。`tests/theme.spec.ts` 新增一条守护钉住这个形状。
|
||||
|
||||
真机证据(242px 宽的小条,每 90ms 采样一次 `::after` 的 `matrix` 平移量):
|
||||
`[-23, 32, 85, 131, 163, -139, -94, -46, 9, 64, 114, 152, 179, 188]` —— 光带在小条左侧
|
||||
(-139)到右侧(+188)之间被连续观察到,单调推进、跨度超过一个条宽;并截到一张行程中的帧。
|
||||
|
||||
### 2. 折叠按钮与展开按钮长得一样 → 两者分向
|
||||
|
||||
三处控件原来共用向下箭头(`IconChevronDownOutlineRegular`):卡片的逐节点「收进已完成聚合」按钮、
|
||||
聚合卡片的「展开」、以及控制条的折叠开关(后者更糟:无论将要折叠还是展开都画同一个清单图标)。
|
||||
现在按**动作方向**区分:
|
||||
|
||||
| 控件 | 折叠方向(chevron up) | 展开方向(chevron down) |
|
||||
|---|---|---|
|
||||
| 卡片小条的逐节点按钮 | ✅ `收进已完成聚合` | — |
|
||||
| 聚合卡片的小条 | — | ✅ `展开已完成的节点` |
|
||||
| 控制条折叠开关 | ✅(当前已展开时) | ✅(当前已折叠时) |
|
||||
|
||||
真机证据(比对三处 `<svg>` 内容):`expandVsCollapse: true`(两个方向确实不同)、
|
||||
`clusterMatchesExpand: true`(控制条在折叠态画的就是聚合卡的展开箭头)、
|
||||
`clusterFlips: true`(状态切换时图标跟着换)、`clusterCollapseMatchesCard: true`
|
||||
(控制条的折叠箭头与卡片逐节点按钮一致)。组件级守护见 `tests/tasks-page.spec.tsx`
|
||||
的「draws COLLAPSE and EXPAND with different glyphs」。
|
||||
|
||||
### 3. 小条里「运行中」与「正在分析请求」没对齐 → 两者统一字体
|
||||
|
||||
字体度量问题,不是布局问题:两者的行盒完全一致(都是 11px/14px、`align-items: center` 下
|
||||
同一位置),但 11px 下 sans 字族的 ascent/descent 是 **11/2**,而 mono 字族是 **10/3** ——
|
||||
基线因此低 1px,蓝字看起来就是没对齐。而中文在这个 mono 栈里本来就走同一个 CJK 回退字族
|
||||
(PingFang SC),mono 只改到句子里的拉丁片段,等于「半句等宽、整体低一像素」。
|
||||
|
||||
修法是让两者共用小条自己的字族(`.barActivity` 不再声明 `font-family`);机器文本的等宽语气
|
||||
留在它该在的地方(上方 meta 行、后台任务行、终端输出井)。`tests/theme.spec.ts` 增加守护:
|
||||
`.barActivity` 不得声明 `font-family`。
|
||||
|
||||
真机证据:真实运行中卡片的小条(3× DPR 截图 `/tmp/dsh-selfcheck/17-bar-real.png`)两段文字
|
||||
同处一行,且两个 span 的 `font-family` 计算值完全相同(断言 `new Set(fonts).size === 1` 通过)。
|
||||
|
||||
## 实施偏差记录(写给复审)
|
||||
|
||||
1. **`process-activity.ts` 是裁剪版而不是原样搬运**:本轮「阶段」= 徽标 + 颜色,逐阶段列表没有消费者,留着就是死代码。裁剪后 44 例 → 30 例(阶段相关用例随机制一起删除)。
|
||||
2. **`CardBar` 的折叠 chevron 用新词条 `tasksFoldOne`(收进已完成聚合)而不是复用 `tasksFoldCollapse`(折叠已完成的节点)**:后者已经是控制条全局开关的可访问名,同一个 aria-label 出现在两个按钮上会让屏幕阅读器与测试都无法区分「折一个」和「折全部」。
|
||||
3. **`TasksAgentNode.depth` 是为此新增的字段**:卡片组色需要层级,而模型的递归本来就知道深度;顺带给图节点加 `data-depth`(用户 CSS 与测试的可寻址面,和树行的 `aria-level` 同性质)。
|
||||
4. **`detectNewJob` 加了第三个参数(观测起点时钟)**:roster 改成推送后,宿主在「该会话没有任务」时会**删掉这个 key**,于是空的首页帧与「还没送到」不可区分 —— 页面挂载时已经在跑的任务会被 id 差集误判成新工作并弹任务页。用 `job.startedAt >= 观测开始时刻` 过滤;自动激活 spec 里两条用例钉住这个语义。
|
||||
5. **两个测试期望被改而不是改代码**:`subagent-live-polling.spec.tsx` 原先断言卡片上有「思考中…」/「hello」文本行 —— 那是旧卡片的 live 行,现在文本行只在小条的活动行里;`subagent-jobs-view.spec.tsx` 的「多任务仍紧凑」用例原先假设抽屉默认折叠,而折叠阈值数的是**代理数**(1 个代理 → 默认展开)。两处都是断言跟随设计,不是放宽。
|
||||
|
||||
## 验证证据(2026-09-27)
|
||||
|
||||
| 项 | 结果 |
|
||||
|---|---|
|
||||
| `pnpm typecheck` | ✅ 0 错误 |
|
||||
| `pnpm lint` | ✅ 0 错误 |
|
||||
| `pnpm test` | ✅ **1240 passed / 9 skipped / 0 failed**(119 文件) |
|
||||
| `pnpm build` + `pnpm pack` | ✅ tarball `dsh-better-sidebar-0.21.1.tgz`,`lib/client.js` 含 `dsh-tasks-bar-sweep` 与卡片类 |
|
||||
| 新增/改写的守护 | `process-activity`(30)/ `process-labels`(10)/ `tasks-model`(depth、phase、foldedIds)/ `tasks-graph-layout`(新高度预算与带宽)/ `tasks-page`(双段结构、`data-running`、chevron→聚合、聚合→全展开、相位徽标)/ `subagent-jobs-view`(14,假 `ctx.jobs`)/ `sidebar-auto-activation`(12,假 `ctx.jobs`)/ `locales`(新词条 ×20 词典) |
|
||||
| `pnpm test:mount` | 见下节(真机挂载 lane) |
|
||||
| 3080 真机自测 | 见下节(Playwright 驱动真实工作 + 截图) |
|
||||
|
||||
### 真机挂载 lane
|
||||
|
||||
`DSH_CMD=/tmp/dsh-rc-home-shim.sh DSH_EXPECT_VERSION=0.1.7-rc.2 bash scripts/e2e-mount.sh` → **7 passed**(日志 `/tmp/e2e-polish.log`)。本地的 shim 是必需的:桌面版 `~/.local/bin/dsh` 把 `DSH_HOME` 硬编码成 `~/.dsh`,lane 的 scratch profile 会写进真实配置目录。lane 同时证明宿主侧挂了 `@deepseek-ai/dsh-api-job-controller/client.js` 与 `@deepseek-ai/dsh-client-ui-jobs/client.js`(本轮 `ctx.jobs` 依赖的服务确实在 web profile 里)。
|
||||
|
||||
### 3080 真机自测(真实工作 + Playwright)
|
||||
|
||||
装法:`cp` 到一个**新文件名**再 `dsh plugin --profile web add file:...` —— 同路径同版本号的 tarball 会被 pnpm 当成已装而不换内容(实测:旧的 client bundle 仍在,页面渲染的还是上一轮的卡片版)。装完 `pm2 restart dsh-web`,从 pm2 日志取一次性 token。
|
||||
|
||||
驱动真实工作(两个后台任务 + 一个子代理),逐项核验:
|
||||
|
||||
| 目标 | 真机证据 |
|
||||
|---|---|
|
||||
| G1 双段式 | 根卡 innerText = `主代理 / 并行启动后台任务与统计子代理 / 运行中 / 正在分析请求`(徽章 + 名称 + 小条),每张卡都有 `[data-card-bar]` |
|
||||
| G2 扫光 | 运行中小条的 `::after` computed style:`animationName` 含 `dsh-tasks-bar-sweep`、`1.5s`、`cubic-bezier(0.33, 0, 0.67, 1)`、`infinite`、`background-size: 45% 100%`,采样时正处在 `6.03%` 的行程中 |
|
||||
| G3 合并活动 | 小条文案 `正在分析请求`(宿主 `chat` 词条的运行时输出,不是插件自造词);结算后读数为 `已调用工具` / `已完成分析` |
|
||||
| G4 折叠聚合 | 一次点击 `button[aria-label="收进已完成聚合"]` 后该节点从画布消失、聚合卡出现(`✓ 1 已完成 / Count files in directory`);点聚合后节点回归 |
|
||||
| G5 徽标与组色 | 卡片徽章 `主代理` / `✓ 1 已完成`;上段左侧 2px 组色描边在截图中可见;相位徽标**未在真机复现**(该实例没有 workflow run) |
|
||||
| G6 窄栏 | 视口收到 820px 时宿主把面板给到 **364px**:面板 `scrollWidth == clientWidth == 364`(无横向溢出)、文档两轴都不可滚、画布 363×672、卡片 **247×134**(= 190×1.30 / 103×1.30,与布局预算逐像素一致)、小条 30px(= 22×1.30)、fit 读数 130% |
|
||||
| G7 后台任务 | 抽屉显示两个真实任务(`运行中 · 0 秒` / 结算后 `已完成 · exit code: 0 · 30 秒`);点开一行后输出浮窗用宿主 `TerminalBlock` 流式显示 `tick 1`(`ctx.jobs.observe` 的非消费流),并带 `终止`(两击)与 `跟随最新` |
|
||||
| 无崩溃 | 无 pageerror、无插件错误条 |
|
||||
|
||||
截图:`/tmp/dsh-selfcheck/01-running-graph.png`(运行态)、`02-job-stream.png`(后台任务输出流)、`03-settled-graph.png`、`04-folded-one.png`、`05-final.png`、`06/08-narrow*.png`(364px 窄栏)。
|
||||
|
||||
**未在真机复现的两项**(如实记录):① 相位徽标/相位框的真机观感(该实例没有 workflow run,无法在不造数据的前提下产生);② 扫光的**像素级**运动(只验证了 computed style 与行程采样,没有做逐帧像素 diff)。另外宿主面板的宽度手柄对本轮的合成拖拽不响应,窄栏是通过视口宽度达成的。
|
||||
|
||||
### 已知取舍(复审可见)
|
||||
|
||||
- 组色只能做成令牌色阶(宿主无图表调色板,皮肤契约禁止字面色),超过 4 档的深度钳到最后一档:颜色不再区分,徽章文本仍区分。
|
||||
- 结算节点的合并摘要只反映「最近一个做过事的范围」;再往前的历史只能进节点详情浮窗。
|
||||
- `Session.snapshotEvents()` 在 0.1.7 标了 `@deprecated`(实现是缓存冻结快照,成本可接受),折叠过程只扫到第一个可汇报的范围边界。
|
||||
@@ -1,87 +0,0 @@
|
||||
# DSH 0.2.0-rc.1 适配记录(v0.24.0)
|
||||
|
||||
> 状态:已实施。支持线从 `^0.1.7-rc.1` 整体前移到 `^0.2.0-rc.1`(**单线**:0.1.7 用户停在 v0.23.0)。
|
||||
> 上游发布:[dsh-v0.2.0-rc.1](https://github.com/deepseek-ai/deepseek-harness/releases/tag/dsh-v0.2.0-rc.1)(npm dist-tag `next`;`latest` 仍是 0.1.7-rc.2)。
|
||||
|
||||
## 0. 结论先行
|
||||
|
||||
**0.2.0-rc.1 对本插件所消费的全部宿主 API 是纯增量**:19 个被用到的宿主包**零值导出删除**,会话格式与子代理描述符版本未动,CLI 与客户端运行时逐字节相同。因此本版**没有为 0.1.7 写任何运行时兼容分支**——唯一必须改的是 peer 下限(否则宿主启动预检会静默禁用整行)。
|
||||
|
||||
## 1. 硬阻塞:caret 跨 minor 不成立
|
||||
|
||||
`packages/boot/app-boot/src/plugin-compatibility.ts`(0.1.7-rc.1 起)遍历 profile 行的 `peerDependencies`,对名字为 `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` 的条目跑
|
||||
`semver.satisfies(宿主版本, 范围, { includePrerelease: true })`,不满足就 `row.disabled = true`,只在 stderr 留一行 `disabling profile plugin …`。
|
||||
|
||||
实测(semver 7,`includePrerelease: true`):
|
||||
|
||||
| range | 对 `0.2.0-rc.1` |
|
||||
| --- | --- |
|
||||
| `^0.1.7-rc.1` | **false**(caret 上界展开为 `<0.2.0-0`) |
|
||||
| `^0.2.0-rc.1` | true |
|
||||
| `>=0.1.7-rc.1 <0.3.0` | true(显式比较器不带 `-0` 上界) |
|
||||
| `>=0.1.1-rc.2 <0.2.0` | true(同上,`dsh-whale-report` 正因此仍能加载) |
|
||||
|
||||
**教训**:`AGENTS.md` §3.4 第 12 条(c) 那条「`includePrerelease` 让判定更宽松(`^0.1.6-alpha.2` 也能满足 `0.1.7-rc.1`)」**只在同一 minor 内成立**;跨 minor 时必须同步抬 peer 下限。用户已选择**单线**方案,不做「同时覆盖两条线」的放宽范围。
|
||||
|
||||
## 2. 适配前实测证据(决定了「零运行时改动」)
|
||||
|
||||
| 检查面 | 方法 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 值导出删除 | 对插件 import 的 19 个宿主包,提取 `lib/*.js` 里 `export {…}` / `exports.x=` 的名字集合做差 | **removed = none**;唯一新增 `ui-primitives` 的 `pointerModality` |
|
||||
| 类型面变化 | `lib/types/**` 逐文件 hash + diff | 仅 3 处:`ui-primitives`(`DisclosureRow` / `TextShimmer` / `Tooltip` 新增可选 prop、overlay 顶部内距改用 frame overlay inset)、`dsh-session`(新增 `ToolCallRecovery`;`interruptedTurnClosers` 仍在)、`dsh-api-remotes`(新增 product-analytics remote 行) |
|
||||
| 会话格式 | 查 `SESSION_FORMAT_VERSION` 与 `session-format-*` 迁移包清单 | 仍 **v4**,无 `session-format-v4-to-v5` |
|
||||
| 子代理描述符 | `grep SUBAGENT_DESCRIPTOR_VERSION` | 仍 **3** |
|
||||
| CLI 契约 | `diff dsh/lib/bin.js` | **字节相同**(`dsh plugin` / `dsh web` / 就绪行 / `--version` 全不变) |
|
||||
| 客户端运行时 | `diff dsh-client-modules/lib/index.js`、客户端包 `provide(...)` 名字、web-app 的 `dsh.client.inject` | 全部相同 |
|
||||
| 测试期依赖 | 对比 `ui-primitives` 产物的裸 import 集合 | 相同 → **无需新增 devDependency**(不重演 0.1.7 §3.3 第 12 条) |
|
||||
| agent-team | 四个包的 `lib/types` hash | 逐字相同 |
|
||||
| web profile 组成 | `web-app/cordis.patch.yml` diff | 新增 desktop-only `product-analytics` / `desktop-product-telemetry`(web 上 `disabled`);`time-context` / `schedule` / `ui-schedule`(本就 `disabled`)移入可选 `dsh-experimental-schedule-bundle`。挂载 lane 的「宿主 tab 恰 1 条(terminal)、browser 缺席」断言不受影响 |
|
||||
|
||||
## 3. 实施内容
|
||||
|
||||
1. `package.json`:14 条 DSH peer → `^0.2.0-rc.1`;27 个 `@deepseek-ai/*` devDependencies 精确钉版 → `0.2.0-rc.1`(`@deepseek-ai/cordis` 4.0.4 / `cordis-plugin-group` 1.0.4 / `cordis-plugin-include` 1.0.9 不变);`version` → `0.24.0`。
|
||||
2. `dsh.plugin.json`:`engines.dsh` → `>=0.2.0-rc.1`(零读者的元数据,但保持诚实)。
|
||||
3. `scripts/e2e-common.sh`:`DSH_NPX_SPEC` 与 `DSH_EXPECT_VERSION` → `0.2.0-rc.1`。
|
||||
4. `.github/workflows/ci.yml`:`npm install -g @deepseek-ai/dsh@0.2.0-rc.1` + 注释补「0.2.0 首个候选版只挂在 `next` 上」。
|
||||
5. `tests/market-manifest.spec.ts`:peer 形状正则钉当前基线 tuple(`/^\^0\.2\.0-[a-z0-9.]+$/`),并把「caret 跨 minor 必失效」写进注释。
|
||||
6. 文档:`README.md` / `README_EN.md`(支持线横幅、shields 徽章、安装对照表新增 0.2.0 行并保留 0.1.7 行的 v0.23.0 指引、最近更新新增 v0.24.0)、`CHANGELOG.md` / `CHANGELOG_EN.md`(v0.24.0 条目)、`AGENTS.md`(§2 钉版句、§3 改为「DSH 版本适配要点」并新增 §3.0 当前基线 + 0.1.7 历史分节)、`docs/external-plugin-guide.md`(当前版本与 peer 下限)。
|
||||
7. **未改任何 `src/**` 运行时代码**——证据见 §2。
|
||||
|
||||
## 4. 已知残留(非本插件可解)
|
||||
|
||||
- `@huanlin/dsh-plugin-better-locale`(**可选**集成;没有任何源码或测试 import 它,运行时靠 `ctx.get('betterLocale')` 结构化探测)的 peer 钉在 `^0.1.0-rc.8`,其最新 0.4.3 也钉 `^0.1.5-rc.2` → 在 0.2.0 上都会被宿主预检禁用。因此 `pnpm peers check` 会报它的 **5 条 unmet peer**;本插件自身的 14 条 peer 全部满足。属上游未适配 0.2.0。
|
||||
- 本机 pnpm 的 registry 是 `registry.npmmirror.com`,对新预发布滞后(本次实测:镜像上 `@deepseek-ai/dsh-attachment@0.2.0-rc.1` 不存在 → `ERR_PNPM_NO_MATCHING_VERSION`)。安装用一次性覆盖:`pnpm install --registry https://registry.npmjs.org`。
|
||||
- `@deepseek-ai/dsh-experimental-agent-team-web-profile` 仍无 0.1.7 / 0.2.0 发布(停在 0.1.6-alpha.2)→ 本机实例升级时该行保持旧版,运行时兼容性属**未验证**(见 §5)。
|
||||
|
||||
## 5. 本机 3080 实例升级(与仓库适配分开执行)
|
||||
|
||||
升级前的 profile 预检(18 个依赖,按宿主同一套 semver 语义逐个判定):
|
||||
|
||||
| 依赖 | DSH peer | 0.2.0-rc.1 |
|
||||
| --- | --- | --- |
|
||||
| `dsh-better-sidebar@0.23.0` | `^0.1.7-rc.1` | **会被禁用** → 本版 v0.24.0 修复 |
|
||||
| `dshmarket@1.35.0` | `^0.1.0-rc.7 \|\| ^0.1.1-rc.2`(最新 1.66.3 的 `dsh-settings` peer 仍是 `^0.1.x`) | **会被禁用**(上游未适配,无法绕过) |
|
||||
| `dsh-whale-report@0.5.4` | `>=0.1.1-rc.2 <0.2.0` | 可加载(显式比较器) |
|
||||
| `dsh-notification@0.1.4` | `*` | 可加载 |
|
||||
| `dsh-feishu-notify` / `dsh-flowglass` / `dsh-lan-access` / `dsh-apollo` / `dsh-tauri*` | 无 DSH peer | 不被预检拦(运行时未验证) |
|
||||
| `@deepseek-ai/dsh-experimental-agent-team-profile` | 无 DSH peer | 升级到 0.2.0-rc.1 |
|
||||
| `@deepseek-ai/dsh-experimental-agent-team-web-profile@0.1.6-alpha.2` | 无 DSH peer | 保持旧版(无 0.2.0 发布),运行时未验证 |
|
||||
|
||||
### 5.1 实测踩到的坑:profile 自己的 `pnpm-workspace.yaml` overrides 必须与宿主同步
|
||||
|
||||
升级后 3080 出现「加载提供商目录失败: settings service is absent: mount @deepseek-ai/dsh-settings with @deepseek-ai/dsh-config-editor in the profile composition」。宿主日志给出了根因链路:
|
||||
|
||||
```
|
||||
dsh: disabling profile plugin row "config-editor": Plugin @deepseek-ai/dsh-config-editor@0.1.7-rc.2
|
||||
is incompatible with dsh 0.2.0-rc.1: peerDependencies {"@deepseek-ai/dsh-hmr":"0.1.7-rc.2",
|
||||
"@deepseek-ai/dsh-app-boot":"0.1.7-rc.2"}
|
||||
settings (@deepseek-ai/dsh-settings): pending (waiting for service: configEditor)
|
||||
```
|
||||
|
||||
profile 的 `pnpm-workspace.yaml` 里有一条上一轮升级留下的 `overrides: '@deepseek-ai/dsh-settings': 0.1.7-rc.2`(它自己的注释就写着「Keep this in lockstep with the host version」)。这条 pin 让 profile 树里的 `dsh-settings` 停在 0.1.7-rc.2,而 `dsh-settings` 用**精确版本**声明 `dsh-config-editor` / `dsh-util-values` → profile 顶层出现 0.1.7-rc.2 的 `dsh-config-editor`,它**遮蔽**了宿主 bundle 自带的 0.2.0-rc.1,于是 0.2.0 的启动预检把 `config-editor` 整行禁用,`settings` 行永远等不到 `configEditor`,设置/模型页的 provider 目录随之失败。
|
||||
|
||||
修法:把该 override 与注释一起抬到 `0.2.0-rc.1` → `pnpm install`(profile 内)→ 重启。修复后 profile 顶层全部收敛到 0.2.0-rc.1(`dsh-atomic-write` / `dsh-brand` / `dsh-config-editor` / `dsh-util-values` / agent-team 三包),`.pnpm` 里 0.1.7-rc.2 的副本归零。
|
||||
|
||||
**教训(对宿主版本升级通用)**:升级 DSH 时,除了 launcher 的 npx 钉版与 profile 依赖,**profile 自己的 `overrides` / `pnpm-workspace.yaml` 里任何 `@deepseek-ai/dsh-*` pin 都必须同批抬版本**;否则 profile 树里会出现旧宿主包并遮蔽 bundle 自带的那一份,而预检只报「某行被禁用」,症状(设置服务缺失)与原因(一条 override)隔着好几层。
|
||||
|
||||
升级步骤(含回滚)见最终交付说明:备份 `~/.dsh/dsh-web.sh` 与 `profiles/web/package.json` → launcher 的 npx 钉版 → `0.2.0-rc.1` → 升 agent-team profile → 装 v0.24.0 tarball → `pm2 restart dsh-web` → 核对路由与 `pm2 logs` 的 `disabling profile plugin` 行。
|
||||
@@ -1,51 +0,0 @@
|
||||
# `files` 接管 id 泄漏:客户端条目替换后报错刷屏 + 文件树空态
|
||||
|
||||
日期:2026-09-28 分支:`fix/native-registration-leak-v2`(PR #777,已合并)→ 后续 `fix/native-leak-guard-tests`
|
||||
|
||||
关联 issue:[#771](https://github.com/omdsh-dev/DSH-better-sidebar/issues/771)(同串报错的独立复现与定位)、[#770](https://github.com/omdsh-dev/DSH-better-sidebar/issues/770)(同串报错,另一条 inject re-prove 猜测)、[#766](https://github.com/omdsh-dev/DSH-better-sidebar/pull/766)(「吸收」方案,已按本文结论关闭)
|
||||
|
||||
## 现象
|
||||
|
||||
```
|
||||
[dsh-better-sidebar] native register files error: sidebarRight: tab type id "dsh-better-sidebar:files" is already registered
|
||||
```
|
||||
|
||||
红色诊断条钉在页面左下角、触发时**同一毫秒刷出多条**;不致命,但 `files`(文件树)接管会**静默缺位**——原生右侧栏的「文件」入口渲染成宿主的空态,直到**刷新页面**才恢复。0.21.1 与 0.22.0(npm `latest`)都带此缺陷。
|
||||
|
||||
## 根因(真机日志 + 代码双向验证)
|
||||
|
||||
官方桌面壳日志 `~/Library/Application Support/dsh-tauri/logs/desktop.frontdesk.log:2370`(2026-09-27 15:51:39.338)第一条就是完整栈:
|
||||
|
||||
```
|
||||
native register files error: Error: cannot create effect on inactive context
|
||||
assertActive ← effect ← inject(slots) ← registerSlots ← registerFilesKind ← sync ← notify
|
||||
```
|
||||
|
||||
随后同一毫秒(.339–.340)**7 条** `already registered`(栈:`register ← registerFilesKind ← sync ← notify`),13 秒后新激活的 `_reload` 路径又报 2 条(该 burst 合计 9 条,与「7 个内置描述符注销 → 7 次 notify」一一对应)。链条:
|
||||
|
||||
1. `service.notify()` 是同步内联(`src/client/service.ts:696`),而 `registerNativeSurface` 把 `sync()` 订阅到它和 store 上(`src/client/native/index.ts:353`;本文行号按 v0.22.0,main 因 #777 的插入而顺移)。
|
||||
2. `sync()` 的清理循环把 `live` 里**两类归属不同的条目**一视同仁:描述符类型(键 = descriptor id)与 `files` 接管(键 = `FILES_KIND`,**永远不在 `wanted` 里**)。于是**每次**通知都会释放接管、并在同一轮里重建它。
|
||||
3. 插件 teardown 时,内置描述符逐个注销 → 每次注销 notify → `sync()` 在**已经 inactive** 的插件 ctx 上运行。此时 `tabs.register` 建在**宿主** ctx 上(宿主 ctx 仍活着)→ id 被取走且返回了 disposer;紧随的 `ctx.slots.inject` 建在**插件** ctx 上 → cordis `assertActive()` 抛 `INACTIVE_EFFECT`(`@deepseek-ai/cordis@4.0.4` `effect()` 首行)→ `live.set(FILES_KIND, …)` 永不执行 → **该 id 在本页生命周期内再也不能注册**。
|
||||
4. 之后每次 notify(同一 burst 里还有 6 次)都重试同一个被占用的 id → `already registered` 刷屏;新一次激活同样注册不上 → 文件树空态。
|
||||
|
||||
**两条一般化的不变量**(已写进 [AGENTS.md](../../AGENTS.md) §3.4 第 9 条):
|
||||
|
||||
- 清理循环必须**跳过 `FILES_KIND`**;接管只由编辑器类型的 `wantsFiles/hasFiles` 分支与 seat disposer 管理。
|
||||
- 「注册成功一半」是**可达状态**(宿主 ctx 与插件 ctx 的寿命不同),因此任何在宿主取走 id **之后**失败的注册必须**回滚释放该 type**(并释放已建槽位),插件侧只留一个下次通知可重试的失败。
|
||||
|
||||
## 修复
|
||||
|
||||
PR #777(贡献者 @yanzhaohui1999)落地上面两条:`sync()` 的清理循环跳过 `FILES_KIND`;`registerSlots` 失败时释放已建槽位;`registerDescriptor` / `registerFilesKind` 在槽位注册失败后回滚已占用的 type;teardown / 清理循环逐个安全释放(一个释放抛错不再中断其余)。
|
||||
|
||||
**否决备选方案**(PR #766 的「吸收」):把宿主抛出的 `already registered` 当作「宿主已持有该行」继续注册槽位。它能让日志安静,但会在 `live` 里记下一份**不拥有**的注册(空 disposer)——旧持有者随后释放该 id 时无人补注册,`files` 接管可能永久缺位;而且在 `tabs.register` 抛错(id 守卫,原子,未取走任何东西)与「取走后才失败」两种情形之间不做区分,掩盖了真正的账本错误。
|
||||
|
||||
## 验证
|
||||
|
||||
- **单测红→绿(实测)**:`tests/native-registration.spec.ts` 在 v0.22.0 源码上 4/4 红,在修复分支上 4/4 绿;`pnpm typecheck` / `pnpm lint` / `pnpm test` 全绿(修复分支的 base 早于 #781,故为 114 files / 1136 passed;合入 main 后为 122 files / 1293 passed / 9 skipped)。
|
||||
- **部署级红→绿(实测)**:`tests/e2e/native-reload.e2e.ts` 在 scratch profile 里对 npm 上的 `dsh-better-sidebar@0.22.0` 触发页面内条目替换(`utimesSync` 已安装的 `lib/client.js`),连续 3 次运行**全红**且每次都是本文那两条栈;换成本地打包的修复版连续 3 次**全绿**(该用例只有 1 个 test,重复跑三次)。该用例已随本 PR 进入挂载 lane(CI `plugin-mount` 绿),触发方式依赖钉住的宿主 0.1.7-rc.1 的 rev 规则(`mtime/ctime/size` 的 sha1)——pin 上调时需同步复核。
|
||||
- 首版用例的两处断言(「失败后还能重新注册」)写在**活着的 id 集合**上,在未修复代码上会**同样通过**(泄漏的 id 恰好构成同一个集合)。跟进 PR 把它们改写到注册表的**事件日志**上,并把槽位失败判据从 `key` 改成 `name::key` —— 两个槽位共用一个 `key`,按 `key` 失败时**第一个**槽就抛,回收循环虽然进了、但手里的 disposer 列表永远是空的(即 `registerSlots` 的部分回滚**释放动作**从未被执行);改后可以让某个描述符的**第二个**槽失败,回收列表非空、释放路径真正被覆盖,且 4 例在 v0.22.0 上仍全部红。
|
||||
|
||||
## 已知限制 / 未做
|
||||
|
||||
- **跨激活的瞬时重复**仍未消除:页面内条目替换时,新激活可能先于旧激活释放 id 而撞上 `already registered`(一次日志噪音,随后自愈)。要消掉它需要「吸收 + 旧持有者释放后补注册」的机制,与上面否决的方案同源,暂不做。
|
||||
- `disposeSafely` 的释放失败只写 `console.error`、**不弹诊断条**(teardown 阶段弹条是噪音;真被占住的 id 会在下一次注册尝试里经 `reportFailure` 显式报出)。理由写在源码注释里。
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 200 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.4 KiB |
@@ -1,185 +0,0 @@
|
||||
# 01 · 需求分析:官方原生右列 vs 装了 apes-sidebar
|
||||
|
||||
> 本篇只回答一个问题:**一个在 DSH 里干活的人,从"官方原生"换到"装了这个包",他的需求和处境发生了什么变化?**
|
||||
> 对比的两端只有两个:**官方原生的右侧栏**,和**装了 apes-sidebar 之后的右侧栏**(那个包是一个整体,九个 tab 一起算)。
|
||||
> 机制与根因只在[附录 A](#附录-a--证据与实现索引)留索引,细节见 [02-系统架构](02-系统架构)、[06-故障排查](06-故障排查)。
|
||||
|
||||
## 一、先钉住"谁在用、他在哪儿干活"
|
||||
|
||||
在 DSH 里,**右侧栏是使用者一天里盯得最久的一块地方**。它不是"偶尔打开的设置页",而是"工作台 + 仪表盘"。
|
||||
|
||||
一个典型使用者(在 DSH 里做项目的人):
|
||||
|
||||
- 中间是对话主线——他和模型一起干活;
|
||||
- 右边是这块地方——看文件、看改动、看子代理、顺手追问、跑命令、看预览;
|
||||
- 他往往同时开着好几个会话、好几个项目。
|
||||
|
||||
这决定了本页所有需求的共同特征:**这些信息"看一眼就够,但要频繁看"。**
|
||||
频繁看的东西必须常驻在视野里——不能要求他"跳出去看一眼,再跳回来"。
|
||||
|
||||
## 二、官方原生的右列:一个「只读的文件浏览器 + 预览器」
|
||||
|
||||
本机宿主(`dsh@0.1.5-rc.2`)里,官方真正放进右列的东西只有三件:
|
||||
|
||||
| 官方包 | 它给用户什么 | 官方原话 |
|
||||
| --- | --- | --- |
|
||||
| `dsh-client-ui-sidebar-right` | **容器**:每会话一块可停靠面板、两种形态(贴靠 / 全屏)、展开按钮在会话 header 角落、最多左右两格、拖拽与浮窗、tab 菜单、内置撤销历史 | "为每个会话持有一个停靠面……拥有导航控制器(`ctx.sidebarRight`)、tab 类型注册表" |
|
||||
| `dsh-client-ui-sidebar-files` | **唯一的页**:「文件」——把会话工作区根目录画成一棵**只读**的树 | "把会话的工作区根目录画成一棵树,逐层经线上列出,并把文件打开到 Sidebar 里" |
|
||||
| `dsh-client-ui-sidebar-documentpreview` | **唯一的查看器**(kind = `text`):预览 Markdown / 代码 / 图片 / PDF / HTML,认不出的扩展名走**纯文本兜底** | "在右侧 Sidebar 预览**可读文件**,无需另开 tab 即可切换已注册的渲染器" |
|
||||
|
||||
一个容易被忽略、但对"用户体验"影响很大的细节:
|
||||
|
||||
> 引导页(罗盘)**只在注册了两个以上入口时才出现**——官方随包只有一个入口(Files),
|
||||
> 所以官方用户的右列**默认直接就是那棵文件树,罗盘从不露面**。
|
||||
> (原文:"恰好一个入口时直接打开对应页面(随包组合中为 Files)"。)
|
||||
|
||||
**一句话概括官方原生的取向:它假设你只是想「看」。**
|
||||
它给你树和预览,都是只读的;**它不假设你要在这块地方「干活」**。
|
||||
|
||||
于是,官方原生用户在右列里**做不到**这些事(每一条都是他能直接感知的缺失):
|
||||
|
||||
| 他想做的事 | 官方原生右列 | 他只能怎么办 |
|
||||
| --- | --- | --- |
|
||||
| 改一个文件 | ❌ 只有预览,没有可写的编辑器 | 跳出去,用外部编辑器打开 |
|
||||
| 知道刚才改了哪些东西 | ❌ 没有变更清单、没有 diff | 切终端敲 `git status` / `git diff`,或另开 diff 工具 |
|
||||
| 看某个子代理干了什么 | ❌ 右列里没有这个入口 | 翻会话树、翻聊天记录去认人 |
|
||||
| 追问一句又不想弄脏主线 | ❌ 没有旁路对话 | 硬着头皮在主对话里问,或另开一个窗口 |
|
||||
| 跑一条命令 | ❌ 没有终端 | 切到系统终端,还得先 `cd` 到对的目录 |
|
||||
| 看页面 / HTML 出来什么样 | ❌ 没有内嵌浏览器 | 切浏览器标签,或手动打开产物文件 |
|
||||
| 知道谁在替我干活、跑到哪了 | ❌ 完全没有 | 只能往上翻记录,翻到一半又忘了上下文 |
|
||||
| 知道这摊活是在哪儿干的 | ❌ 界面不写 | 靠脑子记,或敲 `pwd` 确认 |
|
||||
| 知道这个会话现在什么状态 | ❌ 界面不写 | 靠界面上的小细节猜 |
|
||||
|
||||
## 三、装了 apes-sidebar 之后:同一块地方,从「看」变成「干 + 心里有数」
|
||||
|
||||
apes-sidebar 把这九件事一次性放进右列。它们**是一个整体需求**:把右列从"文件浏览器"变成"工作台 + 仪表盘"。
|
||||
|
||||
| tab | 他真正想要的是什么 | 官方原生时他被迫怎么做 | 装了之后 | 需求被改变成 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 文件(编辑器) | 看**和改**代码,而不丢掉当前上下文 | ⌘Tab 跳到外部编辑器;回来要重新找位置 | 右列里直接看、直接改 | 「看文件不该让我离开现场」 |
|
||||
| 文件变动(git) | 刚才到底改了哪些东西 | 切终端敲命令,或另开 diff 工具 | 变更清单 + 单文件 diff,就在旁边 | 「改了什么必须当场看得见」——**这是信任的前提** |
|
||||
| 子代理 | 进某个子代理,看它到底干了什么 | 翻会话树、翻记录去认人 | 右列里直接进入该子代理 | 「子代理是可进入的,不是只能干等的」 |
|
||||
| 侧边对话 | 追问一句,但不想污染主线 | 硬着头皮在主对话里问,或另开窗口 | 旁路开一个小对话,两边互不干扰 | 「提问不该有副作用」——**性质变了** |
|
||||
| 终端 | 跑一条命令 | 切到系统终端,常常还得先 `cd` | 右列里跑,工作目录天然就是对的 | 「命令的上下文应该跟着会话走」 |
|
||||
| 浏览器 / 预览 | 看这个页面出来是什么样 | 切浏览器标签,或手动打开产物 | 右列内嵌,当场看 | 「产出物要能当场验收」 |
|
||||
| **智能体** | 谁在替我干活、跑到哪了、哪个停了 | **基本没办法**——只能翻记录猜 | 主代理在最上,直系子代理一人一行,带运行/结束状态 | 「派了活之后,我要的是**扫一眼**,不是盯着守」(见 §四·洞察 4) |
|
||||
| **应用** | 这摊活到底是在哪儿干的 | **基本没办法**——靠脑子记或敲 `pwd` | 打开就看到当前会话锚定的工作区 | 「界面应该告诉我我在哪,而不是要我自己记住」 |
|
||||
| **画像** | 这个会话现在是什么状态 | 靠界面小细节猜(在跑?停了?哪一次?) | 标题 / 运行或空闲 / 工作区,写清楚 | 「离席回来,不用重建心智模型」 |
|
||||
|
||||
连**顺序**都在讲同一件事:`10…50` 是操作面(看/改/查/问/跑/验),`60…70` 是态势面(谁在干、在哪儿干、什么状态)。
|
||||
先让"活能在这儿干完",再让"我心里有数"——这是这个包对用户的两级承诺。
|
||||
|
||||
## 四、深度分析:需求到底发生了什么变化
|
||||
|
||||
### 洞察 0 · 官方原生假设"你只是看",而真实使用是"在这儿干活"
|
||||
|
||||
这是整个需求空间的起点。官方原生不是做得不好,而是**取向不同**:只读预览是"查看器"的思路,
|
||||
而使用者把它当"工位"用——他会在这里改一个文件、跑一条命令、瞄一眼改动。
|
||||
**当一块地方被当成工位,只读就会立刻变成绕路。** 这就是这个包存在的全部理由。
|
||||
|
||||
### 洞察 1 · 从「只读」到「可写」:补的不是功能,是主权的归还
|
||||
|
||||
官方只给预览,意味着**每一个"改一下"都要离开这块地方**。装上之后,改、查改动、跑、验四件事都在原地完成。
|
||||
用户省下的不是几次点击,而是**每次离开都要付出的"上下文重建"成本**——回来要重新找到刚才的位置、重新想起来自己在干嘛。
|
||||
需求因此从「我要能看」变成「我要在这儿把活干完」。
|
||||
|
||||
### 洞察 2 · 「留在原地」会变成默认预期(棘轮效应)
|
||||
|
||||
一旦右列里能改文件、能跑命令、能看预览,他对"跳出去一下"的容忍度会**永久下降**。
|
||||
他会开始说「为什么这个还要我切窗口?」——而这句话在他用官方原生时是说不出口的(因为原生本来就没有可切的东西)。
|
||||
**这类需求只前进、不后退**,所以值得一次做对:它一旦被填好,就变成基础设施。
|
||||
|
||||
### 洞察 3 · 消除副作用之后,他会开始"问"(侧边对话)
|
||||
|
||||
侧边对话的价值不在"多一个对话框",而在**把提问的代价从"可能污染主线"降到"零"**。
|
||||
需求的性质变了:以前他的问题是「要不要问?」(一个决策),现在变成「顺手问一下」(一个动作)。
|
||||
**当一件事的代价趋近于零,用户的行为频率会跳一个数量级**——这是需求分析里最值得警惕、也最值得利用的一条规律。
|
||||
|
||||
### 洞察 4 · 看得见 → 敢放手(智能体 tab 的核心价值)
|
||||
|
||||
这是整个包里最值钱的一条。官方原生在这一点上是**彻底空白**:
|
||||
|
||||
- 官方没有子代理视图,也没有任何"队伍"概念 → 他派了活之后**只能等**,或者不停往上翻记录确认;
|
||||
- 这个包的「子代理」让他能**进入**某一个;「智能体」让他**不必进去就看得到全局**——主代理 + 直系子代理,谁在跑、谁停了。
|
||||
|
||||
用户真正的痛点不是"我进不去子代理",而是**"我不敢派太多活"**:
|
||||
因为看不见,他只能用"盯着"来对冲风险,而盯着就失去了并行的意义。
|
||||
|
||||
> **看得见 → 敢放手 → 真的并行 → 才配得上"多智能体"。**
|
||||
> 需求从「我要能看见」升级为「我要能放心」。
|
||||
|
||||
### 洞察 5 · 心智模型从脑子里搬到界面上(应用 / 画像)
|
||||
|
||||
多项目、多会话的人切换时要重建一次"我这是在哪儿"——标题差不多的会话最容易看错,
|
||||
而看错的代价是**写错文件**。官方原生在右列里一个字都不写这件事。
|
||||
「应用」「画像」做的就是**把这份"我记得"外化成"界面写着"**。
|
||||
需求的性质:从「我要记性好」变成「界面不该让我记」。
|
||||
|
||||
### 洞察 6 · "引导页"第一次有了意义:从单一工具到工作台
|
||||
|
||||
官方原生只有一个入口(Files),所以它**默认直接开文件树,罗盘界面根本不会出现**。
|
||||
入口一多,罗盘才登场——用户第一次能"一屏看见这块地方能干什么"。
|
||||
这是**用户层面的分水岭**:官方是"一个工具",装了之后是"一个工作台的目录页"。
|
||||
(对照 §二那个细节:入口数决定了这块地方的形态。)
|
||||
|
||||
### 洞察 7 · 价值随"并行度"放大;而成本几乎为零
|
||||
|
||||
- 单会话、单项目的用户:「应用」「画像」几乎用不上,「智能体」也只有一个主代理;
|
||||
- 一旦同时开几个会话、派几个子代理、换几个项目:这三个 tab 的边际价值**陡增**。
|
||||
|
||||
所以这套东西**不是人人需要,但接到的人离不开**。
|
||||
同时它**不新增数据源、不新增进程、不引入新的失败面**——只是把宿主已经投影出来的信息换个视角呈现
|
||||
(逐条列在 [03-功能说明](03-功能说明))。对使用者意味着:**低风险地换到确定性**,这是它最该守住的边界。
|
||||
|
||||
## 五、由以上分析推出的需求(U1–U6)
|
||||
|
||||
| # | 需求(用户口吻) | 怎么算他满意了 | 现状 |
|
||||
| --- | --- | --- | --- |
|
||||
| U1 | 「我要在这儿把活干完,别老让我跳出去」 | 改文件、查改动、跑命令、看预览、顺手追问,都在右列完成 | ✅ 操作面六个 tab |
|
||||
| U2 | 「我要看得见谁在替我干活」 | 右列点一下就有队伍表;人数/状态一眼可辨;没有子代理时明确说"没有",不是空白 | ✅ 智能体 tab |
|
||||
| U3 | 「我要知道我是在哪儿干的活、这会话什么状态」 | 打开就看到工作区与运行/空闲,不用敲 `pwd`;拿不到应用信息时如实说明 | ◑ 工作区/状态 ✅ · 应用层等宿主 |
|
||||
| U4 | 「我点开 HTML 就该看得见」 | 自包含 HTML 完整显示,而不是一行 `forbidden`;**但不许为此拆掉沙箱** | ✅(多文件需我本人点「临时解锁」) |
|
||||
| U5 | 「我的界面不许自己乱套」 | 右列只有一套侧边栏、一个开合入口;弹窗完整盖在面板之上、按钮点得到 | ✅ |
|
||||
| U6 | 「我敢升级,也退得回去」 | 有"宿主版本 ↔ 插件版本"对照表;每步升级都有分支/tag 可回滚 | ✅ 两条版本线;新宿主线待实机验证 |
|
||||
|
||||
## 六、我们不做的事(省得期待错位)
|
||||
|
||||
- **不重造官方已有的东西。** 官方给了容器与文件树,我们在它里面长东西,不另起一套侧边栏。
|
||||
- **不为"看得见"放宽安全边界。** 需要放宽必须由用户**本人显式点**「临时解锁」。
|
||||
- **不做团队/房间/多智能体协作编排。** 那是宿主的另一类职责。
|
||||
- **不承诺"装上立刻见效"。** 单会话用户可能几乎感觉不到差异(见洞察 7)。
|
||||
|
||||
## 七、已知的「暂时满足不了」(诚实交代)
|
||||
|
||||
| 他想要的 | 现状 | 为什么(一句话) | 何时会有 |
|
||||
| --- | --- | --- | --- |
|
||||
| 知道这摊活属于哪个**应用**、有哪些入口 | 「应用」只显示工作区 | 宿主目前没有向插件提供应用清单的通道 | 宿主提供 feed 后即可接上 |
|
||||
| 一次摊开**完整**子代理树(子代理的子代理) | 只显示**直系** | 宿主投影只给一层 | 同上 |
|
||||
| 沙箱状态下多文件 HTML 的相对资源 | 需点「临时解锁」 | 不透明源沙箱下子请求拿不到宿主信任头(安全边界的必然结果) | 不打算改——这是刻意保留的边界 |
|
||||
|
||||
> 这三条说明同一件事:**需求跑在宿主能力前面**。写出来不是免责,是让读的人知道
|
||||
> "不能用"和"还没做"是两回事。
|
||||
|
||||
## 附录 A · 证据与实现索引
|
||||
|
||||
开发与排障看这里;其余读者可跳过。
|
||||
|
||||
**"官方原生到底有什么"的证据**(本机宿主 `dsh@0.1.5-rc.2`,官方包自带文档):
|
||||
|
||||
- 容器:`@deepseek-ai/dsh-client-ui-sidebar-right/README.zh.md` —— 停靠面、两种形态、扩展席位、引导页。
|
||||
- 唯一的页:`@deepseek-ai/dsh-client-ui-sidebar-files/README.zh.md` —— "文件树 tab 类型……把文件打开到 Sidebar 里"。
|
||||
- 唯一的查看器:`@deepseek-ai/dsh-client-ui-sidebar-documentpreview/README.zh.md` —— "Sidebar tab 的 kind 为 `text`",未知扩展名纯文本兜底。
|
||||
- "官方只有一个入口 ⇒ 罗盘不出现":同上 sidebar-right 文档「引导页」一节。
|
||||
|
||||
**本包一侧的对应实现**(`editor`/`git`/`subagent`/`sidechat`/`terminal`/`browser` 为随包操作面;
|
||||
`agents`/`apps`/`profile` 是本 fork 自增的态势面——对内维护用,对外它是一个包):
|
||||
|
||||
| 需求 | 机制(去哪看) | 关键证据 |
|
||||
| --- | --- | --- |
|
||||
| U5-a 右列只有一套侧边栏 | 每个 tab 注册成原生 tab 类型(`kind = descriptor.id`)渲染进官方右列;自绘面板已移除 | [02-系统架构](02-系统架构)、[05-运维手册](05-运维手册) |
|
||||
| U5-b 弹窗不被面板切 | 宿主容器上的层叠上下文(`isolation`)导致;挂载点改挂 `#root` 而不再挂 `document.body` | [06-故障排查](06-故障排查) |
|
||||
| U4 HTML 预览 | 主框架 `fetch` → `srcDoc`,**`sandbox` 属性未动**;失败回退路由 URL | [06-故障排查](06-故障排查) |
|
||||
| 与官方 `text` 预览的关系 | 本包的编辑器类型以更高优先级接管 `dsh-resource://file/**`,卸载即恢复官方预览 | `docs/external-plugin-guide.md` §「内置类型接管」 |
|
||||
| U6 版本线 | 宿主版本不匹配时宿主**静默禁用**整行(不报错、无日志);`^` 跨 minor 判 false | [04-部署指南](04-部署指南)、[06-故障排查](06-故障排查) |
|
||||
|
||||
<!-- 本页只讲"为什么需要"。实现细节、提交号、测试名一律留在 02–07 与附录索引里。 -->
|
||||
@@ -1,154 +0,0 @@
|
||||
# 02 · 系统架构
|
||||
|
||||
> 本篇说明插件的**分层、接口面、层叠契约与关键不变量**。
|
||||
> 与源码一一对应,可直接当作改动前的「雷区地图」使用。
|
||||
|
||||
## 一、总体分层
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ DSH 宿主(Desktop Electron 壳 / Web profile) │
|
||||
│ ├─ 原生右侧栏 ctx.sidebarRight / sidebarRightTabs(tab 类型注册表) │
|
||||
│ ├─ 会话与任务 ctx.sessions.list(会话摘要、子代理、投影)、ctx.tools │
|
||||
│ ├─ webServer 路由挂载点(插件用 /sidebar/* 前缀注册) │
|
||||
│ └─ 桌面壳层 webserver 外层的请求围栏 + #root 的 content-viewport 标记 │
|
||||
└───────────────┬──────────────────────────────────────────────────────────┘
|
||||
│ ① 宿主 → 插件:cordis 服务注入(inject)
|
||||
│ ② 插件 → 宿主:原生 tab / 资源打开 / 路由
|
||||
┌───────────────┴──────────────────────────────────────────────────────────┐
|
||||
│ 插件 HOST 半(Node,lib/index.js) │
|
||||
│ · src/index.ts 路由注册(/sidebar/*)、信任围栏、会话/文件数据、HTML 交付 │
|
||||
│ · src/trust-fence.ts 请求信任围栏(Host 头 / 令牌) │
|
||||
│ · src/invariant.ts 依赖零的断言与 semver 工具(host/client 共用) │
|
||||
└───────────────┬──────────────────────────────────────────────────────────┘
|
||||
│ 浏览器侧模块(__ModuleLoader__ bundle,非 CJS)
|
||||
┌───────────────┴──────────────────────────────────────────────────────────┐
|
||||
│ 插件 CLIENT 半(浏览器,lib/client.js + 懒加载 chunk) │
|
||||
│ · src/client/index.tsx 应用入口:装配 store/service/原生适配/信息层 │
|
||||
│ · src/client/native/ 原生右侧栏适配(tab 记录、tab 体、tab 芯片标题) │
|
||||
│ · src/client/builtins/ 内置面板:文件 / 文件变动 / 子代理 / 侧边对话 / │
|
||||
│ 终端 / 浏览器 + 文件预览器 │
|
||||
│ · src/client/sidebar/ 自绘底部工作台与开合控件(原生侧边栏之外的部分) │
|
||||
│ · src/client/apes/ 信息层三 tab(本 fork 的增量) │
|
||||
│ · src/client/service.ts ctx.betterSidebar 服务(供其他插件注册) │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 二、构建产物
|
||||
|
||||
| 产物 | 角色 | 备注 |
|
||||
| --- | --- | --- |
|
||||
| `lib/index.js` | host 半(ESM,Node) | 路由、围栏、数据面 |
|
||||
| `lib/invariant.js` | 不变量面(零依赖) | host/client 都要 import,**不得**引入 Node 内置或宿主包 |
|
||||
| `lib/client.js` | client 主 bundle | 浏览器 `window.__ModuleLoader__.load({...})` 形态,**不是** CJS/ESM,Node 里 `require` 会因 `window is not defined` 抛错 |
|
||||
| `lib/client-registry.js` | 懒加载 chunk | 高开销能力(图标/预览库)按需加载 |
|
||||
| `lib/client-editor.js` | 懒加载 chunk | 编辑器(CodeMirror 系) |
|
||||
| `lib/client-locale.js` | 懒加载 chunk | 语言包 |
|
||||
| `lib/client-mermaid.js` | 懒加载 chunk | Mermaid 渲染 |
|
||||
| `lib/client-terminal.js` | 懒加载 chunk | 终端 |
|
||||
| `lib/types/**/*.d.ts` | 类型声明 | 供消费方类型检查 |
|
||||
|
||||
> 懒加载动机:主 bundle 不应静态 import 重型预览库;chunk 通过宿主 `ctx.modules` 解析外部依赖
|
||||
> (入口在 `src/client/index.tsx` 里先 `setChunkModuleSystem(ctx.modules)`)。
|
||||
|
||||
## 三、三层接口面
|
||||
|
||||
### 3.1 宿主 → 插件(cordis 注入)
|
||||
|
||||
```ts
|
||||
// src/index.ts
|
||||
export const name = 'dsh-apes-sidebar'
|
||||
export const inject = ['webServer', 'sessions', 'webRuntime', 'tools']
|
||||
export function apply(ctx: Context, config?: SidebarConfig): void
|
||||
```
|
||||
|
||||
客户端侧读取的宿主面(结构化镜像,运行时由宿主提供):
|
||||
`ctx.sessions.list`(`getSnapshot/subscribe`)、`ctx.sidebarRight`(原生右列与 `openResource`)、
|
||||
`ctx.slots`、`ctx.settings`(挂载行 `config`)、`ctx.modules`(懒加载 chunk 的模块系统)。
|
||||
|
||||
### 3.2 插件 → 宿主
|
||||
|
||||
| 方向 | 手段 |
|
||||
| --- | --- |
|
||||
| 注册 tab 类型与 tab 体 | `src/client/native/registerNativeSurface()` → 原生右侧栏注册表 |
|
||||
| 打开会话内文件 | `ctx.sidebarRight.openResource('dsh-resource://file/…')` |
|
||||
| 暴露 HTTP 能力 | `/sidebar/` 前缀路由:`api` · `file` · `html` · `upload` · `ws/terminal` · `ws/agent-opens` · `ws/agent-terminals` |
|
||||
|
||||
### 3.3 插件 → 其他插件(对外服务)
|
||||
|
||||
`ctx.betterSidebar`(`src/client/service.ts`)对外开放 `registerTab` / `registerFileViewer` 等,
|
||||
并按「能力清单」做前向兼容门控(能力只增不减)。对外契约见仓库内
|
||||
[docs/external-plugin-guide.md](../blob/main/docs/external-plugin-guide.md)。
|
||||
服务版本常量 `SIDEBAR_SERVICE_VERSION` 必须与 `package.json` / `dsh.plugin.json` 的版本三方一致
|
||||
(有单元测试断言这一对)。
|
||||
|
||||
## 四、面板宿主与层叠契约(重点雷区)
|
||||
|
||||
插件的所有面板渲染在一个 **body 级(或壳层 viewport 级)的宿主层**里:
|
||||
|
||||
```css
|
||||
[data-dsh-panel-host] { position: fixed; inset: 0; z-index: 25; pointer-events: none }
|
||||
```
|
||||
|
||||
`z-index: 25` 不是随手取的,它要同时满足:**在 app 内容与壳层 overlay 之上、在宿主交给插件用的
|
||||
overlay 槽之下**。完整取义:
|
||||
|
||||
| 层级 | 对象 |
|
||||
| --- | --- |
|
||||
| `z-index: auto` | app 的流内内容、`.dshDesktopFrame` |
|
||||
| **25** | **本插件的面板宿主** |
|
||||
| 30 | 宿主的动态插件面板(ui-cordis) |
|
||||
| 100+ | app 的 overlay 栈(Toast / onBoarding 等) |
|
||||
| 1000 | `.dshDesktopOverlay[data-shell-overlay]`(宿主给 `shell.overlay` 槽的容器,第三方弹窗在此) |
|
||||
|
||||
**陷阱(已修,`4422001`)**:DSH 桌面壳的有框模式会给上游 `#root` 打标
|
||||
`data-dsh-desktop-content-viewport` 并施加 `isolation: isolate`,使 `#root` 成为一个**层叠上下文**。
|
||||
宿主自己的 overlay 层就在这个上下文内;若插件宿主挂在 `document.body`(上下文之外),
|
||||
它是「带正 z-index 的定位元素」,会画在整个上下文**之上**——把宿主 overlay 里的弹窗(如社区插件市场)盖住。
|
||||
|
||||
```ts
|
||||
// src/client/desktop-env.ts —— 修法:挂进壳层的 content viewport
|
||||
export function panelHostParent(doc: Document = document): HTMLElement {
|
||||
return doc.querySelector<HTMLElement>('#root[data-dsh-desktop-content-viewport]') ?? doc.body
|
||||
}
|
||||
```
|
||||
|
||||
进入该上下文后,25 恢复原意(在 app frame 之上、在 overlay 层 1000 之下);无该标记的部署
|
||||
(纯网页 / 未打标的壳)仍挂 `document.body`,行为不变。重新锚定守卫同时监听
|
||||
`document.body` 的 `data-dsh-desktop-mode`(壳层打标与它是同一个 effect),因此壳层在插件挂载**之后**
|
||||
才框住 `#root` 的情况也能被搬到正确位置。
|
||||
|
||||
## 五、数据流示例
|
||||
|
||||
**会话 → 信息层**
|
||||
|
||||
```
|
||||
ctx.sessions.list.getSnapshot() → { byId, projectionsBySession? }
|
||||
useSyncExternalStore(subscribe) (uSES 安全订阅)
|
||||
├─ apes:agents → rootAncestor(byId, scope.sessionId) 找主代理
|
||||
│ → projectionsBySession[rootId].values.subagentCatalog 取直系子代理
|
||||
│ (无投影时回退 byId 里的 durable 子会话)
|
||||
└─ apes:profile / apes:apps → byId[scope.sessionId](标题 / cwd / running)
|
||||
```
|
||||
|
||||
**文件 → 预览**(HTML 为例)
|
||||
|
||||
```
|
||||
编辑器/diff 面板 → /sidebar/html/<session>/<path> (路由负责路径围栏与体积上限)
|
||||
主框架 fetch 取回文档文本 → 注入 <base href="<该文件的路由目录>">
|
||||
→ iframe srcDoc + sandbox(不透明源)
|
||||
取失败时回退为 iframe src=路由 URL,把路由的答复原样透出
|
||||
```
|
||||
|
||||
## 六、关键不变量(改动前必读)
|
||||
|
||||
1. **host / client 拆分是结构性的**:client bundle 在浏览器运行,不得 import Node 内置;
|
||||
`src/invariant.ts` 两侧共用,必须保持零依赖。
|
||||
2. **`dsh-better-sidebar` 是 peer,永不打包**:一旦被内联,client 会带上第二份侧边栏注册表,
|
||||
tab 注册进死实例,界面上永不出现。
|
||||
3. **tab id 前缀归本 fork**:信息层用 `apes:*`;不得占用上游/宿主内部 id。
|
||||
4. **HTML 预览的沙箱令牌不得放宽**(不加 `allow-same-origin` 等);交付方式只能是「主框架取文档 + `srcDoc`」。
|
||||
5. **颜色只用宿主设计令牌**(`--dsw-alias-*`),不写硬编码色值;几何量(字号/间距/圆角)无公开令牌,内联即可。
|
||||
6. **所有面向用户的字符串走 i18n**,`zh` / `en` 必须同时存在。
|
||||
7. **变更后必须重新同步到 profile**:构建产物非字节确定性,且 profile 的 `file:` 依赖是复制语义,
|
||||
不能假定旧副本可用(见 [04-部署指南](04-部署指南))。
|
||||
@@ -1,109 +0,0 @@
|
||||
# 03 · 功能说明
|
||||
|
||||
> 本篇是**功能清单与行为契约**:有哪些面板、数据从哪来、文案与配色怎么走、安全边界在哪、能配什么。
|
||||
|
||||
## 一、面板与标签页总表
|
||||
|
||||
「来源」列:**上游** = 来自 `dsh-better-sidebar`;**宿主** = DSH 自身提供并占用该槽位;**本 fork** = 本项目的增量。
|
||||
|
||||
| id | 名称 | order | 来源 | 数据 / 行为 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `editor` | 文件 | 10 | 上游 | 文件树 + 编辑器(CodeMirror 系,懒加载 `client-editor`) |
|
||||
| `git` | 文件变动 | 20 | 上游 | 工作区变更、单个 diff 视图 |
|
||||
| `subagent` | 子代理 | 30 | 上游 | 会话的子代理视图 |
|
||||
| `sidechat` | 侧边对话 | 35 | 上游 | 会话内旁路对话 |
|
||||
| `terminal` | 终端 | 40 | 上游 | PTY 终端(懒加载 `client-terminal`;v0.24.1 线起由宿主内置终端承担) |
|
||||
| `browser` | 浏览器 | 50 | 上游 | 内嵌浏览器/HTML 预览宿主 |
|
||||
| `diff` | (内部) | -1 | 上游 | 非用户可见,供内部打开 diff |
|
||||
| **`apes:agents`** | **智能体** | **60** | **本 fork** | 主代理 + 直系子代理拓扑 |
|
||||
| **`apes:apps`** | **应用** | **65** | **本 fork** | 当前会话所属工作区(宿主暂无「应用」feed) |
|
||||
| **`apes:profile`** | **画像** | **70** | **本 fork** | 当前会话摘要(标题 / cwd / 运行态) |
|
||||
|
||||
> order 10–50 是上游/宿主既定带宽,apos 信息层取 60/65/70 留出余量,避免与原生面板抢占位置。
|
||||
|
||||
## 二、信息层三 tab(本 fork 的增量)
|
||||
|
||||
三个 tab 全部是**单实例**(`single: true`),且都按 DSH 标准 `TabDescriptor` 注册,
|
||||
因此自动获得原生侧边栏的全部待遇:进入原生 guide、tab 芯片带图标、可开合与分栏。
|
||||
|
||||
| tab | 展示内容 | 数据来源 | 空态 |
|
||||
| --- | --- | --- | --- |
|
||||
| `apes:agents` | 根(主代理)卡片 + 其直系子代理卡片;每行显示标签、`one-shot`/`continuable`、运行态圆点 | ① `ctx.sessions.list` 的 `byId`(durable 会话摘要);② 宿主的会话投影 `projectionsBySession[sessionId].values.subagentCatalog`(直系子代理清单) | 「当前会话没有子代理」提示 |
|
||||
| `apes:apps` | 当前会话的工作区(cwd) | `byId[scope.sessionId].cwd`,回退 `scope.cwd` | 明确写出「宿主今日无应用 feed」 |
|
||||
| `apes:profile` | 会话标题、运行/空闲、cwd | `byId[scope.sessionId]` | 「无 cwd」提示 |
|
||||
|
||||
**实现要点**:两个来源被归一化成同一种行形状 `{ id, label, mode }`,**活动态统一从 `byId` 读**,
|
||||
于是「投影已就绪」与「仅有 durable 摘要」两种情况下渲染一致;这也是上游 0.1.6→0.1.7 会话 feed
|
||||
迁移(`subagentsByParent` → `projectionsBySession`、`list.current` 删除)后的落地方式。
|
||||
|
||||
## 三、文件预览器
|
||||
|
||||
| id | 适用 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `image` | 图片 | 走媒体路由,受 `mediaLimit` 限制 |
|
||||
| `pdf` | PDF | 内嵌查看 |
|
||||
| `markdown` | Markdown | 渲染(Mermaid 走懒加载 chunk,可手动刷新) |
|
||||
| `html` | HTML | **不透明源沙箱 iframe**,见下节 |
|
||||
| `code` | 代码 | 高亮查看(编辑器懒加载) |
|
||||
| `binary-download` | 二进制 | 只提供下载 |
|
||||
|
||||
### 3.1 HTML 预览的交付方式(重要)
|
||||
|
||||
```
|
||||
路由 /sidebar/html/<session>/<path> ← 路径围栏 + 体积上限(readLimit)
|
||||
│
|
||||
├─ 主框架 fetch(唯一同时被宿主围栏信任、又能带上 Electron 头的角色)
|
||||
│
|
||||
├─ 注入 <base href="<该文件的路由目录>"> ← 让 ./style.css 等相对引用解析回本文件所在目录
|
||||
│
|
||||
└─ iframe srcDoc=<文档> + sandbox=HTML_IFRAME_SANDBOX
|
||||
```
|
||||
|
||||
| 状态 | 自包含 HTML | 多文件 HTML(外链 CSS/JS/图片) |
|
||||
| --- | --- | --- |
|
||||
| 沙箱态(默认) | ✅ 完整可用 | 正文可用,**相对资源不可用**(iframe 源为 `null`,子请求同样被宿主围栏拒绝) |
|
||||
| 临时解锁(无 sandbox 属性) | ✅ | ✅ **相对资源可用**(iframe 继承 GUI 源,成为普通同源请求) |
|
||||
|
||||
设计取舍:**沙箱令牌一个字符都不放宽**。多文件 HTML 想要完整效果需用户显式点「临时解锁」——
|
||||
这是把「放宽边界」的决定权留给用户的显式动作,而不是插件默认降级。
|
||||
|
||||
## 四、安全边界(三层)
|
||||
|
||||
| 层 | 机制 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 预览内容 | `sandbox="allow-scripts allow-popups allow-downloads allow-modals"` | **不含 `allow-same-origin`**:预览文档运行在不透明源里。宿主路由另有 CSP `sandbox` 兜底 |
|
||||
| 插件路由 | 请求信任围栏(`src/trust-fence.ts`) | 校验 Host 头/令牌,拒绝跨站或非授权来源对 `/sidebar/*` 的访问 |
|
||||
| 桌面壳 | Electron 注入的 `x-dsh-desktop-renderer` 头 | 扩展模式下所有路由的前置围栏;只发给主框架与同源子框架。这正是「沙箱 iframe 发不出请求」的根源 |
|
||||
|
||||
## 五、i18n 与主题
|
||||
|
||||
- **语言包**:`src/client/locales.ts` + 19 个 `src/client/locales-*.ts` 覆盖文件;默认 `zh`,缺省回退 `en`。
|
||||
- **本 fork 文案**:集中在 `src/client/apes/locales.ts`,每个 key 同时提供 `zh` / `en`。
|
||||
- **硬性约定**:界面里**不得出现裸字符串**,一律 `t('key')`。
|
||||
- **配色**:只用宿主设计令牌 `--dsw-alias-*`(bg / border / label / state / interactive 系列),
|
||||
写法为 `var(--dsw-alias-xxx, <字面回退>)`,保证在非 DSH 环境(单元测试)也能渲染。
|
||||
**无公开的 `--ds-*` 几何令牌**,字号/间距/圆角内联即可。
|
||||
|
||||
## 六、配置面(挂载行 `config`)
|
||||
|
||||
配置写在插件挂载行上(profile 的 `cordis.patch.yml` 或 bundle patch 的 insert 行),设置页写的也是它:
|
||||
|
||||
| 字段 | 默认 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| `readLimit` | `512 KiB` | 单文件读取上限(超出截断并标记 `truncated`) |
|
||||
| `mediaLimit` | `20 MiB` | 媒体(图片等)读取上限 |
|
||||
| `uploadLimit` | `128 MiB` | 上传上限 |
|
||||
| `listLimit` | `1000` | 目录列举条目上限 |
|
||||
| `terminalsPerSession` | `3` | 每会话终端数上限 |
|
||||
| `shell` / `shellArgs` | 自动解析 | 终端 shell 与其启动参数(`shellArgs` 非空时替换默认 `-l`) |
|
||||
|
||||
例:在挂载行里指定 shell
|
||||
|
||||
```yaml
|
||||
- insert:
|
||||
- id: better-sidebar
|
||||
name: 'dsh-apes-sidebar'
|
||||
config:
|
||||
shell: /bin/zsh
|
||||
shellArgs: ['--noprofile', '--no-rc']
|
||||
```
|
||||
@@ -1,163 +0,0 @@
|
||||
# 04 · 部署指南
|
||||
|
||||
> 目标:在一台机器上把本插件挂进 DSH,并确认它**真的在跑**。
|
||||
> 关键前提:**先确认宿主版本,再选版本线**——选错线的唯一症状是「静默失效」。
|
||||
|
||||
## 一、前置条件
|
||||
|
||||
| 项 | 要求 |
|
||||
| --- | --- |
|
||||
| Node | ≥ 20(`engines.node`);本机验证用 22.x |
|
||||
| pnpm | 11.x(`pnpm install` 会自动触发 `prepare: tsdown` 构建) |
|
||||
| DSH | 见下面的版本矩阵 |
|
||||
| 终端可用性 | 仅 v0.19.1 线需要额外放行 `node-pty` 原生构建(见 §五) |
|
||||
|
||||
### 1.1 宿主版本矩阵(先读这张表)
|
||||
|
||||
| 版本线 | 分支 | 插件版本 | peer 声明 | 适用宿主 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 旧宿主线 | `main` | 1.2.0 | `^0.1.5-rc.1` | DSH `0.1.5-rc.1+`(含 `0.1.5-rc.2`,即 **DSH Desktop 2.0.10** 内置版本) |
|
||||
| 新宿主线 | `sync/v0.24.1` | 1.3.0 | `^0.2.0-rc.1` | DSH `0.2.0-rc.1+` |
|
||||
|
||||
**怎么查宿主版本**(扩展模式桌面版):
|
||||
|
||||
```bash
|
||||
# 内置的 dsh 版本
|
||||
grep -m1 '"version"' "/Applications/DSH Desktop.app/Contents/Resources/app/node_modules/@deepseek-ai/dsh/package.json"
|
||||
# 或看 profile 里装的宿主包
|
||||
grep -m1 -A2 '"@deepseek-ai/dsh"' ~/.dsh/profiles/desktop/package.json
|
||||
```
|
||||
|
||||
**选线决策**
|
||||
|
||||
```
|
||||
宿主 ≥ 0.2.0-rc.1 ? ── 是 ──▶ sync/v0.24.1(1.3.0)
|
||||
└─ 否 ──▶ main(1.2.0)
|
||||
```
|
||||
|
||||
> ⚠️ **peer 不匹配不会报错。** 宿主以 `semver.satisfies(host, peerRange, { includePrerelease: true })` 预检,
|
||||
> caret 跨 minor 判 false 后**整行静默禁用**:不加载、无日志、界面上「像没装」。
|
||||
> 因此「装完没反应」的第一件事永远是核对宿主版本 ↔ peer 范围。
|
||||
|
||||
## 二、安装方式(本 fork 未发布 npm 包)
|
||||
|
||||
`npm view dsh-apes-sidebar` 返回 404 —— **本 fork 不在 npm 上**(README 里的 npm 徽章/命令是沿用上游文档)。
|
||||
实际部署使用「本地源码 + `file:` 链接依赖」:
|
||||
|
||||
### 步骤 1:构建
|
||||
|
||||
```bash
|
||||
cd <插件仓库绝对路径>
|
||||
git checkout main # 或 sync/v0.24.1(按宿主版本选)
|
||||
pnpm install # 会顺带跑 prepare: tsdown
|
||||
pnpm typecheck && pnpm build && pnpm test # 门禁三连
|
||||
```
|
||||
|
||||
产物(`lib/`):`index.js` / `invariant.js`(host)+ `client.js` + 懒加载 chunk
|
||||
(`client-registry` / `client-editor` / `client-locale` / `client-mermaid` / `client-terminal`)+ `types/`。
|
||||
|
||||
### 步骤 2:接进 profile
|
||||
|
||||
编辑 `~/.dsh/profiles/<profile>/package.json`(桌面版为 `desktop`,网页版通常为 `web`):
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"dependencies": {
|
||||
"dsh-apes-sidebar": "file:/绝对路径/dsh-apes-sidebar" // ← 必须是绝对路径
|
||||
},
|
||||
"dsh": {
|
||||
"profile": {
|
||||
"bundles": [
|
||||
"…",
|
||||
"dsh-apes-sidebar" // ← 加入 bundle 栈
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 也可以在 profile 自己的 `cordis.patch.yml` 里手写挂载行(老方式)。**两种方式不要同时用**:
|
||||
> 会双挂载成两个 Node 半、两个侧边栏。包内自带的 `cordis.patch.yml` 是 `dsh.bundle.patch` 层,
|
||||
> 由官方 CLI 安装时自动追加 bundle,并带「聚合包双挂载退让」守卫。
|
||||
|
||||
### 步骤 3:让 profile 重新安装依赖
|
||||
|
||||
```bash
|
||||
cd ~/.dsh/profiles/<profile> && pnpm install
|
||||
```
|
||||
|
||||
**为什么必须重装**:pnpm 对 `file:` 依赖是**复制**语义(不是软链)。源码 `pnpm build` 之后,
|
||||
profile 的 `node_modules/dsh-apes-sidebar/lib` **仍是旧副本**——这就是「改了代码但界面没变」的头号原因。
|
||||
DSH 启动时也会 reconcile profile 的 pnpm workspace,但不要假定它一定帮你刷新。
|
||||
|
||||
### 步骤 4:完整重启
|
||||
|
||||
**⌘Q 完全退出 DSH,再重新启动**(不是关窗口)。client 半有热加载,但 host 半与依赖变更必须走完整重启。
|
||||
|
||||
## 三、验收清单
|
||||
|
||||
重启后逐条核对(每条都对应一个可观察事实):
|
||||
|
||||
- [ ] **右列只有一套侧边栏**、一个开合控件(不再出现自绘的第二套开关)
|
||||
- [ ] 原生 guide 里能看到本插件的 tab 芯片,且**带图标**
|
||||
- [ ] 三个信息层 tab 存在:**智能体 / 应用 / 画像**(order 在原生面板之后)
|
||||
- [ ] `apes:agents` 打开后:有主代理卡片;有子代理时列出直系子代理(标签 + 模式 + 运行态)
|
||||
- [ ] 打开一个 HTML 文件:**沙箱态下自包含 HTML 能正常渲染**(不再是 `forbidden`)
|
||||
- [ ] 打开「社区插件市场」:弹窗**完整盖在**本插件面板之上(面板不切弹窗角)
|
||||
- [ ] 页面左下角**没有**红色诊断条(有的话说明 client 生命周期某处抛错)
|
||||
- [ ] 开发者控制台**没有** `[dsh-apes-sidebar] … error` 前缀的错误
|
||||
|
||||
## 四、网页 profile 与桌面 profile 的差异
|
||||
|
||||
| 项 | 桌面版(DSH Desktop) | 网页版 |
|
||||
| --- | --- | --- |
|
||||
| profile 目录 | `~/.dsh/profiles/desktop` | 通常 `~/.dsh/profiles/web` |
|
||||
| 外壳 | Electron 壳,有「扩展模式/兼容模式」与请求围栏 | 浏览器直连宿主端口 |
|
||||
| 预览 iframe | 受渲染器围栏约束(因此必须走 `srcDoc` 交付) | 无该围栏,但仍受路由 CSP/沙箱约束 |
|
||||
| 面板宿主层级 | 壳层会给 `#root` 打 content-viewport 标记 → 宿主挂进 `#root` | 无标记 → 宿主挂 `document.body` |
|
||||
| 刷新方式 | ⌘Q 完整退出再启动 | 硬刷新(Cmd/Ctrl+Shift+R) |
|
||||
|
||||
## 五、v0.19.1 线特有:放行 node-pty 原生构建
|
||||
|
||||
`main`(上游 v0.19.1 基线)在 `package.json` 中仍有 `node-pty` 依赖;pnpm 11 默认**拦截依赖的构建脚本**,
|
||||
于是首次安装会失败或终端不可用:
|
||||
|
||||
```bash
|
||||
cd ~/.dsh/profiles/<profile>
|
||||
pnpm approve-builds # 选中 node-pty,回车确认
|
||||
pnpm rebuild node-pty # 必要时
|
||||
```
|
||||
|
||||
`sync/v0.24.1`(1.3.0)**不含** `node-pty`(终端交由宿主内置承担),无需此步。
|
||||
(`main` 与 `sync/v0.24.1` 的 `node-pty` 引用数:1 / 0。)
|
||||
|
||||
## 六、切换与卸载
|
||||
|
||||
同一条线只能装一个(两条线都注册 `ctx.betterSidebar`,entry id 冲突,第二个被丢弃)。
|
||||
仓库自带切换脚本:
|
||||
|
||||
```bash
|
||||
bash scripts/switch.sh --status # 只看当前是哪条线
|
||||
bash scripts/switch.sh aps # 切到本 fork 线(本地 file: 安装)
|
||||
bash scripts/switch.sh upstream # 切回上游 dsh-better-sidebar
|
||||
bash scripts/switch.sh upstream 0.18.0 # 指定上游版本
|
||||
bash scripts/switch.sh aps --profile web # 指定 profile
|
||||
```
|
||||
|
||||
**完全卸载**:从 profile 的 `dependencies` 与 `dsh.profile.bundles` 中移除 `dsh-apes-sidebar`,
|
||||
`pnpm install`,⌘Q 完整重启。若曾手写过 `cordis.patch.yml` 挂载行,一并删除(否则会留下悬空 entry)。
|
||||
|
||||
> 若同时装了聚合包(如 `@linxin666/dsh-web-ui-all`)且它内部也挂载了本包,
|
||||
> 包内 bundle patch 的守卫会让**本行退让**;此时请确认聚合包在 bundle 栈中位于本行之前。
|
||||
|
||||
## 七、变更后的重新部署
|
||||
|
||||
```bash
|
||||
# 插件仓库
|
||||
pnpm typecheck && pnpm build && pnpm test
|
||||
# profile
|
||||
cd ~/.dsh/profiles/<profile> && pnpm install
|
||||
# ⌘Q 退出 DSH 再启动
|
||||
```
|
||||
|
||||
改完代码**必须**走完这三步:产物构建非字节确定性,profile 的 `file:` 副本也不会自动跟随。
|
||||
@@ -1,133 +0,0 @@
|
||||
# 05 · 运维手册
|
||||
|
||||
> 本篇是**日常运维、升级与回滚**的操作手册。所有命令都以「先留锚点、再动手、改完回读验证」为原则。
|
||||
|
||||
## 一、版本线与分支策略(当前远端实况)
|
||||
|
||||
| ref | 指向 | 含义 / 用途 |
|
||||
| --- | --- | --- |
|
||||
| `main` | `4422001` | **旧宿主线**:插件 1.2.0,上游基线 v0.19.1,peer `^0.1.5-rc.1`。本机 DSH Desktop 2.0.10 正在跑这条 |
|
||||
| `sync/v0.24.1` | `e5bbef1` | **新宿主线**:插件 1.3.0,上游基线 v0.24.1,peer `^0.2.0-rc.1`。待 ≥0.2.0 宿主实机验证 |
|
||||
| `backup/pre-sync-v0.19.1` | `96cc334` | v0.19.1 re-base **之前**的状态(旧 fork 三角色 tab 版) |
|
||||
| `backup/pre-sync-v0.24.1` | `4422001` | v0.24.1 re-base 之前的 `main`(等价于当前 main,作为下次升级的锚点) |
|
||||
| `tags/apes-v1.1.0-pre-sync` | `96cc334` | 与上面第一支 backup 同一提交,tag 形式的锚点 |
|
||||
| `tags/apes-v1.2.0` | `4422001` | 当前已验收的 1.2.0 线 |
|
||||
|
||||
**纪律**:任何 re-base / 大改之前,先建 `backup/pre-sync-<新基线>` 分支 + `apes-v<版本>` tag,
|
||||
并**推送**到远端(本地锚点在多人/多机场景等于不存在)。
|
||||
|
||||
## 二、回滚剧本
|
||||
|
||||
### 场景 A:某次提交引入回归(单点回退)
|
||||
|
||||
```bash
|
||||
cd <插件仓库>
|
||||
git log --oneline -5 # 定位可疑提交
|
||||
git revert <sha> # 保留历史的回退(推荐)
|
||||
# 或整体丢弃:git reset --hard <上一提交>
|
||||
pnpm typecheck && pnpm build && pnpm test
|
||||
cd ~/.dsh/profiles/<profile> && pnpm install # file: 是复制语义,必须重装
|
||||
# ⌘Q 完全退出 DSH → 重启
|
||||
```
|
||||
|
||||
### 场景 B:整条线回退(例如新线在真机上不可用)
|
||||
|
||||
```bash
|
||||
git checkout main # 或 backup/pre-sync-v0.24.1
|
||||
pnpm install && pnpm build
|
||||
cd ~/.dsh/profiles/<profile> && pnpm install
|
||||
# ⌘Q 重启
|
||||
```
|
||||
|
||||
### 场景 C:profile 依赖/config 被改坏
|
||||
|
||||
```bash
|
||||
cd ~/.dsh/profiles/<profile>
|
||||
cp package.json package.json.bak.$(date +%s) # 先备份
|
||||
bash <插件仓库>/scripts/switch.sh aps # 或 upstream,脚本会重写依赖与 bundle 行
|
||||
pnpm install
|
||||
# ⌘Q 重启
|
||||
```
|
||||
|
||||
**回滚验收**:重启后按 [04-部署指南](04-部署指南) §三 的验收清单逐条核对(至少「右列一套侧边栏」
|
||||
与「三个信息层 tab 存在」两条)。
|
||||
|
||||
## 三、发布 / 变更纪律
|
||||
|
||||
1. **版本三元组必须一致**:
|
||||
- `package.json` 的 `version`
|
||||
- `dsh.plugin.json` 的 `version`
|
||||
- `src/client/service.ts` 的 `SIDEBAR_SERVICE_VERSION`
|
||||
三者不一致会被单元测试(`tests/service.spec.ts`)直接拦下——**不要改其中一个而忘了另一个**。
|
||||
2. **门禁三连必过**:`pnpm typecheck && pnpm build && pnpm test`。当前基线:`main` 131 个测试文件 /
|
||||
1367 通过;`sync/v0.24.1` 141 个文件 / 1515 通过(另有 9 个 skip)。
|
||||
3. **产物非字节确定性**:两次构建的字节数可能相同但 **sha 不同**。因此「改完代码 → 让 profile 用旧副本」
|
||||
永远不成立;必须重新 `pnpm build` + 让 profile 重新安装。
|
||||
4. **发布方式**:本 fork 未发布 npm 包,发布 = 推分支/tag + 目标机器 `git fetch && checkout && pnpm build`
|
||||
+ profile 重装。发布前把 `README.md` 与 `docs/wiki/` 一并更新(wiki 见 §六)。
|
||||
5. **不要顺手改无关文件**:re-base 时保持「只带自己的增量」,避免下次升级冲突面扩大。
|
||||
|
||||
## 四、日常巡检清单
|
||||
|
||||
| 频率 | 检查项 | 命令 / 判据 |
|
||||
| --- | --- | --- |
|
||||
| 每次部署后 | 版本三元组一致 | 见 §三.1 |
|
||||
| 每次部署后 | 产物标记齐全 | `grep -c 'apes:agents' lib/client.js`(>0)、`grep -c 'dsh-desktop-content-viewport' lib/client.js`(>0) |
|
||||
| 每次部署后 | profile 副本已刷新 | 比对 `lib/client.js` 与 `~/.dsh/profiles/<p>/node_modules/dsh-apes-sidebar/lib/client.js` 的 sha256 |
|
||||
| 每次升级宿主/插件 | 宿主版本 ↔ peer 范围 | 见 [04-部署指南](04-部署指南) §1.1(不匹配 = 静默禁用) |
|
||||
| 每月/大改前 | 锚点是否已推远端 | `git ls-remote <remote> \| grep -E 'heads/(main\|sync)\|tags/apes'` |
|
||||
| 异常时 | 诊断条与控制台 | 页面左下角红条;控制台 `[dsh-apes-sidebar]` 前缀 |
|
||||
|
||||
## 五、备份与配置铁律
|
||||
|
||||
- **改任何 profile 配置前先备份**,改完**回读验证**(不要假定写入即生效):
|
||||
|
||||
```bash
|
||||
cp ~/.dsh/profiles/<profile>/package.json{,.bak.$(date +%s)}
|
||||
# 改完:
|
||||
python3 -c "import json;json.load(open('$HOME/.dsh/profiles/<profile>/package.json'));print('JSON OK')"
|
||||
```
|
||||
|
||||
- **DSH 应用配置的类型陷阱**:部分字段(如材质/透明度类的 `macosMaterial`)必须是**字符串**
|
||||
(`"off"` / `"transparent"`)。若被写成 YAML 布尔(`false`),DSH 会启动进**恢复模式**。
|
||||
改完务必回读校验类型:
|
||||
|
||||
```bash
|
||||
python3 -c "import yaml;d=yaml.safe_load(open('$HOME/.dsh/settings.yaml'));v=d.get('macosMaterial');print(v, type(v).__name__)"
|
||||
```
|
||||
|
||||
- **不要回滚/手工编辑** profile 的 `pnpm-lock.yaml`:由 pnpm 重建;手工改会导致解析错乱。
|
||||
- **凭据不进仓库**:远端推送用系统凭据助手(`git credential fill`),仓库内不写 token。
|
||||
|
||||
## 六、百科(wiki)本身的维护
|
||||
|
||||
百科源文件版本化在仓库 `docs/wiki/`,通过脚本推送到 Gitea wiki:
|
||||
|
||||
```bash
|
||||
bash scripts/push-wiki.sh # 推送/更新全部页面(幂等:有则 PATCH,无则 POST)
|
||||
bash scripts/push-wiki.sh 05-运维手册 # 只推一页
|
||||
```
|
||||
|
||||
凭据来自 `git credential fill`(与推送代码同一套,不落盘)。修改文档的推荐流程:
|
||||
**改 `docs/wiki/*.md` → 提交到仓库 → 跑脚本推送**,这样 wiki 与代码同源可回溯。
|
||||
|
||||
## 七、诊断与日志
|
||||
|
||||
| 信号 | 含义 | 位置 |
|
||||
| --- | --- | --- |
|
||||
| 左下角红色诊断条 `[dsh-apes-sidebar] <phase> error: …` | client 生命周期某阶段抛错(mount / native / 懒加载等) | 页面左下角(`z-index: 2147483000`),同时 `console.error` 打前缀 |
|
||||
| 控制台 `[dsh-apes-sidebar] …` | 同上,且带上具体阶段名 | 开发者工具 Console |
|
||||
| 「插件像没装」且**无任何日志** | 宿主兼容性预检失败(peer 不匹配)→ **静默禁用** | 只能靠版本矩阵排查,见 [06-故障排查](06-故障排查) §1 |
|
||||
| 界面正常但面板层级错乱 | 面板宿主挂载点/层叠上下文问题 | 见 [02-系统架构](02-系统架构) §四 |
|
||||
|
||||
## 八、性能与容量
|
||||
|
||||
| 项 | 观察值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `lib/client.js` | ≈ 0.9 MB | 主 bundle,始终加载 |
|
||||
| `lib/client-mermaid.js` | ≈ 7 MB(+12 MB map) | 懒加载,仅在需要渲染 Mermaid 时拉取 |
|
||||
| `lib/client-editor.js` | ≈ 2 MB | 懒加载,仅打开编辑器时 |
|
||||
| `lib/client-terminal.js` | ≈ 0.5 MB | 懒加载,仅打开终端时 |
|
||||
| 全量测试 | 约 3–4 分钟 | 变更后按需跑全量,日常可只跑相关 spec |
|
||||
|
||||
主 bundle 不应静态引入上述重型库;新增重型依赖一律走 `src/client/chunks/`(懒加载 chunk)。
|
||||
@@ -1,208 +0,0 @@
|
||||
# 06 · 故障排查
|
||||
|
||||
> 每条都给出**症状 → 根因(机制)→ 处置 → 验证**。标注 ✅ 的条目是在真机上定位并验证过的案例。
|
||||
|
||||
## 速查表
|
||||
|
||||
| 症状 | 最可能根因 | 跳到 |
|
||||
| --- | --- | --- |
|
||||
| 装完「像没装」,界面无变化且**无任何日志** | peer 范围与宿主版本不匹配 → 静默禁用 | §1 |
|
||||
| 页面出现**两个侧边栏** / 两套开合按钮 | 双挂载(两条线同装,或残留挂载行) | §2 |
|
||||
| HTML 预览只显示 `forbidden` | 桌面壳渲染器围栏 + 不透明源 iframe | §3 |
|
||||
| 插件面板盖住宿主/第三方弹窗(如插件市场) | 面板宿主挂在层叠上下文之外 | §4 |
|
||||
| 终端打不开 / `node-pty` 加载失败 | pnpm 拦截原生构建(仅 v0.19.1 线) | §5 |
|
||||
| 改了代码,界面没变 | 产物未重建 / profile 副本未刷新 / 浏览器缓存 | §6 |
|
||||
| DSH 拒绝加载插件、entry pending | 插件声明(`dsh.plugin.json` / bundle patch)不合法 | §7 |
|
||||
| DSH 启动进**恢复模式** | profile 配置被写成错误类型(如 YAML 布尔) | §8 |
|
||||
|
||||
---
|
||||
|
||||
## §1 装完「像没装」:静默禁用 ✅
|
||||
|
||||
**症状**:profile 里依赖装了、文件也在,但界面没有任何变化;日志里也没有插件相关报错。
|
||||
|
||||
**根因**:宿主启动时做兼容性预检,判定 `peerDependencies` 与自身版本是否满足:
|
||||
|
||||
```
|
||||
semver.satisfies(hostVersion, peerRange, { includePrerelease: true })
|
||||
```
|
||||
|
||||
caret 范围**跨 minor 必然为 false**(例如 `^0.1.5-rc.1` 对 `0.2.0`)。判定失败后宿主的处理是
|
||||
**跳过这一行**——不抛错、不打日志、不加载。
|
||||
|
||||
**处置**:核对宿主版本,改用匹配的版本线(见 [04-部署指南](04-部署指南) §1.1)。
|
||||
|
||||
```bash
|
||||
grep -m1 '"version"' "/Applications/DSH Desktop.app/Contents/Resources/app/node_modules/@deepseek-ai/dsh/package.json"
|
||||
grep -n 'dsh-client-ui-sidebar-right' <插件仓库>/package.json # 看 peer 范围
|
||||
```
|
||||
|
||||
**验证**:重启后右列出现插件 tab;`lib/client.js` 里能 grep 到 `apes:agents`。
|
||||
|
||||
**教训**:这一条没有日志可查,因此**部署前先看版本矩阵**是唯一可靠的预防手段。
|
||||
|
||||
## §2 两个侧边栏 / 两套开合按钮 ✅
|
||||
|
||||
**症状**:窗口里出现两套右侧面板与两组开合控件。
|
||||
|
||||
**根因**:两个方向都可能:
|
||||
1. **旧版遗留**:v0.18.0 基线时插件自绘右侧面板与开关(`toggleCluster`)。升级后自绘部分应已移除。
|
||||
2. **双挂载**:同一条线被挂两次(profile 自己的 `cordis.patch.yml` 里还留着旧挂载行,
|
||||
与 bundle patch 同时生效),或者本 fork 线与上游线同时安装(两者都注册 `ctx.betterSidebar`,
|
||||
entry id 冲突,第二个被丢弃)。
|
||||
|
||||
**处置**:
|
||||
|
||||
```bash
|
||||
# 确认产物里已经没有自绘开关
|
||||
grep -c toggleCluster <插件仓库>/lib/client.js # 期望 0
|
||||
|
||||
# 检查是否双挂载:profile 里是否同时存在 bundle 行 与 手写挂载行
|
||||
grep -n "dsh-apes-sidebar\|dsh-better-sidebar" ~/.dsh/profiles/<profile>/cordis.patch.yml ~/.dsh/profiles/<profile>/package.json
|
||||
```
|
||||
|
||||
- 有残留手写挂载行 → 删除它,只保留一条通道。
|
||||
- 两条线同装 → `bash scripts/switch.sh aps`(或 `upstream`)二选一。
|
||||
- 装了聚合包(如 `@linxin666/dsh-web-ui-all`)→ 确认聚合包在本包之前,让包内守卫生效。
|
||||
|
||||
**验证**:右列只剩一套面板与一个开合控件。
|
||||
|
||||
## §3 HTML 预览显示 `forbidden` ✅
|
||||
|
||||
**症状**:预览 HTML 文件时 iframe 里只有一行裸文本 `forbidden`;而「临时解锁」后又能看。
|
||||
|
||||
**根因(三层)**:
|
||||
|
||||
1. DSH Desktop 在**扩展模式**下给所有路由套了一层请求围栏:`ordinaryBrowserEnabled` 恒为 false,
|
||||
不带 Electron 注入的 `x-dsh-desktop-renderer` 头的请求一律 `403 + "forbidden"`;
|
||||
2. 该头只注入给 **mainFrame** 与**同源**子框架;
|
||||
3. 预览 iframe 的 sandbox 令牌不含 `allow-same-origin`,请求时其源为 `null` → 拿不到头 → 403。
|
||||
|
||||
与「是否升级了插件基线」无关(v0.18.0 起行为一致)。
|
||||
|
||||
**处置**:插件已在 1.2.0+ 用 **`srcDoc` 交付**修复(主框架取文档 → 注入 `<base>` → 注入 iframe),
|
||||
`main` 上对应提交 `4c242ac`。**不要**通过给 iframe 加 `allow-same-origin` 来"修"——那是安全降级。
|
||||
|
||||
**验证**:
|
||||
- 沙箱态下自包含 HTML(含内联脚本)能正常渲染;
|
||||
- 有外链资源的 HTML 在沙箱态下缺资源属**已知限制**,点「临时解锁」后应完整显示。
|
||||
|
||||
**排查命令**(确认宿主围栏在起作用):
|
||||
|
||||
```bash
|
||||
lsof -nP -iTCP -sTCP:LISTEN | grep -i dsh # 找宿主端口(每次启动可能不同)
|
||||
curl -s -o /dev/null -w '%{http_code}\n' "http://127.0.0.1:<端口>/" # 无渲染器头 → 403
|
||||
```
|
||||
|
||||
## §4 插件面板盖住宿主/第三方弹窗 ✅
|
||||
|
||||
**症状**:打开「社区插件市场」(宿主自带 `dsh-community-market`)时,弹窗左下角被本插件的
|
||||
文件面板整齐切掉一块。
|
||||
|
||||
**根因**:桌面壳的有框模式会给上游 `#root` 打标 `data-dsh-desktop-content-viewport` 并施加
|
||||
`isolation: isolate`,使 `#root` 成为**层叠上下文**;宿主的 overlay 层(`.dshDesktopOverlay`,
|
||||
`z-index: 1000`)位于其中。插件面板宿主若挂在 `document.body`(该上下文之外)且是定位元素,
|
||||
就会画在整个上下文之上,把 overlay 里的弹窗盖住。
|
||||
|
||||
**处置**:已修(`4422001`)——面板宿主改为挂进 `#root[data-dsh-desktop-content-viewport]`,
|
||||
无标记时回退 `document.body`。**不要**通过调大 `z-index` 解决(25 的取义见
|
||||
[02-系统架构](02-系统架构) §四;乱调会破坏与宿主其他层级的关系)。
|
||||
|
||||
**验证**:插件市场弹窗完整盖住本插件面板,同时本插件面板仍在对话内容之上。
|
||||
|
||||
## §5 终端不可用 / `node-pty` 加载失败(仅 v0.19.1 线)✅
|
||||
|
||||
**症状**:终端面板报 shell 启动失败或提示 `node-pty` 加载失败;`pnpm install` 阶段报
|
||||
"ignored build scripts"。
|
||||
|
||||
**根因**:pnpm 11 默认**拦截依赖的构建脚本**,`node-pty` 的原生二进制没被编译。
|
||||
(`main` 依赖 `node-pty`;`sync/v0.24.1` 已移除该依赖,终端由宿主内置承担。)
|
||||
|
||||
**处置**:
|
||||
|
||||
```bash
|
||||
cd ~/.dsh/profiles/<profile>
|
||||
pnpm approve-builds # 选中 node-pty
|
||||
pnpm rebuild node-pty # 必要时
|
||||
# ⌘Q 完整退出 DSH 再启动
|
||||
```
|
||||
|
||||
**验证**:终端能开出 shell;`node_modules/node-pty/build/Release/*.node` 存在。
|
||||
|
||||
## §6 改了代码,界面没变 ✅
|
||||
|
||||
**症状**:源码已改、`pnpm build` 也跑了,但界面行为仍是旧的。
|
||||
|
||||
**根因(三个叠加因素)**:
|
||||
|
||||
1. 产物**非字节确定性**,且 profile 的 `file:` 依赖是**复制**语义 → profile 里仍是旧副本;
|
||||
2. `lib/client.js` 是浏览器的 `window.__ModuleLoader__.load({...})` bundle,**不是 CJS/ESM**,
|
||||
在 Node 里 `require` 会因 `window is not defined` 报错——因此不能用「在 Node 里试跑」来验证前端改动;
|
||||
3. host 半的改动必须重启进程(client 半有热加载,host 半没有)。
|
||||
|
||||
**处置**:
|
||||
|
||||
```bash
|
||||
cd <插件仓库> && pnpm build
|
||||
cd ~/.dsh/profiles/<profile> && pnpm install # 刷新 file: 副本
|
||||
# ⌘Q 完整退出 DSH → 重启
|
||||
```
|
||||
|
||||
**验证**:比对两边产物 sha:
|
||||
|
||||
```bash
|
||||
shasum -a 256 <插件仓库>/lib/client.js \
|
||||
~/.dsh/profiles/<profile>/node_modules/dsh-apes-sidebar/lib/client.js
|
||||
```
|
||||
|
||||
## §7 DSH 拒绝加载插件 / entry pending
|
||||
|
||||
**症状**:插件目录存在,但宿主不加载,或 loader 报 entry pending / 恢复模式。
|
||||
|
||||
**根因**:插件的声明面不合法或不被接受,常见于:
|
||||
- `dsh.plugin.json` 的 `engines.dsh` 与宿主不符;
|
||||
- bundle patch / 挂载行的 `insert` 结构不合法(例如把声明写成宿主无法解析的形态);
|
||||
- 同 id 重复挂载(`duplicate loader entry id` / `duplicate prefix route`)。
|
||||
|
||||
**处置**:
|
||||
|
||||
```bash
|
||||
cat <插件仓库>/dsh.plugin.json # 检查 id / engines / main
|
||||
cat <插件仓库>/cordis.patch.yml # 检查 insert 结构与守卫表达式
|
||||
grep -n "cordis.patch\|dsh-apes-sidebar" ~/.dsh/profiles/<profile>/package.json
|
||||
```
|
||||
|
||||
确保只有一条挂载通道、`name` 与实际包名一致(本 fork 为 `dsh-apes-sidebar`),
|
||||
`engines.dsh` 与宿主版本相容。改完 `pnpm install` + **完整重启**。
|
||||
|
||||
**验证**:启动日志无 loader 报错;右列出现插件 tab。
|
||||
|
||||
## §8 DSH 启动进「恢复模式」
|
||||
|
||||
**症状**:DSH 启动后进入恢复/安全模式,插件与侧边栏都不正常。
|
||||
|
||||
**根因**:profile 或应用配置被写成**错误类型**。典型是材质/透明度字段要求**字符串**
|
||||
(`"off"` / `"transparent"`),但被 YAML 序列化成了布尔 `false` —— 宿主按类型校验失败即进恢复模式。
|
||||
|
||||
**处置与验证**:
|
||||
|
||||
```bash
|
||||
python3 -c "import yaml;d=yaml.safe_load(open('$HOME/.dsh/settings.yaml'));v=d.get('macosMaterial');print(repr(v), type(v).__name__)"
|
||||
# 期望:'off' str(或 'transparent' str);若为 False bool → 改回字符串
|
||||
```
|
||||
|
||||
改配置前**先备份**,改完**回读校验类型**(见 [05-运维手册](05-运维手册) §五)。
|
||||
|
||||
---
|
||||
|
||||
## 附:定位手法(本次会话验证有效)
|
||||
|
||||
1. **先按用户可见文案 grep 找归属**:`grep -rl "<界面上的中文短语>" <app>/lib <profile>/node_modules`。
|
||||
若两边都搜不到,说明它来自**运行时下载/缓存**(例如市场插件),再按 `NSUserData` 缓存或
|
||||
`desktop-market/state.json` 之类线索去找提供方。
|
||||
2. **再读提供方自己的 CSS/DOM 结构**:拿到它声明的 z-index、定位方式与所在槽位
|
||||
(如 `renderSlot("shell.overlay")`),据此判断是「它太高」还是「我们太高」。
|
||||
3. **层叠问题先找 `isolation` / `transform` / `filter` / `contain`**:这些才是层叠上下文与包含块的真正来源,
|
||||
比 z-index 数值更关键。
|
||||
4. **像素证据要用原图量**:交给视觉模型的截图会被缩放(1920→1280),裁剪坐标 ≠ 屏幕坐标;
|
||||
用 PIL 在原图上量边界、比对 RGB 剖面,再下结论。
|
||||
@@ -1,164 +0,0 @@
|
||||
# 07 · 开发与测试
|
||||
|
||||
> 面向改代码的人(与 agent)。**先读 [02-系统架构](02-系统架构) §六「关键不变量」**,
|
||||
> 再动手改,能省掉大部分回退。
|
||||
|
||||
## 一、工程约定
|
||||
|
||||
| 项 | 约定 |
|
||||
| --- | --- |
|
||||
| 语言 | TypeScript **strict**(含 `noUncheckedIndexedAccess`)、ESM、导入写 `.ts` 扩展名 |
|
||||
| 构建 | `tsdown`(rolldown),host/client 双产物;懒加载 chunk 走 `src/client/chunks/` |
|
||||
| 包管理 | `pnpm`(11.x);`pnpm install` 会触发 `prepare` 构建 |
|
||||
| 测试 | `vitest`;默认 node 环境,需要 DOM 的 spec 用 `// @vitest-environment jsdom` |
|
||||
| 宿主 peer | `dsh-better-sidebar`(本仓库已改名)**永不打包**,必须留在 peer/neverBundle 一侧 |
|
||||
| 命名 | 信息层 tab id 前缀 `apes:`;插件名 `dsh-apes-sidebar` |
|
||||
| 颜色 | 只用 `--dsw-alias-*` 设计令牌(`var(--token, <回退>)`),不写死色值 |
|
||||
|
||||
## 二、目录结构(`main` 实况)
|
||||
|
||||
```
|
||||
src/
|
||||
index.ts HOST 入口:name / inject / apply(ctx, config),路由与围栏装配
|
||||
invariant.ts 零依赖不变量面(host/client 共用)
|
||||
context-types.ts cordis 服务面契约(ctx.sessions / sidebarRight / …)
|
||||
trust-fence.ts 请求信任围栏(Host 头 / 令牌)
|
||||
html-route.ts /sidebar/html 路由(路径围栏 + 体积上限)
|
||||
config.ts 挂载行配置 schema 与默认值(readLimit / mediaLimit / …)
|
||||
fs-*.ts git.ts … 文件系统、Git、会话、任务、终端等 host 能力
|
||||
client/
|
||||
index.tsx CLIENT 入口(apply):装配 store/service/native/apes
|
||||
service.ts ctx.betterSidebar 服务实现(对外 API + 能力清单)
|
||||
state.ts 侧边栏状态存储(store)
|
||||
native/ 原生右侧栏适配(tab 记录、tab 体、tab 芯片标题)
|
||||
builtins/ 内置面板:editor / git / subagent / sidechat / terminal / browser + 预览器
|
||||
sidebar/ 自绘底部工作台与开合控件
|
||||
apes/ 信息层三 tab(本 fork 增量:tabs.tsx / icons.tsx / locales.ts)
|
||||
changes/ diff/ 文件变动与 diff 视图
|
||||
chunks/ 懒加载 chunk:editor / locale / mermaid / terminal
|
||||
locales*.ts i18n(`locales.ts` + 19 个语言覆盖文件)
|
||||
*.module.css 组件样式(含 sidebar.module.css:面板宿主层契约)
|
||||
tests/ vitest specs(131 个文件,含 e2e/ 的挂载脚本型用例)
|
||||
scripts/ install / switch / e2e 挂载 / 发布辅助
|
||||
docs/ external-plugin-guide.md(对外契约)+ plans/(上游历史设计)+ wiki/(本百科源文件)
|
||||
```
|
||||
|
||||
## 三、门禁(提交前必跑)
|
||||
|
||||
```bash
|
||||
pnpm typecheck # tsc --noEmit(strict)
|
||||
pnpm build # rm -rf lib && tsc(d.ts) && tsdown
|
||||
pnpm test # vitest run
|
||||
```
|
||||
|
||||
当前基线(本次会话实测):`main` = 131 文件 / **1367 通过** + 9 skip;
|
||||
`sync/v0.24.1` = 141 文件 / **1515 通过** + 9 skip。全量 test 约 3–4 分钟。
|
||||
**任何一条不过就不要提交**;三条全绿才算完成。
|
||||
|
||||
## 四、测试体系(按层次)
|
||||
|
||||
| 层次 | 环境 | 手法 | 代表 spec |
|
||||
| --- | --- | --- | --- |
|
||||
| 宿主插件形状 / 对外契约 | node | 断言导出面与语义(含与 `package.json` 的版本一致性) | `smoke.spec.ts` · `plugin-shape.spec.ts` · `service.spec.ts` · `consumer-types.ts` |
|
||||
| 原生侧边栏适配 | node | 用「类型为真、运行时为假」的 mock 注册表 | `native-surface.spec.ts` |
|
||||
| 面板宿主与层叠契约 | node + jsdom | CSS 断言 + DOM 挂载点解析 | `panel-host-css.spec.ts` · `panel-host-parent.spec.ts` |
|
||||
| 预览交付 | node + jsdom | 纯逻辑(`<base>` 注入)+ 客户端挂载(stub `fetch`,`createRoot` + `act`) | `html-preview.spec.ts` · `html-preview-frame.spec.tsx` |
|
||||
| 沙箱契约 | node | 对渲染结果做**字符串断言**(服务端渲染把 `srcDoc` 输出为 camelCase `srcDoc=""`,与客户端设置的真实属性 `srcdoc` 不同 —— 断言方式随之不同) | `sandbox-views.spec.tsx` |
|
||||
| 信息层 tab | node | mock `ctx.sessions.list` 与投影 | `apes-tabs.spec.ts` |
|
||||
| 桌面环境探测 | node | stub 全局(URL stamp / preload 标记) | `desktop-env.spec.ts` |
|
||||
|
||||
约定:
|
||||
- **不引入 jsdom 到默认环境**:需要 DOM 的 spec 在文件头写 `// @vitest-environment jsdom`。
|
||||
- **不依赖真实 DSH 宿主**:需要真宿主的用例归到 `tests/e2e/`(脚本型挂载),不进单元门禁。
|
||||
- 新增「契约」必须**同时新增 pin 它的 spec**(例如本文档提到的 25 层级、沙箱令牌、`srcDoc` 交付方式,
|
||||
都有对应断言)。改契约 = 改代码 + 改 spec,两者必须一起提交。
|
||||
|
||||
## 五、扩展点(对外服务)
|
||||
|
||||
其他插件通过 `ctx.betterSidebar` 接入:
|
||||
|
||||
```ts
|
||||
// 注册一个 tab 类型
|
||||
await ctx.betterSidebar.registerTab(descriptor)
|
||||
// 注册一个文件预览器
|
||||
await ctx.betterSidebar.registerFileViewer(viewer)
|
||||
```
|
||||
|
||||
- 服务按**能力清单**做前向兼容门控(能力只增不减),消费方应据此判断可用性而非版本号比较。
|
||||
- 完整契约、字段表与示例见仓库内 [docs/external-plugin-guide.md](../blob/main/docs/external-plugin-guide.md)。
|
||||
- 改服务面时:**只能新增**;删除或改语义需同步更新能力清单、指南与本百科。
|
||||
|
||||
## 六、上游同步:测量式 re-base(本项目的方法论)
|
||||
|
||||
**不 merge 上游**。上游每个版本差距可达数百文件,直接合并会引入无关变更并放大冲突面;
|
||||
本项目改用「先测量、再重建、后补增量」:
|
||||
|
||||
### 步骤
|
||||
|
||||
**① 测出自己的增量(把改名归一化掉)**
|
||||
|
||||
```python
|
||||
# 对每个变更文件:取出两边 blob,把改名归一化后比较
|
||||
old = git('show', f'{base}:{path}'); new = git('show', f'HEAD:{path}')
|
||||
rename_only = old.replace('dsh-better-sidebar', 'dsh-apes-sidebar') == new
|
||||
```
|
||||
→ 得到三张清单:**仅改名**(自动处理)/ **真改动** / **新增**。数量级参考:v0.19.1→v0.24.1 为
|
||||
78 / 11 / 10。
|
||||
|
||||
**② 确定改名的精确范围(不要重新推断)**
|
||||
|
||||
```bash
|
||||
# 仍保留上游名字的文件 = 有意保留(本仓库为 README.md、package.json、docs/plans/*)
|
||||
comm -12 <(git grep -l 'dsh-better-sidebar' <旧基线> | cut -d: -f2 | sort) \
|
||||
<(git grep -l 'dsh-better-sidebar' HEAD | cut -d: -f2 | sort)
|
||||
```
|
||||
其余文件只做子串替换 `dsh-better-sidebar` → `dsh-apes-sidebar`;**camelCase 服务名 `betterSidebar` 必须保留**。
|
||||
|
||||
**③ 造一个"改名归一化基座",让增量与改名解耦**
|
||||
|
||||
```bash
|
||||
git checkout -B tmp/norm-<旧基线> <旧基线 tag>
|
||||
# → 套同一口径改名 → commit
|
||||
git diff tmp/norm-<旧基线> HEAD -- . ':(exclude)pnpm-lock.yaml' > /tmp/apes-delta.patch
|
||||
# 校验:这条 diff 的文件清单必须恰好等于 ① 的「真改动 + 新增」
|
||||
```
|
||||
|
||||
**④ 在新基线上重建 + 三点合并**
|
||||
|
||||
```bash
|
||||
git checkout -B sync/<新基线 tag> <新基线 tag>
|
||||
# → 套同一口径改名 → commit("rename only")
|
||||
git apply -3 /tmp/apes-delta.patch # 冲突会以 <<<<<<< ours / >>>>>>> theirs 留存(命令本身不失败)
|
||||
```
|
||||
|
||||
**⑤ 逐条判读冲突(只有五种形态)**
|
||||
|
||||
| 形态 | 判定 |
|
||||
| --- | --- |
|
||||
| 版本/身份 JSON | 取**新基线**侧,再改成本 fork 的版本;两侧各自新增的元数据取**并集**(依赖、files 条目、`forkedFrom` 块) |
|
||||
| 上游搬走了某个 import | 取上游的新路径,保留本 fork 的新 import |
|
||||
| 上游新环境里的注册块 | 保留本 fork 的块,放进上游的新上下文 |
|
||||
| 同一函数两边都定义(上游上移/下移了它) | 删除本 fork 那份;用 `grep -c 'const <name>'` 断言只剩 1 处 |
|
||||
| 长文档(README) | 整体取上游原文,再机械套上本 fork 的两处增量:顶部 fork notice + 包名替换(**跳过** `github.com/<上游 org>` 与徽章域名行) |
|
||||
|
||||
**⑥ 迁移检查(API 漂移)+ 门禁**
|
||||
|
||||
- 先**解完所有冲突标记再** `pnpm install`(`prepare: tsdown` 会因为语法错误直接失败),
|
||||
并**删掉 lockfile 重新生成**(不要把旧 lockfile 带过来)。
|
||||
- 逐条跑 `pnpm typecheck` 修类型漂移。经验:漂移通常集中在**本 fork 自己新增的模块**里。
|
||||
上游会在 `context-types.ts` 的注释里写清迁移路径(例:0.1.6 的 `subagentsByParent`
|
||||
→ 0.1.7 的 `projectionsBySession[id].values.subagentCatalog`,`list.current` 被删除),照注释做即可。
|
||||
- 最后跑门禁三连,并且**用 `grep` 核对关键标记**(tab id、`srcDoc`、`dsh-desktop-content-viewport` 等)。
|
||||
|
||||
**⑦ 收尾(纪律)**
|
||||
|
||||
- 安全锚点:`backup/pre-sync-<新基线>` 分支 + `apes-v<版本>` tag,**推送远端**。
|
||||
- **不要把新线同步进正在使用的 profile**:peer 范围可能已跨 minor,同步过去只会「静默失效」,
|
||||
同时把本机环境弄脏。保持 profile 在已验证的线上。
|
||||
|
||||
## 七、提交与分支纪律
|
||||
|
||||
- 提交信息用 `type(scope): 摘要`(`feat` / `fix` / `chore` / `docs` / `refactor`),正文说明**为什么**与验证方式。
|
||||
- 一个提交一件事;re-base 类改动拆成「rename only」+「增量移植」两个提交,便于回溯与回滚。
|
||||
- 分支命名:`sync/<上游 tag>`(跟随上游)、`backup/pre-sync-<基线>`(锚点)、其余按需。
|
||||
- 提交前:门禁三连 + 关键标记 grep + (若涉及部署)提醒 profile 重装与完整重启。
|
||||
@@ -1,82 +0,0 @@
|
||||
# apes-sidebar 项目百科
|
||||
|
||||
> DeepSeek Harness(DSH)的**服务化右侧边栏工作台** + **apes 信息层**。
|
||||
> 本项目是 [`dsh-better-sidebar`](https://github.com/omdsh-dev/DSH-better-sidebar)(MIT)的 fork,
|
||||
> 在其「文件 / 文件变动 / 任务 / 侧边对话 / 终端 / 浏览器」工作台之上,追加三个信息层标签页:
|
||||
> **智能体(agents)/ 应用(apps)/ 画像(profile)**。
|
||||
|
||||
## 一、项目速览
|
||||
|
||||
| 项 | 值 |
|
||||
| --- | --- |
|
||||
| 仓库 | `figmar/apes-sidebar`(公开) |
|
||||
| 默认分支 | `main` |
|
||||
| 许可证 | MIT(fork 自 `omdsh-dev/DSH-better-sidebar`) |
|
||||
| 语言 / 构建 | TypeScript(strict)· `tsdown`(rolldown)· `pnpm` |
|
||||
| 宿主 | DSH Desktop(Electron 壳)或 DSH Web profile |
|
||||
| 上游跟随方式 | 不合并上游提交,而是**测量式 re-base**(见 [07-开发与测试](07-开发与测试)) |
|
||||
|
||||
## 二、两条版本线(重要)
|
||||
|
||||
本仓库同时维护两条互相独立、**不能同时挂载**的版本线(两条线都注册 `ctx.betterSidebar`,
|
||||
cordis entry id 冲突,第二个会被丢弃):
|
||||
|
||||
| 分支 | 版本 | 上游基线 | peer / 宿主要求 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **`main`** | 1.2.0 | `dsh-better-sidebar` **v0.19.1**(`8753096`) | `^0.1.5-rc.1`(DSH **0.1.5-rc.1+**) | 已在本机 DSH Desktop 2.0.10(`dsh 0.1.5-rc.2`)实机运行 |
|
||||
| **`sync/v0.24.1`** | 1.3.0 | `dsh-better-sidebar` **v0.24.1**(`a2751cf`) | `^0.2.0-rc.1`(DSH **0.2.0-rc.1+**) | 构建与测试全绿,**待具备 0.2.0 宿主实机验证** |
|
||||
|
||||
> ⚠️ **宿主的兼容性预检是「静默禁用」,不报错、不加载、无日志。**
|
||||
> 宿主用 `semver.satisfies(host, peerRange, { includePrerelease: true })` 判定;caret 范围跨 minor
|
||||
> 不成立(`^0.1.5-rc.1` 在 `0.2.0` 宿主上为 false),于是整行被跳过。
|
||||
> **现象 = 插件"好像没装"**。选线请先看宿主版本,见 [04-部署指南](04-部署指南)。
|
||||
|
||||
## 三、文档地图
|
||||
|
||||
| 页面 | 内容 |
|
||||
| --- | --- |
|
||||
| [01-需求分析](01-需求分析) | **官方原生 vs 装了本包**:官方右列只有「文件树 + 只读文本预览」(附官方包证据),九个 tab 是一个整体需求;7 条需求变化洞察、U1–U6、已知限制 |
|
||||
| [02-系统架构](02-系统架构) | host/client 拆分、三层接口面、面板宿主层级契约、关键不变量 |
|
||||
| [03-功能说明](03-功能说明) | 全部标签页与预览器、信息层数据来源、i18n、主题令牌、安全边界 |
|
||||
| [04-部署指南](04-部署指南) | 环境矩阵、选线、profile 接线、构建、验收清单、卸载 |
|
||||
| [05-运维手册](05-运维手册) | 版本线与分支策略、回滚剧本、发布纪律、巡检、备份铁律 |
|
||||
| [06-故障排查](06-故障排查) | 症状 → 根因 → 处置(含全部实机验证过的案例) |
|
||||
| [07-开发与测试](07-开发与测试) | 工程约定、目录、门禁、测试体系、扩展点、上游 re-base 方法 |
|
||||
|
||||
## 四、快速开始(TL;DR)
|
||||
|
||||
```bash
|
||||
# 1) 构建(Node ≥ 20,pnpm)
|
||||
pnpm install && pnpm typecheck && pnpm build && pnpm test
|
||||
|
||||
# 2) 接到 DSH profile(以桌面 profile 为例,使用 file: 链接依赖)
|
||||
# 在 ~/.dsh/profiles/desktop/package.json 中:
|
||||
# "dsh-apes-sidebar": "file:/绝对路径/dsh-apes-sidebar"
|
||||
# 并在 dsh.profile.bundles 列表里加入 "dsh-apes-sidebar"
|
||||
|
||||
# 3) 让 profile 重新安装依赖,然后 ⌘Q 完整退出 DSH 再启动
|
||||
```
|
||||
|
||||
完整步骤与坑见 [04-部署指南](04-部署指南)。
|
||||
|
||||
## 五、仓库内的事实源
|
||||
|
||||
| 位置 | 说明 |
|
||||
| --- | --- |
|
||||
| `README.md` | 面向用户的功能与安装说明(主体沿用上游文档,顶部有 fork notice) |
|
||||
| `AGENTS.md` | 面向 agent/协作者的工程约定(构建、布局、硬性规则、测试约定)⚠️ 注意:本文件当前描述的是**另一套 from-scratch 实现**,与 `main` 的 fork 树不一致,以源码为准 |
|
||||
| `docs/external-plugin-guide.md` | 插件接入指南:`ctx.betterSidebar.registerTab / registerFileViewer` 的对外契约 |
|
||||
| `docs/plans/` | 上游历史设计文档(保持上游原样,未随 fork 改名) |
|
||||
| `docs/wiki/` | **本百科的源文件**(Gitea wiki 页面的可版本化副本,由 `scripts/push-wiki.sh` 推送) |
|
||||
| `scripts/switch.sh` | 两线切换脚本(`aps` ↔ `upstream`),含 `--status` |
|
||||
|
||||
## 六、谱系说明(如实交代)
|
||||
|
||||
`figmar/apes-sidebar` 这个仓库先后承载过同一产品族的三代实现:
|
||||
|
||||
1. **v1**:早期 JS bundle 版侧边栏插件(仓库描述里提到的「可配置显示名 / 预设 feyOS」即出自那一代);
|
||||
2. **v2**:from-scratch 重写(`src/index.ts` + `src/client/index.tsx` 结构,五个 `apes:*` tab),
|
||||
即 `AGENTS.md` 所描述的那一套,其产物不在当前 `main` 上;
|
||||
3. **当前线(v0.19.1 / v0.24.1 fork)**:即 `main` 与 `sync/v0.24.1` 上的代码——本百科描述的对象。
|
||||
|
||||
若需要把本百科改写成描述 v2 重写版,请告知,源文件在 `docs/wiki/` 可直接替换。
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"id": "dsh-external/dsh-apes-sidebar",
|
||||
"version": "1.3.0",
|
||||
"main": "./lib/index.js",
|
||||
"description": "更好用的右侧边栏——文件树、编辑器、文件变动、任务和侧边对话都收在这一栏里,每个会话互不干扰。暴露服务供其他插件注册侧边栏页面与文件预览器",
|
||||
"engines": {
|
||||
"dsh": ">=0.2.0-rc.1"
|
||||
},
|
||||
"contributes": {
|
||||
"tools": [],
|
||||
"skills": []
|
||||
},
|
||||
"client": {
|
||||
"main": "./lib/client-registry.js"
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
// ESLint flat config(最小规则集):
|
||||
// - 基础:@eslint/js recommended + typescript-eslint recommended(非 type-checked 档,
|
||||
// 全量 type-checked 需要项目服务且更慢,当前收益不足以引入);
|
||||
// - react-hooks recommended:client 侧是 React 重镇,依赖数组豁免注释
|
||||
// (react-hooks/exhaustive-deps)由此规则消费;
|
||||
// - globals 按运行域分治:client bundle → browser,host/脚本 → node,tests → 混合。
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
// 构建产物 / 工具目录不参与 lint(node_modules 与点开头目录默认也不进)。
|
||||
{
|
||||
ignores: [
|
||||
'lib/',
|
||||
'dist/',
|
||||
'coverage/',
|
||||
'registry/',
|
||||
'playwright-report/',
|
||||
'test-results/',
|
||||
'node_modules/',
|
||||
'.worktrees/',
|
||||
],
|
||||
},
|
||||
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
|
||||
// react-hooks v7 的 recommended 捆绑了 React Compiler 语义规则(purity /
|
||||
// immutability / refs / set-state-in-effect 等):本仓库对 CodeMirror / xterm
|
||||
// 等命令式集成大量依赖 render 期读 ref、effect 内同步 setState 的既有模式,
|
||||
// 57 处存量报错均为噪音级误报,先整体关闭该档,只保留经典双规则
|
||||
// (rules-of-hooks / exhaustive-deps——后者消费代码里带理由的豁免注释)。
|
||||
{
|
||||
rules: Object.fromEntries(
|
||||
[
|
||||
'config',
|
||||
'error-boundaries',
|
||||
'gating',
|
||||
'globals',
|
||||
'immutability',
|
||||
'incompatible-library',
|
||||
'preserve-manual-memoization',
|
||||
'purity',
|
||||
'refs',
|
||||
'set-state-in-effect',
|
||||
'static-components',
|
||||
'unsupported-syntax',
|
||||
'use-memo',
|
||||
].map(rule => [`react-hooks/${rule}`, 'off']),
|
||||
),
|
||||
},
|
||||
|
||||
// 孤儿 eslint-disable 注释(规则未实际触发)直接报错,防止豁免残留。
|
||||
{ linterOptions: { reportUnusedDisableDirectives: 'error' } },
|
||||
|
||||
// no-unused-vars 对齐仓库既有约定:下划线前缀 = 刻意不用的占位(mock 桩等)。
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': ['error', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
}],
|
||||
},
|
||||
},
|
||||
|
||||
// client bundle 运行在浏览器(portal 侧边栏 / 各视图 / 拦截层)。
|
||||
{
|
||||
files: ['src/client/**/*.{js,mjs,cjs,ts,tsx,jsx}'],
|
||||
languageOptions: { globals: globals.browser },
|
||||
},
|
||||
|
||||
// host 侧插件入口与构建/运维脚本跑在 Node。eslint.config.js 自身不 ignore:
|
||||
// 它就是一个普通 ESM 脚本,走 js recommended + node globals 正常通过。
|
||||
{
|
||||
files: ['src/*.{js,mjs,cjs,ts}', 'scripts/**/*.{js,mjs,cjs,ts}', '*.config.ts', 'eslint.config.js'],
|
||||
languageOptions: { globals: globals.node },
|
||||
},
|
||||
|
||||
// tests:Node 里跑(vitest / fs fixtures),jsdom 组件测试又摸浏览器全局。
|
||||
{
|
||||
files: ['tests/**/*.{js,mjs,cjs,ts,tsx,jsx}'],
|
||||
languageOptions: { globals: { ...globals.node, ...globals.browser } },
|
||||
},
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="6.5" y="9" width="23" height="18" rx="2.5" fill="url(#a)" />
|
||||
<rect x="21.5" y="9" width="8" height="18" rx="2.5" fill="url(#b)" />
|
||||
<defs>
|
||||
<linearGradient id="a" x1="6.5" y1="9" x2="29.5" y2="27" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#69B9FF" />
|
||||
<stop offset="1" stop-color="#324DE2" />
|
||||
</linearGradient>
|
||||
<linearGradient id="b" x1="25.5" y1="9" x2="25.5" y2="27" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#45E7A4" />
|
||||
<stop offset="1" stop-color="#05909D" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 663 B |
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"meta": {
|
||||
"title": "Better Sidebar",
|
||||
"description": "A better right sidebar — file tree, editor, changes, tasks and side chat in one column, separate for every conversation."
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"meta": {
|
||||
"title": "Better Sidebar",
|
||||
"description": "更好用的右侧边栏——文件树、编辑器、文件变动、任务和侧边对话都收在这一栏里,每个会话互不干扰。"
|
||||
}
|
||||
}
|
||||
+48
-221
@@ -1,41 +1,52 @@
|
||||
{
|
||||
"name": "dsh-apes-sidebar",
|
||||
"version": "1.3.0",
|
||||
"description": "APE fork of dsh-better-sidebar (base v0.24.1): session-isolated right sidebar — file tree, editor, changes, tasks and side chat — built on the host's native sidebar, plus apes:* information-layer tabs (agents / apps / profile). Forked from omdsh-dev/DSH-better-sidebar under MIT.",
|
||||
"name": "apes-sidebar",
|
||||
"version": "2.0.0",
|
||||
"description": "WorkBuddy/Tutti 风格右侧边栏增强(构建在 dsh-better-sidebar 之上:任务 / Agent / 消息 / 应用 / Profile 五面板信息层)",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/omdsh-dev/DSH-better-sidebar"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/types/index.d.ts",
|
||||
"default": "./lib/index.js"
|
||||
},
|
||||
"./invariant": {
|
||||
"types": "./lib/types/invariant.d.ts",
|
||||
"default": "./lib/invariant.js"
|
||||
},
|
||||
"./client": {
|
||||
"types": "./lib/types/client/index.d.ts",
|
||||
"default": "./lib/client.js"
|
||||
},
|
||||
"./client/service": {
|
||||
"types": "./lib/types/client/service.d.ts",
|
||||
"default": "./lib/client.js"
|
||||
},
|
||||
"./client/api": {
|
||||
"types": "./lib/types/client/service.d.ts",
|
||||
"default": "./lib/client.js"
|
||||
},
|
||||
".": "./lib/index.js",
|
||||
"./invariant": "./lib/invariant.js",
|
||||
"./client": "./lib/client.js",
|
||||
"./src/*": "./src/*",
|
||||
"./package.json": "./package.json",
|
||||
"./locale/*.json": "./locale/*.json"
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"src",
|
||||
"cordis.patch.yml",
|
||||
"scripts",
|
||||
"AGENTS.md",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -rf lib && tsc -p tsconfig.build.json && tsdown",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||
"test": "vitest run",
|
||||
"verify:mount": "node scripts/verify-mount.cjs",
|
||||
"lint:ts": "oxlint src tests tsdown.config.ts vitest.config.ts",
|
||||
"dev": "tsdown --watch",
|
||||
"pack:plugin": "pnpm build && pnpm pack --pack-destination dist"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"dsh-better-sidebar": "^0.18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"dsh-better-sidebar": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.2",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "~18.3.1",
|
||||
"dsh-better-sidebar": "^0.18.0",
|
||||
"lightningcss": "^1.32.0",
|
||||
"oxlint": "^0.15.0",
|
||||
"react": "~18.3.1",
|
||||
"tsdown": "^0.22.2",
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^4.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -47,196 +58,12 @@
|
||||
},
|
||||
"client": {
|
||||
"inject": [
|
||||
"@deepseek-ai/dsh-client-locale",
|
||||
"@deepseek-ai/dsh-client-ui-slots",
|
||||
"@deepseek-ai/dsh-client-ui-conversation",
|
||||
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
||||
"@deepseek-ai/dsh-client-modules"
|
||||
"betterSidebar",
|
||||
"locale",
|
||||
"sessions"
|
||||
],
|
||||
"platform": "web"
|
||||
},
|
||||
"manifestVersion": 1
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
"lib/invariant.js",
|
||||
"lib/client.js",
|
||||
"lib/client-registry.js",
|
||||
"lib/client-editor.js",
|
||||
"lib/client-locale.js",
|
||||
"lib/client-mermaid.js",
|
||||
"lib/types/**/*.d.ts",
|
||||
"src",
|
||||
"scripts/install.sh",
|
||||
"scripts/install.ps1",
|
||||
"cordis.patch.yml",
|
||||
"README.md",
|
||||
"README_EN.md",
|
||||
"LICENSE",
|
||||
"icon.svg",
|
||||
"locale/*.json",
|
||||
"scripts/switch.sh"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsdown",
|
||||
"prepublishOnly": "pnpm build",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"bundle": "tsdown",
|
||||
"watch": "tsdown --watch",
|
||||
"prepare": "tsdown",
|
||||
"test": "vitest run",
|
||||
"test:windows": "vitest run --maxWorkers=1",
|
||||
"test:mount": "bash scripts/e2e-mount.sh",
|
||||
"test:mount:aggregate": "bash scripts/e2e-aggregate-mount.sh",
|
||||
"clean": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\"",
|
||||
"check:consumer-types": "bash scripts/check-consumer-types.sh",
|
||||
"lint": "eslint .",
|
||||
"switch": "bash scripts/switch.sh",
|
||||
"switch:status": "bash scripts/switch.sh --status",
|
||||
"switch:aps": "bash scripts/switch.sh aps",
|
||||
"switch:upstream": "bash scripts/switch.sh upstream"
|
||||
},
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "^4.0.4",
|
||||
"@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-locale": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-sidebar-right": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-session": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-settings": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-subagent": "^0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
|
||||
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@huanlin/dsh-plugin-better-locale": {
|
||||
"optional": true
|
||||
},
|
||||
"@deepseek-ai/dsh-client-ui-sidebar-right": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.10.4",
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-go": "^6.0.1",
|
||||
"@codemirror/lang-html": "^6.4.12",
|
||||
"@codemirror/lang-java": "^6.0.2",
|
||||
"@codemirror/lang-javascript": "^6.2.5",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/lang-markdown": "^6.5.2",
|
||||
"@codemirror/lang-php": "^6.0.2",
|
||||
"@codemirror/lang-python": "^6.2.1",
|
||||
"@codemirror/lang-rust": "^6.0.2",
|
||||
"@codemirror/lang-sql": "^6.10.0",
|
||||
"@codemirror/lang-vue": "^0.1.3",
|
||||
"@codemirror/lang-xml": "^6.1.0",
|
||||
"@codemirror/lang-yaml": "^6.1.3",
|
||||
"@codemirror/language": "^6.12.4",
|
||||
"@codemirror/legacy-modes": "^6.5.3",
|
||||
"@codemirror/search": "^6.7.1",
|
||||
"@codemirror/state": "^6.7.1",
|
||||
"@codemirror/view": "^6.43.8",
|
||||
"@deepseek-ai/schemastery": "^3.18.4",
|
||||
"@lezer/highlight": "^1.2.3",
|
||||
"clsx": "^2.1.1",
|
||||
"dompurify": "^3.4.14",
|
||||
"mermaid": "^11.16.1",
|
||||
"react-icons": "5.7.0",
|
||||
"rxjs": "^7.8.2",
|
||||
"ws": "^8.18.0",
|
||||
"yaml": "^2.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
|
||||
"@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.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-agent-loop": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-attachment": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-locale": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-store": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-host-webserver": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-invariants": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-llm": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-ptc-runtime": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-sandbox": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-sandbox-policy": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-scope": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-session": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-session-persistence": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-session-projection": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-settings": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-subagent": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-system-prompt": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-tools": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-user-approval": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-util-code-language": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-util-time": "0.2.0-rc.1",
|
||||
"@deepseek-ai/dsh-util-workspace-path": "0.2.0-rc.1",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@shikijs/langs": "^4.3.1",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/react": "~18.3.1",
|
||||
"@types/react-dom": "~18.3.1",
|
||||
"@types/ws": "^8.5.10",
|
||||
"anser": "^2.3.5",
|
||||
"cordis": "^4.0.0-rc.8",
|
||||
"diff": "^9.0.0",
|
||||
"eslint": "^10.9.1",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"globals": "^17.12.0",
|
||||
"immer": "^10.1.1",
|
||||
"jsdom": "^29.1.1",
|
||||
"katex": "^0.16.47",
|
||||
"lightningcss": "^1.32.0",
|
||||
"mdast-util-from-markdown": "^2.0.3",
|
||||
"mdast-util-gfm": "^3.1.0",
|
||||
"mdast-util-math": "^3.0.0",
|
||||
"micromark-core-commonmark": "^2.0.3",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"micromark-extension-math": "^3.1.0",
|
||||
"micromark-factory-space": "^2.0.1",
|
||||
"micromark-util-character": "^2.1.1",
|
||||
"micromark-util-classify-character": "^2.0.1",
|
||||
"micromark-util-sanitize-uri": "^2.0.1",
|
||||
"micromark-util-symbol": "^2.0.1",
|
||||
"micromark-util-types": "^2.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"shiki": "^4.3.1",
|
||||
"simple-icons": "^16.32.0",
|
||||
"tsdown": "^0.22.2",
|
||||
"typescript": "^5.6.0",
|
||||
"typescript-eslint": "^8.69.0",
|
||||
"unrun": "^0.2.39",
|
||||
"vitest": "^4.1.8",
|
||||
"zustand": "~4.4.7"
|
||||
},
|
||||
"icon": "./icon.svg",
|
||||
"forkedFrom": {
|
||||
"name": "dsh-better-sidebar",
|
||||
"version": "0.24.1",
|
||||
"commit": "a2751cf",
|
||||
"url": "https://github.com/omdsh-dev/DSH-better-sidebar"
|
||||
}
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Playwright config for the headless-render mount lane (tests/e2e).
|
||||
*
|
||||
* This lane does NOT spawn the server itself: `scripts/e2e-mount.sh` boots a
|
||||
* real `dsh web` instance (with the npm-packed plugin mounted through the
|
||||
* official `dsh plugin add` channel) and injects the base URL through
|
||||
* `DSH_E2E_URL`. The spec's job is to render that URL in a headless Chromium
|
||||
* and prove the plugin mounts without crashing the shell.
|
||||
*
|
||||
* Specs are named `*.e2e.ts` (not `*.spec.ts`) so vitest's default include
|
||||
* never collects them; vitest.config.ts excludes the directory as well.
|
||||
*/
|
||||
import { defineConfig } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
// The lane's specs are named *.e2e.ts (so vitest's default include never
|
||||
// collects them); tell Playwright to pick exactly that shape.
|
||||
testMatch: '**/*.e2e.ts',
|
||||
// A deterministic gate: no retries, one worker, one browser. The whole lane
|
||||
// is a crash probe against one server instance, so serial execution keeps
|
||||
// the assertions meaningful (a crash would trip the very next check).
|
||||
retries: 0,
|
||||
workers: 1,
|
||||
fullyParallel: false,
|
||||
timeout: 120_000,
|
||||
reporter: [
|
||||
['list'],
|
||||
// Local debugging artifact; CI uploads the report dir on failure.
|
||||
['html', { open: 'never' }],
|
||||
],
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
headless: true,
|
||||
viewport: { width: 1440, height: 900 },
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
})
|
||||
Generated
+906
-3103
File diff suppressed because it is too large
Load Diff
+3
-67
@@ -1,68 +1,4 @@
|
||||
packages:
|
||||
- .
|
||||
|
||||
# The @deepseek-ai/* packages declare their dsh-* siblings as peerDependencies
|
||||
# (the DSH host bundle provides them all at mount time). This repo installs
|
||||
# every peer the dev tree needs as a direct devDependency pinned to the
|
||||
# baseline (see package.json), so pnpm resolves them instead of floating to
|
||||
# stale copies — e.g. dsh-attachment 0.1.1-rc.1 predates the
|
||||
# `admitPromptContent` export that dsh-subagent's artifacts import. When
|
||||
# bumping the DSH baseline, audit with `pnpm peers check` and hoist any
|
||||
# newly-unmet transitive peers into devDependencies too.
|
||||
|
||||
allowBuilds:
|
||||
node-pty: true
|
||||
minimumReleaseAgeExclude:
|
||||
- '@deepseek-ai/*'
|
||||
- '@deepseek-ai/cordis-plugin-include@1.0.5-rc.1 || 1.0.6'
|
||||
- '@deepseek-ai/cordis-plugin-loader@1.0.1-rc.1 || 1.0.2'
|
||||
- '@deepseek-ai/cordis@4.0.1-rc.1 || 4.0.1'
|
||||
- '@deepseek-ai/cosmokit@1.8.2-rc.1 || 1.8.2'
|
||||
- '@deepseek-ai/dsh-agent-default-model@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-agent@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-api-remotes@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-atomic-write@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-attachment@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-brand@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-client-connection@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-client-locale@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-client-schema-form@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-client-ui-conversation@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-client-ui-primitives@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-client-ui-settings@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-client-ui-slash@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-client-ui-slots@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-client-web-react@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-commands@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-compact@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-credentials@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-goal@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-host-apiproxy@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-host-directory-picker@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-host-webserver@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-invariants@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-llm-retry@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-llm@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-native-command@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-paths@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-scope@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-session-persistence@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-session-projection-cache@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-session-projection@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-session-query@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-session-title@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-session@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-settings@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-skill@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-storage-domain@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-storage@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-subagent@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-system-prompt@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-timeout@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-token-meter@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-tools@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-type-meta@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-typert-registry@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-user-approval@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-user-interaction@0.0.1-rc.1'
|
||||
- '@deepseek-ai/dsh-workspace@0.0.1-rc.1'
|
||||
- '@deepseek-ai/schemastery@3.18.1-rc.1 || 3.18.1'
|
||||
- '@huanlin/dsh-plugin-better-locale@0.1.0'
|
||||
- dsh-better-sidebar@0.18.0
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Consumer-facing type surface check: build a tiny standalone consumer that
|
||||
# imports `dsh-apes-sidebar/client/service` WITHOUT @types/node and with
|
||||
# skipLibCheck: false, then type-check it with the repo's tsc.
|
||||
#
|
||||
# This is the exact scenario the v0.12.0 API work cares about: the shipped
|
||||
# declaration graph (service.d.ts -> context-types.d.ts -> state/api) must
|
||||
# resolve and type-check for a browser-only plugin author. Run AFTER
|
||||
# `pnpm build` (the check reads lib/types).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Git Bash (MSYS) `ln -s` deep-copies directories by default — copying this
|
||||
# repo (node_modules + full .git history) would be catastrophic. Force real
|
||||
# native symlinks instead: the GitHub Windows runners (Developer Mode, admin
|
||||
# user) can create them, and nativestrict makes ln fail loudly rather than
|
||||
# silently falling back to a copy. Inert on Linux/macOS.
|
||||
export MSYS="winsymlinks:nativestrict"
|
||||
|
||||
# The check reads the BUILT declaration surface (lib/types) — fail loudly
|
||||
# instead of silently passing when the build output is missing.
|
||||
if [ ! -f lib/types/client/service.d.ts ]; then
|
||||
echo "[check-consumer-types] FAIL: lib/types/client/service.d.ts not found — run 'pnpm build' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
mkdir -p "$WORK/node_modules"
|
||||
# Resolve link targets to their PHYSICAL path first: under pnpm,
|
||||
# node_modules/@deepseek-ai/cordis is itself a symlink/junction into .pnpm,
|
||||
# and on Windows a native symlink pointing AT a junction does not traverse
|
||||
# for tsc (TS2307 on a link that visibly exists). Node's NATIVE realpath
|
||||
# (GetFinalPathNameByHandle on Windows) expands junctions, which bash-level
|
||||
# resolution may leave untouched; node is guaranteed here (tsc runs below).
|
||||
resolve_dir() { node -e 'process.stdout.write(require("node:fs").realpathSync.native(process.argv[1]))' "$1"; }
|
||||
ln -s "$(resolve_dir "$(pwd)")" "$WORK/node_modules/dsh-apes-sidebar"
|
||||
# The vendored cordis base (and, through its realpath, cosmokit / standard-schema)
|
||||
# must resolve so the consumer can type `Context` against the DSH runtime scope.
|
||||
mkdir -p "$WORK/node_modules/@deepseek-ai"
|
||||
ln -s "$(resolve_dir "$(pwd)/node_modules/@deepseek-ai/cordis")" "$WORK/node_modules/@deepseek-ai/cordis"
|
||||
# Loud guard: both links must resolve to a package root, or tsc below would
|
||||
# report misleading module-resolution errors.
|
||||
for pkg_json in "$WORK/node_modules/dsh-apes-sidebar/package.json" "$WORK/node_modules/@deepseek-ai/cordis/package.json"; do
|
||||
if [ ! -f "$pkg_json" ]; then
|
||||
echo "[check-consumer-types] FAIL: $pkg_json does not resolve (broken link). Link layer:" >&2
|
||||
ls -la "$WORK/node_modules" "$WORK/node_modules/@deepseek-ai" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
cat > "$WORK/check.ts" <<'EOF'
|
||||
import { SIDEBAR_FEATURES, SIDEBAR_SERVICE_VERSION } from 'dsh-apes-sidebar/client/service'
|
||||
import type {} from 'dsh-apes-sidebar/client/service'
|
||||
import type {
|
||||
BetterSidebarService, FileViewerDescriptor, OpenTabSeed, SidebarSettingsRenderProps,
|
||||
TabComponentProps, TabDescriptor,
|
||||
} from 'dsh-apes-sidebar/client/service'
|
||||
import type { SessionScope, SidebarSnapshot, SidebarState, SidebarStore, SidebarTab } from 'dsh-apes-sidebar/client/service'
|
||||
import type { SidebarPrefs } from 'dsh-apes-sidebar/client/service'
|
||||
// The vendored-cordis path: `Context` from '@deepseek-ai/cordis' plus the
|
||||
// side-effect type import above must expose `ctx.betterSidebar` — the consumer
|
||||
// never needs to import this package's own Context type.
|
||||
import type { Context as VendoredContext } from '@deepseek-ai/cordis'
|
||||
|
||||
declare const ctx: { betterSidebar: BetterSidebarService }
|
||||
const tab: TabDescriptor = {
|
||||
id: 'x:tab',
|
||||
title: 'X',
|
||||
single: true,
|
||||
badge: (_ctx, _scope, state: SidebarState) => state.expanded.length,
|
||||
onOpen: (_tab: SidebarTab, _scope: SessionScope) => {},
|
||||
onClose: (_tab: SidebarTab, _scope: SessionScope) => {},
|
||||
createTab: (state: SidebarState) => ({ tab: { id: 'x:1', type: 'x:tab', title: 'X', meta: {} } }),
|
||||
settings: {
|
||||
toggles: [{ key: 'autoOpenSubagent', title: 'A' }],
|
||||
pluginToggles: [{ key: 'k', title: 'K', type: 'number', min: 0, max: 9 }],
|
||||
render: (props: SidebarSettingsRenderProps) => { props.updatePluginSetting('a', 1); return null },
|
||||
},
|
||||
component: (props: TabComponentProps) => null,
|
||||
}
|
||||
ctx.betterSidebar.registerTab(tab)
|
||||
const viewer: FileViewerDescriptor = {
|
||||
id: 'x:csv', exts: ['csv'], fetchStrategy: 'custom',
|
||||
load: (_path, _scope, signal?: AbortSignal) => { void signal; return Promise.resolve([]) },
|
||||
component: () => null,
|
||||
}
|
||||
ctx.betterSidebar.registerFileViewer(viewer)
|
||||
const seed: OpenTabSeed = { type: 'x:tab', title: 'X', meta: { a: 1 } }
|
||||
ctx.betterSidebar.openTab(seed, { sessionId: 's1' })
|
||||
ctx.betterSidebar.openFile({ sessionId: 's1' }, '/p/a.csv')
|
||||
ctx.betterSidebar.updateTab('t', { meta: { b: 2 } })
|
||||
const snap: SidebarSnapshot = ctx.betterSidebar.getSnapshot()
|
||||
const prefs: SidebarPrefs = snap.prefs
|
||||
const store: SidebarStore = null as unknown as SidebarStore
|
||||
void prefs; void store; void ctx.betterSidebar.version; void ctx.betterSidebar.features
|
||||
void SIDEBAR_SERVICE_VERSION; void SIDEBAR_FEATURES
|
||||
// Vendored-cordis augmentation path.
|
||||
declare const vctx: VendoredContext
|
||||
vctx.betterSidebar.registerTab(tab)
|
||||
vctx.betterSidebar.registerFileViewer(viewer)
|
||||
void vctx.betterSidebar.getSnapshot()
|
||||
EOF
|
||||
|
||||
cat > "$WORK/tsconfig.json" <<'EOF'
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2023", "module": "esnext", "moduleResolution": "bundler",
|
||||
"strict": true, "noEmit": true, "skipLibCheck": false, "types": [],
|
||||
"jsx": "react-jsx", "lib": ["ES2023", "DOM"]
|
||||
},
|
||||
"include": ["check.ts"]
|
||||
}
|
||||
EOF
|
||||
|
||||
echo "[check-consumer-types] type-checking a browser-only consumer (no @types/node)..."
|
||||
echo "[check-consumer-types] pass 1: skipLibCheck: true (the realistic consumer experience) — must be fully clean"
|
||||
./node_modules/.bin/tsc -p "$WORK/tsconfig.json" --skipLibCheck true
|
||||
echo "[check-consumer-types] pass 2: skipLibCheck: false — OUR declaration surface (lib/types/**) must be clean"
|
||||
echo "[check-consumer-types] (upstream cordis/cosmokit declaration noise is filtered out; it predates this plugin)"
|
||||
set +e
|
||||
./node_modules/.bin/tsc -p "$WORK/tsconfig.json" --skipLibCheck false > "$WORK/strict.log" 2>&1
|
||||
STATUS=$?
|
||||
set -e
|
||||
# Fail only on errors that mention OUR package / declarations / the fixture.
|
||||
# Upstream noise (vendored cordis / cosmokit declaration warnings under the
|
||||
# repo's pinned TS lib) lives anywhere under node_modules and is filtered.
|
||||
if grep -E "dsh-apes-sidebar|lib/types|check\.ts" "$WORK/strict.log" | grep -vE "node_modules/[^ ]*(cordis|cosmokit)"; then
|
||||
echo "[check-consumer-types] FAIL: errors in the dsh-apes-sidebar declaration surface:" >&2
|
||||
grep -E "dsh-apes-sidebar|lib/types|check\.ts" "$WORK/strict.log" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$STATUS" -ne 0 ]; then
|
||||
echo "[check-consumer-types] pass 2 note: strict mode only reports upstream declaration noise:"
|
||||
grep -v "dsh-apes-sidebar\|lib/types\|check\.ts" "$WORK/strict.log" | head -5 || true
|
||||
fi
|
||||
echo "[check-consumer-types] OK: the client/service declaration surface is node-free and self-contained."
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# dsh-apes-sidebar 聚合双挂载冒烟(CI + 本地):
|
||||
#
|
||||
# 场景:一个聚合 bundle(tests/fixtures/aggregate-better-sidebar)先以独立
|
||||
# 条目 id 挂载 dsh-apes-sidebar,插件自身的 bundle patch 后到 —— 旧行为
|
||||
# 两个实例都注册 /sidebar/api,整个插件树启动失败(duplicate prefix
|
||||
# route);新行为插件行检测到已有启用中的同包名挂载后自动 disabled,由
|
||||
# 聚合行接管 sidebar。
|
||||
#
|
||||
# 流程:
|
||||
# 1. 打包 fixture 聚合包;
|
||||
# 2. 用官方 CLI 把 fixture 先装进全新 scratch profile,再把插件 tarball
|
||||
# 装进同一 profile(复刻真实安装顺序:聚合先行、dsh plugin add 后到);
|
||||
# 3. 启动真实 `dsh web`(keyless,--port 0);
|
||||
# 4. 断言:日志出现启动 URL、无 "duplicate prefix route"、/sidebar/api
|
||||
# 路由存活(POST 未知方法应返回 404 JSON 而非崩溃)。
|
||||
#
|
||||
# 用法:
|
||||
# bash scripts/e2e-aggregate-mount.sh
|
||||
#
|
||||
# 环境变量(均可省略):
|
||||
# DSH_CMD dsh 命令;缺省 PATH 上的 `dsh`,回退 npx 拉官方包
|
||||
# (同 e2e-mount.sh)
|
||||
# TARBALL 插件 tarball;缺省仓库根 dsh-apes-sidebar-*.tgz(须已 pack)
|
||||
# PORT 固定端口(默认 0 = OS 分配,从日志解析 URL)
|
||||
# DSH_HOME_BASE scratch 根目录(默认系统临时目录)。脚本始终在其下新建
|
||||
# 本调用拥有的独立子目录,只写入/删除该子目录;调用方提供
|
||||
# 的目录本身(可能是真实 ~/.dsh)绝不写入或删除。
|
||||
# KEEP_HOME 非空时保留 scratch 目录(调试用)
|
||||
#
|
||||
# 退出码 = 0 通过;非 0 失败。服务器与 scratch 目录由 trap 兜底清理。
|
||||
# 日志/前置校验/DSH_CMD 与 tarball 解析/scratch profile 三件套/清理 trap/
|
||||
# dsh web 启动与就绪轮询的共享骨架见 scripts/e2e-common.sh。
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
FIXTURE_DIR="$ROOT/tests/fixtures/aggregate-better-sidebar"
|
||||
|
||||
E2E_TAG=e2e-aggregate-mount
|
||||
source "$SCRIPT_DIR/e2e-common.sh"
|
||||
|
||||
DSH_CMD="${DSH_CMD:-dsh}"
|
||||
TARBALL="${TARBALL:-}"
|
||||
PORT="${PORT:-0}"
|
||||
KEEP_HOME="${KEEP_HOME:-}"
|
||||
|
||||
e2e_require_cmd node "DSH 运行需要 Node.js >= 20"
|
||||
e2e_require_cmd pnpm "dsh plugin 转发给 pnpm"
|
||||
e2e_require_cmd npm "打包 fixture 需要"
|
||||
e2e_require_cmd curl
|
||||
|
||||
e2e_resolve_dsh_cmd
|
||||
|
||||
e2e_resolve_tarball || die "未找到插件 tarball:先在本仓库跑 pnpm build && npm pack,或用 TARBALL 指定"
|
||||
|
||||
# ── scratch home ────────────────────────────────────────────────────────────
|
||||
# DSH_HOME 直接用 $SCRATCH(mount lane 用子路径 home/——两 lane 既有差异);
|
||||
# DSH_HOME_BASE 语义与清理 trap 见 e2e-common.sh。
|
||||
e2e_make_scratch dsh-e2e-agg
|
||||
export DSH_HOME="$SCRATCH"
|
||||
LOG_DIR="$DSH_HOME/logs"; mkdir -p "$LOG_DIR"
|
||||
OUT_LOG="$LOG_DIR/dsh-web.out.log"; ERR_LOG="$LOG_DIR/dsh-web.err.log"
|
||||
SERVER_PID=""
|
||||
TMP_DIR=""
|
||||
trap e2e_cleanup EXIT
|
||||
|
||||
# ── 打包 fixture 聚合包 ─────────────────────────────────────────────────────
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
( cd "$FIXTURE_DIR" && npm pack --silent --pack-destination "$TMP_DIR" >/dev/null )
|
||||
FIXTURE_TGZ="$(ls "$TMP_DIR"/*.tgz 2>/dev/null | head -1 || true)"
|
||||
[ -n "$FIXTURE_TGZ" ] || die "fixture 打包失败"
|
||||
|
||||
# ── 引导 scratch profile(web 模板)────────────────────────────────────────
|
||||
# 三件套 heredoc 及 pnpm-workspace 的 minimumReleaseAgeExclude
|
||||
# 理由见 e2e-common.sh 的 e2e_write_profile。
|
||||
PROFILE_DIR="$DSH_HOME/profiles/web"
|
||||
e2e_write_profile "$PROFILE_DIR"
|
||||
|
||||
# ── 组装 profile:聚合先行,插件后到 ──────────────────────────────────────
|
||||
say "安装 fixture 聚合包(先行)…"
|
||||
$DSH_CMD plugin --profile web add "file:$FIXTURE_TGZ"
|
||||
say "安装插件 tarball(后到)…"
|
||||
$DSH_CMD plugin --profile web add "file:$TARBALL"
|
||||
|
||||
# ── 启动真实 dsh web ───────────────────────────────────────────────────────
|
||||
say "启动 dsh web(--port ${PORT},日志 ${LOG_DIR})…"
|
||||
e2e_start_dsh_web "$OUT_LOG" "$ERR_LOG"
|
||||
|
||||
# 等待启动 URL 或进程退出(最多 120s)。这里有意只取 origin:0.1.2-alpha.1+
|
||||
# 的就绪行是 `…/?token=<43字符>` 鉴权 URL,但下方的探活全部打插件的
|
||||
# /sidebar/api/* 路由——webserver carrier 不做鉴权(只有 /api、index 与
|
||||
# remote.mux 升级在 browser auth 之后),origin 拼路径即正确且两版通用。
|
||||
E2E_READY_RE='http://127\.0\.0\.1:[0-9]+'
|
||||
E2E_READY_PICK=tail
|
||||
e2e_wait_dsh_web_ready "$OUT_LOG" || true
|
||||
if [ -z "$URL" ]; then
|
||||
warn "out log 尾部:$(tail -5 "$OUT_LOG" 2>/dev/null || true)"
|
||||
warn "err log 尾部:$(tail -5 "$ERR_LOG" 2>/dev/null || true)"
|
||||
die "dsh web 未在 120s 内启动"
|
||||
fi
|
||||
say "已启动:$URL"
|
||||
|
||||
# ── 断言 ────────────────────────────────────────────────────────────────────
|
||||
if grep -q "duplicate prefix route" "$ERR_LOG" "$OUT_LOG" 2>/dev/null; then
|
||||
die "检测到 duplicate prefix route(双挂载未退让)"
|
||||
fi
|
||||
|
||||
# 真实方法探活:settings.get 是插件侧栏卡偏好的读取端(writeOk → HTTP 200 +
|
||||
# {"ok":true,...};未挂 settings 服务的部署也照回 ok,值取缺省)。选它当哨兵
|
||||
# 的理由:/sidebar/api 是插件自己 fence 的 prefix 路由,未注册的方法统统 404,
|
||||
# 所以 {"ok":true} 只可能来自插件真实注册的 handler —— generic missing-route
|
||||
# 404 无法冒充(P2: Probe a real sidebar API method)。
|
||||
RESP="$(curl -s -X POST "$URL/sidebar/api/settings.get" 2>/dev/null || true)"
|
||||
if ! printf '%s' "$RESP" | grep -q '"ok":true'; then
|
||||
die "/sidebar/api/settings.get 未返回 ok:true(响应:$(printf '%s' "$RESP" | head -c 200))"
|
||||
fi
|
||||
say "/sidebar/api/settings.get → ok:true(真实 handler 存活)"
|
||||
|
||||
STATUS="$(curl -s -o /dev/null -w '%{http_code}' -X POST "$URL/sidebar/api/__e2e_unknown__" 2>/dev/null || true)"
|
||||
say "/sidebar/api POST 未知方法 → HTTP ${STATUS}(期望 404)"
|
||||
[ "$STATUS" = "404" ] || die "/sidebar/api 未知方法未按预期返回 404(HTTP ${STATUS})"
|
||||
|
||||
say "通过:聚合双挂载场景下插件自动退让,侧边栏由聚合行接管。"
|
||||
@@ -1,200 +0,0 @@
|
||||
# =============================================================================
|
||||
# scripts/e2e-common.sh — e2e-mount.sh / e2e-aggregate-mount.sh 的共享骨架:
|
||||
# 日志三件套、前置校验、DSH_CMD / tarball 解析、scratch home 与清理 trap、
|
||||
# scratch profile 三件套、dsh web 后台启动与就绪轮询。
|
||||
#
|
||||
# 纯开发侧库文件,不进 npm 包(package.json files 白名单无它);
|
||||
# install.sh / install.ps1 随 npm 包独立分发,必须自包含,绝不 source 本文件。
|
||||
#
|
||||
# 用法:调用方先设 E2E_TAG(say/warn/die 的日志前缀)再 source:
|
||||
# E2E_TAG=e2e-mount
|
||||
# source "$SCRIPT_DIR/e2e-common.sh"
|
||||
#
|
||||
# 依赖调用方已 `set -euo pipefail` 并定义 ROOT(仓库根绝对路径)。
|
||||
# 两侧脚本有意保留的差异(行为等价,靠参数 / 调用点区分):
|
||||
# - DSH_HOME 子路径:mount 用 $SCRATCH/home,aggregate 直接用 $SCRATCH;
|
||||
# - 就绪行 grep 正则:mount 要带 token 的完整 URL,aggregate 只取 origin
|
||||
# (两侧的理由见各自脚本的就绪行注释);
|
||||
# - 额外清理项:aggregate 的 fixture 打包 TMP_DIR(mount 不设即跳过)。
|
||||
# =============================================================================
|
||||
|
||||
: "${E2E_TAG:?必须先设置 E2E_TAG(日志前缀)再 source e2e-common.sh}"
|
||||
|
||||
# ── 日志三件套 ───────────────────────────────────────────────────────────────
|
||||
say() { printf '\033[32m[%s]\033[0m %s\n' "$E2E_TAG" "$*"; }
|
||||
warn() { printf '\033[33m[%s]\033[0m %s\n' "$E2E_TAG" "$*" >&2; }
|
||||
die() { printf '\033[31m[%s]\033[0m %s\n' "$E2E_TAG" "$*" >&2; exit 1; }
|
||||
|
||||
# ── 前置校验 ─────────────────────────────────────────────────────────────────
|
||||
# e2e_require_cmd <cmd> [用途]:不在 PATH 上即 die(沿用两脚本原有报错文案)。
|
||||
e2e_require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
if [ $# -ge 2 ]; then
|
||||
die "未找到 $1($2)"
|
||||
else
|
||||
die "未找到 $1"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# ── DSH_CMD 解析 ─────────────────────────────────────────────────────────────
|
||||
# PATH 上的 dsh 优先,否则 npx 拉官方包(同 scripts/install.sh)。DSH_CMD
|
||||
# 缺省值(`dsh`)由调用方从环境变量取好传入。
|
||||
#
|
||||
# 回退的 npx 版本是显式钉死的:不钉就会解析 npm 的 `latest` dist-tag,而
|
||||
# 0.2.0 线目前只有预发布(`latest` 仍是 0.1.7-rc.2,0.2.0-rc.1 走 `next`),
|
||||
# 冒烟会静静地挂到一个插件已不支持的宿主上。**必须写精确版本,不能写
|
||||
# `@next`/`@alpha` 之类的 dist-tag**:0.1.7-rc.1 上线时 `alpha` 指的是
|
||||
# 0.1.7-alpha.2,0.2.0-rc.1 又只挂在 `next` 上。钉版必须与 package.json 的
|
||||
# peer 下限同步(caret 跨 minor 不成立:`^0.1.7-rc.1` 实测不满足 0.2.0-rc.1)。
|
||||
DSH_NPX_SPEC="${DSH_NPX_SPEC:-@deepseek-ai/dsh@0.2.0-rc.1}"
|
||||
e2e_resolve_dsh_cmd() {
|
||||
local explicit="${DSH_CMD}"
|
||||
if ! command -v "$DSH_CMD" >/dev/null 2>&1; then
|
||||
if command -v npx >/dev/null 2>&1; then
|
||||
say "PATH 上无 ${DSH_CMD},回退 npx -y --package ${DSH_NPX_SPEC}"
|
||||
DSH_CMD="npx -y --package ${DSH_NPX_SPEC} dsh"
|
||||
else
|
||||
die "未找到 $DSH_CMD 或 npx;请先安装 DSH CLI(npm i -g @deepseek-ai/dsh)或用 DSH_CMD 指定"
|
||||
fi
|
||||
fi
|
||||
e2e_check_dsh_version "$explicit"
|
||||
}
|
||||
|
||||
# 解析出的 CLI 版本必须与 peer 下限同一条支持线。这不是洁癖:宿主只比
|
||||
# 基线早一个预发布时,`dsh plugin add` 的 bundle 协调会静默不写
|
||||
# `dsh.profile.bundles`——CLI 退出码仍是 0,lane 却挂在「挂载未注册」上,
|
||||
# 排查方向完全被带偏(本机 PATH 上的 dsh 一直是上一版,0.1.6-alpha.1 与
|
||||
# 0.1.6-alpha.2 各实测过一次)。DSH_CMD 显式给出时不拦(调用方明确知道自
|
||||
# 己在挂什么),只告警。
|
||||
e2e_check_dsh_version() {
|
||||
local explicit="$1" actual expected
|
||||
expected="${DSH_EXPECT_VERSION:-0.2.0-rc.1}"
|
||||
# `dsh --version` 冷启动要走 npx,给足预算;拿不到版本就只告警。
|
||||
actual="$($DSH_CMD --version 2>/dev/null | tr -d '[:space:]' | head -c 64 || true)"
|
||||
case "$actual" in
|
||||
"$expected"|"$expected"*) return 0 ;;
|
||||
esac
|
||||
if [ "$explicit" = "dsh" ] && [ -n "$actual" ]; then
|
||||
die "PATH 上的 dsh 是 ${actual},本仓库的基线是 ${expected}(peer 下限同一支持线)。请改用 DSH_CMD='npx -y --package @deepseek-ai/dsh@${expected} dsh',或安装该版本。"
|
||||
fi
|
||||
warn "DSH_CMD 版本为 ${actual:-未知},基线是 ${expected}——真机证据落在非基线宿主上。"
|
||||
}
|
||||
|
||||
# ── tarball 解析 ─────────────────────────────────────────────────────────────
|
||||
# TARBALL 已显式给出时原样使用;否则从仓库根 glob 里选。多个候选时取 mtime
|
||||
# 最新——`ls | head -1` 的字典序会拿到旧版本号的历史 tarball,把冒烟挂到
|
||||
# 过期产物上。未找到候选时返回非零,由调用方按各自文案 die。
|
||||
e2e_resolve_tarball() {
|
||||
if [ -z "${TARBALL:-}" ]; then
|
||||
TARBALL="$(ls -t "$ROOT"/dsh-apes-sidebar-*.tgz 2>/dev/null | head -1 || true)"
|
||||
local count
|
||||
count="$(ls "$ROOT"/dsh-apes-sidebar-*.tgz 2>/dev/null | wc -l | tr -d ' ' || true)"
|
||||
[ "$count" -le 1 ] || warn "发现 $count 个 tarball,按 mtime 选用最新:$(basename "$TARBALL")(建议清理其余)"
|
||||
fi
|
||||
[ -n "$TARBALL" ] && [ -f "$TARBALL" ]
|
||||
}
|
||||
|
||||
# ── scratch home 与清理 ──────────────────────────────────────────────────────
|
||||
# 始终在本调用拥有的全新目录里运行:调用方给了 DSH_HOME_BASE(可能是真实
|
||||
# ~/.dsh)时,只在其下新建子目录并只删除该子目录;缺省时直接用系统临时
|
||||
# 目录。调用方提供的目录本身绝不写入或删除。
|
||||
# $1 = mktemp 模板短名(dsh-e2e-mount / dsh-e2e-agg,便于区分两 lane 的残留)。
|
||||
# DSH_HOME 用 $SCRATCH 还是其子路径由调用方自行 export(两 lane 的既有差异)。
|
||||
e2e_make_scratch() {
|
||||
if [ -n "${DSH_HOME_BASE:-}" ]; then
|
||||
SCRATCH="$(mktemp -d "$DSH_HOME_BASE/$1.XXXXXX")"
|
||||
else
|
||||
SCRATCH="$(mktemp -d /tmp/$1.XXXXXX)"
|
||||
fi
|
||||
}
|
||||
|
||||
# EXIT trap 兜底清理:杀 dsh web 后台进程;清 aggregate lane 的 fixture 打包
|
||||
# 目录(TMP_DIR——mount lane 不设该变量即整段跳过);最后按 KEEP_HOME 决定
|
||||
# 是否删除 scratch。结尾显式 exit 回传 trap 触发时的退出码。
|
||||
e2e_cleanup() {
|
||||
local code=$?
|
||||
if [ -n "${SERVER_PID:-}" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
wait "$SERVER_PID" 2>/dev/null || true
|
||||
fi
|
||||
if [ -n "${TMP_DIR:-}" ] && [ -d "${TMP_DIR:-}" ]; then
|
||||
rm -rf "$TMP_DIR"
|
||||
fi
|
||||
if [ -z "${KEEP_HOME:-}" ]; then
|
||||
if [ -d "${SCRATCH:-}" ]; then
|
||||
rm -rf "$SCRATCH"
|
||||
fi
|
||||
else
|
||||
warn "KEEP_HOME 已设置,保留 $SCRATCH"
|
||||
fi
|
||||
exit "$code"
|
||||
}
|
||||
|
||||
# ── scratch profile 三件套 ───────────────────────────────────────────────────
|
||||
# 引导 scratch profile(web 模板,镜像 dsh initProfile);先写
|
||||
# pnpm-workspace.yaml 的 minimumReleaseAgeExclude,避免 pnpm 11 拒绝 <24h 新版本
|
||||
# ——同 install.sh。@deepseek-ai/* 通配与仓库根 pnpm-workspace.yaml 同策:钉的
|
||||
# DSH alpha 常在发布后 24h 内跑 lane,没有豁免会被 minimumReleaseAge 直接拒装。
|
||||
e2e_write_profile() {
|
||||
mkdir -p "$1"
|
||||
cat > "$1/package.json" <<EOF
|
||||
{
|
||||
"name": "dsh-profile-web",
|
||||
"private": true,
|
||||
"dependencies": {},
|
||||
"dsh": {
|
||||
"profile": {
|
||||
"bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
printf '[]\n' > "$1/cordis.patch.yml"
|
||||
cat > "$1/pnpm-workspace.yaml" <<'EOF'
|
||||
packages:
|
||||
- .
|
||||
|
||||
nodeLinker: hoisted
|
||||
autoInstallPeers: false
|
||||
|
||||
minimumReleaseAgeExclude:
|
||||
- dsh-apes-sidebar
|
||||
- '@deepseek-ai/*'
|
||||
EOF
|
||||
}
|
||||
|
||||
# ── dsh web 启动 + 就绪轮询 ──────────────────────────────────────────────────
|
||||
# 后台启动 dsh web(--port 0 = OS 分配,避免端口冲突;keyless 可起)。
|
||||
# $1 = stdout 日志;$2 = stderr 日志(缺省合并进 $1:mount lane 单文件,
|
||||
# aggregate lane 分 out/err 两个文件)。写入全局 SERVER_PID。
|
||||
e2e_start_dsh_web() {
|
||||
if [ $# -ge 2 ]; then
|
||||
$DSH_CMD web --port "$PORT" >"$1" 2>"$2" &
|
||||
else
|
||||
$DSH_CMD web --port "$PORT" >"$1" 2>&1 &
|
||||
fi
|
||||
SERVER_PID=$!
|
||||
}
|
||||
|
||||
# 轮询日志等待就绪行(最多 120s)。$1 = 参与轮询的日志文件;就绪行 ERE 与
|
||||
# 多条命中的取舍(head/tail)由 E2E_READY_RE / E2E_READY_PICK 传入——这是
|
||||
# 两条 lane 有意保留的差异(token URL vs 裸 origin,理由见各自脚本的就绪
|
||||
# 行注释)。URL 提取统一为「命中串按空白分段取末段」:mount 的模式带
|
||||
# `dsh web: ` 前缀(两段)故末段即完整 URL;aggregate 的模式不含空白故末段
|
||||
# 即 origin——与原先两侧的 awk '{print $3}' / 直接取整行等价。
|
||||
# 返回:0 = URL 已写入全局 URL;1 = 120s 超时;2 = 服务进程提前退出。
|
||||
e2e_wait_dsh_web_ready() {
|
||||
URL=""
|
||||
local _
|
||||
for _ in $(seq 1 120); do
|
||||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||
return 2
|
||||
fi
|
||||
if URL="$(grep -oE "$E2E_READY_RE" "$1" | "${E2E_READY_PICK:-head}" -1 | awk '{print $NF}')" && [ -n "$URL" ]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# dsh-apes-sidebar 挂载冒烟编排(CI + 本地):
|
||||
#
|
||||
# 1. 用官方 CLI 把 npm 打包产物(tarball)真实挂载进一个全新 scratch
|
||||
# profile(`dsh plugin --profile web add file:<tarball>`,触发
|
||||
# dsh.profile.bundles 协调,与用户安装路径一致);
|
||||
# 2. 启动真实 `dsh web`(keyless,--port 0 取 OS 分配端口);
|
||||
# 3. 运行 tests/e2e 无头渲染 lane(Playwright Chromium):断言外壳与
|
||||
# 插件挂载、无崩溃标记,并驱动内置 tab 深扫。
|
||||
#
|
||||
# 用法:
|
||||
# bash scripts/e2e-mount.sh [--grep <playwright-filter>]
|
||||
#
|
||||
# 环境变量(均可省略):
|
||||
# DSH_CMD dsh 命令;缺省 PATH 上的 `dsh`,回退 npx 拉官方包
|
||||
# TARBALL 插件 tarball;缺省仓库根 dsh-apes-sidebar-*.tgz(须已 pack)
|
||||
# PORT 固定端口(默认 0 = OS 分配,从日志解析 URL)
|
||||
# DSH_HOME_BASE 覆盖 scratch 根目录(默认系统临时目录)。脚本始终在其下
|
||||
# 新建本调用拥有的独立子目录,只写入/删除该子目录;调用方
|
||||
# 提供的目录本身(可能是真实 ~/.dsh)绝不写入或删除。
|
||||
# KEEP_HOME 非空时保留 scratch home(调试用)
|
||||
#
|
||||
# 退出码 = playwright 的退出码;服务器与 scratch 目录由 trap 兜底清理。
|
||||
# 日志/前置校验/DSH_CMD 与 tarball 解析/scratch profile 三件套/清理 trap/
|
||||
# dsh web 启动与就绪轮询的共享骨架见 scripts/e2e-common.sh。
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
E2E_TAG=e2e-mount
|
||||
source "$SCRIPT_DIR/e2e-common.sh"
|
||||
|
||||
DSH_CMD="${DSH_CMD:-dsh}"
|
||||
PORT="${PORT:-0}"
|
||||
TARBALL="${TARBALL:-}"
|
||||
GREP_FILTER=""
|
||||
if [ "${1:-}" = "--grep" ]; then GREP_FILTER="${2:?--grep 需要参数}"; fi
|
||||
|
||||
e2e_require_cmd node "DSH 运行需要 Node.js >= 20"
|
||||
e2e_require_cmd pnpm "dsh plugin 转发给 pnpm"
|
||||
|
||||
e2e_resolve_dsh_cmd
|
||||
|
||||
e2e_resolve_tarball || die "找不到 tarball(TARBALL 或 \$ROOT/dsh-apes-sidebar-*.tgz)——先运行 pnpm build && pnpm pack"
|
||||
TARBALL="$(cd "$(dirname "$TARBALL")" && pwd)/$(basename "$TARBALL")"
|
||||
say "tarball: $TARBALL"
|
||||
|
||||
# scratch home(每次全新,绝不触碰真实 ~/.dsh;DSH_HOME_BASE 语义见
|
||||
# e2e-common.sh)。DSH_HOME 用子路径 home/(aggregate lane 直接用 $SCRATCH,
|
||||
# 两 lane 既有差异):playwright lane 还需要独立的 workspace 目录,与
|
||||
# DSH_HOME 的 profiles 布局分开。
|
||||
e2e_make_scratch dsh-e2e-mount
|
||||
export DSH_HOME="$SCRATCH/home"
|
||||
WORKSPACE_DIR="$SCRATCH/workspace"
|
||||
LOG_DIR="$SCRATCH"
|
||||
WEB_LOG="$LOG_DIR/web.log"
|
||||
mkdir -p "$DSH_HOME/profiles/web" "$WORKSPACE_DIR"
|
||||
say "scratch home: ${DSH_HOME}(DSH_HOME=${DSH_HOME})"
|
||||
|
||||
SERVER_PID=""
|
||||
trap e2e_cleanup EXIT
|
||||
|
||||
# 步骤 1:引导 scratch profile(三件套 heredoc 及其理由见 e2e-common.sh
|
||||
# 的 e2e_write_profile)
|
||||
PROFILE_DIR="$DSH_HOME/profiles/web"
|
||||
e2e_write_profile "$PROFILE_DIR"
|
||||
|
||||
# 步骤 2:官方 CLI 安装 tarball + bundle 协调(真实挂载路径)
|
||||
say "执行 dsh plugin --profile web add file:$TARBALL ..."
|
||||
$DSH_CMD plugin --profile web add "file:$TARBALL"
|
||||
|
||||
# 步骤 3:校验挂载生效(dsh.profile.bundles 含 dsh-apes-sidebar)
|
||||
if ! node -e '
|
||||
const fs = require("fs");
|
||||
const p = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
const bundles = p.dsh?.profile?.bundles ?? [];
|
||||
process.exit(bundles.includes("dsh-apes-sidebar") ? 0 : 1);
|
||||
' "$PROFILE_DIR/package.json"; then
|
||||
warn "dsh-apes-sidebar 未出现在 dsh.profile.bundles 中——挂载未注册"
|
||||
cat "$PROFILE_DIR/package.json"
|
||||
exit 1
|
||||
fi
|
||||
say "挂载已注册:dsh.profile.bundles 包含 dsh-apes-sidebar"
|
||||
|
||||
# 步骤 4:启动 dsh web(--port 0 = OS 分配,避免端口冲突;keyless 可起)
|
||||
say "启动 dsh web(port=${PORT})..."
|
||||
e2e_start_dsh_web "$WEB_LOG"
|
||||
|
||||
# 就绪行解析:DSH 0.1.2-alpha.1+ 打印的是带一次性 token 的鉴权 URL
|
||||
# (`dsh web: http://127.0.0.1:<port>/?token=<43字符>`,页面导航用它换取
|
||||
# 浏览器 cookie;干净 URL 只会得到 401);0.1.1-rc.x 及更早是裸 origin。
|
||||
# 匹配必须延伸到空白(`[^ ]*`)——在 `/` 处截断会丢掉 token,alpha.1+
|
||||
# 宿主上的整条 lane 都会挂在首屏 401。LAN 后缀 `(LAN: …)` 是下一个
|
||||
# 空格分段,不会被并进来。
|
||||
E2E_READY_RE='dsh web: http://127\.0\.0\.1:[0-9]+[^ ]*'
|
||||
E2E_READY_PICK=head
|
||||
WAIT_RC=0
|
||||
e2e_wait_dsh_web_ready "$WEB_LOG" || WAIT_RC=$?
|
||||
if [ "$WAIT_RC" -eq 2 ]; then
|
||||
echo "=== dsh web 提前退出,日志尾部 ===" >&2
|
||||
tail -30 "$WEB_LOG" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
if [ "$WAIT_RC" -ne 0 ]; then
|
||||
echo "=== 120s 内未等到 dsh web 就绪,日志尾部 ===" >&2
|
||||
tail -40 "$WEB_LOG" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
say "dsh web 就绪:${URL}(pid ${SERVER_PID})"
|
||||
|
||||
# 步骤 5:运行无头渲染 lane
|
||||
say "运行 Playwright 无头渲染 lane..."
|
||||
DSH_E2E_URL="$URL" DSH_E2E_WORKSPACE="$WORKSPACE_DIR" \
|
||||
pnpm exec playwright test ${GREP_FILTER:+--grep "$GREP_FILTER"}
|
||||
|
||||
say "通过:插件挂载到真实 DSH 后无头渲染未崩溃"
|
||||
@@ -1,263 +0,0 @@
|
||||
## =============================================================================
|
||||
# dsh-apes-sidebar 一键安装脚本(官方 CLI 方式,Windows PowerShell 5.1+ / pwsh)
|
||||
#
|
||||
# 通过 DSH 官方插件命令安装 npm 包并自动挂载:
|
||||
# dsh plugin --profile web add dsh-apes-sidebar@<version>
|
||||
#
|
||||
# 包内声明了 dsh.bundle.patch(cordis.patch.yml):CLI 的 bundle 协调会把它
|
||||
# 自动加进 profile 的 dsh.profile.bundles,下次启动即挂载——无需手动写
|
||||
# cordis.patch.yml 挂载行。符合仓库硬约束:不修改 DSH 源码,插件永远作为
|
||||
# 独立包被 profile 引用。
|
||||
#
|
||||
# 用法(任选其一):
|
||||
# # 默认最新版
|
||||
# $script = (irm 'https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1').TrimStart([char]0xFEFF)
|
||||
# & ([scriptblock]::Create($script))
|
||||
# # 指定版本 / 装完重启
|
||||
# $script = (irm 'https://raw.githubusercontent.com/omdsh-dev/DSH-better-sidebar/main/scripts/install.ps1').TrimStart([char]0xFEFF)
|
||||
# & ([scriptblock]::Create($script)) -Version 0.10.2 -Restart
|
||||
# # 本地保存后运行
|
||||
# powershell -ExecutionPolicy Bypass -File install.ps1 -Version 0.10.2 -DryRun
|
||||
#
|
||||
# 参数:
|
||||
# -Version npm 版本号/范围,缺省 latest(自动解析为最新)。
|
||||
# -Profile 目标 profile 名(缺省 web);安装与修复模式均适用。
|
||||
# -Restart 装完后尝试 `pm2 restart dsh-web`(无 pm2 时仅提示)。会断开当前页面会话。
|
||||
# -DryRun 只打印将要执行的操作,不写任何文件。
|
||||
#
|
||||
# 环境变量(均可省略):
|
||||
# DSH_HOME 默认 %USERPROFILE%\.dsh
|
||||
# REGISTRY 默认 https://registry.npmjs.org
|
||||
# DSH_CMD 默认优先 PATH 上的 dsh,缺省回退 npx -y --package @deepseek-ai/dsh
|
||||
#
|
||||
# 说明:
|
||||
# - pnpm 11 的 minimumReleaseAge 会拒绝发布 <24h 的新版本。脚本会预写
|
||||
# minimumReleaseAgeExclude(幂等),放行本插件,避免"重跑一次才成功"。
|
||||
# - 老版本(<0.10.2)用手动挂载行,bundle 通道激活后需移除,否则双挂载。
|
||||
# 脚本会幂等移除 better-sidebar 挂载行。
|
||||
# =============================================================================
|
||||
param(
|
||||
[string]$Version = '',
|
||||
[switch]$Restart,
|
||||
[switch]$DryRun,
|
||||
[string]$Profile = 'web'
|
||||
)
|
||||
|
||||
$PKG = 'dsh-apes-sidebar'
|
||||
$REGISTRY = if ($env:REGISTRY) { $env:REGISTRY } else { 'https://registry.npmjs.org' }
|
||||
|
||||
# DSH_HOME:DSH_HOME 环境变量 > %USERPROFILE% > $HOME
|
||||
if ($env:DSH_HOME) {
|
||||
$DSH_HOME = $env:DSH_HOME
|
||||
} elseif ($env:USERPROFILE) {
|
||||
$DSH_HOME = Join-Path $env:USERPROFILE '.dsh'
|
||||
} else {
|
||||
$DSH_HOME = Join-Path $HOME '.dsh'
|
||||
}
|
||||
$PROFILE_DIR = Join-Path $DSH_HOME "profiles\$Profile"
|
||||
$WS_YML = Join-Path $PROFILE_DIR 'pnpm-workspace.yaml'
|
||||
$PATCH_YML = Join-Path $PROFILE_DIR 'cordis.patch.yml'
|
||||
|
||||
function Say([string]$m) { Write-Host "[install] $m" -ForegroundColor Green }
|
||||
function Warn([string]$m) { Write-Host "[warn] $m" -ForegroundColor Yellow }
|
||||
function Die([string]$m) { Write-Host "[error] $m" -ForegroundColor Red; exit 1 }
|
||||
|
||||
# 解析版本 -> npm spec("x.y.z" / "^x.y.z" / latest)
|
||||
function Resolve-Spec {
|
||||
param([string]$Given)
|
||||
if ([string]::IsNullOrWhiteSpace($Given) -or $Given -eq 'latest') {
|
||||
$v = $null
|
||||
foreach ($tool in @('npm', 'pnpm')) {
|
||||
if (Get-Command $tool -ErrorAction SilentlyContinue) {
|
||||
$v = (& $tool view $PKG version "--registry=$REGISTRY" 2>$null | Select-Object -Last 1)
|
||||
if ($v) { break }
|
||||
}
|
||||
}
|
||||
if ($v) { return ([string]$v).Trim() }
|
||||
Warn '无法联网解析最新版本(npm/pnpm 查询失败),回退为 latest,由 pnpm 直接解析。'
|
||||
Warn '若已知版本号,可显式传入:-Version 0.10.2'
|
||||
return 'latest'
|
||||
}
|
||||
return $Given
|
||||
}
|
||||
|
||||
# 组装 dsh CLI:优先 PATH 上的 dsh,缺省 npx 拉官方包
|
||||
function Get-DshCli {
|
||||
if ($env:DSH_CMD) { return $env:DSH_CMD }
|
||||
if (Get-Command dsh -ErrorAction SilentlyContinue) { return 'dsh' }
|
||||
if (Get-Command npx -ErrorAction SilentlyContinue) { return 'npx' }
|
||||
return $null
|
||||
}
|
||||
|
||||
# 步骤 1(安装与修复共用):预写 workspace 设置(幂等),保证 pnpm 不拦截
|
||||
# 放行本插件新版本(0.20.0 起插件不再依赖需要构建脚本的包,allowBuilds 无需再动)
|
||||
function Ensure-WorkspaceSettings {
|
||||
$wsScript = @'
|
||||
const fs = require("fs");
|
||||
const p = process.argv[2];
|
||||
let t = fs.readFileSync(p, "utf8");
|
||||
const before = t;
|
||||
if (!/^\s*-\s+dsh-apes-sidebar\s*$/m.test(t)) {
|
||||
if (/^\s*minimumReleaseAgeExclude:\s*$/m.test(t)) {
|
||||
t = t.replace(/^(\s*minimumReleaseAgeExclude:\s*)$/m, "$1\n - dsh-apes-sidebar");
|
||||
} else {
|
||||
t += "\nminimumReleaseAgeExclude:\n - dsh-apes-sidebar\n";
|
||||
}
|
||||
}
|
||||
if (t !== before) fs.writeFileSync(p, t);
|
||||
console.log(t === before ? "unchanged" : "updated");
|
||||
'@
|
||||
# PowerShell 5.1 把含内嵌双引号的多行 JS 作为参数传给 `node -e` 时,引号会被
|
||||
# Windows 命令行解析吞掉,导致 JS 语法错误(Expected ',', got ':')。
|
||||
# 改用临时文件方式,兼容 PS 5.1 与 pwsh 7。
|
||||
$wsJs = Join-Path $env:TEMP ("dshbs-ws-" + [guid]::NewGuid().ToString("N") + ".js")
|
||||
Set-Content -LiteralPath $wsJs -Value $wsScript -Encoding UTF8
|
||||
$wsOut = node $wsJs "$WS_YML" 2>&1
|
||||
$wsCode = $LASTEXITCODE
|
||||
Remove-Item -LiteralPath $wsJs -Force -ErrorAction SilentlyContinue
|
||||
$wsResult = (($wsOut | Out-String)).Trim()
|
||||
if ($wsCode -ne 0) { Die "处理 $WS_YML 失败(node 退出码 $wsCode):$wsResult" }
|
||||
if ($wsResult -eq 'updated') {
|
||||
Say "已确保 $WS_YML:minimumReleaseAgeExclude($PKG)"
|
||||
} else {
|
||||
Say 'workspace 设置已就绪,跳过'
|
||||
}
|
||||
}
|
||||
|
||||
# 前置校验
|
||||
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
Die '未找到 node(DSH 运行需要 Node.js >= 20),请先安装 Node.js 并加入 PATH。'
|
||||
}
|
||||
if (-not (Get-Command pnpm -ErrorAction SilentlyContinue)) {
|
||||
Die '未找到 pnpm;安装脚本需要 pnpm >= 10。请先安装或升级 pnpm 并加入 PATH。'
|
||||
}
|
||||
$pnpmVersion = (& pnpm --version 2>$null | Select-Object -Last 1)
|
||||
if ($LASTEXITCODE -ne 0 -or -not $pnpmVersion) {
|
||||
Die '无法读取 pnpm 版本;安装脚本需要 pnpm >= 10。'
|
||||
}
|
||||
$pnpmVersionText = ([string]$pnpmVersion).Trim()
|
||||
$pnpmMajor = 0
|
||||
if (-not [int]::TryParse(($pnpmVersionText -split '\.')[0], [ref]$pnpmMajor)) {
|
||||
Die "无法解析 pnpm 版本(输出:$pnpmVersionText);安装脚本需要 pnpm >= 10。"
|
||||
}
|
||||
if ($pnpmMajor -lt 10) {
|
||||
Die "安装脚本需要 pnpm >= 10;当前版本为 $pnpmVersionText。请升级 pnpm 后重试。"
|
||||
}
|
||||
if (-not (Test-Path $PROFILE_DIR)) {
|
||||
Die "找不到 profile 目录:$PROFILE_DIR(请先安装并运行过一次 dsh web)"
|
||||
}
|
||||
if (-not (Test-Path $WS_YML)) {
|
||||
Die "找不到 $WS_YML(请先初始化 $Profile profile)"
|
||||
}
|
||||
|
||||
$SPEC = Resolve-Spec $Version
|
||||
$CLI = Get-DshCli
|
||||
if (-not $CLI) {
|
||||
Die '未找到 dsh 或 npx。请先安装 DSH(并确保 Node/npm 可用),或用 DSH_CMD 指定。'
|
||||
}
|
||||
Say "目标:$CLI plugin --profile $Profile add $PKG@$SPEC(profile: $PROFILE_DIR)"
|
||||
|
||||
if ($DryRun) {
|
||||
Say "[dry-run] 步骤 1:确保 $WS_YML 含 minimumReleaseAgeExclude($PKG)"
|
||||
Say "[dry-run] 步骤 2:执行 $CLI plugin --profile $Profile add $PKG@$SPEC(安装 + bundle 自动注册)"
|
||||
Say "[dry-run] 步骤 3:校验 dsh.profile.bundles 含 $PKG"
|
||||
Say "[dry-run] 步骤 4:幂等移除 $PATCH_YML 里旧的 better-sidebar 手动挂载行(避免双挂载)"
|
||||
if ($Restart) { Say '[dry-run] 步骤 5:pm2 restart dsh-web' } else { Say '[dry-run] 步骤 5:提示用户手动重启 DSH' }
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 步骤 1:预写 workspace 设置(幂等),放行本插件新版本
|
||||
Ensure-WorkspaceSettings
|
||||
|
||||
# 步骤 2:官方 CLI 安装 + bundle 自动注册(含挂载)
|
||||
if ($CLI -eq 'dsh') {
|
||||
$cliArgs = @('plugin', '--profile', $Profile, 'add', "$PKG@$SPEC")
|
||||
} else {
|
||||
$cliArgs = @('-y', '--package', '@deepseek-ai/dsh', 'dsh', 'plugin', '--profile', $Profile, 'add', "$PKG@$SPEC")
|
||||
}
|
||||
Say "执行 $CLI plugin --profile $Profile add $PKG@$SPEC ..."
|
||||
$addOut = & $CLI @cliArgs 2>&1
|
||||
$addCode = $LASTEXITCODE
|
||||
$addOut | ForEach-Object { $_ }
|
||||
if ($addCode -ne 0) {
|
||||
Warn 'dsh plugin add 失败。已预写 minimumReleaseAgeExclude,仍失败的可能原因:'
|
||||
Warn ' - 网络/登录问题:npm registry 不可达或需要登录。'
|
||||
Warn " - 依赖安装冲突:可手动重试 cd $PROFILE_DIR; pnpm install。"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 步骤 3:校验 bundle 已注册(挂载生效的判据)
|
||||
$pkgJson = Get-Content -Raw (Join-Path $PROFILE_DIR 'package.json') | ConvertFrom-Json
|
||||
$bundles = $pkgJson.dsh.profile.bundles
|
||||
if ($bundles -notcontains $PKG) {
|
||||
Warn 'dsh-apes-sidebar 未出现在 dsh.profile.bundles 中——挂载未注册。'
|
||||
Warn "若上面的 pnpm 输出提示 ignored build scripts,请确认 $WS_YML 的 allowBuilds 后重跑本脚本(0.20.0 起本插件自身不再有构建脚本依赖)。"
|
||||
exit 1
|
||||
}
|
||||
Say "bundle 已注册:dsh.profile.bundles 包含 $PKG(下次启动自动挂载)"
|
||||
|
||||
# 步骤 4:幂等移除旧的 manual 挂载行(避免与 bundle 双挂载)
|
||||
$mountScript = @'
|
||||
const fs = require("fs");
|
||||
const p = process.argv[2];
|
||||
const lines = fs.readFileSync(p, "utf8").split("\n");
|
||||
const out = [];
|
||||
let i = 0;
|
||||
let removed = false;
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
if (/^[ \t]*- insert:\s*$/.test(line)) {
|
||||
const block = [line];
|
||||
let j = i + 1;
|
||||
while (j < lines.length && lines[j].trim() !== "" && !/^-\s/.test(lines[j])) {
|
||||
block.push(lines[j]);
|
||||
j++;
|
||||
}
|
||||
if (block.some((l) => /id:\s*better-sidebar\b/.test(l))) {
|
||||
while (out.length && /^[ \t]*#/.test(out[out.length - 1])) out.pop();
|
||||
i = j;
|
||||
removed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(line);
|
||||
i++;
|
||||
}
|
||||
if (!removed) {
|
||||
console.log("none");
|
||||
} else {
|
||||
const t = out.join("\n").replace(/\n{3,}/g, "\n\n");
|
||||
fs.writeFileSync(p, t);
|
||||
console.log("removed");
|
||||
}
|
||||
'@
|
||||
$mountJs = Join-Path $env:TEMP ("dshbs-mount-" + [guid]::NewGuid().ToString("N") + ".js")
|
||||
Set-Content -LiteralPath $mountJs -Value $mountScript -Encoding UTF8
|
||||
$mountOut = node $mountJs "$PATCH_YML" 2>&1
|
||||
$mountCode = $LASTEXITCODE
|
||||
Remove-Item -LiteralPath $mountJs -Force -ErrorAction SilentlyContinue
|
||||
$mountResult = (($mountOut | Out-String)).Trim()
|
||||
if ($mountCode -ne 0) { Die "处理 $PATCH_YML 失败(node 退出码 $mountCode):$mountResult" }
|
||||
if ($mountResult -eq 'removed') {
|
||||
Say "已从 $PATCH_YML 移除旧的 better-sidebar 手动挂载行(bundle 通道接管挂载)"
|
||||
} else {
|
||||
Say '无旧手动挂载行,跳过'
|
||||
}
|
||||
|
||||
Say "安装完成:$PKG@$SPEC"
|
||||
|
||||
# 步骤 5:重启提示
|
||||
if ($Restart) {
|
||||
if (Get-Command pm2 -ErrorAction SilentlyContinue) {
|
||||
Say '重启 dsh-web(pm2)...'
|
||||
pm2 restart dsh-web
|
||||
if ($LASTEXITCODE -ne 0) { Warn 'pm2 restart 失败,请手动重启 DSH' }
|
||||
} else {
|
||||
Warn '未找到 pm2,请手动重启 DSH(如:pm2 restart dsh-web 或 dsh web)'
|
||||
}
|
||||
} else {
|
||||
Say '下一步:重启 DSH 并硬刷新(Ctrl+Shift+R / Cmd+Shift+R)使新副本生效。'
|
||||
if (Get-Command pm2 -ErrorAction SilentlyContinue) {
|
||||
Say '本机可用:pm2 restart dsh-web(会短暂断开当前页面会话)'
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# dsh-apes-sidebar 一键安装脚本(官方 CLI 方式,macOS / Linux / Windows Git Bash)
|
||||
#
|
||||
# 通过 DSH 官方插件命令安装 npm 包并自动挂载:
|
||||
# dsh plugin --profile web add dsh-apes-sidebar@<version>
|
||||
#
|
||||
# 包内声明了 dsh.bundle.patch(cordis.patch.yml):CLI 的 bundle 协调会把它
|
||||
# 自动加进 profile 的 dsh.profile.bundles,下次启动即挂载——无需手动写
|
||||
# cordis.patch.yml 挂载行。符合仓库硬约束:不修改 DSH 源码,插件永远作为
|
||||
# 独立包被 profile 引用。
|
||||
#
|
||||
# 用法:
|
||||
# bash scripts/install.sh [版本] [--restart] [--dry-run]
|
||||
#
|
||||
# 版本 npm 版本号/范围,缺省为 latest(自动解析为 ^<最新>)。
|
||||
# 示例:0.10.2、^0.10.2、~0.10.2、latest
|
||||
# --profile 目标 profile 名(缺省 web)。
|
||||
# --restart 装完后尝试 `pm2 restart dsh-web`(无 pm2 时仅打印提示)。
|
||||
# 注意:重启会断开当前 DSH 页面会话,默认不自动重启。
|
||||
# --dry-run 只打印将要执行的操作,不写任何文件。
|
||||
# -h/--help 打印本帮助。
|
||||
#
|
||||
# 环境(均可省略,脚本会自动探测):
|
||||
# DSH_HOME 默认 ~/.dsh(Windows Git Bash 下回退 $USERPROFILE/.dsh)
|
||||
# REGISTRY 默认 https://registry.npmjs.org(发布源;装依赖仍走 pnpm 配置)
|
||||
# DSH_CMD 默认优先用 PATH 上的 `dsh`,缺省回退 npx -y --package @deepseek-ai/dsh
|
||||
#
|
||||
# 说明:
|
||||
# - pnpm 11 的 minimumReleaseAge 会拒绝发布 <24h 的新版本。脚本会预写
|
||||
# minimumReleaseAgeExclude(幂等),放行本插件,避免"重跑一次才成功"。
|
||||
# - 老版本(<0.10.2)用手动挂载行,bundle 通道激活后需移除,否则双挂载
|
||||
# (Node 半挂两次、页面两个侧边栏)。脚本会幂等移除 better-sidebar 挂载行。
|
||||
# - 回滚:dsh plugin --profile web remove dsh-apes-sidebar,或把 profile 依赖
|
||||
# 改回 "dsh-apes-sidebar": "link:<路径>" 再 pnpm install。
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# 帮助请求优先处理
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then
|
||||
cat <<'EOF'
|
||||
dsh-apes-sidebar 一键安装脚本
|
||||
|
||||
用法:
|
||||
bash scripts/install.sh [版本] [--restart] [--dry-run] [--profile <名>]
|
||||
|
||||
版本 npm 版本号/范围,缺省 latest(自动解析为最新)。示例:0.10.2、^0.10.2、latest
|
||||
--profile 目标 profile 名(缺省 web)
|
||||
--restart 装完后尝试 `pm2 restart dsh-web`(无 pm2 时仅提示)
|
||||
--dry-run 只打印将要执行的操作,不写任何文件
|
||||
|
||||
环境变量(可省略):DSH_HOME(默认 ~/.dsh)、REGISTRY(npm 源)、DSH_CMD(dsh 命令)
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
DSH_HOME="${DSH_HOME:-${HOME:-${USERPROFILE:-}}/.dsh}"
|
||||
REGISTRY="${REGISTRY:-https://registry.npmjs.org}"
|
||||
PKG="dsh-apes-sidebar"
|
||||
DSH_CMD="${DSH_CMD:-dsh}"
|
||||
|
||||
RESTART=false
|
||||
DRY_RUN=false
|
||||
VERSION_SPEC=""
|
||||
PROFILE_NAME="web"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--restart) RESTART=true ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
--profile)
|
||||
if [ $# -lt 2 ]; then echo "--profile 需要一个 profile 名(如 web)" >&2; exit 2; fi
|
||||
PROFILE_NAME="$2"; shift ;;
|
||||
-h|--help) : ;; # 已在上面处理
|
||||
-*) echo "未知参数: ${1}(用 -h 查看用法)" >&2; exit 2 ;;
|
||||
*) VERSION_SPEC="$1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
PROFILE_DIR="$DSH_HOME/profiles/$PROFILE_NAME"
|
||||
WS_YML="$PROFILE_DIR/pnpm-workspace.yaml"
|
||||
PATCH_YML="$PROFILE_DIR/cordis.patch.yml"
|
||||
|
||||
say() { printf '\033[32m[install]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[33m[warn]\033[0m %s\n' "$*" >&2; }
|
||||
die() { printf '\033[31m[error]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# 步骤 1:预写 workspace 设置(幂等),放行本插件新版本
|
||||
# (0.20.0 起插件不再依赖任何需要构建脚本的包,allowBuilds 无需再动)
|
||||
ensure_workspace_settings() {
|
||||
WS_RESULT="$(node -e '
|
||||
const fs = require("fs");
|
||||
const p = process.argv[1];
|
||||
let t = fs.readFileSync(p, "utf8");
|
||||
const before = t;
|
||||
// minimumReleaseAgeExclude:放行本插件(版本无关),避免 <24h 新版本被拒
|
||||
if (!/^\s*-\s+dsh-apes-sidebar\s*$/m.test(t)) {
|
||||
if (/^\s*minimumReleaseAgeExclude:\s*$/m.test(t)) {
|
||||
t = t.replace(/^(\s*minimumReleaseAgeExclude:\s*)$/m, "$1\n - dsh-apes-sidebar");
|
||||
} else {
|
||||
t += "\nminimumReleaseAgeExclude:\n - dsh-apes-sidebar\n";
|
||||
}
|
||||
}
|
||||
if (t !== before) fs.writeFileSync(p, t);
|
||||
console.log(t === before ? "unchanged" : "updated");
|
||||
' "$WS_YML")"
|
||||
[ "$WS_RESULT" = "updated" ] \
|
||||
&& say "已确保 ${WS_YML}:minimumReleaseAgeExclude(${PKG})" \
|
||||
|| say "workspace 设置已就绪,跳过"
|
||||
}
|
||||
|
||||
# 解析用户给的版本 -> CLI 要用的 npm spec("x.y.z" / "^x.y.z" / latest)
|
||||
resolve_spec() {
|
||||
local given="${1:-latest}"
|
||||
case "$given" in
|
||||
latest)
|
||||
local v=""
|
||||
if command -v npm >/dev/null 2>&1; then
|
||||
v="$(npm view "$PKG" version --registry="$REGISTRY" 2>/dev/null)" || v=""
|
||||
fi
|
||||
if [ -z "$v" ] && command -v pnpm >/dev/null 2>&1; then
|
||||
v="$(pnpm view "$PKG" version --registry="$REGISTRY" 2>/dev/null)" || v=""
|
||||
fi
|
||||
if [ -n "$v" ]; then
|
||||
printf '%s' "$v"
|
||||
else
|
||||
warn "无法联网解析最新版本(npm/pnpm 查询失败),回退为 latest,由 pnpm 直接解析。"
|
||||
warn "若已知版本号,可显式传入:bash scripts/install.sh 0.10.2"
|
||||
printf 'latest'
|
||||
fi
|
||||
;;
|
||||
*) printf '%s' "$given" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 组装 dsh CLI 调用:优先 PATH 上的 dsh,缺省 npx 拉官方包
|
||||
dsh_cli() {
|
||||
if command -v "$DSH_CMD" >/dev/null 2>&1; then
|
||||
printf '%s' "$DSH_CMD"
|
||||
elif command -v npx >/dev/null 2>&1; then
|
||||
printf 'npx -y --package @deepseek-ai/dsh@0.1.7-alpha.1 dsh'
|
||||
else
|
||||
die "未找到 dsh 或 npx。请先安装 DSH(并确保 Node/npm 可用),或用 DSH_CMD 指定 dsh 路径。"
|
||||
fi
|
||||
}
|
||||
|
||||
# 前置校验
|
||||
command -v node >/dev/null 2>&1 || die "未找到 node(DSH 运行需要 Node.js ≥ 20),请先安装 Node.js 并加入 PATH。"
|
||||
|
||||
[ -d "$PROFILE_DIR" ] || die "找不到 profile 目录:${PROFILE_DIR}(请先安装并运行过一次 dsh web)"
|
||||
[ -f "$WS_YML" ] || die "找不到 ${WS_YML}(请先初始化 ${PROFILE_NAME} profile)"
|
||||
|
||||
SPEC="$(resolve_spec "$VERSION_SPEC")"
|
||||
CLI="$(dsh_cli)"
|
||||
say "目标:$CLI plugin --profile $PROFILE_NAME add $PKG@${SPEC}(profile: ${PROFILE_DIR})"
|
||||
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
say "[dry-run] 步骤 1:确保 $WS_YML 含 minimumReleaseAgeExclude(${PKG})"
|
||||
say "[dry-run] 步骤 2:执行 $CLI plugin --profile $PROFILE_NAME add $PKG@${SPEC}(安装 + bundle 自动注册)"
|
||||
say "[dry-run] 步骤 3:校验 dsh.profile.bundles 含 $PKG"
|
||||
say "[dry-run] 步骤 4:幂等移除 $PATCH_YML 里旧的 better-sidebar 手动挂载行(避免双挂载)"
|
||||
if [ "$RESTART" = true ]; then say "[dry-run] 步骤 5:pm2 restart dsh-web"; else say "[dry-run] 步骤 5:提示用户手动重启 DSH"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 步骤 1:预写 workspace 设置(幂等),保证 pnpm 不拦截构建、不放行旧版本策略
|
||||
ensure_workspace_settings
|
||||
|
||||
# 步骤 2:官方 CLI 安装 + bundle 自动注册(含挂载)
|
||||
say "执行 $CLI plugin --profile $PROFILE_NAME add $PKG@$SPEC ..."
|
||||
if ! $CLI plugin --profile "$PROFILE_NAME" add "$PKG@$SPEC" 2>&1 | tail -n +1; then
|
||||
warn "dsh plugin add 失败。已预写 minimumReleaseAgeExclude,仍失败的可能原因:"
|
||||
warn " - 网络/登录问题:npm registry 不可达或需要登录。"
|
||||
warn " - 依赖安装冲突:可手动重试 cd $PROFILE_DIR && pnpm install。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 校验 bundle 已注册(挂载生效的判据)
|
||||
if ! node -e '
|
||||
const fs = require("fs");
|
||||
const p = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||||
const bundles = p.dsh?.profile?.bundles ?? [];
|
||||
process.exit(bundles.includes(process.argv[2]) ? 0 : 1);
|
||||
' "$PROFILE_DIR/package.json" "$PKG"; then
|
||||
warn "dsh-apes-sidebar 未出现在 dsh.profile.bundles 中——挂载未注册。"
|
||||
warn "若上面的 pnpm 输出提示 ignored build scripts,请确认 $WS_YML 的 allowBuilds 后重跑本脚本(0.20.0 起本插件自身不再有构建脚本依赖)。"
|
||||
exit 1
|
||||
fi
|
||||
say "bundle 已注册:dsh.profile.bundles 包含 ${PKG}(下次启动自动挂载)"
|
||||
|
||||
# 步骤 3:幂等移除旧的 manual 挂载行(避免与 bundle 双挂载)
|
||||
MOUNT_RESULT="$(node -e '
|
||||
const fs = require("fs");
|
||||
const p = process.argv[1];
|
||||
const lines = fs.readFileSync(p, "utf8").split("\n");
|
||||
const out = [];
|
||||
let i = 0;
|
||||
let removed = false;
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
if (/^[ \t]*- insert:\s*$/.test(line)) {
|
||||
const block = [line];
|
||||
let j = i + 1;
|
||||
while (j < lines.length && lines[j].trim() !== "" && !/^-\s/.test(lines[j])) {
|
||||
block.push(lines[j]);
|
||||
j++;
|
||||
}
|
||||
if (block.some((l) => /id:\s*better-sidebar\b/.test(l))) {
|
||||
while (out.length && /^[ \t]*#/.test(out[out.length - 1])) out.pop();
|
||||
i = j;
|
||||
removed = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(line);
|
||||
i++;
|
||||
}
|
||||
if (!removed) {
|
||||
console.log("none");
|
||||
} else {
|
||||
const t = out.join("\n").replace(/\n{3,}/g, "\n\n");
|
||||
fs.writeFileSync(p, t);
|
||||
console.log("removed");
|
||||
}
|
||||
' "$PATCH_YML")"
|
||||
[ "$MOUNT_RESULT" = "removed" ] \
|
||||
&& say "已从 $PATCH_YML 移除旧的 better-sidebar 手动挂载行(bundle 通道接管挂载)" \
|
||||
|| say "无旧手动挂载行,跳过"
|
||||
|
||||
say "安装完成:$PKG@$SPEC"
|
||||
|
||||
# 步骤 4:重启提示
|
||||
if [ "$RESTART" = true ]; then
|
||||
if command -v pm2 >/dev/null 2>&1; then
|
||||
say "重启 dsh-web(pm2)..."
|
||||
pm2 restart dsh-web || warn "pm2 restart 失败,请手动重启 DSH"
|
||||
else
|
||||
warn "未找到 pm2,请手动重启 DSH(如:pm2 restart dsh-web 或 dsh web)"
|
||||
fi
|
||||
else
|
||||
say "下一步:重启 DSH 并硬刷新(Cmd/Ctrl+Shift+R)使新副本生效。"
|
||||
if command -v pm2 >/dev/null 2>&1; then
|
||||
say "本机可用:pm2 restart dsh-web(会短暂断开当前页面会话)"
|
||||
fi
|
||||
fi
|
||||
@@ -1,61 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Assemble the plugin-registry installable root (`registry/`) from the built
|
||||
* `lib/` output. Run after `pnpm build`:
|
||||
*
|
||||
* pnpm build
|
||||
* node scripts/package-registry.mjs
|
||||
* dsh registry install ./registry
|
||||
* dsh registry enable dsh-external/dsh-apes-sidebar
|
||||
*
|
||||
* The registry install copies the whole source directory (`cp -r`), so
|
||||
* installing the repo root would drag in `node_modules/` (machine-local
|
||||
* `link:` devDependency symlinks) and `.git/`. This
|
||||
* staging directory carries exactly the files the manifest references plus
|
||||
* the docs — same layout as the dsh-subagent-tree `registry/` pattern. It is
|
||||
* gitignored and rebuilt from scratch on every run.
|
||||
*/
|
||||
import { cpSync, existsSync, mkdirSync, rmSync } from 'node:fs'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const root = resolve(fileURLToPath(import.meta.url), '..', '..')
|
||||
const out = join(root, 'registry')
|
||||
|
||||
/** Files copied into registry/, preserving relative paths (lib/ stays lib/). */
|
||||
const files = [
|
||||
'dsh.plugin.json',
|
||||
'lib/index.js',
|
||||
'lib/invariant.js',
|
||||
'lib/client-registry.js',
|
||||
'lib/client-registry.js.map',
|
||||
// Lazy chunks: the client fetches these from the host's /sidebar/bundle
|
||||
// route on first use (see src/client/chunk-loader.ts / src/bundle-route.ts).
|
||||
// EVERY `CHUNK_NAMES` entry belongs here — the route reads the file out of
|
||||
// the installed copy, so a chunk left out of this list 404s on the registry
|
||||
// channel (`locale` was). tests/manifest-consistency.spec.ts guards the list
|
||||
// against src/bundle-route.ts.
|
||||
'lib/client-editor.js',
|
||||
'lib/client-editor.js.map',
|
||||
'lib/client-mermaid.js',
|
||||
'lib/client-mermaid.js.map',
|
||||
'lib/client-locale.js',
|
||||
'lib/client-locale.js.map',
|
||||
'README.md',
|
||||
'LICENSE',
|
||||
]
|
||||
|
||||
rmSync(out, { recursive: true, force: true })
|
||||
mkdirSync(out, { recursive: true })
|
||||
for (const file of files) {
|
||||
const source = join(root, file)
|
||||
if (!existsSync(source)) {
|
||||
console.error(`missing ${file} — run \`pnpm build\` first`)
|
||||
process.exit(1)
|
||||
}
|
||||
const target = join(out, file)
|
||||
mkdirSync(dirname(target), { recursive: true })
|
||||
cpSync(source, target)
|
||||
}
|
||||
console.log(`registry/ assembled (${files.length} files)`)
|
||||
console.log('next: dsh registry install ./registry && dsh registry enable dsh-external/dsh-apes-sidebar')
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user