Compare commits

..
Author SHA1 Message Date
figmar 96cc334143 spike(A): publish apes:profile as an official right-sidebar tab type
Phase A spike of the C8 root fix: instead of only rendering in the fork's
own shell, apes:profile now also registers with DSH's OFFICIAL right
sidebar through its documented two-stage extension protocol:

  1. ctx.sidebarRightTabs.register({ id, kind, priority:'extension',
     title, guide })  — the type declaration
  2. ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register(
     { name:'sidebar.right.pane.tab', key: id }, Body))  — the keyed body

The registry is reached through ctx.inject(['sidebarRightTabs'], ...) (the
same pattern intercept.tsx uses for the async remote.session namespace), so
activation order cannot strand the bridge and a deployment without the
official sidebar stays a silent no-op — the fork's own shell is untouched.

The body reuses the existing ApesProfileView (content is shared, never
duplicated); only the shell-scope binding differs. Guide entry order 70
keeps the apes trio's position in the official entry list.

Verified: typecheck clean; build clean; 1255 passed / 9 skipped (120 files,
6 new bridge specs pinning the two-stage contract, both no-op paths, and the
HMR-safe unwind).

Local commit only — not pushed until the in-app spike is confirmed.
2026-10-03 20:42:13 +08:00
figmar 9ea9e9b9fa fix(c8): match native icon family — outline glyphs at native scale
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s
The apes tab glyphs (智能体/应用/画像) were fill-only paths spanning only
10-12 of the 16 viewBox units, so they rendered visibly smaller and lighter
than the DSH native glyphs (which fill ~92-96% of the box) in the + new-tab
menu and the right-panel tile grid.

Rewrite them in this repo's own outline recipe (src/client/icons.tsx): 16x16
viewBox, artwork on 1.5-14.5, 1.5px currentColor stroke, round caps/joins.
Renamed IconAgentFill16/IconGaugeFill16/IconUserFill16 ->
IconAgentOutline16/IconAppsOutline16/IconUserOutline16.
2026-10-03 20:05:45 +08:00
figmar 2f3944a8bd fix(c8): fill-style icons — visual unity with native tabs
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s
Root cause: DSH's @deepseek-ai/dsh-client-ui-primitives only ships
outline variants (mask+stroke) for agent/gauge/user icons, while native
tabs use fill:currentColor paths (solid, heavy visual weight). This made
the apes tabs look 'lighter' than the official ones in the + menu.

Fix: custom fill-style SVG icons in src/client/apes/icons.tsx
(IconAgentFill16, IconGaugeFill16, IconUserFill16) — matching the
fill:currentColor path style of DSH native tabs (IconCodeOutline16,
IconFolderOpen16, etc.).

Also: order 40/45/50 → 60/65/70 (avoid clash with terminal=40,
browser=50 in the native tab order).
2026-10-03 19:31:00 +08:00
figmar fbd183e93f fix(c8): order 60/65/70 — 不与原生 terminal(40)/browser(50) 冲突
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s
2026-10-03 19:20:20 +08:00
figmar cebc21aef2 fix(c8): sidebar entry unity — DSH Icon*16 + order 40-50
Root cause of the visual split:
1. Icon system mismatch: apes used react-icons (FiCpu/FiGrid/FiUser,
   static 14px ReactNode) while native builtins use DSH's Icon*16
   components (function form, host-controlled size). Two different
   icon libraries → inconsistent visual style.
2. Order 901-903 pushed apes tabs far after native builtins (10-35),
   creating two visually separate groups in the tab strip.

Fix:
- icon: react-icons Fi* → @deepseek-ai/dsh-client-ui-primitives
  Icon*16 (function form, same as builtins):
    agents  → IconAgentPresetOutline16
    apps    → IconGaugeOutline16
    profile → IconUserOutline16
- order: 901/902/903 → 40/45/50 (immediately after native builtins)
- SIDEBAR_SERVICE_VERSION + dsh.plugin.json version → 1.1.0
  (lockstep with package.json, required by manifest tests)

Gate: typecheck ✓ / build ✓ / test 119 files · 1249 passed · 9 skipped · 0 failed ✓
2026-09-16 02:44:35 +08:00
figmar aa28c1fbc7 feat(v1.1.0): adapt to DSH 2.0.10 (0.1.5-rc.2), drop apes:tasks, DSW token styles
C1-C5 env repair:
- align all @deepseek-ai/dsh-* peer+devDeps to 0.1.5-rc.2 (DSH 2.0.10 ships it)
- fix 0.1.5-rc.2 npm packaging defect: 15 primitives runtime deps (anser,
  shiki, katex, marked, ... ) were mis-declared as devDeps in the published
  package -> add to local devDependencies so vitest can resolve them
- fix sidechat-seed contract: dsh-session@0.1.5 assertAssistantSettlementShape
  now requires a top-level data.stream: Array on assistant messages -> seed
  helper emits stream: []

C6 slim tabs:
- remove apes:tasks (overlaps the official DSH session tree in the native
  sidebar); keep apes:agents / apes:apps / apes:profile
- clean dead code: ApesTasksView + job helpers import + apes i18n dead keys
  (tasks/status/duration) and the unused menu-title keys

C7 style align:
- apes view inline colors -> DSW alias tokens (--dsw-alias-bg/border/label/
  state-success/interactive) with literal fallbacks; geometry (font/spacing/
  radius) stays inline (DSH exposes no --ds-* geometry tokens to plugin panels)

gate: typecheck ok / build ok / test 119 files · 1249 passed
2026-09-16 01:57:46 +08:00
figmar 34e86141bd feat(scripts): switch.sh — apes↔upstream 两线对称切换
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s
- 同一 profile 内二选一(双装不可行: 同 cordis entry id 冲突)
- 探测当前线 → 移除对手线依赖+bundle → 写目标线 → pnpm install → 校验 bundle
- 幂等可重复跑; --status 只读; --dry-run 不写文件; --profile 指定 profile
- aps 线用 file: 本地源码, upstream 线用 registry latest/指定版本
- 修复 pnpm spec 双 @ bug (值不含包名, pnpm 从 key 取包名)
- 双向真实切换验证通过 (aps→upstream→aps, 临时 profile 副本, EXIT=0)
- package.json 加 switch/switch:status/switch:aps/switch:upstream 入口
- switch.sh 加入 files (发布时带上)
2026-09-07 02:25:15 +08:00
figmar c0441d3a7e docs: README 补 v1.0.0 fork 版本更新说明 (4 个 apes tab + 数据接入)
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s
2026-09-07 02:02:55 +08:00
figmar 427266a191 feat(apes): Phase 2 数据接入 — 4 个信息层 tab 接真实会话数据
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s
- apes:tasks  接 jobsBySession + running sessions(live 任务列表:
  label/status/kind/耗时 + owner 归属,复用 collectTreeJobs/orderJobs)
- apes:agents 接 subagent 拓扑(rootAncestor + subagentsByParent 目录,
  parent→children activity running/inactive, 降级用 byId origin 过滤)
- apes:profile 接 byId[current](当前会话画像: title/cwd/running + agent 状态)
- apes:apps    展示 cwd/workspace 信息(无 DSH host feed, 保留说明)

数据源: ctx.sessions.list (useSyncExternalStore, uSES 安全订阅)
复用 pure helper: subagent-lineage.ts (rootAncestor/isSideThreadSummary)
  + subagent-jobs.ts (collectTreeJobs/orderJobs/isJobLive/jobDotState)
UI 独立内联样式, 不耦合 builtin CSS module / StateDot (保持 apes/ 隔离)

locales: 加 status/duration/empty/workspace 文案 (zh+en, 含 {param} 插值)
gate: 1250 passed | typecheck exit 0 | tsdown build complete
2026-09-07 01:45:39 +08:00
figmar a7bcdef21b feat(apes): Phase 1b — 补 apes:apps 信息层 tab (4 tab 齐活)
CI / ci (push) Canceled after 0s
CI / ci-windows (push) Canceled after 0s
CI / plugin-mount (push) Canceled after 0s
- tabs.tsx: +FiGrid icon, +AppsView placeholder, +apes:apps descriptor (order 902, profile 推到 903)
- locales.ts: +apesApps/apesAppsTitle/apesAppsHint zh/en 4 key
- tests: 3→4 tab 断言 (id 列表/contract/order 严格递增/disposer/i18n zh+en)

4 个 apes:* tab: tasks(900) / agents(901) / apps(902) / profile(903)
gate: typecheck + build + test 1250 passed / 9 skipped / 0 failed (无回归)
2026-09-06 21:32:49 +08:00
figmar 257b8b4eea feat(apes): Phase 1 — 注册 3 个 apes:* 信息层 tab (tasks/agents/profile)
- 独立 src/client/apes/ 模块 (D1=B), index.tsx 仅 3 处最小改动
- 3 个静态占位 tab (react-icons FiList/FiCpu/FiUser), order 900+ 排在上游 builtins 之后
- 隔离 i18n 命名空间 'apes' (不碰上游 betterSidebar 字典), zh/en 双语
- index.tsx: attachApesLocale + 字典注册 + registerApesTabs effect
- tests/apes-tabs.spec.ts: 9 测试 (注册契约 + disposer + i18n zh/en/兜底)

gate: typecheck + build + test 1250 passed / 9 skipped / 0 failed

apps tab 延后 Phase 1b, messages 已砍 (无 DSH message-stream API)
2026-09-06 21:10:13 +08:00
figmar 2a7e030b3b chore: fork dsh-better-sidebar v0.18.0 as dsh-apes-sidebar v1.0.0
- Rename dsh-better-sidebar → dsh-apes-sidebar across 74 files (262 occurrences)
- Bump version 0.18.0 → 1.0.0 (package.json + dsh.plugin.json + SIDEBAR_SERVICE_VERSION)
- Add forkedFrom metadata to package.json (upstream pin: commit 9e1a034)
- Add fork notice banner to README.md (MIT attribution preserved)
- Keep original MIT copyright (c) 2026 dsh-external
- Full gate green: typecheck + build (6 bundles) + test (1241 passed / 9 skipped)
2026-09-06 18:09:09 +08:00
224 changed files with 9986 additions and 12092 deletions
+3 -3
View File
@@ -16,7 +16,7 @@ body:
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-better-sidebar / I confirmed I am on the latest dsh-better-sidebar version
- label: 我已确认使用最新版本 dsh-apes-sidebar / I confirmed I am on the latest dsh-apes-sidebar version
required: false
- type: dropdown
@@ -58,8 +58,8 @@ body:
id: plugin-version
attributes:
label: 插件版本 / Plugin version
description: dsh-better-sidebar 的版本号 | The dsh-better-sidebar version
placeholder: "如 0.13.1(`dsh plugin --profile web add dsh-better-sidebar@latest` 安装的版本)"
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
+5 -9
View File
@@ -35,14 +35,12 @@ jobs:
# config; the runner has none by default.
- name: Configure git identity for tests
run: |
git config --global user.name "dsh-better-sidebar-ci"
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
@@ -84,14 +82,12 @@ jobs:
# identity and rely on ambient config, which the runner lacks.
- name: Configure git identity for tests
run: |
git config --global user.name "dsh-better-sidebar-ci"
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
@@ -131,10 +127,10 @@ jobs:
# 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.
# DSH 0.1.5-alpha.2 is published under the `alpha` dist-tag; the lanes
# DSH 0.1.2-rc.1 is published under the `next` dist-tag; the lanes
# speak the slash-only RPC dialect (tests/e2e/host-protocol.ts).
- name: Install dsh CLI (pinned)
run: npm install -g @deepseek-ai/dsh@0.1.5-alpha.2
run: npm install -g @deepseek-ai/dsh@0.1.2-rc.1
# lib/ is gitignored; the tarball is produced from this build.
- name: Build and pack the npm tarball
@@ -147,7 +143,7 @@ jobs:
run: pnpm test:mount
# Aggregate double-mount regression: a fixture aggregate bundle mounts
# dsh-better-sidebar under its own entry id BEFORE the plugin's bundle
# 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
+5 -9
View File
@@ -1,6 +1,6 @@
name: Release
# Publishes `dsh-better-sidebar` to npm via npm Trusted Publishing (OIDC) —
# 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
@@ -12,7 +12,7 @@ name: Release
# supports the stable DSH line); stable versions publish as `latest`.
#
# One-time npm-side setup (manual, on npmjs.com):
# 1. package `dsh-better-sidebar` -> Settings -> Publishing access
# 1. package `dsh-apes-sidebar` -> Settings -> Publishing access
# -> Trusted Publishers -> "Add Trusted Publisher".
# 2. Provider: GitHub Actions
# Organization or user: omdsh-dev
@@ -66,7 +66,7 @@ jobs:
# (see tests/smoke.spec.ts); the runner has none by default.
- name: Configure git identity for tests
run: |
git config --global user.name "dsh-better-sidebar-ci"
git config --global user.name "dsh-apes-sidebar-ci"
git config --global user.email "ci@dsh.invalid"
- name: Install dependencies
@@ -88,9 +88,6 @@ jobs:
- name: Typecheck
run: pnpm typecheck
- name: Lint
run: pnpm lint
- name: Test
run: pnpm test
@@ -98,9 +95,8 @@ jobs:
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.
# verification order (typecheck → 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
+10 -11
View File
@@ -1,7 +1,7 @@
# dsh-better-sidebar 仓库规则(AGENTS)
> 本文只含**项目全局开发规则**(面向贡献者与 agent)。
> 消费插件接入 API 全参考(`ctx.betterSidebar` 服务、TabDescriptor / FileViewerDescriptor 全字段、声明式设置、原生右侧栏承载面、皮肤契约等)→ [docs/external-plugin-guide.md](docs/external-plugin-guide.md);逐特性设计史(含实施偏差记录)→ [docs/plans/](docs/plans/)。
> 消费插件接入 API 全参考(`ctx.betterSidebar` 服务、TabDescriptor / FileViewerDescriptor 全字段、声明式设置、自由窗口、皮肤契约等)→ [docs/external-plugin-guide.md](docs/external-plugin-guide.md);逐特性设计史(含实施偏差记录)→ [docs/plans/](docs/plans/)。
---
@@ -17,29 +17,28 @@
## 2. CI 挂载冒烟(`plugin-mount` job / `pnpm test:mount`)
「npm 打包 → 真实挂载 → 无头渲染」门禁(证明打包产物在真实 DSH 挂载后不 crash):`pnpm build && pnpm pack` 产 tarball → `scripts/e2e-mount.sh` 装进全新 scratch profile(`dsh plugin --profile web add <tarball>`)并启动真实 `dsh web`(keyless,`--port 0`)→ `tests/e2e/mount.e2e.ts`(Playwright)断言 `[data-dsh-better-sidebar]` 挂载、无错误条/pageerror/console 错误,展开 DSH 原生右侧栏后经其 guide 页逐个打开插件 tab 类型(含终端懒加载 chunk),再经插件文件树(原生 `files` kind 接管)打开 seed 文件强制加载 editor chunk(`client-editor.js`),并跑 mermaid / README 预览与 sidechat 宿主路由烟测。
「npm 打包 → 真实挂载 → 无头渲染」门禁(证明打包产物在真实 DSH 挂载后不 crash):`pnpm build && pnpm pack` 产 tarball → `scripts/e2e-mount.sh` 装进全新 scratch profile(`dsh plugin --profile web add file:<tarball>`)并启动真实 `dsh web`(keyless,`--port 0`)→ `tests/e2e/mount.e2e.ts`(Playwright)断言 `[data-dsh-better-sidebar]` 挂载、无错误条/pageerror/console 错误,经「+ 菜单」逐个打开内置 tab(含终端懒加载 chunk),再经文件树打开 seed 文件强制加载 editor chunk(`client-editor.js`)。
本地:`pnpm build && pnpm pack && pnpm exec playwright install chromium && pnpm test:mount`。CI 钉 `@deepseek-ai/dsh@0.1.5-alpha.2`(`alpha` dist-tag;peer 下限 `^0.1.5-alpha.2`)。e2e spec 命名 `*.e2e.ts` + vitest `exclude` 双保险;**改 `exclude` 必须保留默认排除项**(exclude 整体替换默认值)。
本地:`pnpm build && pnpm pack && pnpm exec playwright install chromium && pnpm test:mount`。CI 钉 `@deepseek-ai/dsh@0.1.2-rc.1`(`next` dist-tag;peer 下限 `^0.1.2-rc.1`)。e2e spec 命名 `*.e2e.ts` + vitest `exclude` 双保险;**改 `exclude` 必须保留默认排除项**(exclude 整体替换默认值)。
---
## 3. DSH 0.1.5 适配要点(0.1.5-alpha.2+ 基线)
## 3. DSH 0.1.2 适配要点(0.1.2-rc.1+ 基线)
**v0.19.0-alpha.1 起仅支持 DSH 0.1.5-alpha.2+**(peer 下限 `^0.1.5-alpha.2`,CI 钉 `@deepseek-ai/dsh@0.1.5-alpha.2`,DSH alpha 线发在 `alpha` dist-tag)。alpha.1 用户停留在 v0.19.0-alpha.0——alpha.2 删除了指南条目的 `description` 字段并改了文件地址语法,插件不再对 alpha.1 做运行时兼容。0.1.2-rc.1 稳定线用户继续用 v0.18.x(npm `latest`)——0.1.5 的会话事件模型与文件打开漏斗都变了,插件不再对 0.1.2 做运行时兼容。发版:release.yml 按版本号是否含 `-` 自动选 `alpha`/`latest` dist-tag。
**v0.18.0 起插件毕业正式版(npm `latest`),仅支持 DSH 0.1.2-rc.1+**(peer 下限 `^0.1.2-rc.1`,CI 钉 `@deepseek-ai/dsh@0.1.2-rc.1`,DSH rc.x 发在 `next` dist-tag)。v0.18.0-alpha.0 进入 alpha 通道时已删除对 0.1.0-rc.8 ~ 0.1.1-rc.2 的兼容层——双方言 RPC(点分回退)、MarkdownText 双形状 labels、`dsh-client-runtime` externals/inject 残留行;stable DSH 用户请固定装 v0.17.1(npm 上的旧 latest,0.1.2-alpha.x 宿主继续用 npm `alpha` 的 v0.18.0-alpha.0)。发版:release.yml 按版本号是否含 `-` 自动选 `alpha`/`latest` dist-tag。
宿主契约(均经真机挂载冒烟 14/14 验证):
1. **一次性 token 鉴权**:就绪行 `dsh web: http://127.0.0.1:<port>/?token=<43字符>`(导航换签名 cookie,干净 URL 401)。`e2e-mount.sh` 的 URL grep 必须延伸到空白(`[^ ]*`,在 `/` 截断丢 token);e2e 统一走 `tests/e2e/host.ts`(token 必选:`parseLaunchUrl` 对裸 origin 直接抛错),带 stamps 导航走 `gotoPage()`(先 addCookies 再直达——token 换 cookie 的 303 会丢弃同 URL 其它 query 参数)。插件 `/sidebar/*` 路由不受影响,同源 fetch 照旧。
2. **Remote gateway 斜杠 RPC(唯一方言)**:`POST /api/workspace/create`,payload 恰为 `{args: {...}}`,**args 按控制器 TS 参数名包装**(`workspace/create`、`session/create` → `{args:{request:{...}}}`;`session/list` 参数名 `_request` **不可省略**——`{}` 也被拒 `args fields do not match the descriptor`);envelope `method` 与路径一致,点分路径 404。请求由 `tests/e2e/host-protocol.ts` 的 `rpcAttempt` 构造、`tests/e2e-host-protocol.spec.ts` 锁定;要调新方法先在真机验参数名再进 `RPC_ARGS_KEY`。
3. **`MarkdownText` labels 嵌套契约**:必填 `labels: { code: { copyLabel, copiedLabel }, footnotes }`(漏传回退硬编码中文)。四个渲染点(mermaid.tsx / MarkdownHtml.tsx / TextEditor.tsx / SideChatView.tsx)统一走 `src/client/markdown-labels.tsx` 的 `markdownTextProps()`。
4. **侧边对话转录走自有路由,不碰客户端宿主 RPC**:`ctx.connection.api`(含 `sessions.history`)在 alpha.1 整体移除,继任 `session/follow|page` 又对 `origin:'subagent'` 会话强制 subagent 地址(普通 `{kind:'session'}` 被 `agent-busy` 拒)且分页 `throughSeq` 不得超当前游标。转录因此由 **`sidechat.events`** 插件路由供给(`src/sidechat-routes.ts`:live 读 `agent.session.snapshotEvents()`、冷读 `sessionPersistence.inspect`,服务端 `session/end-seed` 切割 + `afterSeq` 增量);`ctx.connection` 镜像与 inject 已删。**0.1.5 起实时增量不在日志里**:`assistant/chunk` 事件被删除,进行中的模型增量改由 `agent/assistant-stream` 瞬时帧发布(`start` / `chunk` / `end`,不入会话日志),结算时才落 `assistant/message`(内嵌 `stream`)或 `assistant/attempt`(失败尝试,内嵌 `stream`)。插件在宿主侧 `src/assistant-live.ts` 折叠这些帧成有界缓冲,`sidechat.events` 的 `live` 字段按「当前 attempt 全量、每次轮询替换」下发(不是增量),转录映射与继承快照都读它。设计见 [docs/plans/2026-08-20-sidechat-tab-design.md](docs/plans/2026-08-20-sidechat-tab-design.md) §10。
4. **侧边对话转录走自有路由,不碰客户端宿主 RPC**:`ctx.connection.api`(含 `sessions.history`)在 alpha.1 整体移除,继任 `session/follow|page` 又对 `origin:'subagent'` 会话强制 subagent 地址(普通 `{kind:'session'}` 被 `agent-busy` 拒)且分页 `throughSeq` 不得超当前游标。转录因此由 **`sidechat.events`** 插件路由供给(`src/sidechat-routes.ts`:live 读 `agent.session.snapshotEvents()`(0.1.2-alpha.4 起的按需读 API,前身 `Session.events` 属性已删)、冷读 `sessionPersistence.inspect`,服务端 `session/end-seed` 切割 + `afterSeq` 增量);`ctx.connection` 镜像与 inject 已删。设计见 [docs/plans/2026-08-20-sidechat-tab-design.md](docs/plans/2026-08-20-sidechat-tab-design.md) §10。
5. **`dsh-settings` 无运行时 `settingsNamespace`**:命名空间合法性校验转为编译期模板字面量 `SettingsNamespaceInput`(小写字母开头 + `[a-z0-9-]` 尾部),`'dsh-better-sidebar'` 字面量直接过——宿主侧直接传常量(`src/index.ts` 的 settings inject)。
6. **`dsh-subagent` 的 `SUBAGENT_DESCRIPTOR_VERSION` 2 → 3**:sidechat 种子的 `subagent/descriptor` 版本由宿主包盖章,插件不硬编码;测试断言跟随常量(`tests/sidechat-routes.spec.ts`),勿钉字面量。
7. **`@deepseek-ai/dsh-client-runtime` 包已消亡**(继任 seed 是裸名 `dsh-client-store`,无 `/client` 子路径):peerDependencies、devDependencies、`dsh.client.inject`、chunk externals 白名单(`src/client/chunk-loader.ts` / `tsdown.config.ts` / `tests/chunk-loader.spec.ts` / `tests/manifest-consistency.spec.ts` 四处同步)均已无该条目。
8. **e2e scratch profile 的 `minimumReleaseAgeExclude` 含 `'@deepseek-ai/*'`**(`scripts/e2e-mount.sh` / `e2e-aggregate-mount.sh`,与仓库根 `pnpm-workspace.yaml` 同策):alpha 版本常在发布后 24h 内跑 lane,pnpm 11 的 `minimumReleaseAge` 默认会拒装新鲜包。
9. **插件开发树的 dsh-* 传递 peer 需提升为 devDependencies**(alpha.3 首见):`dsh-subagent` 等 npm 包把 `dsh-attachment` 等 dsh-* 姊妹包全部声明为 peerDependencies(由宿主 bundle 树统一提供,宿主侧无此问题),插件仓库若只直接依赖其中一部分,其余 peer 在 pnpm 下会解析到树上残留的旧版——如 `dsh-attachment@0.1.1-rc.1` 缺 `admitPromptContent` 导出、`dsh-subagent` 产物 import 它时测试加载即崩。因此 devDependencies 需涵盖 dev 树实际触达的全部 peer(attachment / code-runtime / scope / session-projection / system-prompt / user-approval / util-time 七个即为此提升,与直接依赖同款精确钉版);`@deepseek-ai/cordis` peer 自 alpha.3 起要求 `^4.0.2`(上游全线 peer 已升)。适配新 alpha 版本时先跑 `pnpm peers check`,把新失配的传递 peer 一并提升进 devDependencies。例外:**`dsh-client-locale` 的 peer/devDep 允许落后于基线**(alpha.5 时上游停在 0.1.2-alpha.3 未发新版)——peer 下限 `^0.1.2-alpha.3` 天然容纳同 tuple 的 alpha.5 运行时,此时保持旧钉版并在 `pnpm-workspace.yaml` 注明,待上游发版再追平;rc.1 起上游恢复发版,peer/devDep 已与基线一并钉 `0.1.2-rc.1`,该例外消除。**alpha.2 起还有一类:上游包自身的 `dependencies` 丢失**——`@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 树不会——vitest 一碰 primitives 就 `Cannot find package 'anser'`。此时把 alpha.1 声明的同一组版本提升进 devDependencies(`clsx` 已在 dependencies 无需重复)。
10. **右侧栏是 DSH 原生栏,插件只提供 tab 类型**:聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)统一走 `ctx.sidebarRight.openResource(fileAddressFor(sessionId, cwd, path))`(`packages/client/ui-chat/src/client/apply.ts` 是唯一调用点),`remote.session.openWorkspacePath` 在 0.1.5 客户端已无调用者,插件的 openpath 拦截随之删除。插件的每个 `TabDescriptor` 注册成原生 tab 类型(`kind = descriptor.id`,`extension` 带)+ 原生 tab 体(`sidebar.right.pane.tab` keyed 槽,key = `dsh-better-sidebar:<id>`);`editor` 类型同时认领 `dsh-resource://file/**`(压过内置 `text` 的 `fallback`),并接管内置 `files` 页面 kind(`openTab('files')` 打开插件文件树,注销即复位)。`ctx.sidebarRight.openTab/openResource/close` 只对**在屏会话**写入;跨会话用具体类上的 `openTabIn/openResourceIn/closeIn`(不在 `ISidebarRight` 接口里,需结构化探测),目标会话未挂载时排队到上屏重放(`src/client/native/surface.ts`)。原生 tab 的插件侧状态(合成 `SidebarTab`、树展开集合、实例编号)在 `src/client/native/tab-adapter.tsx`;原生布局只在内存,不做持久化。**注册必须等服务、不能等槽声明**:原生栏先声明 `sidebar.right.pane.tab` 再 `provide('sidebarRightTabs')`,真机 profile(web,0.1.5-alpha.1)上槽声明回调里 `ctx.get('sidebarRightTabs')` 仍是 undefined(实测 3 秒后才出现),按槽触发注册会静默什么都不注册且永不重试——用 `ctx.inject(['sidebarRightTabs'], cb)` 驱动类型注册(`tests/native-surface.spec.ts` 以「槽先触发、服务后到达」的顺序守护),槽声明只用来挂 tab 体。指南 §0 列了全部行为差异。**alpha.2 起(v0.19.0-alpha.1)**:指南条目删除 `description`(原生指南渲染成「图标+标题」胶囊),插件随之下线 `TabDescriptor.description` 与 6 个 `guideDesc*` 词条;新建标签页的默认页改为从注册表选(恰好 1 个指南条目 → 直接开它,0 或 ≥2 → 开指南),`revealIfOpened` 对「页面」在同一 pane 内强制去重。原生 tab 体宿主 `.paneBody` 是**有确定高度的块级滚动容器**(非 flex 容器),native 适配层因此给每个 tab 体包一层 `height:100%` 的列 flex 宿主(`sidebar.module.css` 的 `.nativeTabHost`),tab 组件根继续用 `flex:1`/`height:100%`——否则根盒塌成内容高度,sidechat 的输入框就贴不到面板底。全局面板(根级 `main` keyed 槽 + `sidebar.panellist` + `ctx.layout.selectPanel/beginNavigation`、`rightbar` 改根级并新增 `rightbar.session`)插件**不接入**,只做兼容。底部工作台的中心列锚点改认 `[data-slot="main.conversation"]`(并跳过 `display: contents` 祖先,同时保留 alpha.1 的 `[data-slot="conversation"]`);文件地址语法跟随 alpha.2(`fileAddressFor` 一律 session 作用域、绝对路径保留前导 `/`、`parseFileAddress` 前缀解析并去 `?`/`#`)。
11. **自定义种子必须带 fork 标记对(`meta.isSeeded: true` + `inheritedEventCount`)**:dsh-session 契约「只给 seed 不标 isSeeded,种子算重放历史而非继承前缀」——缺标记时 `Session.ownEvents()` 含整个种子,子会话的 inbox 折(`inboxProjectionDefinition`,0.1.5 起 `@deepseek-ai/dsh-agent` 只导出 `Inbox` 接口,实现是该投影)重放种子里的 `agent/inbox/spliced`,**继承父会话切割时刻未领取的 inbox 输入**(排队用户消息 next-turn、长回合中工具结果上下文/steering next-step——后者即「上下文很长时侧边对话先把之前的 User msg 发出去」的根因,幽灵消息排在 boundary 之前最先发给模型)。宿主 `session.fork`(api-session-controller)的调用即规范形态;回归由 `tests/sidechat-seed-validation.spec.ts`(真实 `Session.create` + 对 `ownEvents()` 跑 inbox 折)守护。0.1.5 复评:无更优雅的 sidechat 宿主 API(`ContinuableStartSpec` 仍无 seed 字段),`AgentRegistry.create` 接缝补齐标记即为规范用法;`Session.create` 的 header `version` 必须用 `SESSION_FORMAT_VERSION`(0.1.5 是 `3` 字面量类型),勿钉 `0`。
9. **插件开发树的 dsh-* 传递 peer 需提升为 devDependencies**(alpha.3 首见):`dsh-subagent` 等 npm 包把 `dsh-attachment` 等 dsh-* 姊妹包全部声明为 peerDependencies(由宿主 bundle 树统一提供,宿主侧无此问题),插件仓库若只直接依赖其中一部分,其余 peer 在 pnpm 下会解析到树上残留的旧版——如 `dsh-attachment@0.1.1-rc.1` 缺 `admitPromptContent` 导出、`dsh-subagent` 产物 import 它时测试加载即崩。因此 devDependencies 需涵盖 dev 树实际触达的全部 peer(attachment / code-runtime / scope / session-projection / system-prompt / user-approval / util-time 七个即为此提升,与直接依赖同款精确钉版);`@deepseek-ai/cordis` peer 自 alpha.3 起要求 `^4.0.2`(上游全线 peer 已升)。适配新 alpha 版本时先跑 `pnpm peers check`,把新失配的传递 peer 一并提升进 devDependencies。例外:**`dsh-client-locale` 的 peer/devDep 允许落后于基线**(alpha.5 时上游停在 0.1.2-alpha.3 未发新版)——peer 下限 `^0.1.2-alpha.3` 天然容纳同 tuple 的 alpha.5 运行时,此时保持旧钉版并在 `pnpm-workspace.yaml` 注明,待上游发版再追平;rc.1 起上游恢复发版,peer/devDep 已与基线一并钉 `0.1.2-rc.1`,该例外消除。
10. **聊天文件打开漏斗是 `remote.session.openWorkspacePath`**(`ctx.workspaces.openPath` 已随 alpha 删除,`IWorkspaces` 无此方法):「聊天区文件在侧边栏打开」拦截在 `ctx.inject(['remote.session'], …)` 内以 **defineProperty 数据属性遮蔽**该命名空间方法(gateway client 的方法是 accessor 属性、异步挂载、contribution 重挂载时服务重建——inject 回调重跑即自愈);实现见 `src/client/openpath-intercept.ts`,设计见 [docs/plans/2026-08-31-openpath-intercept-alpha-design.md](docs/plans/2026-08-31-openpath-intercept-alpha-design.md)。
---
@@ -67,6 +66,6 @@
## 6. 文档与测试地图
- **接入 API 全参考**:[docs/external-plugin-guide.md](docs/external-plugin-guide.md)(消费插件开发者向;§0 原生栏承载面 / §4 Tab API / §5 FileViewer API / §7 服务方法 / §10 平台陷阱 / §11 已移除的自由窗口 / §12 皮肤契约 / §15 真实案例)。
- **接入 API 全参考**:[docs/external-plugin-guide.md](docs/external-plugin-guide.md)(消费插件开发者向;§4 Tab API / §5 FileViewer API / §7 服务方法 / §10 平台陷阱 / §11 自由窗口 / §12 皮肤契约 / §15 真实案例)。
- **设计文档**:[docs/plans/](docs/plans/)(30+ 份逐特性设计,含实施偏差记录)。
- **关键测试守护**:`tests/service.spec.ts` / `builtins.spec.ts`(注册表与内置清单:7 tab + 6 viewer)/ `market-manifest.spec.ts`(市场约束)/ `e2e-host-protocol.spec.ts`(RPC 双协议)/ `native-surface.spec.ts`(原生右侧栏承载面)/ `theme.spec.ts`(皮肤契约)/ `plugin-list.spec.ts`(推荐插件目录)/ `fs-search.spec.ts`(host 文件名搜索)。
- **关键测试守护**:`tests/service.spec.ts` / `builtins.spec.ts`(注册表与内置清单:7 tab + 6 viewer)/ `market-manifest.spec.ts`(市场约束)/ `e2e-host-protocol.spec.ts`(RPC 双协议)/ `free-window.spec.tsx`(自由窗口)/ `theme.spec.ts`(皮肤契约)/ `plugin-list.spec.ts`(推荐插件目录)/ `fs-search.spec.ts`(host 文件名搜索)。
+4 -6
View File
@@ -1,6 +1,7 @@
# dsh-better-sidebar 命令面薄封装:目标仅转发 package.json scripts(唯一事实源),
# 新增/改名 script 不需要动这里;本文件只补目标发现(help)与 CI 门禁聚合
# (check / mount / mount-aggregate / registry)。
# 注:lint 目标留给引入 ESLint 的后续 PR。
.DEFAULT_GOAL := help
@@ -16,14 +17,11 @@ build: ## 构建(清 lib/ → tsc → tsdown)
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
check: ## 聚合校验门禁:typecheck → build → test → check:consumer-types(对齐 CI)
pnpm typecheck && pnpm build && pnpm test && pnpm check:consumer-types
clean: ## 清理构建产物与测试报告(lib/、*.tgz、playwright-report/、test-results/)
rm -rf lib playwright-report test-results
@@ -41,4 +39,4 @@ mount-aggregate: ## 聚合双挂载回归:build + pack → pnpm test:mount:agg
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
.PHONY: help install build typecheck test check clean pack mount mount-aggregate registry
+48 -56
View File
@@ -1,4 +1,6 @@
# dsh-better-sidebar
# dsh-apes-sidebar
> **Fork notice** — This project is a fork of [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) v0.18.0 (commit `9e1a034`), distributed under the MIT License. Original copyright (c) 2026 dsh-external. This fork adds the `apes:*` information-layer tabs (tasks / agents / apps / messages / profile) on top of the upstream sidebar. See `package.json → forkedFrom` for the exact upstream pin.
<!-- Hero -->
<div align="center">
@@ -9,7 +11,7 @@
<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.18.1 正式版):0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a>
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本(v0.18.0 正式版):0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a>
<a href="https://github.com/topics/dsh-better-sidebar"><img alt="插件生态:GitHub topic dsh-better-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" /> <img alt="插件接入" src="https://img.shields.io/badge/-插件接入-4d6bfe" /><br /><br />
<b>右侧栏 + 底部面板双工作台</b>,并把 <code>ctx.betterSidebar</code> 服务开放给所有插件——<br />
@@ -44,10 +46,11 @@
- **🌐 内嵌浏览器**:多开网页 tab,后退 / 前进 / 刷新;内容运行在沙箱 iframe;外链默认按协议分流——HTTP 在侧边栏打开、HTTPS 走系统浏览器(设置页可分别调整)
- **💻 真实终端**:xterm.js + node-pty 真实 shell,断线重连回放;可选为模型注入 `terminal_*` 工具
- **📂 模型侧边栏打开(可选)**:全局设置开启后注入 `sidebar_open` 工具——模型可主动在侧边栏打开文件 / 文件夹(树以该目录为根)/ HTTP(S) 网页
- **🌿 文件变动**:Git 视角(真 diff / 历史 / 暂存·提交·还原 / worktree·子仓库选择)与本轮文件视角(模型读 / 写 / 编辑实时追踪,按文件分组、按类型筛选)**双视角合一**;统一 diff 渲染(改蓝配对 + 行内字符级高亮 + 语法着色(含 mjs/cjs/mts/cts、CSS/SCSS/Less、HTML/XML/SVG/Vue、GraphQL、JSONC/JSON5)+ 上下文折叠),底部可拖拽预览面板,可一键展开为独立 diff tab(落进工作台的 diff 分栏);`.md` 操作(读 / 写 / 编辑)预览头部可切换**阅读模式**——经共享 MarkdownText 渲染 GFM 表格 / 任务列表 / 删除线 / 脚注 / 数学公式,本地图片自动改写为 `/sidebar/file` 媒体路由;含 ```mermaid 围栏时走编辑器同款懒加载 mermaid 渲染器(图可点击缩放 / 平移);**敏感内容脱敏**——凭据形态路径整文件遮罩、普通文件按内容形态遮值(api_key: / Bearer / sk- / AKIA / ghp_ / PEM 等,字段名保留),默认开启、预览面板一键开关(localStorage 记忆),仅影响显示、不改会话数据。已知边界:mermaid 无引号节点标签含被遮密钥时,图回退源码(规避:标签加引号);`.html` 操作(读 / 写 / 编辑)预览头部可切换**渲染模式**——复用编辑器同款 `/sidebar/html` 路由 iframe,相对资源(./style.css、img/x.png)同路由解析,分段读取也渲染完整文档,恒定沙箱(opaque origin + CSP 头,无逃生门);`.pdf` 操作(读 / 写 / 编辑)同样可切换**渲染模式**——复用编辑器同款 PDF 预览(媒体路由字节流 + 显式 Blob,浏览器原生查看器内嵌,附下载入口)
- **🌿 文件变动**:Git 视角(真 diff / 历史 / 暂存·提交·还原 / worktree·子仓库选择)与本轮文件视角(模型读 / 写 / 编辑实时追踪,按文件分组、按类型筛选)**双视角合一**;统一 diff 渲染(改蓝配对 + 行内字符级高亮 + 语法着色 + 上下文折叠),底部可拖拽预览面板,可一键展开为独立 diff tab(默认自由浮窗,可在设置改为面板下半 split)
- **🧩 后台任务页**:subagent 拓扑 + 后台任务(退出码 / 实时输出 / 强制终止)
- **💬 侧边对话(beta)**:Codex 风格的侧边线程——继承主会话完整上下文(含进行中的回合与工具调用)独立运行,不进入主会话;线程内可持续追问,一键「保存为新会话」提升为顶层会话
- **🖥️ 原生右侧栏 + 底部工作台**:右列交给 DSH 0.1.5 的原生右侧栏——插件把每个 tab 类型注册成原生 tab(文件打开走 `dsh-resource://file/**`,并接管内置「文件」页 / 文件树),插件自己只保留底部工作台(分栏 / 终端 / 随会话持久化),开合按钮挂在会话头右侧
- **🪟 双工作台**:右侧栏 + 底部面板;拖 Tab 拆分 / 合并分栏(可跨面板),移动端自动合并全宽抽屉
- **🪟 自由窗口**:把标签栏的任一 tab 拖到主会话区域——成为可移动 / 缩放 / 置顶的悬浮窗口(默认 390×780),拖回侧边栏 pane 即停靠,随会话持久化;`features` 含 `'floatWindows'`,插件 tab 无差别支持
- **📌 固定终端**:右键终端 Tab 可「固定到工作区 / 固定到全局」——固定后切换会话不消失,在 TabBar 内联呈现(跨会话虚拟 Tab,点击就地激活,PTY 按 home 会话 id+tab 直连宿主 PTY,无需切回宿主会话);Agent 终端被 reconcile 移除时豁免保留
- **🔁 会话隔离**:布局 / Tab / 面板按会话持久化,陈旧状态自动净化
- **⚙️ 声明式设置**:设置页「侧边卡片」逐项独立开关,二级设置经齿轮弹窗
@@ -61,7 +64,7 @@
**前置**:已装好 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.18.1 正式版):0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a>
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本(v0.18.0 正式版):0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a>
> 📌 **正式版**:`v0.18.0` 起适配 DSH **0.1.2-rc.1+**(npm dist-tag `latest`),不再支持 0.1.0-rc.8 ~ 0.1.1-rc.2——DSH stable(≤ 0.1.1-rc.2)用户请固定安装 `dsh-better-sidebar@0.17.1`(`@latest` 已由 v0.18.0 接管);停留在 0.1.2-alpha.x 的宿主请先升级 DSH,或继续用 `dsh-better-sidebar@alpha`(v0.18.0-alpha.0)。
@@ -165,7 +168,7 @@ dsh registry enable dsh-external/dsh-better-sidebar
| **🖥️ CodeMirror 代码编辑器**<br/><div align="center"><img width="420" alt="CodeMirror 代码编辑器" src="https://github.com/user-attachments/assets/b44b488e-568c-4ee0-b96c-e9c906598a77" /></div> | **🖼️ 图片内联预览**<br/><div align="center"><img width="420" alt="图片内联预览" src="https://github.com/user-attachments/assets/f9a58c30-5b7a-48b5-9e22-37d7e071f593" /></div> |
| **💻 真实终端**<br/><sub>xterm.js + node-pty 真实 shell(不是模拟器):断线重连 transcript 回放、shell / shellArgs 可配置(设置页或 `cordis.patch.yml`)、可选为模型注入 `terminal_*` 工具(agent 可直接开终端跑命令)。</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(同一渲染栈)。</sub><br/><div align="center"><img width="420" alt="文件变动" src="https://github.com/user-attachments/assets/e7fc1220-305f-4bca-8583-e77ab4f4fa78" /></div> |
| **🌐 内嵌浏览器**<br/><sub>多开网页 tab:后退 / 前进 / 刷新 / 地址栏;内容运行在**不透明源沙箱 iframe**(界面实时显示沙箱状态,可按页面临时解锁);聊天里的外链点击可被接管到侧边栏打开(按协议分流,可配)。</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(含接管内置「文件」页与文件预览),聊天里的文件点击直接落到原生栏;插件自有底部面板可与其同时展开,拖 Tab 到分栏边缘**拆分**、拖到中间**合并**,高度拖上缘调节;开合按钮在会话头右侧。</sub><br/><div align="center"><img width="420" alt="双工作台(右侧栏 + 底部面板)" src="https://github.com/user-attachments/assets/dfdb875e-a1a8-4d4b-8340-353736b1708f" /></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> | **🪟 双工作台:右侧栏 + 底部面板 + 分栏**<br/><sub>右侧栏与底部面板可同时展开;拖 Tab 到分栏边缘**拆分**、拖到中间**合并**(可跨面板);面板宽高左缘/上缘拖拽调节;移动端自动合并为全宽抽屉;把 tab 拖到主会话区域可变为**自由窗口**(悬浮 / 缩放 / 置顶,拖回 pane 停靠)。</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`。</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> |
## 🌐 插件生态
@@ -260,69 +263,59 @@ GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sidebar
## 🆕 最近更新
**支持的 DSH 版本**:<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本(v0.18.1 正式版):0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a> · 完整发布历史见 [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases)
**支持的 DSH 版本**:<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="支持的 DSH 版本(v0.18.0 正式版):0.1.5-rc.2+" src="https://img.shields.io/badge/DSH-0.1.5--rc.2%2B-4d6bfe" /></a> · 完整发布历史见 [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases)
### v0.19.0-alpha.1
### v1.1.0(fork · DSH 2.0.10 / 0.1.5-rc.2 适配 + 精简 + 风格对齐)
> 🧪 **alpha 通道**(npm dist-tag `alpha`,安装 `dsh-better-sidebar@alpha`):本版仅支持 **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`)。
> 📌 **DSH 2.0.10 适配**:跟随 DSH 2.0.10 内发的 `@deepseek-ai/dsh-*@0.1.5-rc.2`,peer + devDeps 全量对齐。
**✨ 新功能**
**🗑 精简 tab(去 `apes:tasks`)**
- 🪟 **每个 tab 体都填满面板**(#609):原生右侧栏的 tab 体宿主是「有确定高度的块级滚动容器」而非 flex 容器,此前插件各 tab 的根只写 `flex: 1`,在块容器里塌成内容高度——侧边对话的输入框因此贴不到面板底(转录一长就被推出可视区)。现在 native 适配层统一给每个 tab 体包一层 `height: 100%` 的列 flex 宿主,插件全部 tab(含第三方 `registerTab` 注册的 descriptor)恢复与底部工作台一致的填满语义。
- 移除 `apes:tasks` —— 与 DSH 原生侧栏的官方会话树功能重叠,保留 **3 个** `apes:*` 信息层 tab:`apes:agents` / `apes:apps` / `apes:profile`
- 同步清理死代码:`ApesTasksView` + `collectTreeJobs`/`orderJobs`/`isJobLive`/`jobDotState` import + apes i18n 死 key(tasks/status/duration)
**🐛 修复**
**🎨 风格对齐 DSW 设计令牌**
- 🧭 **跟随 DSH 0.1.5-alpha.2 的文件地址语法**:`fileAddressFor` 一律产出 session 作用域地址、绝对路径保留前导 `/`;`parseFileAddress` 改为前缀解析并忽略 `?`/`#` 后缀。
- 🪟 **底部工作台的中心列定位跟随 alpha.2 全局面板改动**:`conversation` 槽改为根级 `main` keyed 槽下的 `main.conversation`,定位器改认新 key 并跳过 `display: contents` 槽宿主(alpha.1 的旧 key 仍兼容)。
- apes 视图内联样式的颜色全部改用 DSW alias 令牌(`--dsw-alias-bg-*` / `--dsw-alias-border-*` / `--dsw-alias-label-*` / `--dsw-alias-state-success-*` / `--dsw-alias-interactive-*`),与 DSH 原生侧栏视觉统一;`var(...)` 带字面 fallback 保证非 DSH 环境(单测 harness)仍可渲染
- 字号 / 间距 / 圆角保持内联值(DSH 未对插件面板公开 `--ds-*` 几何令牌,其自身 vendor bundle 亦内联)
**🧰 CI 与内部**
**🔧 0.1.5-rc.2 适配修复**
- 基线推进到 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 个指南条目则直接打开它)。
- primitives npm 发布缺陷(15 个运行时依赖被误列为 devDeps)→ 补进本地 `devDependencies` 供 vitest 内联解析
- sidechat seed 校验契约升级:`dsh-session@0.1.5` 的 `assertAssistantSettlementShape` 要求 assistant 消息的 `data` 顶层带 `stream: Array` → 测试 seed helper 对齐
- 完整 gate:typecheck ✓ / build ✓ / test 119 files · 1249 passed ✓
### v0.19.0-alpha.0
---
> 🧪 **alpha 通道**(npm dist-tag `alpha`,安装 `dsh-better-sidebar@alpha`):本版仅支持 **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`)。
### v1.0.0(fork · apes 信息层)
**✨ 新功能**
> 📌 **fork 独立版本线**:自 upstream `dsh-better-sidebar@v0.18.0`(commit `9e1a034`)fork,新增 `apes:*` 信息层 tab。upstream 历史版本见下方 v0.18.0 段。
- 🖥️ **接入 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`。
**🪧 新增 4 个 `apes:*` 信息层 tab**(独立模块 `src/client/apes/`,与 upstream builtin tab 解耦)
**🐛 修复**
- **`apes:tasks` 任务**:接 `ctx.sessions.list` 的 `jobsBySession` + running sessions —— live 任务列表(label / 运行中·完成·失败 / kind / 耗时 + owner 归属)
- **`apes:agents` 智能体**:接 `subagentsByParent` 目录(降级 `byId`)—— subagent 拓扑(main → 直接 children 的 activity:running / inactive)
- **`apes:apps` 应用**:展示当前工作区 cwd(`scope.cwd` + `byId[current].cwd`;无 DSH host feed,保留说明占位)
- **`apes:profile` 画像**:接 `byId[current]` —— 当前会话画像(title / cwd / running / agent 状态)
- 侧边对话转录 / `jobs.output` 回放 / fork 继承等 8 处会话事件读取跟随 0.1.5 契约;自定义种子补齐 fork 标记对,避免继承父会话未领取的 inbox 输入。
**🔌 数据接入**
**🧰 CI 与内部**
- 数据源 `ctx.sessions.list.getSnapshot()` 经 `useSyncExternalStore` 响应式订阅(snapshot 含 `current` / `byId` / `subagentsByParent` / `jobsBySession`)
- 复用 upstream pure helper(无 UI / Node 依赖):`subagent-lineage.ts`(`rootAncestor` / `isSideThreadSummary`)+ `subagent-jobs.ts`(`collectTreeJobs` / `orderJobs` / `isJobLive` / `jobDotState`)
- UI 独立内联样式,不耦合 builtin CSS module / `StateDot`(保持 apes 模块隔离,cherry-pick upstream 时只碰 `index.tsx` 一处)
- 真机挂载冒烟门禁钉 0.1.5-alpha.1,e2e 增加「展开原生栏 → 经引导页逐个打开插件 tab 类型」的巡检;typecheck / lint / 单测 / 挂载车道全绿。
### 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)
**🌏 i18n**:4 个视图全部走 `t(key)`,`zh` 默认 + `en` fallback,状态 / 耗时 / 空态均有双语词条
### 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-better-sidebar@0.17.1`,停留在 0.1.2-alpha.x 的宿主继续用 `dsh-better-sidebar@alpha`(v0.18.0-alpha.0)。
自 v0.17.1 以来的全部更改(v0.18.1-alpha.0 / v0.19.0-alpha.0 两个中间版本号未发布,内容一并并入本版):
**🔗 宿主线毕业正式版**
- **进入 DSH 0.1.2 线并毕业**:v0.18.0-alpha.0 删除 0.1.1-rc.x 兼容层、修复 alpha.1+ 侧边对话转录空白(#472);基线经 alpha.3(#497)、alpha.5(#516,`Session.events` → `snapshotEvents()`)逐步上行;聊天文件打开漏斗迁移 `remote.session.openWorkspacePath`(#494)、「Show in folder」reveal 滚动限界(#453)。本版钉版基线 **DSH 0.1.2-rc.1**(相对 alpha.5 零源码变化、纯版本升级;`dsh-client-locale` 恢复发版,全线对齐 rc.1;CI 真机挂载冒烟 14/14)
**✨ 新功能**
- 🌿 **「文件变动」统一 tab**(#475):Git 视角(真 diff / 历史 / 暂存·提交·还原 / worktree·子仓库选择)与本轮文件视角(模型读/写/编辑实时追踪)双视角合一;统一 diff 渲染(改蓝配对 + 行内字符级高亮 + 语法着色 + 上下文折叠)、底部可拖拽预览面板、一键展开独立 diff tab
@@ -348,12 +341,9 @@ GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sidebar
- 新收录 10+ 插件:dsh-better-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
### 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 适配线复用(见上)。
> 🧪 **alpha 通道**:本版仅支持 **DSH 0.1.2-alpha.x**(peer 下限 `^0.1.2-alpha.5`,npm dist-tag `alpha`,安装 `dsh-better-sidebar@alpha`)。该版本号未单独发布,内容已并入 **v0.18.0** 正式版。
- 🔗 **适配 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(升级启动修复)经核实不触及本插件其余表面。
@@ -423,6 +413,9 @@ GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sidebar
- 🔧 **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` 属非法形状)
<details>
<summary><b>历史版本(v0.12.0 – v0.15.2)</b></summary>
### v0.15.2
自 v0.15.1 以来的全部更改:
@@ -541,7 +534,7 @@ GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sidebar
推荐添加QQ群(577011007)
<div align="center">
<img width="220" alt="微信群二维码" src="https://github.com/user-attachments/assets/cbf211c6-e5b8-49c3-a412-7210c0b33d73" />
<img width="220" alt="微信群二维码" src="https://github.com/user-attachments/assets/39caafc7-9629-4b13-bb2b-eac17eab5b6a" />
<img width="220" alt="QQ群二维码" src="https://github.com/user-attachments/assets/9be34629-26ef-4537-aad4-1393c147f81c" />
</div>
@@ -561,7 +554,7 @@ GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sidebar
从 v0.4.0 起暴露 `ctx.betterSidebar` 服务,其他插件可注册侧边栏页面与文件预览器(内置 8 tab + 6 viewer 亦通过同一服务注册)。v0.12.1 补齐基座能力(完整类型导出、能力探测、状态订阅、tab 角标、生命周期回调、定向打开、插件自有设置等)。
完整接入文档(全字段、匹配算法、HMR 陷阱、声明式设置、版本探测、原生栏承载面与皮肤契约):**[`docs/external-plugin-guide.md`](./docs/external-plugin-guide.md)**;仓库开发规则(硬约束 / CI / 发版)见 [`AGENTS.md`](./AGENTS.md)。
完整接入文档(全字段、匹配算法、HMR 陷阱、声明式设置、版本探测、自由窗口与皮肤契约):**[`docs/external-plugin-guide.md`](./docs/external-plugin-guide.md)**;仓库开发规则(硬约束 / CI / 发版)见 [`AGENTS.md`](./AGENTS.md)。
### ➕ 添加插件(推荐插件目录)
@@ -574,7 +567,6 @@ GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sidebar
```sh
pnpm install # @deepseek-ai/* devDependencies 已发布(基线 0.1.2-rc.1,next dist-tag),直接解析、无需令牌
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
+10 -67
View File
@@ -9,7 +9,7 @@
<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.18.1): 0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a>
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="Supported DSH versions (v0.18.0): 0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a>
<a href="https://github.com/topics/dsh-better-sidebar"><img alt="Plugin ecosystem: GitHub topic dsh-better-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 &amp; preview" src="https://img.shields.io/badge/-Edit%20%26%20preview-4d6bfe" /> <img alt="Embedded browser" src="https://img.shields.io/badge/-Embedded%20browser-4d6bfe" /> <img alt="Real terminal" src="https://img.shields.io/badge/-Real%20terminal-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 />
@@ -44,10 +44,11 @@
- **🌐 Embedded Browser**: multiple web tabs with back / forward / refresh; content runs in a sandboxed iframe; external links are routed by protocol by default — HTTP opens in the sidebar, HTTPS goes to the system browser (both adjustable in settings)
- **💻 Real Terminal**: xterm.js + node-pty real shell, reconnect with transcript replay; optionally injects `terminal_*` tools for the model
- **📂 Model-driven sidebar opens (opt-in)**: with the global setting on, the `sidebar_open` tool lets the model actively open files / folders (tree rooted there) / HTTP(S) pages in the sidebar
- **🌿 Changes**: 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, grouped by file with kind filters); a unified diff renderer (mod pairing + intra-line character highlights + syntax coloring incl. mjs/cjs/mts/cts, CSS/SCSS/Less, HTML/XML/SVG/Vue, GraphQL, JSONC/JSON5 + context folding), a draggable bottom preview pane, and one-click expansion into a dedicated diff tab; `.md` ops (read / write / edit) offer a **reading-mode** toggle in the preview header — the shared MarkdownText renders GFM tables / task lists / strikethrough / footnotes / math, with local images rewritten through the /sidebar/file media route; documents with ```mermaid fences render through the editor's lazy mermaid renderer (click-to-zoom / pan diagrams); **secret redaction** — credential-shaped paths mask whole files and ordinary files mask secret-shaped values (api_key: / Bearer / sk- / AKIA / ghp_ / PEM …, field names kept), on by default with a one-click preview-pane toggle (persisted in localStorage), display-only — session data untouched. Known edge: an unquoted mermaid label containing a redacted secret breaks the diagram (falls back to source); workaround: quote the label. `.html` ops (read / write / edit) gain a **Render** toggle in the preview head — the editor's `/sidebar/html` route iframe, with relative assets resolving inside the route and segmented reads rendering the full document; always sandboxed (opaque origin + CSP header, no escape hatch). `.pdf` ops (read / write / edit) gain the same **Render** toggle — the editor's PDF preview reused verbatim (media-route bytes + explicit Blob, the browser's native viewer inline, with a download fallback)
- **🌿 Changes**: 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, grouped by file with kind filters); 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
- **🧩 Background Tasks**: agent topology + background tasks (exit codes / live output / force-kill)
- **💬 Side Chat (beta)**: Codex-style side threads — the child inherits the parent's FULL context (completed turns + the pending question + the in-progress turn's assistant output and tool activity, honestly frozen as "interrupted") and runs independently without entering the main conversation; threads support continuous follow-ups (auto-resumed after a DSH restart) and one-click "Save as new session" promotion to a top-level session
- **🖥️ Native right sidebar + plugin bottom workbench**: the right column belongs to DSH 0.1.5's own sidebar — the plugin registers every tab type as a native tab (file opens go through `dsh-resource://file/**`, and the built-in Files page / file tree is taken over), keeping only its own bottom workbench (split panes / terminals / per-session persistence) whose toggle sits in the session header
- **🪟 Dual Workbench**: right sidebar + bottom panel; drag tabs to split / merge panes (cross-panel), mobile auto-merges into a full-width drawer
- **🪟 Free Windows**: drag any tab onto the main conversation area to turn it into a movable / resizable / raiseable floating window (default 390×780); drag it back onto a pane to dock; persisted per session. `features` includes `'floatWindows'` and plugin tabs are supported identically
- **📌 Pinned Terminals**: right-click a terminal tab to "Pin to Workspace / Pin Globally" — pinned terminals survive session switches and surface inline in the TabBar as virtual tabs (click activates in-place, PTY connects directly to the home session's PTY via WS, no session jump needed); agent terminals exempted from reconcile removal
- **🔁 Session Isolation**: layout / tabs / panels persisted per session, stale state auto-purged
- **⚙️ Declarative Settings**: per-item toggles in the "Side Cards" settings section, secondary settings via the gear dialog
@@ -61,7 +62,7 @@
**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.18.1): 0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a>
<a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="Supported DSH versions (v0.18.0): 0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a>
> 📌 **Stable release**: starting with `v0.18.0` the plugin targets DSH **0.1.2-rc.1+** (npm dist-tag `latest`) and drops 0.1.0-rc.8 ~ 0.1.1-rc.2 — stable-DSH (≤ 0.1.1-rc.2) users should stay pinned to `dsh-better-sidebar@0.17.1` (`@latest` now points at v0.18.0); hosts still on 0.1.2-alpha.x should upgrade DSH first, or keep `dsh-better-sidebar@alpha` (v0.18.0-alpha.0).
@@ -165,7 +166,7 @@ Update: `git pull && pnpm install && pnpm build` → `node scripts/package-regis
| **🖥️ CodeMirror editor**<br/><div align="center"><img width="420" alt="CodeMirror editor" src="https://github.com/user-attachments/assets/b44b488e-568c-4ee0-b96c-e9c906598a77" /></div> | **🖼️ Inline image preview**<br/><div align="center"><img width="420" alt="Inline image preview" src="https://github.com/user-attachments/assets/f9a58c30-5b7a-48b5-9e22-37d7e071f593" /></div> |
| **💻 Real Terminal**<br/><sub>xterm.js + node-pty real shell (not an emulator): transcript replay on reconnect, configurable shell / shellArgs (settings page or `cordis.patch.yml`), and optional `terminal_*` model tools so the agent can open terminals and run commands itself.</sub><br/><div align="center"><img width="420" alt="Real 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). 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> |
| **🌐 Embedded Browser**<br/><sub>Multiple web tabs with back / forward / reload / address bar; content runs in an **opaque-origin sandboxed iframe** (live sandbox status in the UI, per-page temporary unlock available); external-link clicks in the chat can be taken over into the sidebar (protocol-based routing, configurable).</sub><br/><div align="center"><img width="420" alt="Embedded 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 the built-in Files page and file previews), so clicking a file in the chat lands there directly; 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> |
| **💬 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> | **🪟 Dual Workbench: Sidebar + Bottom Panel + Split Panes**<br/><sub>The right sidebar and the bottom panel can stay open together; drag a tab to a pane edge to **split**, to the middle to **merge** (works across panels); panel width/height drag from the left/top edge; on mobile everything merges into a full-width drawer; drag a tab onto the main conversation area to turn it into a **free window** (float / resize / raise, drag back onto a pane to dock).</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`.</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> |
## 🌐 Plugin Ecosystem
@@ -264,64 +265,7 @@ The GitHub topic [`dsh-better-sidebar`](https://github.com/topics/dsh-better-sid
<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.18.1): 0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a> · full release history on the [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases) page
### v0.19.0-alpha.1
> 🧪 **alpha channel** (npm dist-tag `alpha`, install `dsh-better-sidebar@alpha`): 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-better-sidebar@alpha`): 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)
**Supported DSH versions**: <a href="https://www.npmjs.com/package/@deepseek-ai/dsh?activeTab=versions"><img alt="Supported DSH versions (v0.18.0): 0.1.2-rc.1+" src="https://img.shields.io/badge/DSH-0.1.2--rc.1%2B-4d6bfe" /></a> · full release history on the [Releases](https://github.com/omdsh-dev/DSH-better-sidebar/releases) page
### v0.18.0
@@ -358,9 +302,9 @@ All changes since v0.17.1 (the two intermediate version numbers v0.18.1-alpha.0
- 10+ new curated plugins: dsh-better-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)
### v0.19.0-alpha.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).
> 🧪 **Alpha track**: this release targets **DSH 0.1.2-alpha.x only** (peer floor `^0.1.2-alpha.5`, npm dist-tag `alpha`, install `dsh-better-sidebar@alpha`). 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.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.
@@ -562,7 +506,7 @@ All changes since v0.14.0:
Since v0.4.0 the plugin exposes the `ctx.betterSidebar` service — other plugins can register sidebar pages and file viewers (the 8 built-in tabs + 6 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).
Full integration docs (complete fields, matching algorithm, HMR pitfalls, declarative settings, version detection, float windows 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)
@@ -575,7 +519,6 @@ The dashed cards at the end of the "Sidebar content" / "File viewers" grids in t
```sh
pnpm install # @deepseek-ai/* devDependencies resolve (baseline 0.1.2-rc.1, next 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
+7 -7
View File
@@ -1,12 +1,12 @@
# dsh-better-sidebar bundle patch
# dsh-apes-sidebar bundle patch
#
# 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-better-sidebar@<version>
# dsh plugin --profile <name> add dsh-apes-sidebar@<version>
#
# — the command reconciles `dsh.profile.bundles` against installed packages
# and, seeing this declaration, appends `dsh-better-sidebar` to the bundle
# 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.
@@ -23,7 +23,7 @@
#
# - insert:
# - id: better-sidebar
# name: 'dsh-better-sidebar'
# name: 'dsh-apes-sidebar'
# config:
# shell: /bin/zsh
# shellArgs:
@@ -35,7 +35,7 @@
# `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-better-sidebar`; the aggregate instance then owns the sidebar. The
# `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`
@@ -45,5 +45,5 @@
# fail loudly at the loader as before.
- insert:
- id: better-sidebar
name: 'dsh-better-sidebar'
disabled: !!js "[...ctx.loader.entries()].some((e) => e.options.name === 'dsh-better-sidebar' && e.options.id !== 'better-sidebar' && !e.disabled)"
name: 'dsh-apes-sidebar'
disabled: !!js "[...ctx.loader.entries()].some((e) => e.options.name === 'dsh-apes-sidebar' && e.options.id !== 'better-sidebar' && !e.disabled)"
+18 -43
View File
@@ -2,38 +2,12 @@
> 面向 **消费插件开发者**:如何让你的插件向 better-sidebar 注册新的侧边栏页面(tab)和文件类型预览器。
>
> 适用版本:**v0.4.0+**(`ctx.betterSidebar` 服务);声明式设置 **v0.4.1+**;text/number 设置行 **v0.11.0+**;badge/生命周期/定向打开/插件设置/版本探测 **v0.12.0+**;select 设置行(`settingSelect`)与外链认领(`urlTarget`)**v0.13.0+**;统一 `@deepseek-ai/cordis` 类型基底 **v0.15.2+**;终端固定(pin)**v0.17.0+**。当前版本 **v0.19.0-alpha.1**(alpha 通道,仅支持 DSH **0.1.5-alpha.2+**;**0.1.5-alpha.1 不再支持**——alpha.1 用户请用 v0.19.0-alpha.0,那是它的最后一版;0.1.2-rc.1 稳定线请用 v0.18.x)。**v0.19.0 移除了自绘右侧面板与自由窗口**(见 §0、§11)。
> 适用版本:**v0.4.0+**(`ctx.betterSidebar` 服务);声明式设置 **v0.4.1+**;text/number 设置行 **v0.11.0+**;badge/生命周期/定向打开/插件设置/版本探测 **v0.12.0+**;select 设置行(`settingSelect`)与外链认领(`urlTarget`)**v0.13.0+**;统一 `@deepseek-ai/cordis` 类型基底 **v0.15.2+**;自由窗口(`floatWindows`)**v0.16.0+**;终端固定(pin)**v0.17.0+**。当前版本 **v0.18.0**(正式版,仅支持 DSH 0.1.2-rc.1+;旧宿主 stable 线为 v0.17.1)。
> 权威代码:`src/client/service.ts`(服务实现)、`src/client/builtins/`(内置 8 tab + 6 viewer 参考实现)、`lib/types/client/service.d.ts`(类型声明)。
> 仓库开发规则(硬约束 / CI / 发版)见 [AGENTS.md](../AGENTS.md)。
---
## 0. 承载面:DSH 原生右侧栏 + 插件底部工作台(v0.19.0-alpha.0 起)
从 v0.19.0-alpha.0 起,**右列完全属于 DSH**:你的 tab 渲染在 **DSH 自己的右侧栏**里(`ctx.sidebarRight` / `ctx.sidebarRightTabs`),插件把每个 `TabDescriptor` 注册成原生 tab 类型(`kind = descriptor.id`)+ 一个原生 tab 体。插件自己只保留**底部工作台**(分栏树、终端、会话内持久化)。对你的接入代码**没有影响**——仍然只调用 `ctx.betterSidebar`:
- `registerTab` / `registerFileViewer` 签名不变;
- `openTab` / `openFile` 默认落到原生右侧栏;新增可选 `OpenTabSeed.target`(`'right'` 默认 / `'bottom'` 落插件的底部工作台);
- `updateTab` / `closeTab` / `activateTab` 认识原生 tab id(插件为每个原生 tab 维护一条合成 `SidebarTab` 记录,`tab.meta` / `tab.path` 的写入照旧生效)。
行为差异(写在这里以免踩坑):
| 事项 | 说明 |
|---|---|
| 生命周期回调 | 原生面只有「一次打开」,不区分新建/聚焦,因此只触发 `onOpen`(`onActivate` 仅在插件自己的底部工作台里触发) |
| 去重 | 原生按 `(kind, 地址)` 去重:有 `createTab` 的类型每次新开一个 tab(terminal / browser / sidechat / diff),其余聚焦已有 tab;`dedupeKey` 的自定义语义不参与原生面 |
| 布局持久化 | 原生栏的布局**只在内存**(刷新后回到折叠默认),插件自己的底部工作台仍然持久化 |
| 跨会话打开 | 目标会话的右侧栏 store 未挂载时,打开会排队到该会话上屏后重放 |
| 内置类型接管 | 插件的 `editor` 类型以 `extension` 优先级认领 `dsh-resource://file/**`(压过内置 `ui-sidebar-documentpreview` 的 `text` 预览——即 `fallback` 带),并接管内置 `files` 页面 kind(`openTab('files')` 打开插件的文件树);插件卸载/禁用时内置实现自动复位 |
| 终端上限 | 终端 tab 的数量上限只统计插件自己底部工作台里的终端;原生栏里的终端不计入 |
| 底部工作台的开合 | 落到底部工作台的打开一律展开它(新建与聚焦都算),因此 `openTab` 的落点永远可见;开合按钮注册在 DSH 会话头的 utilities 槽(`conversation.session.header.utilities`),不在插件自己的宿主里 |
| 新建标签页列表 | 每个 tab 类型在原生 guide 里占一行:标题取 `title` + 图标取 `icon`(**无第二行说明**——alpha.2 起原生指南把条目渲染成「图标+标题」胶囊,`description` 已从宿主契约删除,插件随之移除 `TabDescriptor.description` 与 6 个 `guideDesc*` 词条);`hidden: true` 的类型不占行。插件的 `editor` 类型不再单独占行(它认领的文件资源由 `files` 接管页承载同一视图) |
| 新建面板的种子(alpha.2) | 在新会话打开原生新面板时,宿主从已注册的 guide 条目里播种:恰好 1 个条目 → 直接打开那一页;0 或 ≥2 个条目 → 打开指南。`revealIfOpened` 打开的「页面」在**同一 pane 内**强制去重(已在该 pane 就不再新建);由已有 tab 地址驱动的打开不受该去重影响 |
| alpha.2 全局面板(不接入) | 插件**不采用** alpha.2 引入的全局主面板模型——根级 keyed `main` 槽(预留 key `conversation`,由 ui-conversation 注册为 `main.conversation`)、根级 `sidebar.panellist` 列表槽(`SidebarPanelMetadata` / `SidebarPanelIconOwnerProps`)、`ctx.layout.selectPanel(MainPanelId|null)` / `beginNavigation()` / `dispose()`、全局标准 prop `usePanelInfo`,以及改根级并新增会话级 `rightbar.session` 子槽的 `rightbar`——这些只作兼容保留,不向其迁移 |
| 已移除 | 插件自绘右侧面板(含宽度拖拽 / 新会话默认宽度)与**自由窗口**(`features` 里的 `'floatWindows'` 已删除,v0.18.x 及更早版本的消费者请勿再 gate 该能力);`openByDefault` / `defaultWidthPercent` / `changesDiffFloat` 三个设置项同步删除(旧文档里的键会被忽略) |
---
## 1. 总览:你能扩展什么
better-sidebar 从 v0.4.0 起把自己改造成一个**注册表服务**:
@@ -388,7 +362,7 @@ ctx.effect(() => {
| id | order | single | hidden | 用途 |
|---|---|---|---|---|
| `editor` | 10 | 否(按 path 去重) | 否 | 唯一「文件窗口」(编辑/预览 + 资源管理)。chrome 恒合并形态:路径输入框 + 编辑器控件 + 可开关内嵌文件树(全局搜索 `fs.search`;状态存 `tab.meta.treeOpen/treeWidth`)。`editorExplorer`:关(默认)= 按 path 新开,无路径窗口 = 纯资源管理器;开 = 树点击/Enter 经 `updateTab` 原地切换(id/meta 不变),无路径窗口 = 带 chrome 空窗口。树右键「在新 Tab 中打开」「在侧边打开」(pane 右侧 split)。新会话 seed 空文件窗口(`title:'Files'`);旧 `explorer` tab 经 `sanitizeState` 迁移 |
| `git` | 20 | 是 | 是(本轮文件操作数) | 「文件变动」统一 tab(id 保留 `git` 以兼容持久化布局):**Git 视角**(原 Git 面板:staged/unstaged / 提交 / 历史 / worktree·子仓库选择)+ **本轮文件视角**(原 file-trace:模型读/写/编辑实时折叠,按文件分组、类型筛选);会话事件经插件自有宿主路由 `changes.ops` 供给(live 日志优先、冷会话回放持久化记录,`afterSeq` 增量),badge 读 tab 轮询写入的同步缓存。两视角共用底部可拖拽预览面板(`tab.meta.lens/previewH` 持久化),diff 渲染统一走 `src/client/diff/`(`DiffRows`/`DiffFiles`:mod 配对 + 行内高亮 + 语法着色 + 上下文折叠);Git 目标可展开为独立 diff tab(落进工作台的 diff 分栏) |
| `git` | 20 | 是 | 是(本轮文件操作数) | 「文件变动」统一 tab(id 保留 `git` 以兼容持久化布局):**Git 视角**(原 Git 面板:staged/unstaged / 提交 / 历史 / worktree·子仓库选择)+ **本轮文件视角**(原 file-trace:模型读/写/编辑实时折叠,按文件分组、类型筛选);会话事件经插件自有宿主路由 `changes.ops` 供给(live 日志优先、冷会话回放持久化记录,`afterSeq` 增量),badge 读 tab 轮询写入的同步缓存。两视角共用底部可拖拽预览面板(`tab.meta.lens/previewH` 持久化),diff 渲染统一走 `src/client/diff/`(`DiffRows`/`DiffFiles`:mod 配对 + 行内高亮 + 语法着色 + 上下文折叠);Git 目标可展开为独立 diff tab——落点由二级设置 `changesDiffFloat` 决定(默认**自由浮窗**居中弹出,可选面板下半 split),设置项见 tab descriptor 的 `settings.toggles` select |
| `subagent` | 30 | 是 | 否 | 子代理拓扑 |
| `sidechat` | 35 | 否(`sidechat:<uuid>`,按 `meta.threadId` 去重) | 否 | 侧边对话(每对话一 Tab):打开即建空线程(首条消息赢得标签并同步标题);线程 = 插件自建子会话(种子继承父会话上下文,进行中回合以 `interrupted` 闭合;种子带合法 `subagent/descriptor`,SubagentView 按 `Side: ` 前缀过滤),`origin:'subagent'` 隐藏于主列表;走 `/sidebar/api/sidechat.*` 路由;头部菜单切换/重开(`parkSidechatReopen` + 确定性 id),关 Tab 释放 live agent;重开经 `collectOwnEvents` 回源到种子边界;「保存为新会话」= `session.fork`(`this` 敏感)。[设计文档](plans/2026-08-20-sidechat-tab-design.md) |
| `terminal` | 40 | 否(`terminal:<n>`) | 否 | 终端。v0.17.0+ 右键「固定到工作区/全局」:跨会话不消失,TabBar 内联虚拟 Tab(`pinned:<homeSessionId>:<tabId>`),就地按 home scope 连 PTY;global 全会话可见、workspace 仅同 cwd;`tab.pin = { scope, homeCwd? }` 随会话持久化,渲染期解析(`collectPinnedTabs` → `createPinnedVirtualTab` → `injectPinnedIntoTree`) |
@@ -610,10 +584,10 @@ interface BetterSidebarService {
// ── v0.12.0+ ──────────────────────────────────────────────────────────
/** 插件版本(如 '0.17.1';与 package.json 同步,测试守护) */
readonly version: string
/** 能力清单(只增不删,唯一例外:v0.19.0 删除了 'floatWindows'):
* 'badge' | 'tabLifecycle' | 'updateTab' | 'openFile' | 'targetedOpen' |
* 'stateSubscription' | 'tabMeta' | 'pluginSettings' | 'urlTarget' |
* 'settingSelect' ——用 `features.includes('xxx')` 按能力 gate。 */
/** 单调能力清单(只增不删):'badge' | 'tabLifecycle' | 'updateTab' |
* 'openFile' | 'targetedOpen' | 'stateSubscription' | 'tabMeta' |
* 'pluginSettings' | 'urlTarget' | 'settingSelect' | 'floatWindows'
* ——用 `features.includes('xxx')` 按能力 gate。 */
readonly features: readonly string[]
/** 当前快照:激活 sessionId + 其状态(面板几何/打开的 tabs/展开集)+ prefs。
* session 未激活时 state/sessionId 为 undefined。 */
@@ -756,18 +730,20 @@ ctx.effect(() =>
| **i18n 跟随** | 文案跟随 DSH `ctx.locale`(词典在 `betterSidebar` 命名空间;Host-backed `locale.preference` 优先于浏览器语言并实时切换;缺失回退浏览器)。消费插件**不要**依赖内部 `t()`——标题传字符串或 `() => string` |
| **第三语言覆盖(ja 等)** | 可选 peer `@huanlin/dsh-plugin-better-locale`(optional)提供 ja/ko 覆盖,**借用 DSH 英文槽位**(仅 DSH=en 时生效,zh 下惰性)。经 `ctx.get('betterLocale')` 注入 `t()`;未安装整段 no-op |
| **懒加载 chunk** | 重依赖(xterm/CodeMirror)在独立 bundle(`lib/client-<name>.js`),经 `/sidebar/bundle` 按需下发;factory 赋到 `globalThis.__dshChunks__[<name>]`,由 `src/client/chunk-loader.ts` 物化,**不经** `__ModuleLoader__`。对消费插件透明 |
| **聊天文件打开漏斗(0.1.5 宿主)** | 聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)统一走 DSH 原生右侧栏的 `ctx.sidebarRight.openResource(fileAddressFor(sessionId, cwd, path))`(`packages/client/ui-chat/src/client/apply.ts` 是唯一调用点);`remote.session.openWorkspacePath` 在 0.1.5 客户端**已无调用者**,better-sidebar 的 openpath 拦截随之删除。你的插件若要观测/旁路聊天文件打开,注册原生 tab 类型认领 `dsh-resource://file/**`(见 [AGENTS.md §3](AGENTS.md) 第 10 条);不要假设 `ctx.workspaces.openPath`(已删除)或旧 `remote.session` 漏斗存在 |
| **原生 tab 体的高度契约** | 你的 tab 组件被挂在**全高列 flex 宿主**里:native 适配层给每个 tab 体(含 orphaned 回退)包一层 `height:100%` 的 `sidebar.module.css` `.nativeTabHost`(带 `data-dsh-native-tab-host`)——根用 `flex: 1` / `height: 100%` 并加 `min-height: 0` 填满面板。宿主的面板体 `.paneBody`(`dsh-client-ui-dockkit`)是**有确定高度的块级滚动容器,不是 flex 容器**,因此只靠外层 flex 容器支撑的根会塌成内容高度(sidechat 的输入框就贴不到面板底);DSH 自己的 tab 体(`ui-sidebar-documentpreview` 的文档预览、`ui-sidebar-files` 的 FilesBody)正是因此在根上声明 `height: 100%` |
| **聊天文件打开漏斗(alpha 宿主)** | 聊天里一切文件打开(工具行 / 产物行 / 正文提及 / 行内代码路径)汇入 `ctx.remote.session.openWorkspacePath`(Typert remote 命名空间,cordis 服务 key `remote.session`,方法为 **accessor 属性**、异步挂载)。better-sidebar 的「聊天区文件在侧边栏打开」即在 `ctx.inject(['remote.session'], …)` 内以 defineProperty 遮蔽该方法(`src/client/openpath-intercept.ts`)。你的插件若要观测/旁路聊天文件打开,走同一服务;不要假设 pre-alpha 的 `ctx.workspaces.openPath` 存在(alpha 的 `IWorkspaces` 已无此方法) |
---
## 11. 自由窗口(v0.16.0 – v0.18.x,**v0.19.0 已移除**)
## 11. 自由窗口(v0.16.0,`features` 含 `'floatWindows'`)
自由窗口(tab 拖出侧边栏成为可移动/缩放的悬浮窗,`SidebarState.floats`、`floatTab`/`dockFloat`/`raiseFloat`、`features` 里的 `'floatWindows'`、右键「移动到自由窗口」、`src/client/FreeWindow.tsx`)在 **v0.19.0-alpha.0 整体删除**:右列交给 DSH 原生右侧栏后,插件只剩底部工作台,浮窗既没有语义归属也没有可用手势(拖出目标区域现在是原生栏的属地)。
任意 tab(含你注册的)可拖出侧边栏成为悬浮**自由窗口**——对你的组件基本透明,只需知道以下语义:
- 升级到 v0.19.x 后请**不要**再 gate `features.includes('floatWindows')`,也不要引用 `floatTab` 等符号(类型声明里已不存在);
- 旧文档里持久化的 `floats` 字段会被 `sanitizeState` 直接忽略,会话不会因此加载失败;
- 需要「同一个 tab 在别处看」的场景,改用原生栏打开(`openTab` 默认落点)或底部工作台分栏(`OpenTabSeed.target: 'bottom'`)。
- **拖出**:tab 拖到主会话区域(conversation 列)松开即浮动(`floatTab`:移入 `SidebarState.floats`,默认 390×780 按视口钳制居中,清空 pane 折叠)。检测在 `Sidebar.tsx`(`body[data-dsh-tab-dragging]` 门控);窄视口禁用。右键「移动到自由窗口」始终可用。
- **窗口操作**(`src/client/FreeWindow.tsx`,`[data-dsh-panel-host]` 内、z-42):头部拖动移动;拖到 pane(`[data-dsh-pane]`)松开**停靠**(`dockFloat`);右下角缩放(最小 320×200);点击置顶(层叠 = `floats` 数组序);X = `closeTab`(触发 `onClose`、释放终端)。
- **持久化**:`floats` 随会话进 localStorage(`dsh-sidebar:v1:<sessionId>`);`sanitizeState` 宽容校验(非法条目单独丢弃、几何钳入视口、diff/ephemeral 不持久化)。
- **服务语义**:`openTab` 聚焦命中浮动 tab = 置顶窗口(不重复开);`closeTab`/`activateTab` 关窗/置顶并触发回调;agent 终端 reconcile 移除已消失的浮动窗口。**浮窗内你的组件 `visible` 恒 true**。
- **稳定寻址面**:`[data-dsh-float-window]` / `[data-dsh-float-id]` / `[data-dsh-pane]` / `[data-dsh-float-dock-over]` / `[class*='floatDropHint']`;全令牌驱动,头部 `-webkit-app-region: no-drag`。
- **⚠️ portal 事件劫持陷阱**:拖拽表面子树含 portal 覆盖层时(如你的组件在 tab 头部区域渲染弹层),portal 后代合成事件**沿 React 树冒泡**回 `onPointerDown`,会被误判为拖拽开始(吞点击 + 抢 pointer,菜单/X 失灵;jsdom 不走此路径,只有 e2e 能抓)。拖拽起点必须带**同源守卫**:`event.currentTarget.contains(event.target)` 为假或目标在 `button` 内时直接返回(回归:`tests/free-window.spec.tsx` portaled-menu 用例)。
---
@@ -780,7 +756,7 @@ ctx.effect(() =>
- **面板表面**:右/底面板背景 = `var(--dsw-alias-bg-layer-1)`。**绝不消费 `--dsw-specific-sidebar-fill`**(宿主左导航专属,皮肤按左导航语义覆盖它,面板消费会失去填充)。换面板表面 = 覆写 `--dsw-alias-bg-layer-1`。
- **终端/编辑器表面**:`effectiveTokenValue` 读 `--dsw-alias-bg-base`——`transparent` 与 alpha < 0.9 的半透明值回退不透明底色(文字不叠背景画,issue #90);≥ 0.9 放行。
- **根锚点**:宿主 div 带 `data-dsh-better-sidebar`(append 到 body);其内**面板宿主层** `[data-dsh-panel-host]`(`fixed; inset:0; z-25; pointer-events:none; overflow:hidden+clip`,v0.13.1+),面板/开关簇 absolute 定位,免疫中间层 transform 劫持;页面级 transform 触发 `data-dsh-panel-host-degraded` 降级。`overflow` 级联是**契约**(`hidden` 兜底 + `clip` 收尾,`tests/panel-host-css.spec.ts` 守护):`hidden` 盒子仍是滚动容器,脚本滚动或浏览器 scroll-into-view 修正(焦点移入视口外区域、嵌套 iframe/工作台加载时抢焦点、面板滑出动画中 focus() 落点)会沿最近可滚祖先滚走整层——面板与开关簇集体偏离视口角(computed left/right 仍"正确",偏移藏在盒子自身 scroll offset 里);`clip` 裁剪语义相同但不产生滚动盒,任何路径都滚不动这层。皮肤作用域覆盖限定在 `[data-dsh-better-sidebar]` 内。
- **布局变量**(`<html>` 上,面板打开时有效):`--dsh-sidebar-width` / `--dsh-sidebar-height`。右面板宽度 = AppFrame 的 `padding-right` 预留(新版 `#root [data-dsh-frame]` / rc.8 `#root > [data-slot="root"] > div` 双锚点),AppFrame border box 保持完整桌面视口宽度(Harness 以此判定桌面/窄屏布局,避免插件面板展开误入窄屏);AppFrame 的 details 拖拽手柄按同一变量向左平移贴合列边缘。底部面板仍走 centerCol `margin-bottom`;centerCol 锚点 = **JS 标注**(禁止 `nth-child`):侧栏 shell 的定位器给测得的 centerCol 节点打 `[data-dsh-center-col]` 标签(`Sidebar.tsx` locate,节点更换/HMR 时随 ref 迁移),`layout.css` 用 `#root [data-dsh-center-col]` 选中(`drag-layout.e2e.ts` 断言恰一节点且为对话槽宿主的父级——alpha.2 起 shell 把 `#root [data-slot="main.conversation"]` 解析进列并跳过 `display: contents` 祖先(`center-column.ts` 的 `CENTER_COLUMN_SELECTOR` 同时认 `main.conversation` 与 alpha.1 的 `conversation`),定位器从槽宿主向上取第一个非 `contents` 的祖先;frame 宽度与桌面 Session Log 由 `desktop-layout.e2e.ts` 断言)。
- **布局变量**(`<html>` 上,面板打开时有效):`--dsh-sidebar-width` / `--dsh-sidebar-height`。右面板宽度 = AppFrame 的 `padding-right` 预留(新版 `#root [data-dsh-frame]` / rc.8 `#root > [data-slot="root"] > div` 双锚点),AppFrame border box 保持完整桌面视口宽度(Harness 以此判定桌面/窄屏布局,避免插件面板展开误入窄屏);AppFrame 的 details 拖拽手柄按同一变量向左平移贴合列边缘。底部面板仍走 centerCol `margin-bottom`;centerCol 锚点 = **JS 标注**(禁止 `nth-child`):侧栏 shell 的定位器给测得的 centerCol 节点打 `[data-dsh-center-col]` 标签(`Sidebar.tsx` locate,节点更换/HMR 时随 ref 迁移),`layout.css` 用 `#root [data-dsh-center-col]` 选中(`drag-layout.e2e.ts` 断言恰一节点且为 `[data-slot="conversation"]` 的父级;frame 宽度与桌面 Session Log 由 `desktop-layout.e2e.ts` 断言)。
- **桌面信号与标题栏**(v0.14.1+ 四方案模型 `SidebarPrefs.titleBarScheme`,唯一决策点 `src/client/titlebar-strip.ts` 纯函数):
- 壳信号(只读,不自动触发修改):URL `dsh-desktop-mode` / `dsh-desktop-platform` / 可选 `dsh-desktop-titlebar-inset`(0–120 clamp)。
- **strip 取值链**:⓪ `web` 方案强制 0;① `navigator.windowControlsOverlay` 真实几何(`wco.ts` 订阅 `geometrychange`,**为 0 也权威**,`visible=false` 幽灵 API 视为缺失);② URL inset;③ 壳预设 `stripFor`(仅 `preset`);④ 手动 `titleBarStripPx`(仅 `custom`);⑤ 0。驱动 `body[data-dsh-title-bar-compat]` + `--dsh-title-bar-strip`。
@@ -885,9 +861,8 @@ better-sidebar 的内置 tab 和 viewer 就是参考实现("吃狗粮"),
- **`src/client/builtins/`**:7 个内置 tab(tabs.tsx)+ 6 个内置 viewer(viewers.tsx)的注册代码 + 聚合与 disposer 生命周期(index.ts);Office 预览见 plugins-viewers.ts
- **`src/client/service.ts`**:`BetterSidebarService` 接口 + `createBetterSidebarService` 工厂实现(含匹配算法、dedupe、createTab、启用态 gating)
- **`src/client/Sidebar.tsx`**:底部工作台外壳 + `TabContent` 分发(查 `getTab` → 调 descriptor.component;未注册 → `<OrphanedTab/>`)、`+` 菜单构建(order 排序 + available disabled + 禁用过滤)
- **`src/client/native/`**:原生右侧栏接入(tab 类型注册、合成 `SidebarTab` 适配、资源地址、跨会话打开排队)
- **`src/client/sidebar/bottom-toggle.tsx`**:底部工作台开合按钮(注册进 DSH 会话头 utilities 槽)
- **`src/client/Sidebar.tsx`**:`TabContent` 分发(查 `getTab` → 调 descriptor.component;未注册 → `<OrphanedTab/>`)、`+` 菜单构建(order 排序 + available disabled + 禁用过滤)、自由窗口拖拽检测
- **`src/client/FreeWindow.tsx`**:自由窗口(移动/停靠/缩放/置顶,§11)
- **`src/client/SideCardSection.tsx`**:声明式设置页(注册表驱动清单 + 嵌套设置行 + 开关持久化)
- **`src/client/api.ts`**:`/sidebar` API 的封装(复制其 fetch 模式到你的插件)
- **`src/client/plugins-tabs.ts`** / **`plugins-viewers.ts`**:推荐插件目录(「添加插件」弹窗数据源;加一条数据即上架,`tests/plugin-list.spec.ts` 守护)
@@ -96,5 +96,3 @@
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 标记即为规范用法。
@@ -89,8 +89,6 @@ Cursor (SSH)
**实施偏差(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(不处理,注释说明)。
@@ -98,18 +96,13 @@ Cursor (SSH)
- 自定义编辑器仅支持 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/file-tree-open-with.spec.tsx`(jsdom):右键 → 子菜单/图钉;pin 不选中不关闭;选子项回调关闭菜单;SSH 标签后缀;未接线隐藏。
- `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`。
@@ -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,80 +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 |
## 决策
- **基线 = 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)。
+3 -3
View File
@@ -1,10 +1,10 @@
{
"id": "dsh-external/dsh-better-sidebar",
"version": "0.19.0-alpha.1",
"id": "dsh-external/dsh-apes-sidebar",
"version": "1.1.0",
"main": "./lib/index.js",
"description": "VSCode 风格右侧侧边栏:文件资源管理器 / 编辑器 / 终端 / Git / 浏览器,按会话隔离。暴露服务供其他插件注册侧边栏页面和文件预览器",
"engines": {
"dsh": ">=0.1.5-alpha.2"
"dsh": ">=0.1.2-rc.1"
},
"contributes": {
"tools": [],
-88
View File
@@ -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 } },
},
)
+58 -63
View File
@@ -1,12 +1,18 @@
{
"name": "dsh-better-sidebar",
"version": "0.19.0-alpha.1",
"description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes a service for other plugins to register sidebar tabs and file viewers.",
"name": "dsh-apes-sidebar",
"version": "1.1.0",
"description": "APE fork of dsh-better-sidebar v0.18.0: VSCode-like right sidebar (explorer / editor / terminal / git / browser) + apes:* information-layer tabs (agents / apps / profile). Forked from omdsh-dev/DSH-better-sidebar under MIT.",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/omdsh-dev/DSH-better-sidebar"
},
"forkedFrom": {
"name": "dsh-better-sidebar",
"version": "0.18.0",
"commit": "9e1a034",
"url": "https://github.com/omdsh-dev/DSH-better-sidebar"
},
"publishConfig": {
"access": "public"
},
@@ -49,7 +55,6 @@
"@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"
],
"platform": "web"
@@ -67,6 +72,7 @@
"src",
"scripts/install.sh",
"scripts/install.ps1",
"scripts/switch.sh",
"cordis.patch.yml",
"README.md",
"README_EN.md",
@@ -84,25 +90,27 @@
"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.2",
"@deepseek-ai/dsh-agent": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-client-locale": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-client-ui-sidebar-right": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-host-webserver": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-invariants": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-llm": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-session": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-settings": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-subagent": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-tools": "^0.1.5-alpha.2",
"@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
"@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
"@deepseek-ai/dsh-subagent": "^0.1.5-rc.2",
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
@@ -110,9 +118,6 @@
"peerDependenciesMeta": {
"@huanlin/dsh-plugin-better-locale": {
"optional": true
},
"@deepseek-ai/dsh-client-ui-sidebar-right": {
"optional": true
}
},
"dependencies": {
@@ -149,44 +154,47 @@
},
"devDependencies": {
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-agent": "0.1.5-alpha.2",
"@deepseek-ai/dsh-agent-loop": "0.1.5-alpha.2",
"@deepseek-ai/dsh-attachment": "0.1.5-alpha.2",
"@deepseek-ai/dsh-client-locale": "0.1.5-alpha.2",
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-alpha.2",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-alpha.2",
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-alpha.2",
"@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.5-alpha.2",
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-alpha.2",
"@deepseek-ai/dsh-code-runtime": "0.1.5-alpha.2",
"@deepseek-ai/dsh-host-webserver": "0.1.5-alpha.2",
"@deepseek-ai/dsh-invariants": "0.1.5-alpha.2",
"@deepseek-ai/dsh-llm": "0.1.5-alpha.2",
"@deepseek-ai/dsh-scope": "0.1.5-alpha.2",
"@deepseek-ai/dsh-session": "0.1.5-alpha.2",
"@deepseek-ai/dsh-session-persistence": "0.1.5-alpha.2",
"@deepseek-ai/dsh-session-projection": "0.1.5-alpha.2",
"@deepseek-ai/dsh-settings": "0.1.5-alpha.2",
"@deepseek-ai/dsh-subagent": "0.1.5-alpha.2",
"@deepseek-ai/dsh-system-prompt": "0.1.5-alpha.2",
"@deepseek-ai/dsh-tools": "0.1.5-alpha.2",
"@deepseek-ai/dsh-user-approval": "0.1.5-alpha.2",
"@deepseek-ai/dsh-util-time": "0.1.5-alpha.2",
"@deepseek-ai/dsh-agent": "0.1.5-rc.2",
"@deepseek-ai/dsh-attachment": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
"@deepseek-ai/dsh-code-runtime": "0.1.5-rc.2",
"@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
"@deepseek-ai/dsh-invariants": "0.1.5-rc.2",
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
"@deepseek-ai/dsh-scope": "0.1.5-rc.2",
"@deepseek-ai/dsh-session": "0.1.5-rc.2",
"@deepseek-ai/dsh-session-projection": "0.1.5-rc.2",
"@deepseek-ai/dsh-settings": "0.1.5-rc.2",
"@deepseek-ai/dsh-subagent": "0.1.5-rc.2",
"@deepseek-ai/dsh-system-prompt": "0.1.5-rc.2",
"@deepseek-ai/dsh-tools": "0.1.5-rc.2",
"@deepseek-ai/dsh-user-approval": "0.1.5-rc.2",
"@deepseek-ai/dsh-util-time": "0.1.5-rc.2",
"@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",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"anser": "^2.3.5",
"cordis": "^4.0.0-rc.8",
"jsdom": "^29.1.1",
"katex": "^0.16.47",
"lightningcss": "^1.32.0",
"react": "^18.2.0",
"react-dom": "18.2.0",
"tsdown": "^0.22.2",
"typescript": "^5.6.0",
"unrun": "^0.2.39",
"vitest": "^4.1.8",
"anser": "^2.3.5",
"shiki": "^4.3.1",
"@shikijs/langs": "^4.3.1",
"katex": "^0.16.47",
"mdast-util-from-markdown": "^2.0.3",
"mdast-util-gfm": "^3.1.0",
"mdast-util-math": "^3.0.0",
@@ -197,19 +205,6 @@
"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",
"shiki": "^4.3.1",
"react": "^18.2.0",
"react-dom": "18.2.0",
"tsdown": "^0.22.2",
"typescript": "^5.6.0",
"unrun": "^0.2.39",
"vitest": "^4.1.8",
"@eslint/js": "^10.0.1",
"eslint": "^10.9.1",
"eslint-plugin-react-hooks": "^7.1.1",
"globals": "^17.12.0",
"typescript-eslint": "^8.69.0"
"micromark-util-symbol": "^2.0.1"
}
}
+195
View File
@@ -0,0 +1,195 @@
{
"name": "dsh-apes-sidebar",
"version": "1.0.0",
"description": "APE fork of dsh-better-sidebar v0.18.0: VSCode-like right sidebar (explorer / editor / terminal / git / browser) + apes:* information-layer tabs (tasks / agents / apps / messages / profile). Forked from omdsh-dev/DSH-better-sidebar under MIT.",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/omdsh-dev/DSH-better-sidebar"
},
"forkedFrom": {
"name": "dsh-better-sidebar",
"version": "0.18.0",
"commit": "9e1a034",
"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"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"engines": {
"node": ">=20"
},
"packageManager": "pnpm@11.8.0",
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
},
"client": {
"inject": [
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-slots",
"@deepseek-ai/dsh-client-ui-conversation",
"@deepseek-ai/dsh-client-modules"
],
"platform": "web"
}
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/client.js",
"lib/client-registry.js",
"lib/client-terminal.js",
"lib/client-editor.js",
"lib/client-mermaid.js",
"lib/types/**/*.d.ts",
"src",
"scripts/install.sh",
"scripts/install.ps1",
"scripts/switch.sh",
"cordis.patch.yml",
"README.md",
"README_EN.md",
"LICENSE"
],
"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: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",
"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.2",
"@deepseek-ai/dsh-agent": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
"@deepseek-ai/dsh-host-webserver": "^0.1.2-rc.1",
"@deepseek-ai/dsh-invariants": "^0.1.2-rc.1",
"@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
"@deepseek-ai/dsh-session": "^0.1.2-rc.1",
"@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
"@deepseek-ai/dsh-subagent": "^0.1.2-rc.1",
"@deepseek-ai/dsh-tools": "^0.1.2-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
}
},
"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",
"@lezer/highlight": "^1.2.3",
"clsx": "^2.1.1",
"dompurify": "^3.4.14",
"mermaid": "^11.16.1",
"node-pty": "^1.1.0",
"react-icons": "5.7.0",
"rxjs": "^7.8.2",
"schemastery": "^3.18.0",
"ws": "^8.18.0"
},
"devDependencies": {
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-agent": "0.1.2-rc.1",
"@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-locale": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-conversation": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-settings": "0.1.2-rc.1",
"@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
"@deepseek-ai/dsh-code-runtime": "0.1.2-rc.1",
"@deepseek-ai/dsh-host-webserver": "0.1.2-rc.1",
"@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
"@deepseek-ai/dsh-llm": "0.1.2-rc.1",
"@deepseek-ai/dsh-scope": "0.1.2-rc.1",
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
"@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
"@deepseek-ai/dsh-settings": "0.1.2-rc.1",
"@deepseek-ai/dsh-subagent": "0.1.2-rc.1",
"@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
"@deepseek-ai/dsh-tools": "0.1.2-rc.1",
"@deepseek-ai/dsh-user-approval": "0.1.2-rc.1",
"@deepseek-ai/dsh-util-time": "0.1.2-rc.1",
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
"@playwright/test": "^1.62.1",
"@types/node": "^24.0.0",
"@types/react": "~18.3.1",
"@types/react-dom": "~18.3.1",
"@types/ws": "^8.5.10",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"cordis": "^4.0.0-rc.8",
"jsdom": "^29.1.1",
"lightningcss": "^1.32.0",
"react": "^18.2.0",
"react-dom": "18.2.0",
"tsdown": "^0.22.2",
"typescript": "^5.6.0",
"unrun": "^0.2.39",
"vitest": "^4.1.8"
}
}
+195
View File
@@ -0,0 +1,195 @@
{
"name": "dsh-apes-sidebar",
"version": "1.0.0",
"description": "APE fork of dsh-better-sidebar v0.18.0: VSCode-like right sidebar (explorer / editor / terminal / git / browser) + apes:* information-layer tabs (tasks / agents / apps / messages / profile). Forked from omdsh-dev/DSH-better-sidebar under MIT.",
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/omdsh-dev/DSH-better-sidebar"
},
"forkedFrom": {
"name": "dsh-better-sidebar",
"version": "0.18.0",
"commit": "9e1a034",
"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"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"engines": {
"node": ">=20"
},
"packageManager": "pnpm@11.8.0",
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
},
"client": {
"inject": [
"@deepseek-ai/dsh-client-locale",
"@deepseek-ai/dsh-client-ui-slots",
"@deepseek-ai/dsh-client-ui-conversation",
"@deepseek-ai/dsh-client-modules"
],
"platform": "web"
}
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/client.js",
"lib/client-registry.js",
"lib/client-terminal.js",
"lib/client-editor.js",
"lib/client-mermaid.js",
"lib/types/**/*.d.ts",
"src",
"scripts/install.sh",
"scripts/install.ps1",
"scripts/switch.sh",
"cordis.patch.yml",
"README.md",
"README_EN.md",
"LICENSE"
],
"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: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",
"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.2",
"@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
"@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
"@deepseek-ai/dsh-subagent": "^0.1.5-rc.2",
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
"@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
}
},
"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",
"@lezer/highlight": "^1.2.3",
"clsx": "^2.1.1",
"dompurify": "^3.4.14",
"mermaid": "^11.16.1",
"node-pty": "^1.1.0",
"react-icons": "5.7.0",
"rxjs": "^7.8.2",
"schemastery": "^3.18.0",
"ws": "^8.18.0"
},
"devDependencies": {
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-agent": "0.1.5-rc.2",
"@deepseek-ai/dsh-attachment": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
"@deepseek-ai/dsh-code-runtime": "0.1.5-rc.2",
"@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
"@deepseek-ai/dsh-invariants": "0.1.5-rc.2",
"@deepseek-ai/dsh-llm": "0.1.5-rc.2",
"@deepseek-ai/dsh-scope": "0.1.5-rc.2",
"@deepseek-ai/dsh-session": "0.1.5-rc.2",
"@deepseek-ai/dsh-session-projection": "0.1.5-rc.2",
"@deepseek-ai/dsh-settings": "0.1.5-rc.2",
"@deepseek-ai/dsh-subagent": "0.1.5-rc.2",
"@deepseek-ai/dsh-system-prompt": "0.1.5-rc.2",
"@deepseek-ai/dsh-tools": "0.1.5-rc.2",
"@deepseek-ai/dsh-user-approval": "0.1.5-rc.2",
"@deepseek-ai/dsh-util-time": "0.1.5-rc.2",
"@huanlin/dsh-plugin-better-locale": "^0.1.0",
"@playwright/test": "^1.62.1",
"@types/node": "^24.0.0",
"@types/react": "~18.3.1",
"@types/react-dom": "~18.3.1",
"@types/ws": "^8.5.10",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"cordis": "^4.0.0-rc.8",
"jsdom": "^29.1.1",
"lightningcss": "^1.32.0",
"react": "^18.2.0",
"react-dom": "18.2.0",
"tsdown": "^0.22.2",
"typescript": "^5.6.0",
"unrun": "^0.2.39",
"vitest": "^4.1.8"
}
}
+240 -1631
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# Consumer-facing type surface check: build a tiny standalone consumer that
# imports `dsh-better-sidebar/client/service` WITHOUT @types/node and with
# 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
@@ -35,14 +35,14 @@ mkdir -p "$WORK/node_modules"
# (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-better-sidebar"
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-better-sidebar/package.json" "$WORK/node_modules/@deepseek-ai/cordis/package.json"; do
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
@@ -51,14 +51,14 @@ for pkg_json in "$WORK/node_modules/dsh-better-sidebar/package.json" "$WORK/node
done
cat > "$WORK/check.ts" <<'EOF'
import { SIDEBAR_FEATURES, SIDEBAR_SERVICE_VERSION } from 'dsh-better-sidebar/client/service'
import type {} from 'dsh-better-sidebar/client/service'
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-better-sidebar/client/service'
import type { SessionScope, SidebarSnapshot, SidebarState, SidebarStore, SidebarTab } from 'dsh-better-sidebar/client/service'
import type { SidebarPrefs } from 'dsh-better-sidebar/client/service'
} 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.
@@ -126,14 +126,14 @@ 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-better-sidebar|lib/types|check\.ts" "$WORK/strict.log" | grep -vE "node_modules/[^ ]*(cordis|cosmokit)"; then
echo "[check-consumer-types] FAIL: errors in the dsh-better-sidebar declaration surface:" >&2
grep -E "dsh-better-sidebar|lib/types|check\.ts" "$WORK/strict.log" >&2
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-better-sidebar\|lib/types\|check\.ts" "$WORK/strict.log" | head -5 || true
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."
+3 -3
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
# =============================================================================
# dsh-better-sidebar 聚合双挂载冒烟(CI + 本地):
# dsh-apes-sidebar 聚合双挂载冒烟(CI + 本地):
#
# 场景:一个聚合 bundle(tests/fixtures/aggregate-better-sidebar)先以独立
# 条目 id 挂载 dsh-better-sidebar,插件自身的 bundle patch 后到 —— 旧行为
# 条目 id 挂载 dsh-apes-sidebar,插件自身的 bundle patch 后到 —— 旧行为
# 两个实例都注册 /sidebar/api,整个插件树启动失败(duplicate prefix
# route);新行为插件行检测到已有启用中的同包名挂载后自动 disabled,由
# 聚合行接管 sidebar。
@@ -22,7 +22,7 @@
# 环境变量(均可省略):
# DSH_CMD dsh 命令;缺省 PATH 上的 `dsh`,回退 npx 拉官方包
# (同 e2e-mount.sh)
# TARBALL 插件 tarball;缺省仓库根 dsh-better-sidebar-*.tgz(须已 pack)
# TARBALL 插件 tarball;缺省仓库根 dsh-apes-sidebar-*.tgz(须已 pack)
# PORT 固定端口(默认 0 = OS 分配,从日志解析 URL)
# DSH_HOME_BASE scratch 根目录(默认系统临时目录)。脚本始终在其下新建
# 本调用拥有的独立子目录,只写入/删除该子目录;调用方提供
+3 -3
View File
@@ -57,9 +57,9 @@ e2e_resolve_dsh_cmd() {
# 过期产物上。未找到候选时返回非零,由调用方按各自文案 die。
e2e_resolve_tarball() {
if [ -z "${TARBALL:-}" ]; then
TARBALL="$(ls -t "$ROOT"/dsh-better-sidebar-*.tgz 2>/dev/null | head -1 || true)"
TARBALL="$(ls -t "$ROOT"/dsh-apes-sidebar-*.tgz 2>/dev/null | head -1 || true)"
local count
count="$(ls "$ROOT"/dsh-better-sidebar-*.tgz 2>/dev/null | wc -l | tr -d ' ' || true)"
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" ]
@@ -134,7 +134,7 @@ allowBuilds:
protobufjs: true
minimumReleaseAgeExclude:
- dsh-better-sidebar
- dsh-apes-sidebar
- '@deepseek-ai/*'
EOF
}
+7 -7
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# =============================================================================
# dsh-better-sidebar 挂载冒烟编排(CI + 本地):
# dsh-apes-sidebar 挂载冒烟编排(CI + 本地):
#
# 1. 用官方 CLI 把 npm 打包产物(tarball)真实挂载进一个全新 scratch
# profile(`dsh plugin --profile web add file:<tarball>`,触发
@@ -14,7 +14,7 @@
#
# 环境变量(均可省略):
# DSH_CMD dsh 命令;缺省 PATH 上的 `dsh`,回退 npx 拉官方包
# TARBALL 插件 tarball;缺省仓库根 dsh-better-sidebar-*.tgz(须已 pack)
# TARBALL 插件 tarball;缺省仓库根 dsh-apes-sidebar-*.tgz(须已 pack)
# PORT 固定端口(默认 0 = OS 分配,从日志解析 URL)
# DSH_HOME_BASE 覆盖 scratch 根目录(默认系统临时目录)。脚本始终在其下
# 新建本调用拥有的独立子目录,只写入/删除该子目录;调用方
@@ -44,7 +44,7 @@ e2e_require_cmd pnpm "dsh plugin 转发给 pnpm"
e2e_resolve_dsh_cmd
e2e_resolve_tarball || die "找不到 tarball(TARBALL 或 \$ROOT/dsh-better-sidebar-*.tgz)——先运行 pnpm build && pnpm pack"
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"
@@ -72,18 +72,18 @@ e2e_write_profile "$PROFILE_DIR"
say "执行 dsh plugin --profile web add file:$TARBALL ..."
$DSH_CMD plugin --profile web add "file:$TARBALL"
# 步骤 3:校验挂载生效(dsh.profile.bundles 含 dsh-better-sidebar)
# 步骤 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-better-sidebar") ? 0 : 1);
process.exit(bundles.includes("dsh-apes-sidebar") ? 0 : 1);
' "$PROFILE_DIR/package.json"; then
warn "dsh-better-sidebar 未出现在 dsh.profile.bundles 中——挂载未注册"
warn "dsh-apes-sidebar 未出现在 dsh.profile.bundles 中——挂载未注册"
cat "$PROFILE_DIR/package.json"
exit 1
fi
say "挂载已注册:dsh.profile.bundles 包含 dsh-better-sidebar"
say "挂载已注册:dsh.profile.bundles 包含 dsh-apes-sidebar"
# 步骤 4:启动 dsh web(--port 0 = OS 分配,避免端口冲突;keyless 可起)
say "启动 dsh web(port=${PORT})..."
+10 -10
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
# =============================================================================
# dsh-better-sidebar 一键安装脚本(官方 CLI 方式,macOS / Linux / Windows Git Bash)
# dsh-apes-sidebar 一键安装脚本(官方 CLI 方式,macOS / Linux / Windows Git Bash)
#
# 通过 DSH 官方插件命令安装 npm 包并自动挂载:
# dsh plugin --profile web add dsh-better-sidebar@<version>
# dsh plugin --profile web add dsh-apes-sidebar@<version>
#
# 包内声明了 dsh.bundle.patch(cordis.patch.yml):CLI 的 bundle 协调会把它
# 自动加进 profile 的 dsh.profile.bundles,下次启动即挂载——无需手动写
@@ -38,8 +38,8 @@
# minimumReleaseAgeExclude(幂等),放行本插件,避免"重跑一次才成功"。
# - 老版本(<0.10.2)用手动挂载行,bundle 通道激活后需移除,否则双挂载
# (Node 半挂两次、页面两个侧边栏)。脚本会幂等移除 better-sidebar 挂载行。
# - 回滚:dsh plugin --profile web remove dsh-better-sidebar,或把 profile 依赖
# 改回 "dsh-better-sidebar": "link:<路径>" 再 pnpm install。
# - 回滚:dsh plugin --profile web remove dsh-apes-sidebar,或把 profile 依赖
# 改回 "dsh-apes-sidebar": "link:<路径>" 再 pnpm install。
# =============================================================================
set -euo pipefail
@@ -47,7 +47,7 @@ set -euo pipefail
for arg in "$@"; do
if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then
cat <<'EOF'
dsh-better-sidebar 一键安装 / 依赖修复脚本
dsh-apes-sidebar 一键安装 / 依赖修复脚本
用法:
bash scripts/install.sh [版本] [--restart] [--dry-run] [--profile <名>]
@@ -67,7 +67,7 @@ done
DSH_HOME="${DSH_HOME:-${HOME:-${USERPROFILE:-}}/.dsh}"
REGISTRY="${REGISTRY:-https://registry.npmjs.org}"
PKG="dsh-better-sidebar"
PKG="dsh-apes-sidebar"
DSH_CMD="${DSH_CMD:-dsh}"
RESTART=false
@@ -118,11 +118,11 @@ if (!/^\s*allowBuilds:\s*$/m.test(t)) {
}
}
// minimumReleaseAgeExclude:放行本插件(版本无关),避免 <24h 新版本被拒
if (!/^\s*-\s+dsh-better-sidebar\s*$/m.test(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-better-sidebar");
t = t.replace(/^(\s*minimumReleaseAgeExclude:\s*)$/m, "$1\n - dsh-apes-sidebar");
} else {
t += "\nminimumReleaseAgeExclude:\n - dsh-better-sidebar\n";
t += "\nminimumReleaseAgeExclude:\n - dsh-apes-sidebar\n";
}
}
if (t !== before) fs.writeFileSync(p, t);
@@ -224,7 +224,7 @@ if ! node -e '
const bundles = p.dsh?.profile?.bundles ?? [];
process.exit(bundles.includes(process.argv[2]) ? 0 : 1);
' "$PROFILE_DIR/package.json" "$PKG"; then
warn "dsh-better-sidebar 未出现在 dsh.profile.bundles 中——挂载未注册。"
warn "dsh-apes-sidebar 未出现在 dsh.profile.bundles 中——挂载未注册。"
warn "若上面的 pnpm 输出提示 ignored build scripts,请确认 $WS_YML 的 allowBuilds 后重跑本脚本。"
exit 1
fi
+2 -2
View File
@@ -6,7 +6,7 @@
* pnpm build
* node scripts/package-registry.mjs
* dsh registry install ./registry
* dsh registry enable dsh-external/dsh-better-sidebar
* 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
@@ -54,4 +54,4 @@ for (const file of files) {
cpSync(source, target)
}
console.log(`registry/ assembled (${files.length} files)`)
console.log('next: dsh registry install ./registry && dsh registry enable dsh-external/dsh-better-sidebar')
console.log('next: dsh registry install ./registry && dsh registry enable dsh-external/dsh-apes-sidebar')
+294
View File
@@ -0,0 +1,294 @@
#!/usr/bin/env bash
# =============================================================================
# dsh-apes-sidebar 两线切换脚本(apes fork 线 ↔ upstream better-sidebar 线)
#
# 同一个 DSH profile 内**二选一**切换侧边栏插件——双装不可行(两条线都注册
# 进 `ctx.betterSidebar`,cordis entry id 冲突,第二个被丢弃)。切换 =
# 换 profile 依赖 + 换 dsh.profile.bundles + pnpm install + 提示重启。
#
# aps 本 fork 线(dsh-apes-sidebar,含 4 个 apes:* 信息层 tab)
# 依赖:dsh-apes-sidebar: <file: 本地路径>(本地开发)或
# dsh-apes-sidebar@<版本>(npm,发布后)
# upstream upstream 线(dsh-better-sidebar,官方 8 tab + 生态)
# 依赖:dsh-better-sidebar@<版本>(registry,默认 latest)
#
# 用法:
# bash scripts/switch.sh aps # 切到本 fork 线(本地 file: 安装)
# bash scripts/switch.sh upstream # 切到 upstream 线(registry 安装)
# bash scripts/switch.sh upstream 0.18.0 # 指定 upstream 版本
# bash scripts/switch.sh --status # 只看当前线,不改任何东西
#
# 通用 flag(两线均适用):
# --profile <名> 目标 profile(缺省 desktop)
# --dry-run 只打印将执行的操作,不写任何文件
# -h/--help 打印本帮助
#
# 环境(均可省略,脚本自动探测):
# DSH_HOME 默认 ~/.dsh
# REGISTRY 默认 https://registry.npmjs.org
# DSH_CMD 默认 PATH 上的 dsh,缺省 npx -y --package @deepseek-ai/dsh
# APS_SRC aps 线本地源码路径(缺省 = 本仓库根,即 file: 指向自己)
#
# 机制(幂等,可重复跑):
# 1. 探测当前线(依赖里哪个包在)
# 2. 与目标线相同 → 提示后直接退出(不重装)
# 3. 移除另一线的依赖 + 从 bundles 摘除另一线的 bundle
# 4. 写入目标线依赖 + 确保 workspace 放行 node-pty/protobufjs 构建
# 5. cd profile && pnpm install(reconcileProfilePnpmWorkspace 会重建
# node_modules——这是预期行为,两线不共存)
# 6. 确保 bundles 含目标线包名(bundle 通道挂载生效的判据)
# 7. 幂等移除 cordis.patch.yml 里对手线的手动挂载行(避免双挂载)
# 8. 提示重启 DSH
#
# 回滚:跑 `bash scripts/switch.sh <另一线>` 即可切回去。
# =============================================================================
set -euo pipefail
for arg in "$@"; do
if [ "$arg" = "-h" ] || [ "$arg" = "--help" ]; then
cat <<'EOF'
dsh-apes-sidebar 两线切换(同一 profile 内二选一)
用法:
bash scripts/switch.sh <aps|upstream> [版本]
bash scripts/switch.sh --status
aps 切到本 fork 线(dsh-apes-sidebar,4 个 apes:* tab)
upstream 切到 upstream 线(dsh-better-sidebar,官方)
<版本> 仅对 upstream 生效(缺省 latest)
--status 只看当前线,不改动
--profile 目标 profile(缺省 desktop)
--dry-run 只打印操作,不写文件
-h/--help 本帮助
环境变量:DSH_HOME、REGISTRY、DSH_CMD、APS_SRC(aps 本地源码路径)
EOF
exit 0
fi
done
DSH_HOME="${DSH_HOME:-${HOME:-${USERPROFILE:-}}/.dsh}"
REGISTRY="${REGISTRY:-https://registry.npmjs.org}"
DSH_CMD="${DSH_CMD:-dsh}"
# aps 线本地源码:缺省 = 本仓库根(脚本在 scripts/ 下,根 = 上一级)
APS_SRC="${APS_SRC:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
APS_PKG="dsh-apes-sidebar"
UP_PKG="dsh-better-sidebar"
DRY_RUN=false
STATUS_ONLY=false
TARGET=""
UP_VERSION=""
PROFILE_NAME="desktop"
while [ $# -gt 0 ]; do
case "$1" in
--status) STATUS_ONLY=true ;;
--dry-run) DRY_RUN=true ;;
--profile)
if [ $# -lt 2 ]; then echo "--profile 需要一个 profile 名(如 desktop)" >&2; exit 2; fi
PROFILE_NAME="$2"; shift ;;
aps|upstream) TARGET="$1" ;;
-h|--help) : ;;
-*) echo "未知参数: ${1}(用 -h 查看用法)" >&2; exit 2 ;;
*) UP_VERSION="$1" ;; # 版本参数(仅 upstream 用)
esac
shift
done
PROFILE_DIR="$DSH_HOME/profiles/$PROFILE_NAME"
PKG_JSON="$PROFILE_DIR/package.json"
WS_YML="$PROFILE_DIR/pnpm-workspace.yaml"
PATCH_YML="$PROFILE_DIR/cordis.patch.yml"
say() { printf '\033[32m[switch]\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; }
command -v node >/dev/null 2>&1 || die "未找到 node(DSH 运行需要 Node.js ≥ 20)。"
[ -d "$PROFILE_DIR" ] || die "找不到 profile 目录:${PROFILE_DIR}(请先初始化 ${PROFILE_NAME} profile)"
[ -f "$PKG_JSON" ] || die "找不到 ${PKG_JSON}(profile 未初始化)"
# ── 探测当前线(依赖里哪个包在)─────────────────────────────────────────
current_line() {
node -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
const deps = p.dependencies || {};
if (deps["dsh-apes-sidebar"]) console.log("aps");
else if (deps["dsh-better-sidebar"]) console.log("upstream");
else console.log("none");
' "$PKG_JSON"
}
if [ "$STATUS_ONLY" = true ]; then
CUR="$(current_line)"
say "profile: ${PROFILE_NAME} (${PROFILE_DIR})"
case "$CUR" in
aps) say "当前线:aps(${APS_PKG})" ;;
upstream) say "当前线:upstream(${UP_PKG})" ;;
none) say "当前线:无(两条线都未装)" ;;
esac
# 附带显示 bundles 里的 sidebar 条目
node -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
const b = (p.dsh && p.dsh.profile && p.dsh.profile.bundles) || [];
const side = b.filter(x => /sidebar/.test(x));
console.log("bundles 里的 sidebar 条目: " + (side.length ? side.join(", ") : "(无)"));
' "$PKG_JSON"
exit 0
fi
[ -n "$TARGET" ] || die "缺少目标线(aps | upstream)。用 --status 查看当前线。"
CUR="$(current_line)"
case "$TARGET" in
aps) T_PKG="$APS_PKG"; T_DEP="file:${APS_SRC}" ;;
upstream)
T_PKG="$UP_PKG"
# pnpm 从依赖 key 取包名,值只给版本 spec(不含包名,否则双 @)
case "${UP_VERSION:-latest}" in
latest) T_DEP="latest" ;;
*) T_DEP="$UP_VERSION" ;;
esac
;;
esac
[ "$CUR" = "$TARGET" ] && {
say "当前已是 ${TARGET} 线(${T_PKG}),无需切换。"
say "如需强制重装:先跑 --status 确认,再手动移除依赖后重跑。"
exit 0
}
OTHER_PKG=""
case "$TARGET" in
aps) OTHER_PKG="$UP_PKG" ;;
upstream) OTHER_PKG="$APS_PKG" ;;
esac
say "切换:${CUR} → ${TARGET}(profile: ${PROFILE_NAME})"
say "目标依赖:${T_DEP}"
if [ "$DRY_RUN" = true ]; then
say "[dry-run] 步骤 1:移除 ${OTHER_PKG:-(无)} 依赖 + 从 bundles 摘除 ${OTHER_PKG:-(无)}"
say "[dry-run] 步骤 2:写入 ${T_PKG} 依赖(${T_DEP})"
say "[dry-run] 步骤 3:确保 ${WS_YML} 放行 node-pty/protobufjs 构建"
say "[dry-run] 步骤 4:cd ${PROFILE_DIR} && pnpm install(重建 node_modules)"
say "[dry-run] 步骤 5:确保 bundles 含 ${T_PKG}"
say "[dry-run] 步骤 6:幂等移除 ${PATCH_YML} 里 ${OTHER_PKG:-对手线} 的手动挂载行"
say "[dry-run] 步骤 7:提示重启 DSH"
exit 0
fi
# ── 步骤 1+2+5 合并:一次 node 调用完成依赖 + bundles 的对称改写 ─────────
# 移除对手线依赖 + bundle,写入目标线依赖 + 确保目标 bundle 在列。幂等。
node -e '
const fs = require("fs");
const p = process.argv[1];
const keep = process.argv[2]; // 目标包名
const depSpec = process.argv[3]; // 目标依赖 spec(file:... 或 pkg@ver)
const remove = process.argv[4]; // 对手包名(可能为空串)
const t = JSON.parse(fs.readFileSync(p, "utf8"));
t.dependencies = t.dependencies || {};
// 移除对手线依赖
if (remove) { const had = t.dependencies[remove]; delete t.dependencies[remove]; if (had) console.log("removed dep: " + remove + "=" + had); }
// 写入目标线依赖
t.dependencies[keep] = depSpec;
// bundles:摘除对手线,确保目标线在列
const prof = (t.dsh && t.dsh.profile) || {};
const b = prof.bundles || [];
const nb = b.filter(x => x !== remove);
if (!nb.includes(keep)) nb.push(keep);
t.dsh = t.dsh || {}; t.dsh.profile = prof; prof.bundles = nb;
fs.writeFileSync(p, JSON.stringify(t, null, 2) + "\n");
console.log("deps now: " + JSON.stringify(t.dependencies));
console.log("bundles now: " + nb.join(", "));
' "$PKG_JSON" "$T_PKG" "$T_DEP" "$OTHER_PKG"
say "依赖 + bundles 已改写"
# ── 步骤 3:确保 workspace 放行 node-pty/protobufjs 构建(幂等)─────────
if [ -f "$WS_YML" ]; then
WS_RESULT="$(node -e '
const fs = require("fs");
const p = process.argv[1];
let t = fs.readFileSync(p, "utf8");
const before = t;
t = t.replace(/^(\s*)(node-pty|protobufjs):.*$/gm, "$1$2: true");
if (!/^\s*allowBuilds:\s*$/m.test(t)) {
t += "\nallowBuilds:\n node-pty: true\n protobufjs: true\n";
} else {
for (const k of ["node-pty", "protobufjs"]) {
if (!new RegExp("^\\s*" + k + ":\\s*true\\s*$", "m").test(t)) {
t = t.replace(/^(\s*allowBuilds:\s*)$/m, "$1\n " + k + ": true");
}
}
}
if (t !== before) fs.writeFileSync(p, t);
console.log(t === before ? "unchanged" : "updated");
' "$WS_YML")"
[ "$WS_RESULT" = "updated" ] && say "已确保 ${WS_YML}:allowBuilds(node-pty/protobufjs: true)" \
|| say "workspace 设置已就绪,跳过"
else
warn "找不到 ${WS_YML},跳过 workspace 预写(若 pnpm install 报 ignored build scripts 需手动补)"
fi
# ── 步骤 4:pnpm install(重建 node_modules——两线不共存,这是预期)──────
command -v pnpm >/dev/null 2>&1 || die "未找到 pnpm。请安装 pnpm 9+ 并加入 PATH。"
say "执行 cd ${PROFILE_DIR} && pnpm install(reconcileProfilePnpmWorkspace 会重建 node_modules)..."
if ! (cd "$PROFILE_DIR" && pnpm install); then
die "pnpm install 失败。请确认网络可用、依赖 spec 正确,然后重试。"
fi
# ── 步骤 5 校验:bundles 含目标包(挂载生效判据)────────────────────────
if ! node -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
const b = (p.dsh && p.dsh.profile && p.dsh.profile.bundles) || [];
process.exit(b.includes(process.argv[2]) ? 0 : 1);
' "$PKG_JSON" "$T_PKG"; then
warn "${T_PKG} 未出现在 dsh.profile.bundles——挂载未注册,请检查 ${PKG_JSON}。"
exit 1
fi
say "bundle 已注册:dsh.profile.bundles 含 ${T_PKG}(下次启动自动挂载)"
# ── 步骤 6:幂等移除对手线在 cordis.patch.yml 里的手动挂载行 ─────────────
if [ -f "$PATCH_YML" ]; then
MOUNT_RESULT="$(node -e '
const fs = require("fs");
const p = process.argv[1];
const removePkg = process.argv[2];
if (!removePkg) { console.log("none"); process.exit(0); }
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) => new RegExp("id:\\s*" + removePkg.replace(/[-/]/g, "\\$&") + "\\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" "$OTHER_PKG")"
[ "$MOUNT_RESULT" = "removed" ] && say "已从 ${PATCH_YML} 移除 ${OTHER_PKG} 的手动挂载行" \
|| say "无 ${OTHER_PKG} 手动挂载行,跳过"
else
say "无 ${PATCH_YML},跳过手动挂载行清理"
fi
# ── 步骤 7:提示重启 ────────────────────────────────────────────────────
say "切换完成:${CUR} → ${TARGET}(${T_PKG})"
say "下一步:重启 DSH 并硬刷新(Cmd/Ctrl+Shift+R)使新副本生效。"
+3 -3
View File
@@ -158,9 +158,9 @@ async function classifyTarget(raw: string, cwd: string): Promise<{ kind: AgentOp
info = await stat(target)
} catch (error) {
const code = (error as NodeJS.ErrnoException).code
if (code === 'ENOENT') throw new Error(`"${raw}" does not exist (resolved to "${target}")`, { cause: error })
if (code === 'EACCES' || code === 'EPERM') throw new Error(`"${target}" is not readable`, { cause: error })
throw new Error(`cannot open "${target}": ${error instanceof Error ? error.message : String(error)}`, { cause: error })
if (code === 'ENOENT') throw new Error(`"${raw}" does not exist (resolved to "${target}")`)
if (code === 'EACCES' || code === 'EPERM') throw new Error(`"${target}" is not readable`)
throw new Error(`cannot open "${target}": ${error instanceof Error ? error.message : String(error)}`)
}
const title = basenameOf(target)
return { kind: info.isDirectory() ? 'folder' : 'file', target, title: title === '' ? raw : title }
+12 -59
View File
@@ -149,56 +149,22 @@ function signalNameOf(signal: number | null | undefined): string | null {
return SIGNAL_NAMES[signal] ?? `signal ${signal}`
}
/**
* Compile a wait needle into a RegExp. The needle is treated as a JavaScript
* regular expression; a pattern that fails to compile ( e.g. an unbalanced
* group typed as a literal ) degrades to verbatim substring matching so
* legacy literal needles keep working.
*/
function compileNeedle(needle: string): RegExp | null {
try {
return new RegExp(needle)
} catch {
return null
}
}
/**
* Locate the first occurrence of `needle` in `transcript`, returning its
* line/column plus the actual matched text — for alternation patterns
* ( e.g. `(BUILD_OK|BUILD_FAIL)` ) the match tells which alternative hit.
* `re` is the precompiled form of `needle` (from {@link compileNeedle});
* `null` means verbatim substring matching.
*/
function locateNeedle(
transcript: string,
needle: string,
re: RegExp | null,
): { line: number; column: number; match: string } | undefined {
/** Locate the first occurrence of `needle` in `transcript`, returning its line/column. */
function locateNeedle(transcript: string, needle: string): { line: number; column: number } | undefined {
if (needle === '') return undefined
// Regex hit → use the match index/text; literal fallback → indexOf with
// the needle itself as the matched text.
let hit: { index: number; text: string } | undefined
if (re !== null) {
re.lastIndex = 0
const m = re.exec(transcript)
hit = m === null ? undefined : { index: m.index, text: m[0] }
} else {
const idx = transcript.indexOf(needle)
hit = idx === -1 ? undefined : { index: idx, text: needle }
}
if (hit === undefined) return undefined
const idx = transcript.indexOf(needle)
if (idx === -1) return undefined
// Walk the transcript up to the match index, counting newlines to derive
// the 0-based line; the column is the offset within that line.
let line = 0
let lineStart = 0
for (let i = 0; i < hit.index; i += 1) {
for (let i = 0; i < idx; i += 1) {
if (transcript.charCodeAt(i) === 0x0a /* \n */) {
line += 1
lineStart = i + 1
}
}
return { line, column: hit.index - lineStart, match: hit.text }
return { line, column: idx - lineStart }
}
/** One live agent terminal. */
@@ -242,17 +208,12 @@ export type AgentTerminalWaitResult =
| {
/** The needle was found in the transcript. */
kind: 'found'
/** The pattern that was awaited. */
/** The matched substring. */
needle: string
/** 0-based line index (in the retained transcript) where the needle first appeared. */
line: number
/** 0-based column index within that line where the match starts. */
column: number
/**
* The text that actually matched — for multi-outcome patterns
* ( e.g. `(BUILD_OK|BUILD_FAIL)` ) this tells which alternative matched.
*/
match: string
/** Elapsed wall-clock milliseconds from the wait start to the match. */
elapsedMs: number
}
@@ -495,12 +456,7 @@ export class AgentPtyRegistry {
* make event-driven wakeups unreliable. A 50ms poll is fast enough for
* interactive use and simple enough to be obviously correct.
* @param uuid - terminal to watch.
* @param needle - JavaScript regular expression to search for
* (case-sensitive); a pattern that fails to compile falls back to
* verbatim substring matching. May cover several outcomes at once
* ( e.g. `(BUILD_OK|BUILD_FAIL)` for build success vs failure ) — the
* returned `match` reports the text that actually matched, so callers
* can tell which outcome hit.
* @param needle - substring to search for (case-sensitive, verbatim).
* @param timeoutMs - max wait; default 10000 (10s). Clamped to ≥100ms.
* @param signal - caller-owned cancellation; aborts the wait re-throwing.
* @returns one of `found` / `timeout` / `exited`.
@@ -518,18 +474,15 @@ export class AgentPtyRegistry {
const timeout = Math.max(100, Math.floor(timeoutMs))
const start = Date.now()
const deadline = start + timeout
// Compile the needle once (regex semantics; invalid patterns degrade to
// verbatim matching) and reuse the compiled form across every poll.
const re = compileNeedle(needle)
// Fast path: already exited, or the needle is already in the transcript
// (a `terminal_send` may have produced the expected output before this
// call even started).
if (handle.exited) {
return { kind: 'exited', needle, exitCode: handle.exitCode ?? null, exitSignal: signalNameOf(handle.exitSignal) }
}
const firstHit = locateNeedle(handle.transcript, needle, re)
const firstHit = locateNeedle(handle.transcript, needle)
if (firstHit !== undefined) {
return { kind: 'found', needle, line: firstHit.line, column: firstHit.column, match: firstHit.match, elapsedMs: Date.now() - start }
return { kind: 'found', needle, line: firstHit.line, column: firstHit.column, elapsedMs: Date.now() - start }
}
// Poll loop: check the transcript every 50ms, exit on match / exit /
// abort / timeout. The handle is read live each iteration (its transcript
@@ -539,9 +492,9 @@ export class AgentPtyRegistry {
if (handle.exited) {
return { kind: 'exited', needle, exitCode: handle.exitCode ?? null, exitSignal: signalNameOf(handle.exitSignal) }
}
const hit = locateNeedle(handle.transcript, needle, re)
const hit = locateNeedle(handle.transcript, needle)
if (hit !== undefined) {
return { kind: 'found', needle, line: hit.line, column: hit.column, match: hit.match, elapsedMs: Date.now() - start }
return { kind: 'found', needle, line: hit.line, column: hit.column, elapsedMs: Date.now() - start }
}
if (Date.now() >= deadline) {
return { kind: 'timeout', needle, timeoutMs: timeout, totalLines: handle.transcript.split('\n').length }
-140
View File
@@ -1,140 +0,0 @@
/**
* Process-local live assistant stream buffer (DSH 0.1.5+).
*
* 0.1.2 appended a durable `assistant/chunk` session event for every model
* delta, so the side-chat transcript and the inherited in-progress snapshot
* could read streaming text straight out of the session log. 0.1.5 removed
* that event: an in-flight attempt now publishes `agent/assistant-stream`
* frames (start / chunk / end) that are NOT part of the log, and the durable
* record lands only at settlement — `assistant/message` (with the exact
* `stream` embedded) or `assistant/attempt` (a failed attempt that committed
* no message, also with its `stream`).
*
* This module folds those frames into a bounded per-session buffer of
* normalized chunks — the same information the old `assistant/chunk` events
* carried — so the plugin keeps a live transcript and an honest in-progress
* snapshot. The buffer is cleared when an attempt ends, so a settled step
* never duplicates its durable message.
*/
import type { Context } from './context-types.ts'
/** One normalized live delta, keyed by its attempt and dense position. */
export interface AssistantLiveChunk {
/** The attempt these chunks belong to (DSH `LlmAttemptId`). */
readonly attemptId: string
/** The turn the attempt belongs to. */
readonly turn: number
/** The step the attempt belongs to. */
readonly step: number
/** Dense zero-based position within the attempt (DSH's own index). */
readonly index: number
/** Safe-integer timestamp of the frame (reused by the durable stream). */
readonly time: number
/** The raw model stream chunk (`text-delta` / `reasoning-delta` / …). */
readonly chunk: Record<string, unknown>
}
/** The plugin's read face over the live frames of every session. */
export interface AssistantLiveBuffer {
/**
* The live chunks of one session's active attempt, in index order.
* @param sessionId - the session whose attempt is streaming.
* @returns the chunks; empty when nothing is streaming for that session.
*/
chunksFor(sessionId: string): readonly AssistantLiveChunk[]
/** Stop observing the agent stream. */
dispose(): void
}
/** Per-attempt buffer ceiling; beyond it the oldest deltas are dropped. */
export const LIVE_CHUNK_CAP = 4000
/** How many sessions may hold a live buffer at once (the oldest is dropped). */
export const LIVE_SESSION_CAP = 64
/** One session's active attempt as it accumulates. */
interface Attempt {
readonly attemptId: string
readonly turn: number
readonly step: number
readonly chunks: AssistantLiveChunk[]
/** The next dense frame index this attempt accepts (independent of the
* window below, which drops the oldest chunks once the cap is reached). */
nextIndex: number
}
/** A frame's `chunk` payload must be a JSON record to be worth buffering. */
function chunkOf(value: unknown): Record<string, unknown> | undefined {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: undefined
}
/** A finite non-negative integer, or undefined. */
function countOf(value: unknown): number | undefined {
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : undefined
}
/**
* Fold `agent/assistant-stream` frames into per-session live buffers.
*
* Frames arrive for every attached agent, so the buffer is keyed by the
* emitting session and ignores anything it cannot identify. An out-of-order
* or mismatched chunk drops the attempt rather than splicing a gap: a
* transcript with a hole is worse than one that settles at the next durable
* event.
* @param ctx - host plugin context (its `on` subscribes the session feed).
* @param cap - per-attempt chunk ceiling.
* @returns the read face and a disposer unbinding the listener.
*/
export function createAssistantLiveBuffer(ctx: Context, cap: number = LIVE_CHUNK_CAP): AssistantLiveBuffer {
const attempts = new Map<string, Attempt>()
const off = ctx.on('agent/assistant-stream', (payload) => {
const record = payload as { agent?: unknown; frame?: unknown } | null
const frame = record?.frame as Record<string, unknown> | undefined
if (frame === undefined) return
const session = (record?.agent as { session?: { id?: unknown } } | undefined)?.session
const sessionId = typeof session?.id === 'string' ? session.id : undefined
if (sessionId === undefined) return
const type = frame['type']
if (type === 'end') {
attempts.delete(sessionId)
return
}
const attemptId = typeof frame['attemptId'] === 'string' ? frame.attemptId : undefined
if (attemptId === undefined) return
if (type === 'start') {
const turn = countOf(frame['turn'])
const step = countOf(frame['step'])
if (turn === undefined || step === undefined) return
const known = attempts.has(sessionId)
attempts.delete(sessionId)
// Only a NEW session may evict the oldest tracked one.
if (!known && attempts.size >= LIVE_SESSION_CAP) {
const oldest = attempts.keys().next().value
if (oldest !== undefined) attempts.delete(oldest)
}
attempts.set(sessionId, { attemptId, turn, step, chunks: [], nextIndex: 0 })
return
}
if (type !== 'chunk') return
const attempt = attempts.get(sessionId)
if (attempt === undefined || attempt.attemptId !== attemptId) return
const index = countOf(frame['index'])
const chunk = chunkOf(frame['chunk'])
// Dense positions only: a gap means the buffer missed frames (a
// replacement attempt, a reconnect) and its text would be wrong.
if (index === undefined || chunk === undefined || index !== attempt.nextIndex) {
attempts.delete(sessionId)
return
}
attempt.nextIndex = index + 1
const time = countOf(frame['time']) ?? 0
attempt.chunks.push({ attemptId, turn: attempt.turn, step: attempt.step, index, time, chunk })
if (attempt.chunks.length > cap) attempt.chunks.splice(0, attempt.chunks.length - cap)
})
return {
chunksFor: sessionId => attempts.get(sessionId)?.chunks ?? [],
dispose: () => { off() },
}
}
+3 -78
View File
@@ -8,13 +8,12 @@
* very content it shows. Rendering goes through the shared {@link DiffFiles}
* renderer — the same one the changes tab's inline preview uses.
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { IconRefreshOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SessionScope } from './api.ts'
import { api } from './api.ts'
import type { SidebarDiffRef } from './state.ts'
import { DiffFiles } from './diff/DiffFiles.tsx'
import { displayPath, foldRowsFromContents, type DiffFile, type DiffRow, type FoldSegment } from './diff/rows.ts'
import { t } from './locales.ts'
import { resolveSidebarPath } from './produced-files.ts'
import css from './sidebar.module.css'
@@ -31,11 +30,6 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif
const [error, setError] = useState<string | null>(null)
const [data, setData] = useState<DiffData | null>(null)
const [tick, setTick] = useState(0)
// The staged flag of the side ACTUALLY rendered: when the requested side's
// diff came back empty the load falls back to the other side, and the fold
// expansion must read that side's revisions (else the sliced line numbers
// land on the wrong contents).
const [effectiveStaged, setEffectiveStaged] = useState<boolean | null>(null)
const refresh = useCallback((): void => { setTick(value => value + 1) }, [])
@@ -45,7 +39,6 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif
setLoading(true)
setError(null)
setData(null)
setEffectiveStaged(null)
const load = async (): Promise<void> => {
try {
if (diff.kind === 'commit') {
@@ -60,10 +53,7 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif
// file staged after its tab opened). Both sides empty means the
// file genuinely has no text changes.
const other = await api.gitDiff(scope, diff.path, !diff.staged, diff.worktree)
if (other.diff !== '') {
result = other
if (!cancelled) setEffectiveStaged(!diff.staged)
}
if (other.diff !== '') result = other
}
if (result.diff !== '') {
if (!cancelled) setData({ diff: result.diff })
@@ -92,71 +82,6 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif
return () => { cancelled = true }
}, [sessionId, cwd, diff, tick])
// ── On-demand git fold expansion: a fold's hidden rows come from both
// sides' full contents (git.show / fsRead), fetched ONCE per file so
// sibling folds share the request, then sliced by each fold's line
// ranges. The cache dies with the ref or a refresh tick. ──────────────
const foldContents = useRef(new Map<string, Promise<{ old: string; new: string }>>())
useEffect(() => { foldContents.current = new Map() }, [diff, tick])
const foldLoader = useMemo(() => {
const sidesOf = (file: DiffFile): Promise<{ old: string; new: string }> => {
// Both sides empty cannot cover a non-empty fold — treat it as a failed
// fetch so the fold degrades to the unavailable marker instead of
// silently expanding to nothing (the symptom of a bad rev or path
// reading null on both sides).
const ofSides = (oldContent: string | null, newContent: string | null): { old: string; new: string } => {
if ((oldContent ?? '') === '' && (newContent ?? '') === '') throw new Error('no content on either side')
return { old: oldContent ?? '', new: newContent ?? '' }
}
const fetchSides = async (): Promise<{ old: string; new: string }> => {
const scope: SessionScope = { sessionId, cwd, ...(diff.repoRoot !== undefined ? { repoRoot: diff.repoRoot } : {}) }
if (diff.kind === 'commit') {
// The patch's -m --first-parent shape: old side from the parent,
// new side from the commit (a root commit's parent read fails → '').
const [oldSide, newSide] = await Promise.all([
file.oldPath === '/dev/null'
? Promise.resolve({ content: null })
: api.gitShow(scope, `${diff.hashFull}^`, displayPath(file.oldPath), diff.worktree),
file.newPath === '/dev/null'
? Promise.resolve({ content: null })
: api.gitShow(scope, diff.hashFull, displayPath(file.newPath), diff.worktree),
])
return ofSides(oldSide.content, newSide.content)
}
// Worktree change: staged is HEAD vs index, unstaged is index vs
// worktree (the worktree side reads the live file).
const staged = effectiveStaged ?? diff.staged
if (staged) {
const [oldSide, newSide] = await Promise.all([
file.oldPath === '/dev/null'
? Promise.resolve({ content: null })
: api.gitShow(scope, 'HEAD', displayPath(file.oldPath), diff.worktree),
file.newPath === '/dev/null'
? Promise.resolve({ content: null })
: api.gitShow(scope, ':0', displayPath(file.newPath), diff.worktree),
])
return ofSides(oldSide.content, newSide.content)
}
const [oldSide, worktree] = await Promise.all([
file.oldPath === '/dev/null'
? Promise.resolve({ content: null })
: api.gitShow(scope, ':0', displayPath(file.oldPath), diff.worktree),
api.fsRead(scope, resolveSidebarPath(diff.repoRoot ?? diff.worktree ?? cwd, displayPath(file.newPath))).catch(() => null),
])
return ofSides(oldSide.content, worktree !== null && worktree.kind === 'text' ? worktree.content : null)
}
const path = displayPath(file.newPath === '/dev/null' ? file.oldPath : file.newPath)
let promise = foldContents.current.get(path)
if (promise === undefined) {
promise = fetchSides()
foldContents.current.set(path, promise)
}
return promise
}
return (file: DiffFile, segment: FoldSegment): Promise<readonly DiffRow[]> =>
sidesOf(file).then(sides => foldRowsFromContents(segment, sides.old, sides.new))
}, [sessionId, cwd, diff, effectiveStaged])
return (
<div className={css.gitDiffTab}>
<div className={css.gitDiffTabHeader}>
@@ -179,7 +104,7 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif
<>
{data.untracked !== undefined
? <DiffFiles diff="" untrackedPath={diff.kind === 'worktree' ? diff.path : ''} untrackedContent={data.untracked} />
: <DiffFiles diff={data.diff} resolveFold={foldLoader} />}
: <DiffFiles diff={data.diff} />}
{data.diff === '' && data.untracked === undefined && (
<div className={css.gitEmpty}>{t('diffEmpty')}</div>
)}
+5 -22
View File
@@ -40,9 +40,8 @@ import { TreePanel } from './TreePanel.tsx'
import { t } from './locales.ts'
import { relativeTo } from './paths.ts'
import { resolveSidebarPath } from './produced-files.ts'
import { closePathTabs, retargetPathTabs } from './tree-mutations.ts'
import type { EditorToolbarControls, EditorToolbarState, FileViewerDescriptor } from './service.ts'
import { firstLeaf, insertLeafAt, leafWithTab, mintTabId, type SidebarStore, type SidebarTab } from './state.ts'
import { firstLeaf, insertLeafAt, leafWithTab, mintTabId, treeOf, type SidebarStore, type SidebarTab } from './state.ts'
import css from './sidebar.module.css'
type EditorLoad =
@@ -178,7 +177,8 @@ export function EditorHost(props: {
*/
const openFileSide = (absolute: string): void => {
store.reduce((state) => {
const pane = leafWithTab(state.bottomSplits, tab.id) ?? firstLeaf(state.bottomSplits)
const key = treeOf(state, tab.id)
const pane = leafWithTab(state[key], tab.id) ?? firstLeaf(state[key])
const fresh: SidebarTab = {
id: mintTabId(),
type: 'editor',
@@ -186,8 +186,8 @@ export function EditorHost(props: {
path: absolute,
meta: { treeOpen: false },
}
const { node, leafId } = insertLeafAt(state.bottomSplits, pane.id, 'row', fresh, false)
return { ...state, bottomSplits: node, activePane: leafId }
const { node, leafId } = insertLeafAt(state[key], pane.id, 'row', fresh, false)
return { ...state, [key]: node, activePane: leafId }
})
}
@@ -225,16 +225,6 @@ export function EditorHost(props: {
})
}
// Tree mutations reconcile the OPEN tabs (both split trees, the bottom
// panel, free windows): a rename retargets its tab to the new path; a
// delete closes tabs at or under the removed path. See tree-mutations.ts.
const onPathRenamed = (oldPath: string, newPath: string): void => {
retargetPathTabs(ctx, store, oldPath, newPath)
}
const onPathDeleted = (path: string): void => {
closePathTabs(ctx, store, path)
}
// The viewer's toolbar, hoisted into THIS header: the text editor reports
// its state and registers its commands (both null/absent for viewers
// without a toolbar — image, pdf, binary download).
@@ -348,9 +338,6 @@ export function EditorHost(props: {
}
apply(planFirstMatch(ctx.get('betterSidebar')?.matchFileViewer(path), mediaUrlOf))
return () => { cancelled = true; controller.abort() }
// The deps are deliberately granular: the scope object's identity churns,
// only its sessionId / cwd fields gate the (re)fetch.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scope.sessionId, scope.cwd, path, ctx, showEmpty, isDir, reloadSeq])
// Save-then-refresh in preview mode (issue #167 part C): the edge into
@@ -399,8 +386,6 @@ export function EditorHost(props: {
onOpenWith={openWith}
onToggleOpenWithPin={toggleOpenWithPin}
onReferenceFile={onReferenceFile}
onPathRenamed={onPathRenamed}
onPathDeleted={onPathDeleted}
/>
</div>
)
@@ -523,8 +508,6 @@ export function EditorHost(props: {
onOpenWith={openWith}
onToggleOpenWithPin={toggleOpenWithPin}
onReferenceFile={onReferenceFile}
onPathRenamed={onPathRenamed}
onPathDeleted={onPathDeleted}
/>
</div>
)}
+17 -210
View File
@@ -24,16 +24,14 @@ import { useCallback, useEffect, useMemo, useRef, useState, type DragEvent, type
import { createPortal } from 'react-dom'
import clsx from 'clsx'
import {
Button, IconChevronRightOutline14, IconCloseFill14, IconCodeOutline16, IconCopyOutline16, IconDownloadOutline16,
IconEditOutline16, IconLinkOutline16, IconTrashOutline16, Menu, Modal, type MenuEntry, type MenuItem, writeClipboard,
IconChevronRightOutline14, IconCodeOutline16, IconCopyOutline16, IconDownloadOutline16,
IconLinkOutline16, Menu, type MenuEntry, type MenuItem, writeClipboard,
} from '@deepseek-ai/dsh-client-ui-primitives'
import { SiCursor, SiZedindustries } from 'react-icons/si'
import { VscFile, VscFolder, VscFolderOpened, VscLinkExternal, VscPin, VscPinned } from 'react-icons/vsc'
import { api, downloadUrl, isOutsideWorkspaceMessage, type FsEntry } from './api.ts'
import { FenceErrorNotice } from './FenceErrorNotice.tsx'
import { IconUploadOutline16, IconVscode16 } from './icons.tsx'
import { isImeComposition } from './ime-guard.ts'
import { useSubmenuFlip } from './menu-flip.ts'
import type { OpenWithTarget } from './open-with.ts'
import { relativeTo } from './paths.ts'
import { t } from './locales.ts'
@@ -135,10 +133,6 @@ export function FileTree(props: {
onToggleOpenWithPin?: (targetId: string) => void
/** Insert `@<relative path>` into the composer draft (file vs directory). */
onReferenceFile: (path: string, isDir: boolean) => void
/** A rename landed (old row path → new path): the caller retargets open tabs. */
onPathRenamed?: (oldPath: string, newPath: string) => void
/** A delete landed: the caller closes tabs at or under the removed path. */
onPathDeleted?: (path: string, isDir: boolean) => void
/** Bump to wipe the level cache and reload the visible set. */
refreshTick: number
/** Upload into `dir` (absolute, inside the workspace); runs in the caller. */
@@ -146,22 +140,13 @@ export function FileTree(props: {
/** True while an upload is in flight (drops are ignored). */
busy: boolean
}) {
const { sessionId, cwd, store, expanded, revealed, onToggle, onOpenFile, onOpenFileNewTab, onOpenFileSide, openWithTargets, openWithPinned, openWithSsh, onOpenWith, onToggleOpenWithPin, onReferenceFile, onPathRenamed, onPathDeleted, refreshTick, onUploadRequest, busy } = props
const { sessionId, cwd, store, expanded, revealed, onToggle, onOpenFile, onOpenFileNewTab, onOpenFileSide, openWithTargets, openWithPinned, openWithSsh, onOpenWith, onToggleOpenWithPin, onReferenceFile, refreshTick, onUploadRequest, busy } = props
const [data, setData] = useState<Record<string, LevelData>>({})
const dataRef = useRef(data)
/** The row whose path was just copied ("copied" label replaces its button). */
const [copiedPath, setCopiedPath] = useState<string | null>(null)
/** Open context menu: the row path (and whether it is a directory) plus the cursor position. */
const [rowMenu, setRowMenu] = useState<{ path: string; isDir: boolean; x: number; y: number } | null>(null)
// The row menu's "open with" submenu is the one submenu that can tower past
// the viewport; publish its flip geometry for layout.css while it is open.
useSubmenuFlip(rowMenu)
/** The row being renamed inline: its path plus the edit buffer. */
const [renaming, setRenaming] = useState<{ path: string; value: string } | null>(null)
/** The delete awaiting the confirmation modal's yes. */
const [confirmDelete, setConfirmDelete] = useState<{ path: string; isDir: boolean; name: string } | null>(null)
/** The last mutation failure (dismissable strip above the tree). */
const [actionError, setActionError] = useState<string | null>(null)
/** Whether a file drag hovers the tree (drives the portaled drop zone). */
const [dropOver, setDropOver] = useState(false)
/** The directory a drag is hovering right now (null = body, drop to root). */
@@ -283,63 +268,6 @@ export function FileTree(props: {
loadDir(dir)
}, [loadDir])
/**
* Settle the tree after one rename/delete landed at `prefix`: drop every
* cached level at or under the old path, collapse the now-stale expanded
* directories below it, and reload the immediate parent so the new shape
* shows without a full refresh tick.
*/
const pruneTree = (prefix: string): void => {
const parent = parentOf(prefix)
const under = (key: string): boolean => key === prefix || key.startsWith(`${prefix}/`) || key.startsWith(`${prefix}\\`)
for (const key of Object.keys(dataRef.current)) {
if (under(key)) delete dataRef.current[key]
}
if (parent !== prefix) delete dataRef.current[parent]
setData({ ...dataRef.current })
for (const dir of expanded) {
if (under(dir)) onToggle(dir)
}
if (parent !== prefix) retryDir(parent)
}
/** Client-side twin of the server's name rule (the server re-validates). */
const validRename = (name: string): boolean =>
name !== '' && name !== '.' && name !== '..' && !name.includes('/') && !name.includes('\\')
/** Commit the inline rename: trim, no-op guard, then fs.rename + settle. */
const commitRename = (path: string, raw: string): void => {
setRenaming(null)
const name = raw.trim()
if (cwd === undefined || name === baseName(path) || !validRename(name)) {
if (name !== baseName(path) && !validRename(name)) setActionError(t('renameInvalid'))
return
}
api.fsRename({ sessionId, cwd }, path, name)
.then((result) => {
setActionError(null)
pruneTree(path)
onPathRenamed?.(path, result.path)
})
.catch((error: unknown) => {
setActionError(error instanceof Error ? error.message : String(error))
})
}
/** Run the confirmed delete: fs.remove + settle + close affected tabs. */
const performDelete = (target: { path: string; isDir: boolean }): void => {
if (cwd === undefined) return
api.fsRemove({ sessionId, cwd }, target.path)
.then(() => {
setActionError(null)
pruneTree(target.path)
onPathDeleted?.(target.path, target.isDir)
})
.catch((error: unknown) => {
setActionError(error instanceof Error ? error.message : String(error))
})
}
// The caller's refresh tick wipes the cache (declared BEFORE the load
// effect so the reload below sees the empty cache).
const lastTick = useRef(refreshTick)
@@ -445,14 +373,13 @@ export function FileTree(props: {
if (openWithTargets === undefined || onOpenWith === undefined || openWithTargets.length === 0) return []
const pinnedIds = openWithPinned ?? []
/** Brand marks for the built-ins (monochrome silhouettes, currentColor);
* reveal gets the folder glyph, custom editors a generic code mark.
* The compact menu's icon slot is 14px, so every mark renders at 14. */
* reveal gets the folder glyph, custom editors a generic code mark. */
const itemIcon = (target: OpenWithTarget): ReactNode => {
if (target.kind === 'reveal') return <VscFolderOpened size={14} />
if (target.id === 'vscode') return <IconVscode16 size={14} />
if (target.id === 'cursor') return <SiCursor size={14} />
if (target.id === 'zed') return <SiZedindustries size={14} />
return <IconCodeOutline16 size={14} />
if (target.kind === 'reveal') return <VscFolderOpened size={16} />
if (target.id === 'vscode') return <IconVscode16 size={16} />
if (target.id === 'cursor') return <SiCursor size={16} />
if (target.id === 'zed') return <SiZedindustries size={16} />
return <IconCodeOutline16 size={16} />
}
const pinned = openWithTargets
.filter(target => pinnedIds.includes(target.id))
@@ -485,7 +412,7 @@ export function FileTree(props: {
onToggleOpenWithPin?.(target.id)
}}
>
{pinnedNow ? <VscPinned size={12} /> : <VscPin size={12} />}
{pinnedNow ? <VscPinned size={14} /> : <VscPin size={14} />}
</span>
</span>
),
@@ -506,7 +433,7 @@ export function FileTree(props: {
<IconChevronRightOutline14 size={14} className={css.openWithChevron} aria-hidden />
</span>
),
icon: <VscLinkExternal size={14} />,
icon: <VscLinkExternal size={16} />,
submenu,
},
]
@@ -520,58 +447,6 @@ export function FileTree(props: {
const expandedSet = useMemo(() => new Set(expanded), [expanded])
const revealedSet = useMemo(() => new Set(revealed), [revealed])
/**
* Focus + select the rename editor once, on mount. The callback identity
* must stay STABLE (useCallback []): an inline arrow re-runs on every
* render (detach null → attach el), and focus()/select() mid-keystroke
* would reselect the whole buffer while typing. A plain <input>, not the
* primitives Input — that one forwards no ref, and focus+select is the
* entire point here.
*/
const renameInputRef = useCallback((el: HTMLInputElement | null): void => {
if (el !== null) {
el.focus()
el.select()
}
}, [])
/** The inline rename editor replacing one row (dirs and files alike):
* same indent/icon/height for a seamless swap, Enter/blur commits,
* Escape cancels, IME composition keys never reach the handlers (the
* shared isImeComposition guard). */
const renderRenameRow = (entry: FsEntry, depth: number): ReactNode => (
<div
key={entry.path}
className={clsx(css.explorerRow, css.explorerRenaming)}
style={{ paddingLeft: depth * 22 + 6 }}
>
{entry.isDir
? (expandedSet.has(entry.path) ? <VscFolderOpened size={14} /> : <VscFolder size={14} />)
: <VscFile size={14} />}
<input
ref={renameInputRef}
className={css.explorerRenameInput}
value={renaming?.value ?? ''}
aria-label={t('rename')}
spellCheck={false}
onChange={(event) => {
setRenaming(prev => prev === null ? prev : { ...prev, value: event.target.value })
}}
onKeyDown={(event) => {
if (isImeComposition(event)) return
if (event.key === 'Enter') {
event.preventDefault()
commitRename(entry.path, renaming?.value ?? '')
} else if (event.key === 'Escape') {
event.preventDefault()
setRenaming(null)
}
}}
onBlur={() => { commitRename(entry.path, renaming?.value ?? '') }}
/>
</div>
)
const renderLevel = (dir: string, depth: number): ReactNode => {
const level = data[dir]
if (level === undefined) {
@@ -596,10 +471,6 @@ export function FileTree(props: {
}
const entries = level.entries ?? []
return entries.map(entry => {
// The row being renamed renders as its editor (no button semantics:
// an editor is not a click target — and a nested interactive inside
// role="button" would be invalid anyway).
if (renaming?.path === entry.path) return renderRenameRow(entry, depth)
if (entry.isDir) {
const isOpen = expandedSet.has(entry.path)
return (
@@ -680,22 +551,6 @@ export function FileTree(props: {
<div className={css.explorerEmpty}>{t('noSession')}</div>
) : (
<>
{/* The last mutation failure (rename/delete): dismissable, raw
server text — the same show-the-truth policy the fence notice
uses. Any later action or a fresh attempt clears it. */}
{actionError !== null && (
<div className={css.explorerActionError} role="alert">
<span className={css.explorerActionErrorText}>{actionError}</span>
<button
type="button"
className={css.explorerActionErrorClose}
aria-label={t('dismiss')}
onClick={() => { setActionError(null) }}
>
<IconCloseFill14 />
</button>
</div>
)}
<div
className={clsx(css.explorerRow, dropTarget === root && css.explorerRowDropTarget)}
style={{ paddingLeft: 6 }}
@@ -795,31 +650,22 @@ export function FileTree(props: {
items={[
// The open escapes head the FILE menu (dirs only get copy).
...(rowMenu?.isDir === false && onOpenFileNewTab !== undefined
? [{ id: 'open-new-tab', label: t('openFileNewTab'), icon: <IconCodeOutline16 size={14} /> }]
? [{ id: 'open-new-tab', label: t('openFileNewTab'), icon: <IconCodeOutline16 size={16} /> }]
: []),
...(rowMenu?.isDir === false && onOpenFileSide !== undefined
? [{ id: 'open-side', label: t('openFileSide'), icon: <VscFolderOpened size={14} /> }]
? [{ id: 'open-side', label: t('openFileSide'), icon: <VscFolderOpened size={16} /> }]
: []),
...openWithEntries(),
// Download applies to files only (the host route refuses directories).
...(rowMenu?.isDir === false
? [{ id: 'download', label: t('download'), icon: <IconDownloadOutline16 size={14} /> }]
? [{ id: 'download', label: t('download'), icon: <IconDownloadOutline16 size={16} /> }]
: []),
// Upload into a directory (incl. the workspace root row).
...(rowMenu?.isDir === true
? [{ id: 'upload-here', label: t('uploadHere'), icon: <IconUploadOutline16 size={14} /> }]
: []),
{ id: 'relative', label: t('copyRelative'), icon: <IconCopyOutline16 size={14} /> },
{ id: 'absolute', label: t('copyAbsolute'), icon: <IconCopyOutline16 size={14} /> },
// Explorer mutations close the menu; the workspace ROOT row is the
// session itself — never renamable or deletable (server double-guards).
...(rowMenu !== null && rowMenu.path !== cwd
? [
{ id: 'mutate-sep', type: 'separator' } as MenuEntry,
{ id: 'rename', label: t('rename'), icon: <IconEditOutline16 size={14} /> },
{ id: 'delete', label: t('delete'), icon: <IconTrashOutline16 size={14} />, danger: true },
]
? [{ id: 'upload-here', label: t('uploadHere'), icon: <IconUploadOutline16 size={16} /> }]
: []),
{ id: 'relative', label: t('copyRelative'), icon: <IconCopyOutline16 size={16} /> },
{ id: 'absolute', label: t('copyAbsolute'), icon: <IconCopyOutline16 size={16} /> },
]}
onSelect={(id) => {
const target = rowMenu
@@ -846,55 +692,16 @@ export function FileTree(props: {
fileInputRef.current?.click()
return
}
if (id === 'rename') {
setRenaming({ path: target.path, value: baseName(target.path) })
return
}
if (id === 'delete') {
setConfirmDelete({ path: target.path, isDir: target.isDir, name: baseName(target.path) })
return
}
copyPath(
id === 'relative' ? relativeTo(cwd ?? '', target.path) : target.path,
target.path,
)
}}
portal
compact
align="start"
getAnchorRect={() => (rowMenu === null ? null : new DOMRect(rowMenu.x, rowMenu.y, 0, 0))}
anchor={<span />}
/>
{/* The delete confirmation: destructive and permanent (no host trash),
so it always lands here first — the same Cancel/Confirm shape the
git lens uses for discard/revert/cherry-pick. */}
<Modal
open={confirmDelete !== null}
onClose={() => { setConfirmDelete(null) }}
title={confirmDelete === null ? '' : t('deleteTitle', { name: confirmDelete.name })}
closeLabel={t('cancel')}
footer={(
<>
<Button variant="outline" onClick={() => { setConfirmDelete(null) }}>{t('cancel')}</Button>
<Button
variant="primary"
onClick={() => {
const pending = confirmDelete
if (pending === null) return
setConfirmDelete(null)
performDelete(pending)
}}
>
{t('delete')}
</Button>
</>
)}
>
<p className={css.explorerConfirmDesc}>
{confirmDelete === null ? '' : t(confirmDelete.isDir ? 'deleteDescDir' : 'deleteDescFile')}
</p>
</Modal>
</div>
)
}
+337
View File
@@ -0,0 +1,337 @@
/**
* One free window: a tab dragged out of the workbench floating over the
* conversation area at viewport coordinates (rendered inside the panel host,
* so desktop-shell transforms can never hijack its fixed containing block).
*
* The header drags the window with the panel-resize pattern — pointer
* capture + per-frame direct DOM writes + a store commit on release — and
* doubling as the DOCK-BACK gesture: while the pointer is over a workbench
* pane ([data-dsh-pane], either panel), that pane highlights live and
* releasing docks the tab into it (center merge); releasing anywhere else
* just moves the window. The SE corner resizes, any press raises (the
* floats array's order is the stacking order), the header right-click menu
* and the X button dock / close. The tab content reuses the regular tab
* renderer, so every tab type (terminal, editor, plugin tabs) floats
* unchanged.
*/
import { useEffect, useRef, useState, type ReactNode } from 'react'
import clsx from 'clsx'
import { IconCloseFill14, Menu } from '@deepseek-ai/dsh-client-ui-primitives'
import type { FloatWindow, SidebarTab } from './state.ts'
import { FLOAT_MIN_H, FLOAT_MIN_W } from './state.ts'
import { t } from './locales.ts'
import css from './sidebar.module.css'
/** A drag's mutable bookkeeping (start geometry + the last applied values). */
interface DragState {
/** Which handle owns the drag (the abort path reads it from this ref —
* the React `dragging` state lags a render behind pointerdown). */
mode: 'move' | 'resize'
pointerX: number
pointerY: number
startX: number
startY: number
startW: number
startH: number
/** Last geometry actually written to the DOM (the cancel-path fallback). */
applied: { x: number; y: number; w: number; h: number }
/** Settled after the final pointer event (the up position wins over the
* rAF-pending value — a fast flick coalesces its tail into the up event). */
committed: boolean
}
/** The pane under a viewport point, if any (rect hit-test; the dragged
* window itself is not a pane, so it cannot shadow the targets). */
function paneAt(x: number, y: number): HTMLElement | null {
for (const pane of document.querySelectorAll<HTMLElement>('[data-dsh-pane]')) {
const rect = pane.getBoundingClientRect()
if (rect.width === 0 || rect.height === 0) continue
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) return pane
}
return null
}
/** Pointer-capture helpers tolerant of environments without the API (jsdom
* lacks setPointerCapture — component tests dispatch plain MouseEvents, so
* the optional calls keep them driving the drag; real browsers always have
* it and a missing pointerId can never occur there). */
const capturePointer = (element: HTMLElement, pointerId: number): void => {
element.setPointerCapture?.(pointerId)
}
const releasePointer = (element: HTMLElement, pointerId: number): void => {
element.releasePointerCapture?.(pointerId)
}
/** Whether the element holds the pointer (assumed true without the API). */
const holdsPointer = (element: HTMLElement, pointerId: number): boolean => {
return element.hasPointerCapture?.(pointerId) !== false
}
export function FreeWindow(props: {
float: FloatWindow
renderTab: (tab: SidebarTab, active: boolean, paneId: string) => ReactNode
getTabIcon?: (tab: SidebarTab) => ReactNode
onRaise: () => void
onMove: (x: number, y: number) => void
onResize: (w: number, h: number) => void
/** Dock the tab back; the pane id, or null for the active pane. */
onDock: (paneId: string | null) => void
onClose: () => void
}) {
const { float, renderTab, getTabIcon, onRaise, onMove, onResize, onDock, onClose } = props
const rootRef = useRef<HTMLDivElement>(null)
const dragRef = useRef<DragState | null>(null)
// The pane currently highlighted as the dock target (a direct DOM write:
// per-frame React state would re-render every mounted tab at drag cadence).
const dockTargetRef = useRef<HTMLElement | null>(null)
// rAF coalescing: pointermove outruns the display, one write per frame.
const frameRef = useRef<number | null>(null)
const pendingRef = useRef<{ x: number; y: number; w: number; h: number } | null>(null)
const [dragging, setDragging] = useState<'move' | 'resize' | null>(null)
const [menu, setMenu] = useState<{ x: number; y: number } | null>(null)
useEffect(() => () => {
if (frameRef.current !== null) cancelAnimationFrame(frameRef.current)
dockTargetRef.current?.removeAttribute('data-dsh-float-dock-over')
}, [])
/** Apply the pending geometry to the DOM (one rAF per frame at most). */
const scheduleApply = (geo: { x: number; y: number; w: number; h: number }): void => {
pendingRef.current = geo
if (frameRef.current !== null) return
frameRef.current = requestAnimationFrame(() => {
frameRef.current = null
const pending = pendingRef.current
const drag = dragRef.current
const root = rootRef.current
if (pending === null || drag === null || root === null) return
drag.applied = pending
root.style.left = `${pending.x}px`
root.style.top = `${pending.y}px`
root.style.width = `${pending.w}px`
root.style.height = `${pending.h}px`
})
}
/** Flush the pending frame synchronously (the release path's last write). */
const flushNow = (): void => {
const pending = pendingRef.current
const drag = dragRef.current
const root = rootRef.current
if (pending === null || drag === null || root === null) return
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current)
frameRef.current = null
}
pendingRef.current = null
drag.applied = pending
root.style.left = `${pending.x}px`
root.style.top = `${pending.y}px`
root.style.width = `${pending.w}px`
root.style.height = `${pending.h}px`
}
const clearDockHighlight = (): void => {
dockTargetRef.current?.removeAttribute('data-dsh-float-dock-over')
dockTargetRef.current = null
}
/** Release a drag: `pane` (when set) docks instead of moving. */
const finishDrag = (mode: 'move' | 'resize', geo: { x: number; y: number; w: number; h: number }, pane: HTMLElement | null): void => {
const drag = dragRef.current
if (drag === null || drag.committed) return
drag.committed = true
dragRef.current = null
setDragging(null)
const target = pane ?? dockTargetRef.current
clearDockHighlight()
if (mode === 'move' && target !== null) {
onDock(target.getAttribute('data-dsh-pane'))
} else if (mode === 'move') {
onMove(geo.x, geo.y)
} else {
onResize(geo.w, geo.h)
}
}
/** Cancel-path settle (pointercancel / lostpointercapture): the last
* APPLIED geometry is the user-visible truth — commit it, never roll back
* (the panel drags' issue-#247 semantics). The mode comes from the ref —
* the React `dragging` state can still be null when the cancel lands
* before the pointerdown re-render commits. */
const abortDrag = (): void => {
const drag = dragRef.current
if (drag === null || drag.committed) return
flushNow()
finishDrag(drag.mode, drag.applied, null)
}
const clampMove = (x: number, y: number): { x: number; y: number; w: number; h: number } => {
const vw = window.innerWidth
const vh = window.innerHeight
return {
x: Math.min(Math.max(x, 0), Math.max(0, vw - float.w)),
y: Math.min(Math.max(y, 0), Math.max(0, vh - float.h)),
w: float.w,
h: float.h,
}
}
const clampResize = (w: number, h: number): { x: number; y: number; w: number; h: number } => {
const vw = window.innerWidth
const vh = window.innerHeight
return {
x: float.x,
y: float.y,
w: Math.round(Math.min(Math.max(w, FLOAT_MIN_W), Math.max(FLOAT_MIN_W, vw - float.x))),
h: Math.round(Math.min(Math.max(h, FLOAT_MIN_H), Math.max(FLOAT_MIN_H, vh - float.y))),
}
}
return (
<div
ref={rootRef}
className={clsx(css.floatWindow, dragging !== null && css.floatWindowDragging)}
data-dsh-float-window
data-dsh-float-id={float.id}
style={{ left: float.x, top: float.y, width: float.w, height: float.h }}
onPointerDown={() => { onRaise() }}
>
<div
className={css.floatHeader}
onPointerDown={(event) => {
if (event.button !== 0) return
// Portaled overlays (the header Menu) live at document.body in the
// DOM, yet their events still bubble through the React tree into
// this handler — without the containment guard a menu-row click
// would start a drag, preventDefault (killing the row's compat
// mousedown) and capture the pointer (retargeting the release at
// the header, so the row's click never fires). Buttons inside the
// header (the close X) need the same exclusion: a capture here
// swallows their click.
if (!(event.target instanceof Node) || !event.currentTarget.contains(event.target)) return
if (event.target instanceof Element && event.target.closest('button') !== null) return
event.preventDefault()
capturePointer(event.currentTarget, event.pointerId)
dragRef.current = {
mode: 'move',
pointerX: event.clientX,
pointerY: event.clientY,
startX: float.x,
startY: float.y,
startW: float.w,
startH: float.h,
applied: { x: float.x, y: float.y, w: float.w, h: float.h },
committed: false,
}
setDragging('move')
}}
onPointerMove={(event) => {
const drag = dragRef.current
if (drag === null || !holdsPointer(event.currentTarget, event.pointerId)) return
const geo = clampMove(drag.startX + (event.clientX - drag.pointerX), drag.startY + (event.clientY - drag.pointerY))
scheduleApply(geo)
const target = paneAt(event.clientX, event.clientY)
if (target !== dockTargetRef.current) {
clearDockHighlight()
if (target !== null) target.setAttribute('data-dsh-float-dock-over', '')
dockTargetRef.current = target
}
}}
onPointerUp={(event) => {
if (dragRef.current === null || !holdsPointer(event.currentTarget, event.pointerId)) return
releasePointer(event.currentTarget, event.pointerId)
flushNow()
const geo = clampMove(
dragRef.current.startX + (event.clientX - dragRef.current.pointerX),
dragRef.current.startY + (event.clientY - dragRef.current.pointerY),
)
finishDrag('move', geo, paneAt(event.clientX, event.clientY))
}}
onPointerCancel={() => { abortDrag() }}
onLostPointerCapture={() => { abortDrag() }}
onContextMenu={(event) => {
event.preventDefault()
setMenu({ x: event.clientX, y: event.clientY })
}}
>
{getTabIcon?.(float.tab) ?? null}
<span className={css.floatTitle} title={float.tab.title}>{float.tab.title}</span>
<button
type="button"
className={css.floatClose}
aria-label={t('close')}
onClick={(event) => { event.stopPropagation(); onClose() }}
>
<IconCloseFill14 />
</button>
<Menu
open={menu !== null}
onClose={() => { setMenu(null) }}
items={[
{ id: 'dock', label: t('dockToSidebar') },
{ id: 'close', label: t('close') },
]}
onSelect={(id) => {
setMenu(null)
if (id === 'dock') onDock(null)
else if (id === 'close') onClose()
}}
portal
align="start"
getAnchorRect={() => (menu === null ? null : new DOMRect(menu.x, menu.y, 0, 0))}
anchor={<span />}
/>
</div>
{/* The tab cell reuses .paneTab (the pane's per-tab wrapper): the
column-flex chain (window → .floatContent → .paneTab) is what
gives the component a definite width via align-stretch — the exact
sizing contract a tab gets inside a pane. */}
<div className={css.floatContent}>
<div className={css.paneTab}>{renderTab(float.tab, true, float.id)}</div>
</div>
<div
className={css.floatResize}
onPointerDown={(event) => {
if (event.button !== 0) return
if (!(event.target instanceof Node) || !event.currentTarget.contains(event.target)) return
event.preventDefault()
capturePointer(event.currentTarget, event.pointerId)
dragRef.current = {
mode: 'resize',
pointerX: event.clientX,
pointerY: event.clientY,
startX: float.x,
startY: float.y,
startW: float.w,
startH: float.h,
applied: { x: float.x, y: float.y, w: float.w, h: float.h },
committed: false,
}
setDragging('resize')
}}
onPointerMove={(event) => {
const drag = dragRef.current
if (drag === null || !holdsPointer(event.currentTarget, event.pointerId)) return
scheduleApply(clampResize(
drag.startW + (event.clientX - drag.pointerX),
drag.startH + (event.clientY - drag.pointerY),
))
}}
onPointerUp={(event) => {
if (dragRef.current === null || !holdsPointer(event.currentTarget, event.pointerId)) return
releasePointer(event.currentTarget, event.pointerId)
flushNow()
const geo = clampResize(
dragRef.current.startW + (event.clientX - dragRef.current.pointerX),
dragRef.current.startH + (event.clientY - dragRef.current.pointerY),
)
finishDrag('resize', geo, null)
}}
onPointerCancel={() => { abortDrag() }}
onLostPointerCapture={() => { abortDrag() }}
/>
</div>
)
}
+13 -17
View File
@@ -10,31 +10,32 @@
* Security posture: every HTML string (block leaves, inline text, wrapper
* open-tag attributes) goes through DOMPurify with an explicit denylist on
* top of its defaults (no script/style/iframe/forms), anchors are forced to
* open in a new tab with noopener, and local media goes through the
* session-scoped `/sidebar/file` media route — markdown-syntax image
* destinations are rewritten via `markdown-images.ts` and `src` attributes
* through `resolveLocalMediaDest` (the same trust fence).
* open in a new tab with noopener, and local media `src` attributes are
* rewritten through the session-scoped `/sidebar/file` media route — the same
* trust fence the markdown image rewriter (`markdown-images.ts`) uses.
*/
import { useLayoutEffect, useMemo, useRef } from 'react'
import { createElement, type ReactNode } from 'react'
import DOMPurify from 'dompurify'
import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
import type { ComponentType } from 'react'
import { markdownTextProps } from './markdown-labels.tsx'
import { resolveLocalMediaDest, rewriteLocalImageUrls } from './markdown-images.ts'
import { lazyChunkComponent } from './lazy-chunk.tsx'
import { resolveLocalMediaDest } from './markdown-images.ts'
import {
analyzeHtmlSegment,
type AnalyzedMarkdownHtml,
} from './markdown-html.ts'
import { splitMermaidBlocks } from './mermaid-blocks.ts'
import { splitMermaidBlocks, type MermaidMarkdownProps } from './mermaid-blocks.ts'
import type { SessionScope } from './api.ts'
import css from './sidebar.module.css'
/** The chunk-resident markdown renderer (mermaid lazy chunk), shared with the
* legacy no-HTML preview path in TextEditor. Defined in mermaid-lazy.tsx (a
* light module) so core-bundle consumers can import the stub without
* dragging DOMPurify along. */
export { LazyMermaidMarkdown } from './mermaid-lazy.tsx'
import { LazyMermaidMarkdown } from './mermaid-lazy.tsx'
* legacy no-HTML preview path in TextEditor. */
export const LazyMermaidMarkdown = lazyChunkComponent<MermaidMarkdownProps>(
'mermaid',
(mod) => mod.MermaidMarkdown as ComponentType<MermaidMarkdownProps> | undefined,
)
/** Everything the sanitizers need to resolve local media + scope the route. */
export interface MarkdownHtmlMedia {
@@ -234,12 +235,7 @@ export function MarkdownDocument({ info, media, codeLabels }: MarkdownDocumentPr
const prepared = useMemo<PreparedSegment[]>(() => info.segments.map((segment): PreparedSegment => {
if (segment.kind === 'markdown') {
const defs = info.referenceDefinitions
const raw = defs === '' ? segment.text : `${segment.text}\n\n${defs}`
// MarkdownText drops non-http(s) image destinations (chat-security
// stance), so rewrite local ones into /sidebar/file media URLs first —
// the same trust fence the sanitized HTML leaves below go through.
// Idempotent: already-absolute media URLs pass through untouched.
const text = rewriteLocalImageUrls(raw, media.scope, media.path, media.origin)
const text = defs === '' ? segment.text : `${segment.text}\n\n${defs}`
return {
kind: 'markdown',
text,
-3
View File
@@ -40,9 +40,6 @@ export function PdfView(props: { scope: SessionScope; path: string; title: strin
controller.abort()
if (objectUrl !== undefined) URL.revokeObjectURL(objectUrl)
}
// Granular scope fields: the scope object's identity churns, only its
// sessionId / cwd fields gate the fetch.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scope.sessionId, scope.cwd, path])
useEffect(() => {
+3 -3
View File
@@ -7,7 +7,7 @@
* errors in the sidebar shell itself (Workbench, drag layout, …) — a full
* swap keeps the page alive.
* - PER-TAB (Sidebar.tsx TabContent, `css.tabBoundaryError`): a crashing
* viewer/editor shows a strip inside ITS OWN pane; the workbench shell, the
* viewer/editor shows a strip inside ITS OWN pane; the toggle cluster, the
* other tabs, and the panel itself stay alive (issue #31 — a tab crash
* must never take down the whole sidebar).
*
@@ -26,14 +26,14 @@ export class RenderBoundary extends Component<{ children?: ReactNode; className?
}
componentDidCatch(error: Error, info: ErrorInfo): void {
console.error('[dsh-better-sidebar] render error:', error, info.componentStack)
console.error('[dsh-apes-sidebar] render error:', error, info.componentStack)
}
render(): ReactNode {
if (this.state.error !== null) {
return (
<div className={this.props.className}>
<span>dsh-better-sidebar: {this.state.error}</span>
<span>dsh-apes-sidebar: {this.state.error}</span>
<button
type="button"
className={css.terminalRetry}
+76 -6
View File
@@ -54,9 +54,13 @@ import clsx from 'clsx'
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
import {
clampWidthPercent,
TITLE_BAR_STRIP_MAX,
TITLE_BAR_STRIP_MIN,
WIDTH_PERCENT_MAX,
WIDTH_PERCENT_MIN,
type SidebarPrefs,
type TitleBarScheme,
} from '../prefs-shared.ts'
import { api } from './api.ts'
import { parsePrefs } from './prefs.ts'
@@ -569,6 +573,7 @@ export function SettingsBody(props: {
*/
export function SideCardSection({ store, service }: SideCardSectionProps) {
const [prefs, setPrefs] = useState<SidebarPrefs>(() => store.getPrefs())
const [widthDraft, setWidthDraft] = useState<string>(String(store.getPrefs().defaultWidthPercent))
const [error, setError] = useState<string | null>(null)
// Which feature's secondary settings popup is open (null = closed).
const [settingsFor, setSettingsFor] = useState<TabDescriptor | FileViewerDescriptor | null>(null)
@@ -620,7 +625,9 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
if (cancelled) return
revisionRef.current = view.revision
if (dirtyRef.current) return
setPrefs(parsePrefs(view.value))
const next = parsePrefs(view.value)
setPrefs(next)
setWidthDraft(String(next.defaultWidthPercent))
}).catch(() => { /* the store's defaults stay authoritative */ })
return () => { cancelled = true }
}, [])
@@ -651,7 +658,9 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
/** Settle one commit: success adopts the server values, failure reverts. */
const applyOutcome = (previous: SidebarPrefs, outcome: { ok: boolean; prefs: SidebarPrefs }): void => {
setPrefs(outcome.ok ? outcome.prefs : previous)
const settled = outcome.ok ? outcome.prefs : previous
setPrefs(settled)
setWidthDraft(String(settled.defaultWidthPercent))
}
/** Optimistically apply one pref patch, then commit (revert on failure). */
@@ -664,6 +673,10 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
void commit(patch).then(outcome => applyOutcome(previous, outcome))
}
const onToggle = (next: boolean): void => {
applyPref({ openByDefault: next })
}
/** Flip one per-tab enable switch (merge into the tabsEnabled map). */
const onToggleTab = (id: string, next: boolean): void => {
applyPref({ tabsEnabled: { ...optimisticRef.current.tabsEnabled, [id]: next } })
@@ -766,6 +779,20 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
return raw
}
const commitWidth = (): void => {
const parsed = Number(widthDraft)
if (!Number.isFinite(parsed)) {
setWidthDraft(String(prefs.defaultWidthPercent))
return
}
const clamped = clampWidthPercent(parsed)
const previous = prefs
setPrefs({ ...previous, defaultWidthPercent: clamped })
setWidthDraft(String(clamped))
setError(null)
void commit({ defaultWidthPercent: clamped }).then(outcome => applyOutcome(previous, outcome))
}
/**
* One SMALL toggle card for the responsive inventory grid: the card's main
* area is the switch (click to flips, visual state IS the state), the icon
@@ -841,6 +868,51 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
{/* 常规: the DSH settings-row recipe — title/desc left, control right. */}
<div className={css.group}>
<div className={css.groupHeading}>{t('settingsGeneralTitle')}</div>
<div className={css.row}>
<span className={css.rowText}>
<span className={css.title}>{t('settingsOpenTitle')}</span>
<span className={css.desc}>{t('settingsOpenDesc')}</span>
</span>
<Switch
label={t('settingsOpenTitle')}
checked={prefs.openByDefault}
onChange={onToggle}
/>
</div>
<div className={css.row}>
<span className={css.rowText}>
<span className={css.title}>{t('settingsWidthTitle')}</span>
<span className={css.desc}>{t('settingsWidthDesc')}</span>
</span>
<span className={css.control}>
<Input
type="number"
className={css.percentInput}
value={widthDraft}
min={WIDTH_PERCENT_MIN}
max={WIDTH_PERCENT_MAX}
step={1}
aria-label={t('settingsWidthTitle')}
onChange={event => { setWidthDraft(event.currentTarget.value) }}
onBlur={commitWidth}
onKeyDown={event => {
if (event.key === 'Enter') event.currentTarget.blur()
}}
/>
<span className={css.suffix}>{t('settingsWidthSuffix')}</span>
</span>
</div>
<div className={css.row}>
<span className={css.rowText}>
<span className={css.title}>{t('settingsOpenPathTitle')}</span>
<span className={css.desc}>{t('settingsOpenPathDesc')}</span>
</span>
<Switch
label={t('settingsOpenPathTitle')}
checked={prefs.interceptOpenPath}
onChange={(next) => { applyPref({ interceptOpenPath: next }) }}
/>
</div>
<div className={css.row}>
<span className={css.rowText}>
<span className={css.title}>{t('settingsOpenToolsTitle')}</span>
@@ -875,11 +947,9 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
...getShellPresets().map(preset => ({
value: `preset:${preset.id}`,
title: preset.title,
// The preset desc is i18n-friendly (string or () => string)
// — resolve it like every other settings text here.
desc: preset.detect?.(detectedEnv) === true
? `${textOf(preset.desc)}(${t('settingsSchemeDetectedSuffix')})`
: textOf(preset.desc),
? `${preset.desc}(${t('settingsSchemeDetectedSuffix')})`
: preset.desc,
})),
{ value: 'custom', title: t('settingsSchemeCustomTitle'), desc: t('settingsSchemeCustomDesc') },
]}
+15 -46
View File
@@ -58,7 +58,6 @@ import {
sideThreadRows,
threadHasCompletedTurn,
threadTrailingPending,
type SidechatLiveEvent,
type SidechatThreadInfo,
} from '../sidechat-core.ts'
import {
@@ -70,7 +69,6 @@ import {
type SidechatTranscriptRow,
} from './sidechat-transcript.ts'
import { api } from './api.ts'
import { usePolling } from './use-polling.ts'
import { t } from './locales.ts'
import type { SessionScope } from './api.ts'
import type { SidebarTab } from './state.ts'
@@ -113,8 +111,6 @@ const inFlightStarts = new Set<string>()
* the afterSeq delta and never re-download what they already hold). */
interface ThreadCache {
entries: SidebarHistoryEntry[]
/** The CURRENT attempt's live deltas (replaced every pull, never merged). */
live: SidechatLiveEvent[]
}
/** Row-render labels (locale-dependent, memoized once per mount). */
@@ -386,7 +382,7 @@ export function SideChatView(props: {
const [info, setInfo] = useState<SidechatThreadInfo | null>(null)
const [menuOpen, setMenuOpen] = useState(false)
const cacheRef = useRef<ThreadCache>({ entries: [], live: [] })
const cacheRef = useRef<ThreadCache>({ entries: [] })
// The previous poll's rows (see the mapping's reuse pass below).
const prevRowsRef = useRef<SidechatTranscriptRow[]>([])
const controllerRef = useRef<AbortController | null>(null)
@@ -460,18 +456,14 @@ export function SideChatView(props: {
try {
const cache = cacheRef.current
const afterSeq = cache.entries.at(-1)?.event.seq
const { events, live } = await api.sidechatEvents(childId, afterSeq, controller.signal)
// The live set is the CURRENT attempt, so it replaces whatever the
// previous pull held; a settled step drops out of the host buffer and
// its durable assistant/message takes over in the mapping.
cache.live = live ?? []
const { events } = await api.sidechatEvents(childId, afterSeq, controller.signal)
if (events.length > 0) {
// Wire events arrive as parsed JSON; the mirror narrows data to the
// record the mapping reads.
const incoming = events.map(event => ({ event: event as SidebarSessionEvent }))
cache.entries = mergeBySeq(cache.entries, incoming)
setRevision(value => value + 1)
}
setRevision(value => value + 1)
} catch {
// Aborted by a newer pull or a wire failure: keep the last rows.
}
@@ -489,7 +481,7 @@ export function SideChatView(props: {
// Reset the transcript cache whenever the binding changes, then focus
// the composer — it owns the first message of a fresh thread.
useEffect(() => {
cacheRef.current = { entries: [], live: [] }
cacheRef.current = { entries: [] }
prevRowsRef.current = []
controllerRef.current?.abort()
setError(null)
@@ -501,25 +493,17 @@ export function SideChatView(props: {
}
}, [threadId, fetchInfo])
// One transcript pull on every input change (attach, visibility flip,
// run-state flip — the last one catches a thread's terminal state once it
// stops running).
// Poll while the tab is visible and the thread runs.
useEffect(() => {
if (!visible || threadId === undefined) return
void fetchThread(threadId)
// `running` is not read here, but re-triggering this pull on run-state
// flips is load-bearing (see above); the badge fetch rides the ticks.
}, [visible, threadId, running, fetchThread])
// Poll while the tab is visible and the thread runs: transcript deltas +
// badge refresh on a fixed cadence. Each pull self-guards (fetchThread
// aborts its predecessor; a late settle keeps the last rows).
const pollTick = useCallback(async (): Promise<void> => {
if (threadId === undefined) return
void fetchThread(threadId)
void fetchInfo(threadId)
}, [threadId, fetchThread, fetchInfo])
usePolling(visible && running && threadId !== undefined, pollTick, { intervalMs: POLL_MS })
if (!running) return
const timer = window.setInterval(() => {
void fetchThread(threadId)
void fetchInfo(threadId)
}, POLL_MS)
return () => { window.clearInterval(timer) }
}, [visible, threadId, running, fetchThread, fetchInfo])
useEffect(() => () => { controllerRef.current?.abort() }, [])
@@ -539,24 +523,8 @@ export function SideChatView(props: {
// The previous poll's rows ride into the mapping so unchanged rows keep
// their object identity (see reuseRows): the 2s poll re-renders only the
// changed tail instead of re-parsing markdown for the whole transcript.
// The live deltas are appended AFTER the durable events (they are the
// in-flight tail), and a delta whose step already settled durably is
// dropped: the durable message is authoritative and would otherwise render
// twice.
const rows = useMemo(() => {
const cache = cacheRef.current
const settled = new Set(
cache.entries
.filter(entry => entry.event.type === 'assistant/message')
.map(entry => {
const data = entry.event.data as { turn?: unknown; step?: unknown }
return `${String(data.turn)}:${String(data.step)}`
}),
)
const live = cache.live
.filter(event => !settled.has(`${String(event.data.turn)}:${String(event.data.step)}`))
.map(event => ({ event: event as SidebarSessionEvent }))
const next = threadId === undefined ? [] : transcriptRows([...cache.entries, ...live], prevRowsRef.current)
const next = threadId === undefined ? [] : transcriptRows(cacheRef.current.entries, prevRowsRef.current)
prevRowsRef.current = next
return next
},
@@ -606,6 +574,7 @@ export function SideChatView(props: {
}
}
return items
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [threads])
const growComposer = (): void => {
@@ -719,7 +688,7 @@ export function SideChatView(props: {
)}
items={menuItems}
selectedId={threadId}
onSelect={(id) => { if (id === '$new') openNewThread(); else openExistingThread(id) }}
onSelect={(id) => { id === '$new' ? openNewThread() : openExistingThread(id) }}
onClose={() => { setMenuOpen(false) }}
align="end"
portal
+1225 -209
View File
File diff suppressed because it is too large Load Diff
+39 -28
View File
@@ -53,7 +53,6 @@ import {
type TreeJob,
} from './subagent-jobs.ts'
import { api, type JobOutputResult } from './api.ts'
import { usePolling } from './use-polling.ts'
import { IconStopOutline16 } from './icons.tsx'
import { t } from './locales.ts'
import css from './SubagentView.module.css'
@@ -181,30 +180,52 @@ function SubagentLiveLines(props: { live: LastActivity | undefined }) {
/**
* One shared live-preview poller for the whole Subagent tree. Unlike the old
* per-card `subagents.history` timers, this sends at most ONE `subagents.live`
* request at a time (the shared poller's self-scheduling mode arms the next
* tick only after the previous request settles, so a slow host never sees
* abort/restart storms); a response settling after the poller stopped (page
* hidden, tree re-rooted) is dropped via the aborted signal.
* request at a time: a recursive timeout starts only after the previous
* request settles, so a slow host never sees abort/restart storms.
*/
function useSubagentLive(
rootId: string | undefined,
active: boolean,
): Readonly<Record<string, LastActivity>> {
const [live, setLive] = useState<Record<string, LastActivity>>({})
const controllerRef = useRef<AbortController | undefined>(undefined)
// A new tree must never inherit another root's live previews.
useEffect(() => { setLive({}) }, [rootId])
const poll = useCallback(async (signal: AbortSignal): Promise<void> => {
if (rootId === undefined) return
const result = await api.subagentsLive(rootId, signal)
if (!signal.aborted) setLive(result.live)
}, [rootId])
usePolling(rootId !== undefined && active, poll, {
intervalMs: POLL_MS,
mode: 'self-scheduling',
immediate: true,
})
useEffect(() => {
if (rootId === undefined || !active) return
const targetRootId = rootId
let disposed = false
let timer: number | undefined
const schedule = (): void => {
if (disposed) return
timer = window.setTimeout(() => { void load() }, POLL_MS)
}
async function load(): Promise<void> {
if (disposed) return
const controller = new AbortController()
controllerRef.current = controller
try {
const result = await api.subagentsLive(targetRootId, controller.signal)
if (!disposed) setLive(result.live)
} catch {
// Keep the last known live map; the next scheduled poll retries.
} finally {
if (controllerRef.current === controller) controllerRef.current = undefined
if (!disposed) schedule()
}
}
void load()
return () => {
disposed = true
if (timer !== undefined) window.clearTimeout(timer)
controllerRef.current?.abort()
controllerRef.current = undefined
}
}, [rootId, active])
return live
}
@@ -394,9 +415,6 @@ function JobOutputPane(props: {
if (!active || !isJobLive(job)) return
const timer = window.setInterval(() => { void load() }, JOB_POLL_MS)
return () => { window.clearInterval(timer) }
// isJobLive reads only job.status; whole-job identity churns on every
// catalog refresh and must not restart the poll interval.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [load, active, job.status])
useEffect(() => () => { controllerRef.current?.abort() }, [])
@@ -407,8 +425,6 @@ function JobOutputPane(props: {
if (!isJobLive(job) || typeof state !== 'object' || state.text.length === 0) return
const pre = preRef.current
if (pre !== null) pre.scrollTop = pre.scrollHeight
// Same as the poll effect above: only the status transition matters.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state, job.status])
return (
@@ -640,9 +656,7 @@ export function SubagentView(props: {
useCallback(() => sessions.list.getSnapshot(), [sessions]),
)
const byId = list.byId
// Memoized so the empty-catalog fallback keeps a stable identity — a fresh
// `{}` per render would invalidate every catalog-dependent memo/effect.
const catalogs = useMemo(() => list.subagentsByParent ?? {}, [list.subagentsByParent])
const catalogs = list.subagentsByParent ?? {}
// The topology root: the main agent of the current session's tree.
const rootId = useMemo(() => rootAncestor(byId, sessionId), [byId, sessionId])
@@ -666,9 +680,6 @@ export function SubagentView(props: {
if (rootId === undefined || !active) return
observe(rootId, true)
return () => {
// The cleanup must release everything observed AT cleanup time (the set
// mutates as branches open), so reading the ref here is the point.
// eslint-disable-next-line react-hooks/exhaustive-deps
for (const parentSessionId of observedRef.current) {
sessions.setSubagentCatalogOpen?.(parentSessionId, false)
}
@@ -704,7 +715,7 @@ export function SubagentView(props: {
try {
sessions.openSubagent?.(address)
} catch (error) {
console.error('[dsh-better-sidebar] openSubagent failed:', error)
console.error('[dsh-apes-sidebar] openSubagent failed:', error)
}
}, [sessions, onOpenChild])
@@ -714,7 +725,7 @@ export function SubagentView(props: {
try {
sessions.open?.(rootId)
} catch (error) {
console.error('[dsh-better-sidebar] open session failed:', error)
console.error('[dsh-apes-sidebar] open session failed:', error)
}
}, [sessions, rootId])
+14 -12
View File
@@ -4,8 +4,9 @@
* button cluster, and the + menu that opens new tabs (explorer / git /
* terminal). Tabs are draggable; dropping onto another tab inserts before it,
* dropping on the strip background appends to this pane. Right-clicking a
* tab opens the tab context menu (close / close others / close to the left /
* close to the right, the close ones scoped to this pane).
* tab opens the tab context menu (float as a free window / close / close
* others / close to the left / close to the right, the close ones scoped to
* this pane).
*/
import { useEffect, useRef, useState, type ReactNode } from 'react'
import clsx from 'clsx'
@@ -15,7 +16,6 @@ import {
import type { SidebarTab } from './state.ts'
import { isAgentTabId } from './state.ts'
import { isPinnedVirtualTab } from './pinned.ts'
import { useSubmenuFlip } from './menu-flip.ts'
import { IconPinOutline16 } from './icons.tsx'
import { t } from './locales.ts'
import css from './sidebar.module.css'
@@ -67,6 +67,9 @@ export function TabBar(props: {
newTabOptions: NewTabOption[]
/** Drop of a tab from any pane: (payload, insertBeforeTabId | null). */
onDropTab: (payload: TabDragPayload, before: string | null) => void
/** Float a tab out as a free window (the tab context menu's entry; the
* drag-to-conversation gesture is handled at the Sidebar shell level). */
onFloatTab: (tabId: string) => void
/**
* Pin/unpin a terminal tab (v0.17.0+). Called with `'workspace'` or
* `'global'` to pin (the shell snapshots the home cwd), or `null` to
@@ -81,15 +84,12 @@ export function TabBar(props: {
getTabBadge?: (tab: SidebarTab) => ReactNode
}) {
const {
paneId, tabs, active, onActivate, onClose, onNewTab, newTabOptions, onDropTab, onPinTab, getTabIcon, getTabBadge,
paneId, tabs, active, onActivate, onClose, onNewTab, newTabOptions, onDropTab, onFloatTab, onPinTab, getTabIcon, getTabBadge,
} = props
const [menuOpen, setMenuOpen] = useState(false)
// The tab right-click context menu: the target tab plus the cursor
// position (the portaled Menu anchors there, following the git lens/FileTree).
const [tabMenu, setTabMenu] = useState<{ tabId: string; x: number; y: number } | null>(null)
// The strip sits at the panel's top, so the pin submenu must grow downward;
// the flip hook derives that from the cursor y (upper half → "down").
useSubmenuFlip(tabMenu)
const [dragOver, setDragOver] = useState(false)
const listRef = useRef<HTMLDivElement>(null)
// The context target's index in the render-time tab snapshot; -1 when the
@@ -265,7 +265,6 @@ export function TabBar(props: {
setMenuOpen(false)
}}
portal
compact
align="end"
anchor={(
<button
@@ -294,8 +293,9 @@ export function TabBar(props: {
// The target tab drives the pin entry's shape: terminal tabs
// get either a "Pin ▸" submenu (unpinned) or a single "Unpin"
// row (pinned). Non-terminal tabs and missing onPinTab get no
// pin entry at all. Pinned VIRTUAL tabs (injected from other
// sessions) get a stripped menu: only Unpin + Close (no
// pin entry at all — the menu stays exactly the legacy 5-item
// shape. Pinned VIRTUAL tabs (injected from other sessions)
// get a stripped menu: only Unpin + Close (no float, no
// close-others/left/right — those are pane-scoped operations
// that don't apply to cross-session virtual tabs).
const targetTab = tabMenuIndex >= 0 ? tabs[tabMenuIndex] : undefined
@@ -320,6 +320,7 @@ export function TabBar(props: {
]
}
return [
{ id: 'float', label: t('moveToFreeWindow') },
...pinEntries,
{ id: 'close', label: t('close') },
{ id: 'closeOthers', label: t('closeOtherTabs'), ...(tabs.length <= 1 ? { disabled: true } : {}) },
@@ -333,7 +334,9 @@ export function TabBar(props: {
setTabMenu(null)
const index = tabs.findIndex(tab => tab.id === target.tabId)
if (index < 0) return
if (id === 'pinWorkspace') {
if (id === 'float') {
onFloatTab(target.tabId)
} else if (id === 'pinWorkspace') {
onPinTab?.(target.tabId, 'workspace')
} else if (id === 'pinGlobal') {
onPinTab?.(target.tabId, 'global')
@@ -352,7 +355,6 @@ export function TabBar(props: {
}
}}
portal
compact
align="start"
getAnchorRect={() => (tabMenu === null ? null : new DOMRect(tabMenu.x, tabMenu.y, 0, 0))}
anchor={<span />}
+15 -29
View File
@@ -37,7 +37,6 @@ import { FitAddon } from '@xterm/addon-fit'
import { writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
import '@xterm/xterm/css/xterm.css'
import { t } from './locales.ts'
import { ONE_DARK, ONE_LIGHT } from './one-dark-palette.ts'
import { openWhenSized } from './open-when-sized.ts'
import { api, type SessionScope, type TerminalDepsStatus } from './api.ts'
import { agentUuidOf, isAgentTabId, type SidebarStore } from './state.ts'
@@ -61,13 +60,6 @@ const FAILURE_LIMIT = 3
*/
const PTY_DEPS_MISSING = 'pty-deps-missing'
/**
* 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:'
/** The degraded-mode payload rendered by {@link TerminalDepsBanner}. */
type TerminalDepsInfo = Extract<TerminalDepsStatus, { ok: false }>
@@ -75,24 +67,24 @@ type TerminalDepsInfo = Extract<TerminalDepsStatus, { ok: false }>
* Curated ANSI palettes for the terminal. The surface colors (background,
* foreground, cursor, selection) ride the theme tokens so the terminal
* blends with the panel in both schemes; the 16 ANSI colors are the same
* designed syntax families the app's code surfaces use — one-dark for
* dark, one-light for light (one-dark-palette.ts, shared with the
* CodeMirror themes) — read live so a scheme flip re-themes in place.
* designed palettes the app's code surfaces use (one-dark family for dark,
* one-light family for light), read live so a scheme flip re-themes in
* place.
*/
const ANSI_DARK: Record<string, string> = {
black: ONE_DARK.black, red: ONE_DARK.red, green: ONE_DARK.green, yellow: ONE_DARK.yellow,
blue: ONE_DARK.blue, magenta: ONE_DARK.magenta, cyan: ONE_DARK.cyan, white: ONE_DARK.gray,
brightBlack: ONE_DARK.faintGray, brightRed: ONE_DARK.red, brightGreen: ONE_DARK.green,
brightYellow: ONE_DARK.yellow, brightBlue: ONE_DARK.blue, brightMagenta: ONE_DARK.magenta,
brightCyan: ONE_DARK.cyan, brightWhite: ONE_DARK.white,
black: '#282c34', red: '#e06c75', green: '#98c379', yellow: '#e5c07b',
blue: '#61afef', magenta: '#c678dd', cyan: '#56b6c2', white: '#abb2bf',
brightBlack: '#5c6370', brightRed: '#e06c75', brightGreen: '#98c379',
brightYellow: '#e5c07b', brightBlue: '#61afef', brightMagenta: '#c678dd',
brightCyan: '#56b6c2', brightWhite: '#ffffff',
}
const ANSI_LIGHT: Record<string, string> = {
black: ONE_LIGHT.black, red: ONE_LIGHT.red, green: ONE_LIGHT.green, yellow: ONE_LIGHT.yellow,
blue: ONE_LIGHT.blue, magenta: ONE_LIGHT.magenta, cyan: ONE_LIGHT.cyan, white: ONE_LIGHT.gray,
brightBlack: ONE_LIGHT.faintGray, brightRed: ONE_LIGHT.red, brightGreen: ONE_LIGHT.green,
brightYellow: ONE_LIGHT.yellow, brightBlue: ONE_LIGHT.blue, brightMagenta: ONE_LIGHT.magenta,
brightCyan: ONE_LIGHT.cyan, brightWhite: ONE_LIGHT.offWhite,
black: '#383a42', red: '#e45649', green: '#50a14f', yellow: '#c18401',
blue: '#0184bc', magenta: '#a626a4', cyan: '#0997b3', white: '#a0a1a7',
brightBlack: '#4f525e', brightRed: '#e45649', brightGreen: '#50a14f',
brightYellow: '#c18401', brightBlue: '#0184bc', brightMagenta: '#a626a4',
brightCyan: '#0997b3', brightWhite: '#fafafa',
}
/** The xterm theme for the current scheme (surface from tokens, ANSI curated). */
@@ -249,12 +241,6 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store:
})
return
}
// 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
}
// A server-side refusal carries a close code + reason; retrying it
// forever would only spin the banner, so surface it with a retry.
if (event.code === 1011 && event.reason !== '') {
@@ -267,7 +253,7 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store:
failures += 1
if (failures >= FAILURE_LIMIT) {
const detail = event.reason !== '' ? ` (${event.code}: ${event.reason})` : ` (${event.code})`
console.error('[dsh-better-sidebar] terminal connection failed:', event.code, event.reason, url)
console.error('[dsh-apes-sidebar] terminal connection failed:', event.code, event.reason, url)
setFatal(`${t('terminalConnectFailed')}${detail}`)
return
}
@@ -337,7 +323,7 @@ export function TerminalView(props: { scope: SessionScope; tabId: string; store:
fit.fit()
sendResize()
} catch (error) {
console.error('[dsh-better-sidebar] xterm open failed:', error)
console.error('[dsh-apes-sidebar] xterm open failed:', error)
}
})
+14 -14
View File
@@ -31,18 +31,27 @@ import { SandboxStatusBar } from './SandboxStatusBar.tsx'
import { appendToDraft } from './conversation-draft.ts'
import { useSelectionPopup } from './selection-popup.ts'
import { buildSelectionInsert, linesOfSelection } from './selection-payload.ts'
import { lazyChunkComponent } from './lazy-chunk.tsx'
import { analyzeMarkdownHtml } from './markdown-html.ts'
import { LazyMermaidMarkdown, MarkdownDocument, type MarkdownHtmlMedia } from './MarkdownHtml.tsx'
import { MdToc } from './md-toc.tsx'
import { splitMermaidBlocks } from './mermaid-blocks.ts'
import { t } from './locales.ts'
import { HTML_IFRAME_SANDBOX } from './html-preview.ts'
import type { EditorToolbarState, FileViewerProps } from './service.ts'
import css from './sidebar.module.css'
/** Previewable files (rendered output vs source editing). */
type ViewMode = 'preview' | 'edit'
/**
* The sandbox tokens of the HTML preview iframe. NO allow-same-origin (the
* preview must stay in an opaque origin — with the route's own origin it
* could read session data) and NO allow-top-navigation (a previewed page
* must not hijack the GUI). The user can disable the sandbox per-feature
* in the side card settings (warned); the toggle below reflects it.
*/
export const HTML_IFRAME_SANDBOX = 'allow-scripts allow-popups allow-downloads allow-modals'
/** Per-file preview scroll memory. Module-level so it survives viewer
* remounts: the save-then-switch-to-preview reload (EditorHost #215 case B)
* rebuilds the whole TextEditor instance, and without this the preview
@@ -108,16 +117,13 @@ export function TextEditor(props: FileViewerProps) {
setDirty(false)
setSaveState('idle')
selectionPopup.hide()
// hide() reads a live ref; the reset must fire only on a content (file)
// swap, and the hook object's identity churns on every render.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [content])
// A different file switches the remembered preview scroll position to that
// file's own entry (first open: none, so the preview starts at the top).
useEffect(() => {
previewScrollRef.current = previewScrollMemory.get(previewScrollKey(scope, path)) ?? 0
}, [scope, path])
}, [path])
// Create the CodeMirror editor once the content is loaded. The view owns
// the document; React only tracks dirty state through the update listener
@@ -214,7 +220,6 @@ export function TextEditor(props: FileViewerProps) {
// The keymap's save() reads live refs; scope/path are stable for a
// tab's lifetime, and the dark flip is handled by the reconfigure
// effect below (recreating the view here would drop the draft).
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [content, path])
// Scheme flip: re-theme in place (the compartment holds only the
@@ -271,9 +276,6 @@ export function TextEditor(props: FileViewerProps) {
view.requestMeasure()
})
})
// The reveal reads the live document/view refs; only the flip into
// preview triggers it.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mode])
// Snapshot the live document into the draft whenever the preview needs
@@ -338,11 +340,9 @@ export function TextEditor(props: FileViewerProps) {
() => (markdown && mode === 'preview' ? splitMermaidBlocks(previewMdText) : []),
[markdown, mode, previewMdText],
)
/** Raw-HTML analysis (block runs lifted out + inline gate). Non-null for
* every markdown preview, so the render below always takes the split
* renderer — its markdown runs rewrite local image destinations internally
* (see MarkdownHtml.tsx). The legacy single-pass branches (fed the
* pre-rewritten `previewText`) are dead in the current wiring. */
/** Raw-HTML analysis (block runs lifted out + inline gate). Non-null only
* for documents that actually contain HTML — plain markdown keeps the
* legacy single-pass render path below, byte-for-byte. */
const htmlInfo = useMemo(
() => (markdown && mode === 'preview' ? analyzeMarkdownHtml(previewMdText) : null),
[markdown, mode, previewMdText],
+1 -7
View File
@@ -63,15 +63,11 @@ export function TreePanel(props: {
onOpenWith?: (targetId: string, path: string) => void
onToggleOpenWithPin?: (targetId: string) => void
onReferenceFile: (path: string, isDir: boolean) => void
/** A tree rename landed (passed through to FileTree for tab retargeting). */
onPathRenamed?: (oldPath: string, newPath: string) => void
/** A tree delete landed (passed through to FileTree for tab closing). */
onPathDeleted?: (path: string, isDir: boolean) => void
/** Full-window presentation: the panel fills its host instead of docking
* at a fixed width. */
full?: boolean
}) {
const { sessionId, cwd, store, expanded, revealed, onToggle, onOpenFile, onOpenFileNewTab, onOpenFileSide, openWithTargets, openWithPinned, openWithSsh, onOpenWith, onToggleOpenWithPin, onReferenceFile, onPathRenamed, onPathDeleted, full } = props
const { sessionId, cwd, store, expanded, revealed, onToggle, onOpenFile, onOpenFileNewTab, onOpenFileSide, openWithTargets, openWithPinned, openWithSsh, onOpenWith, onToggleOpenWithPin, onReferenceFile, full } = props
const [query, setQuery] = useState('')
const [results, setResults] = useState<{ matches: string[]; truncated: boolean } | null>(null)
const [error, setError] = useState<string | null>(null)
@@ -256,8 +252,6 @@ export function TreePanel(props: {
onOpenWith={onOpenWith}
onToggleOpenWithPin={onToggleOpenWithPin}
onReferenceFile={onReferenceFile}
onPathRenamed={onPathRenamed}
onPathDeleted={onPathDeleted}
refreshTick={refreshTick}
onUploadRequest={startUpload}
busy={busy}
+9 -15
View File
@@ -43,7 +43,7 @@ const COPIED_FEEDBACK_MS = 1500
/** The modal body: the GitHub topic button + the recommended plugin list
* with per-entry jump/copy buttons (extracted for direct testing). */
export function PluginListBody(props: { service: BetterSidebarService; kind: PluginKind }) {
const { kind } = props
const { service, kind } = props
// Which entry's copy button currently shows the "已复制" feedback.
const [copiedId, setCopiedId] = useState<string | null>(null)
// Live catalog filter (name / id / description). A free-text search keeps
@@ -56,9 +56,8 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
const needle = query.trim().toLowerCase()
const matches = (entry: PluginEntry): boolean => {
if (needle === '') return true
const name = typeof entry.name === 'function' ? entry.name() : entry.name
const description = typeof entry.description === 'function' ? entry.description() : entry.description
return name.toLowerCase().includes(needle)
return entry.name.toLowerCase().includes(needle)
|| entry.id.toLowerCase().includes(needle)
|| description.toLowerCase().includes(needle)
}
@@ -94,12 +93,8 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
window.open(entry.url, '_blank', 'noopener')
}
/** One catalog row (extracted so the group render stays flat). The name
* resolves like the description (string or () => string) so it follows
* the active locale; a plain-string entry keeps its raw name. */
const renderEntry = (entry: PluginEntry): ReactNode => {
const name = typeof entry.name === 'function' ? entry.name() : entry.name
return (
/** One catalog row (extracted so the group render stays flat). */
const renderEntry = (entry: PluginEntry): ReactNode => (
<div key={entry.id} className={css.pluginEntry}>
<div className={css.pluginEntryHead}>
{/* The name is a BUTTON on the same window.open path as the
@@ -109,16 +104,16 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
<button
type="button"
className={css.pluginName}
aria-label={`${t('openPlugin')}: ${name}`}
aria-label={`${t('openPlugin')}: ${entry.name}`}
onClick={() => { jump(entry) }}
>
{name}
{entry.name}
</button>
<span className={css.pluginEntryActions}>
<button
type="button"
className={css.pluginJumpBtn}
aria-label={`${t('openPlugin')}: ${name}`}
aria-label={`${t('openPlugin')}: ${entry.name}`}
onClick={() => { jump(entry) }}
>
{t('openPlugin')}
@@ -126,7 +121,7 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
<button
type="button"
className={css.pluginCopyBtn}
aria-label={`${t('copyInstall')}: ${name}`}
aria-label={`${t('copyInstall')}: ${entry.name}`}
onClick={() => { copy(entry) }}
>
{copiedId === entry.id ? t('copied') : t('copy')}
@@ -138,8 +133,7 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
</div>
<code className={css.pluginInstall}>{entry.install}</code>
</div>
)
}
)
return (
<div className={css.pluginList}>
+50
View File
@@ -0,0 +1,50 @@
/**
* Tab glyphs for the apes tabs (智能体 / 应用 / 画像).
*
* These follow the SAME recipe as this repo's own icon set in
* `src/client/icons.tsx` — the one the sidebar already uses for 终端 /
* 浏览器 / 文件变动: a 16×16 viewBox whose artwork spans 1.5–14.5, a 1.5px
* `currentColor` stroke, round caps/joins, `fill: none`.
*
* That recipe is what makes an icon sit at the native visual weight in the
* `+` new-tab menu and the right-panel tile grid: DSH's own glyphs fill
* ~92–96% of the box, so artwork padded any tighter renders visibly small
* next to them.
*
* (An earlier revision drew these as fill-only paths spanning only 10–12 of
* the 16 units — that is why they read as a separate, lighter family.)
*/
import type { ReactNode } from 'react'
/** Agent glyph: a rounded robot head with two fill eyes. */
export function IconAgentOutline16({ size = 16, className }: { size?: number; className?: string }): ReactNode {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<rect x="1.5" y="3" width="13" height="11.5" rx="4.2" stroke="currentColor" strokeWidth="1.5" />
<circle cx="5.8" cy="8.6" r="1.25" fill="currentColor" />
<circle cx="10.2" cy="8.6" r="1.25" fill="currentColor" />
</svg>
)
}
/** Apps glyph: a 2×2 grid of rounded squares. */
export function IconAppsOutline16({ size = 16, className }: { size?: number; className?: string }): ReactNode {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<rect x="1.5" y="1.5" width="5.5" height="5.5" rx="1.8" stroke="currentColor" strokeWidth="1.5" />
<rect x="9" y="1.5" width="5.5" height="5.5" rx="1.8" stroke="currentColor" strokeWidth="1.5" />
<rect x="1.5" y="9" width="5.5" height="5.5" rx="1.8" stroke="currentColor" strokeWidth="1.5" />
<rect x="9" y="9" width="5.5" height="5.5" rx="1.8" stroke="currentColor" strokeWidth="1.5" />
</svg>
)
}
/** Profile glyph: a head over shoulders. */
export function IconUserOutline16({ size = 16, className }: { size?: number; className?: string }): ReactNode {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<circle cx="8" cy="4.9" r="3.4" stroke="currentColor" strokeWidth="1.5" />
<path d="M1.5 14.5c0-3.3 2.8-5.3 6.5-5.3s6.5 2 6.5 5.3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
)
}
+43
View File
@@ -0,0 +1,43 @@
/**
* apes:* information-layer tabs — the fork's own contribution on top of the
* upstream dsh-better-sidebar. Three tabs ship (agents / apps / profile);
* `apes:tasks` was removed (C6) because it overlaps the official DSH session
* tree in the native sidebar, and `messages` is deferred (no DSH
* message-stream API yet — tracked separately).
*
* Isolated under `src/client/apes/` so the upstream `builtins/` tree stays
* untouched: cherry-picking upstream releases only has to keep the single
* `registerApesTabs(...)` call site in `index.tsx` (see D1 = module B).
*
* Every descriptor is registered through the same `BetterSidebarService`
* the builtins use (dogfood), so the apes tabs get the full sidebar
* lifecycle for free: + menu, enable/disable switches, badge pills,
* dedupe/focus, and HMR-safe disposal via the returned disposer.
*/
import type { Context } from '../../context-types.ts'
import type { BetterSidebarService } from '../service.ts'
import { apesTabs } from './tabs.tsx'
// Re-exported so index.tsx can register the apes dictionaries + attach the
// locale service in one import (keeps the apes module self-contained).
export { APES_LOCALE_NS, apesZh, apesEn, attachApesLocale } from './locales.ts'
/**
* Register the apes:* tabs with the sidebar service. Returns a disposer that
* unregisters all of them (cordis auto-invokes it on fiber disposal, so HMR
* re-activation re-registers cleanly — same contract as registerBuiltins).
*/
export function registerApesTabs(
_ctx: Context,
service: BetterSidebarService,
): () => void {
const disposers: (() => void)[] = []
for (const tab of apesTabs) {
disposers.push(service.registerTab(tab))
}
return () => {
for (const d of disposers) {
try { d() } catch { /* already disposed */ }
}
}
}
+90
View File
@@ -0,0 +1,90 @@
/**
* apes:* tab i18n — isolated from the upstream `betterSidebar` locale
* namespace (D1 = module B). The fork keeps its own `apes` namespace and
* its own `t()` so that adding apes keys never touches the upstream
* `locales.ts` (whose `CopyKey = keyof typeof zh` is closed over its own
* dicts). The `t()` mirrors the upstream implementation's resolution order
* (active-locale chain → key fallback) but reads the apes dicts; it is NOT
* wired to the better-locale override store in Phase 1 (those third-language
* overrides ride the lazy `locale` chunk that only covers `betterSidebar`
* entries — apes keys fall back to zh/en, which is acceptable for the
* placeholder round and gets the override integration in Phase 2).
*/
/** The apes locale namespace (registered with ctx.locale under this key). */
export const APES_LOCALE_NS = 'apes'
/** The zh dictionary (source of truth). */
export const apesZh = {
// View titles
apesAgentsTitle: '智能体',
apesAppsTitle: '应用',
apesProfileTitle: '画像',
// Agents view
apesAgentsEmpty: '当前会话没有子智能体',
apesAgentsRoot: '主智能体',
apesAgentRunning: '运行中',
apesAgentInactive: '空闲',
apesAgentOneShot: '一次性',
apesAgentContinuable: '可续',
// Profile view
apesProfileCurrent: '当前会话',
apesProfileCwd: '工作目录',
apesProfileNoCwd: '未知',
apesProfileRunning: '运行中',
apesProfileIdle: '空闲',
/** Guide-page capsule description (official right sidebar's entry list). */
apesProfileGuideDesc: '当前会话的身份与工作目录',
// Apps view
apesAppsNoFeed: '当前 DSH 版本未提供工作区应用数据源',
apesAppsWorkspace: '工作区',
} as const
/** The en dictionary (checked against zh — every zh key must appear here). */
export const apesEn: Record<keyof typeof apesZh, string> = {
apesAgentsTitle: 'Agents',
apesAppsTitle: 'Apps',
apesProfileTitle: 'Profile',
apesAgentsEmpty: 'No subagents in this session',
apesAgentsRoot: 'Main agent',
apesAgentRunning: 'running',
apesAgentInactive: 'idle',
apesAgentOneShot: 'one-shot',
apesAgentContinuable: 'continuable',
apesProfileCurrent: 'Current session',
apesProfileCwd: 'Working directory',
apesProfileNoCwd: 'unknown',
apesProfileRunning: 'running',
apesProfileIdle: 'idle',
apesProfileGuideDesc: "This session's identity and working directory",
apesAppsNoFeed: 'This DSH version exposes no workspace-app feed',
apesAppsWorkspace: 'Workspace',
}
export type ApesCopyKey = keyof typeof apesZh
/** Resolve the currently active DSH locale ('zh' | 'en' today). */
export function attachApesLocale(service: { getSnapshot(): { active: string } } | undefined): void {
apesLocaleService = service
}
let apesLocaleService: { getSnapshot(): { active: string } } | undefined
/**
* Translate an apes key. Resolution order: active-locale chain (zh dict when
* the DSH preference is zh, en dict otherwise) → key fallback (shows the key
* itself rather than `undefined`). Mirrors the upstream t() minus the
* better-locale override (Phase 2).
*/
export function t(key: ApesCopyKey, params?: Record<string, string | number>): string {
const active = apesLocaleService?.getSnapshot().active ?? 'zh'
const dict = active.toLowerCase().startsWith('zh') ? apesZh : apesEn
let text: string | undefined = dict[key]
if (text === undefined) text = key
if (params !== undefined) {
for (const [name, value] of Object.entries(params)) {
text = text.replaceAll(`{${name}}`, String(value))
}
}
return text
}
+187
View File
@@ -0,0 +1,187 @@
/**
* Native right-sidebar bridge (Phase A spike) — registers apes:* tabs as
* OFFICIAL `@deepseek-ai/dsh-client-ui-sidebar-right` tab types through the
* public two-stage protocol its README documents:
*
* 1. TYPE — `ctx.sidebarRightTabs.register({ id, kind, priority, title, guide? })`
* a static declaration with no runtime hooks; the `id` is the type's
* identity in the tab system AND the key its body registers under.
* 2. BODY — `ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register(
* { name: 'sidebar.right.pane.tab', key: <id> }, Body))`
* the keyed seat dispatches a tab's body to the type registered for its
* `kind`; the body reads `useTabInfo()` (injected by the seat) for
* `{ sidebar, panel, tab }`.
*
* ACTIVATION ORDER — `sidebarRightTabs` is installed by that package's own
* `apply()` through `ctx.reflect.provide(...)`, so it may not exist yet when
* our plugin activates. We therefore reach it through `ctx.inject(['sidebarRightTabs'], …)`
* (the same pattern intercept.tsx uses for the async `remote.session`
* namespace): the callback runs when the service exists and re-runs after a
* remount, so the bridge is order-independent and HMR-safe. Deployments
* without the official right sidebar simply never see the callback — the
* fork's own shell stays the only surface, which is also what the render-time
* `guide`/type registration tolerates.
*
* SCOPE — the spike registers exactly ONE tab (`apes:profile` / 画像) to prove
* the path end-to-end before deciding on the full nine-tab migration. The
* existing `service.registerTab` route in `tabs.tsx` is deliberately left
* untouched, so both surfaces coexist during the spike and the whole spike
* reverts by deleting this file plus its one call site.
*
* Side effect worth knowing (inherent to the official default-page rule): the
* dock's default page depends on how many guide entries are registered —
* exactly one auto-opens it, zero or many open the guide page. Our guide entry
* therefore turns a fresh-empty dock from "Files" into the guide page listing
* every entry capsule.
*/
import { useMemo, type ReactNode } from 'react'
import { IconUserOutline16 } from './icons.tsx'
import { t } from './locales.ts'
import { ApesProfileView } from './tabs.tsx'
import type { TabComponentProps } from '../service.ts'
/** The apes type's identity in the official tab system. Package-scoped so it
* can never collide with a native or third-party registration. */
const PROFILE_ID = 'dsh-apes-sidebar/profile'
/** The kind the panel opens the tab with (`openTab(kind)`); `apes:` prefixed so
* it reads as ours in the official registry. */
const PROFILE_KIND = 'apes:profile'
/** The official registry's definition shape (the subset we use). */
interface NativeTabDefinition {
id: string
kind: string
priority?: 'builtin' | 'extension' | 'fallback'
title: () => string
guide?: Array<{
order: number
title: () => string
description?: () => string
icon?: (props: { size?: number; className?: string }) => ReactNode
}>
}
/** The registry service face the official package provides. */
interface NativeTabRegistry {
register(definition: NativeTabDefinition): () => void
}
/** The slots service face (structural subset of `@deepseek-ai/dsh-client-ui-slots`). */
interface NativeSlots {
inject(key: string, callback: () => () => void): () => void
register(options: {
name: string
key?: string
locale?: string
inject?: (...args: unknown[]) => Record<string, unknown>
}, component: (props: Record<string, unknown>) => ReactNode): () => void
}
/** What the seat hands a tab body in addition to our own injected props. */
interface NativeTabBodyProps {
useTabInfo?: () => unknown
sessionId?: string
ctx: TabComponentProps['ctx']
}
/** The tab info the seat's `useTabInfo()` hook returns (the subset we read). */
interface NativeTabInfo {
sidebar?: { expanded?: boolean; fullscreen?: boolean }
panel?: { id?: string }
tab?: { id?: string; title?: string; visible?: boolean }
}
/** Fallback reader for an absent seat hook (never in production — the seat
* always injects `tabInfo`; this keeps the body hook-safe either way, since
* the fallback calls no hooks of its own). */
function fallbackTabInfo(): NativeTabInfo {
return { sidebar: { expanded: true }, panel: { id: '' }, tab: { visible: true } }
}
/**
* The official surface's body for 画像: the SAME `ApesProfileView` the fork
* already ships, re-bound to the official tab scope. The view reads only
* `props.ctx` (the sessions feed) and `props.scope` (sessionId / cwd), so the
* shell-only props are filled with placeholders — which is the spike's whole
* point: the panel content is shared, never duplicated.
*/
function ProfileNativeBody(props: NativeTabBodyProps): ReactNode {
const useInfo = props.useTabInfo ?? fallbackTabInfo
const info = useInfo() as NativeTabInfo | undefined
const sessionId = props.sessionId
?? props.ctx.sessions.list.getSnapshot().current
?? ''
const scope = useMemo(() => ({ sessionId }), [sessionId])
const viewProps = {
ctx: props.ctx,
scope,
visible: info?.tab?.visible ?? true,
store: undefined,
tab: {
id: info?.tab?.id ?? PROFILE_ID,
type: PROFILE_KIND,
title: info?.tab?.title ?? t('apesProfileTitle'),
},
} as unknown as TabComponentProps
return <ApesProfileView {...viewProps} />
}
/**
* Register the apes:profile tab as an official right-sidebar tab type.
* @param ctx — the client context whose `inject` waits for the registry service.
* @returns a disposer that disposes the inject fiber (which unwinds both stages).
*/
export function registerApesNativeTabs(ctx: unknown): () => void {
const anyCtx = ctx as {
inject?: (deps: string[], callback: (fctx: unknown) => void) => { dispose(): void }
}
if (typeof anyCtx.inject !== 'function') return () => {}
const fiber = anyCtx.inject(['sidebarRightTabs'], (fctx: unknown) => {
const inner = fctx as {
get?: (name: string) => unknown
slots?: NativeSlots
effect?: (callback: () => () => void, label?: string) => unknown
}
const registry = inner.get?.('sidebarRightTabs') as NativeTabRegistry | undefined
const slots = inner.slots
if (registry === undefined || slots === undefined || typeof inner.effect !== 'function') return
inner.effect(() => {
const disposers: Array<() => void> = []
try {
// Stage 1 — the type. A page type: it recognizes no resource address
// and is opened by kind; `extension` is the band for types contributed
// from outside the product (highest rank, so it can also take over a
// builtin of the same kind).
disposers.push(registry.register({
id: PROFILE_ID,
kind: PROFILE_KIND,
priority: 'extension',
title: () => t('apesProfileTitle'),
guide: [{
order: 70,
title: () => t('apesProfileTitle'),
description: () => t('apesProfileGuideDesc'),
icon: IconUserOutline16,
}],
}))
// Stage 2 — the body, dispatched through the keyed seat. `inject`
// hands the body the client context the view reads its feed from.
disposers.push(slots.inject('sidebar.right.pane.tab', () => slots.register({
name: 'sidebar.right.pane.tab',
key: PROFILE_ID,
inject: () => ({ ctx }),
}, ProfileNativeBody as unknown as (props: Record<string, unknown>) => ReactNode)))
} catch (error) {
// A half-registered bridge must never take the fork's own sidebar
// down: unwind what landed and let the own-shell surface carry on.
console.error('[dsh-apes-sidebar] native right-sidebar registration failed:', error)
}
return () => {
for (const dispose of disposers.reverse()) {
try { dispose() } catch { /* already disposed */ }
}
}
}, 'dsh-apes-sidebar: apes native tab (profile)')
})
return () => { void fiber.dispose() }
}
+237
View File
@@ -0,0 +1,237 @@
/**
* apes:* tab views (D2 = data wiring). Each tab reads live data from the
* sessions list feed (`ctx.sessions.list`) via `useSyncExternalStore` and
* reuses the fork's PURE lineage/job helpers — these are framework-free and
* live outside `builtins/`, so importing them does not couple the apes module
* to upstream UI and survives cherry-picks.
*
* Three information-layer tabs (C6: `apes:tasks` removed — it overlaps the
* official DSH session tree in the native sidebar):
* - apes:agents → the subagent topology rooted at the main agent of the
* current session (rootAncestor + per-parent catalogs).
* - apes:apps → no DSH host feed today: shows the workspace (cwd) of the
* current session, otherwise an honest "no feed" note.
* - apes:profile → the current session's summary row (title, cwd, running).
*
* Visual unity (C7): colors use the DSW design-token aliases
* (`--dsw-alias-*`) so the apes panels match the native DSH sidebar
* surface; the runtime defines these on :root. Font sizes / spacing /
* radius stay inline (DSH exposes no public `--ds-*` geometry alias for
* plugin panels — its own vendor bundles inline those too).
*
* Data sources (structural mirror — runtime provided by the DSH host):
* - `ctx.sessions.list.getSnapshot()` → { current, byId, subagentsByParent? };
* `subscribe(cb)` is uSES-safe.
* - `scope.sessionId` = the currently-open session.
*/
import { useCallback, useMemo, useState, type CSSProperties } from 'react'
import { useSyncExternalStore } from 'react'
import { IconAgentOutline16, IconAppsOutline16, IconUserOutline16 } from './icons.tsx'
import type { TabComponentProps, TabDescriptor } from '../service.ts'
import type {
SidebarSessionList,
SidebarSessionSummary,
SidebarSubagentCatalog,
SidebarSubagentChildEntry,
} from '../../context-types.ts'
import { rootAncestor, isSideThreadSummary } from '../subagent-lineage.ts'
import { t } from './locales.ts'
/**
* DSW design-token color aliases. The DSH runtime defines these on :root
* (same set the native dsh-client-ui-* vendor bundles consume). We reference
* them by name via `var(...)` with a literal fallback so the panels still
* render in a non-DSH context (unit harness) with a sane dark palette.
*/
const DSW = {
bgBase: 'var(--dsw-alias-bg-base, #1a1a1a)',
bgLayer1: 'var(--dsw-alias-bg-layer-1, #202020)',
borderL1: 'var(--dsw-alias-border-l1, #2a2a2a)',
borderL2: 'var(--dsw-alias-border-l2, #303030)',
labelPrimary: 'var(--dsw-alias-label-primary, #e8e8e8)',
labelSecondary: 'var(--dsw-alias-label-secondary, #a0a0a0)',
labelTertiary: 'var(--dsw-alias-label-tertiary, #7a7a7a)',
success: 'var(--dsw-alias-state-success-primary, #52c41a)',
interactiveHover: 'var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06))',
} as const
/** Shared inline styles (KISS: one object, no CSS module). Colors via DSW. */
const S: Record<string, CSSProperties> = {
body: { padding: 12, display: 'flex', flexDirection: 'column', gap: 12 },
title: { margin: 0, fontSize: 14, fontWeight: 600, color: DSW.labelPrimary },
hint: { margin: 0, fontSize: 12, color: DSW.labelTertiary },
list: { display: 'flex', flexDirection: 'column', gap: 8 },
card: { border: `1px solid ${DSW.borderL1}`, borderRadius: 8, padding: 10, display: 'flex', flexDirection: 'column', gap: 6, background: DSW.bgLayer1 },
cardRow: { display: 'flex', alignItems: 'center', gap: 8 },
cardLabel: { fontSize: 13, fontWeight: 500, flex: 1, minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: DSW.labelPrimary },
cardSecondary: { fontSize: 11, color: DSW.labelSecondary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
badge: { fontSize: 11, color: DSW.labelTertiary, flexShrink: 0 },
}
/** A small colored status dot (self-contained, not the builtin StateDot). */
function Dot(props: { color: string }) {
const style: CSSProperties = {
width: 8, height: 8, borderRadius: '50%', background: props.color, flexShrink: 0, display: 'inline-block',
}
return <span aria-hidden style={style} />
}
/** Subscribe to the sessions list feed (uSES-safe). Returns the live snapshot. */
function useSessionList(ctx: TabComponentProps['ctx']): SidebarSessionList {
const sessions = ctx.sessions
return useSyncExternalStore(
useMemo(() => (callback: () => void) => sessions.list.subscribe(callback), [sessions]),
useCallback(() => sessions.list.getSnapshot(), [sessions]),
)
}
/** One level of the subagent topology (rooted at the main agent). */
function agentChildren(
byId: Readonly<Record<string, SidebarSessionSummary>>,
parentSessionId: string,
): SidebarSessionSummary[] {
return Object.values(byId).filter(
s => s.origin === 'subagent' && s.parentId === parentSessionId && !isSideThreadSummary(s),
)
}
/** apes:agents — the subagent topology of the current session's tree. */
function ApesAgentsView(props: TabComponentProps) {
const list = useSessionList(props.ctx)
const { byId, subagentsByParent } = list
const rootId = useMemo(() => rootAncestor(byId, props.scope.sessionId), [byId, props.scope.sessionId])
const rootSummary = rootId === undefined ? undefined : byId[rootId]
const rootCatalog = rootId === undefined ? undefined : subagentsByParent?.[rootId]
// The topology: the main agent + its direct subagent children (one level,
// with per-child activity). A lazy catalog (subagentsByParent) is preferred
// for the child list when present; otherwise fall back to durable byId rows.
// (`.filter` does not narrow a union — narrow explicitly to child entries.)
const children: SidebarSubagentChildEntry[] = (rootCatalog?.entries ?? [])
.filter((e): e is SidebarSubagentChildEntry => e.kind === 'child')
.concat(rootCatalog === undefined
? agentChildren(byId, rootId ?? '').map<SidebarSubagentChildEntry>(s => ({
kind: 'child', id: s.id, mode: 'continuable',
activity: s.running === true ? 'running' : 'inactive',
label: s.displayTitle, hasChildren: agentChildren(byId, s.id).length > 0,
}))
: [])
return (
<div style={S.body}>
<h2 style={S.title}>{t('apesAgentsTitle')}</h2>
{rootSummary === undefined && children.length === 0
? <p style={S.hint}>{t('apesAgentsEmpty')}</p>
: (
<div style={S.list}>
{/* Root (main agent) node */}
{rootSummary !== undefined && (
<div style={S.card}>
<div style={S.cardRow}>
<Dot color={rootSummary.running === true ? DSW.success : DSW.labelTertiary} />
<span style={S.cardLabel} title={rootSummary.displayTitle}>{t('apesAgentsRoot')}: {rootSummary.displayTitle}</span>
</div>
<div style={S.cardRow}>
<span style={S.badge}>{rootSummary.running === true ? t('apesAgentRunning') : t('apesAgentInactive')}</span>
</div>
</div>
)}
{/* Direct subagent children */}
{children.map(entry => {
const summary = byId[entry.id]
const running = entry.activity === 'running'
const label = entry.label ?? summary?.displayTitle ?? entry.id
return (
<div key={entry.id} style={{ ...S.card, marginLeft: 14, borderLeft: `2px solid ${DSW.borderL2}` }}>
<div style={S.cardRow}>
<Dot color={running ? DSW.success : DSW.labelTertiary} />
<span style={S.cardLabel} title={label}>{label}</span>
</div>
<div style={S.cardRow}>
<span style={S.cardSecondary}>
{entry.mode === 'one-shot' ? t('apesAgentOneShot') : t('apesAgentContinuable')}
{' · '}
{running ? t('apesAgentRunning') : t('apesAgentInactive')}
</span>
</div>
</div>
)
})}
</div>
)}
</div>
)
}
/** apes:profile — the current session's identity (title, cwd, running state). */
export function ApesProfileView(props: TabComponentProps) {
const list = useSessionList(props.ctx)
const current = list.current
const summary = current === undefined ? undefined : list.byId[current]
const cwd = summary?.cwd ?? props.scope.cwd
return (
<div style={S.body}>
<h2 style={S.title}>{t('apesProfileTitle')}</h2>
{summary === undefined
? <p style={S.hint}>{t('apesProfileNoCwd')}</p>
: (
<div style={S.list}>
<div style={S.card}>
<div style={S.cardRow}>
<Dot color={summary.running === true ? DSW.success : DSW.labelTertiary} />
<span style={S.cardLabel} title={summary.displayTitle}>{t('apesProfileCurrent')}</span>
</div>
<div style={S.cardRow}>
<span style={S.badge}>{summary.running === true ? t('apesProfileRunning') : t('apesProfileIdle')}</span>
</div>
</div>
<div style={S.card}>
<div style={S.cardRow}>
<span style={S.cardLabel}>{summary.displayTitle}</span>
</div>
<div style={S.cardRow}>
<span style={S.cardSecondary}>{t('apesProfileCwd')}: {cwd ?? t('apesProfileNoCwd')}</span>
</div>
</div>
</div>
)}
</div>
)
}
/** apes:apps — no DSH host feed today; shows the current workspace (cwd). */
function ApesAppsView(props: TabComponentProps) {
const list = useSessionList(props.ctx)
const current = list.current
const summary = current === undefined ? undefined : list.byId[current]
const cwd = summary?.cwd ?? props.scope.cwd
return (
<div style={S.body}>
<h2 style={S.title}>{t('apesAppsTitle')}</h2>
<p style={S.hint}>{t('apesAppsNoFeed')}</p>
<div style={S.card}>
<div style={S.cardRow}>
<span style={S.cardLabel}>{t('apesAppsWorkspace')}</span>
</div>
<div style={S.cardRow}>
<span style={S.cardSecondary}>{cwd ?? t('apesProfileNoCwd')}</span>
</div>
</div>
</div>
)
}
/**
* The three apes:* tab descriptors (C6: `apes:tasks` removed — overlaps the
* official DSH session tree). Order 901-903: agents/apps/profile. Each is
* already shaped as `TabDescriptor` (component + single-instance + icon
* element + i18n title function) so `registerApesTabs` can hand them straight
* to `service.registerTab`. Information-layer tabs are single-instance.
*/
export const apesTabs: TabDescriptor[] = [
{ id: 'apes:agents', title: () => t('apesAgentsTitle'), icon: (size: number) => <IconAgentOutline16 size={size} />, order: 60, single: true, component: ApesAgentsView },
{ id: 'apes:apps', title: () => t('apesAppsTitle'), icon: (size: number) => <IconAppsOutline16 size={size} />, order: 65, single: true, component: ApesAppsView },
{ id: 'apes:profile', title: () => t('apesProfileTitle'), icon: (size: number) => <IconUserOutline16 size={size} />, order: 70, single: true, component: ApesProfileView },
]
+3 -20
View File
@@ -8,7 +8,7 @@
*/
import { encodeHtmlUrl } from '../html-route.ts'
import type { LastActivity } from '../subagent-activity.ts'
import type { SidechatLiveEvent, SidechatLogEvent, SidechatThreadInfo } from '../sidechat-core.ts'
import type { SidechatLogEvent, SidechatThreadInfo } from '../sidechat-core.ts'
import type { SidebarSessionEvent } from '../context-types.ts'
import type { BrowserProbeResult } from './browser.ts'
@@ -278,15 +278,6 @@ export const api = {
call<FsTextResult | FsBinaryResult>('fs.read', scopePayload(scope, { path }), signal),
fsWrite: (scope: SessionScope, path: string, content: string) =>
call<{ ok: true }>('fs.write', scopePayload(scope, { path, content })),
/** Rename one tree row within its directory (single-segment name; the
* server refuses existing destinations, the workspace root, and — while
* the fence is armed — anything resolving outside the workspace). */
fsRename: (scope: SessionScope, path: string, name: string) =>
call<{ path: string }>('fs.rename', scopePayload(scope, { path, name })),
/** Permanently delete one tree row (recursive for directories; a symlink
* row unlinks the link only). The UI confirms before calling this. */
fsRemove: (scope: SessionScope, path: string) =>
call<{ path: string }>('fs.remove', scopePayload(scope, { path })),
/** Upload one file's raw bytes into `dir` (keeps the folder tree via
* `relativePath`); the host streams it under the session workspace. */
uploadFile: (scope: SessionScope, dir: string, relativePath: string, body: Blob, signal?: AbortSignal) =>
@@ -316,11 +307,6 @@ export const api = {
/** Full patch text of one commit (diff display for the history rows). */
gitCommitDiff: (scope: SessionScope, hash: string, worktree?: string, signal?: AbortSignal) =>
call<{ diff: string }>('git.commit-diff', gitPayload(scope, worktree, { hash }), signal),
/** One file's content at a revision (`git show <rev>:<path>`); null when the
* revision has no such path. The diff views' on-demand hunk-fold expansion
* reads both sides' full contents through this. */
gitShow: (scope: SessionScope, rev: string, path: string, worktree?: string, signal?: AbortSignal) =>
call<{ content: string | null }>('git.show', gitPayload(scope, worktree, { rev, path }), signal),
/** The session's file-tool events for the changes tab's session lens: the
* `tool/call` + `tool/result` rows past `afterSeq` (0 = whole window),
* capped to the recent window host-side. The client runtime exposes no
@@ -390,12 +376,9 @@ export const api = {
call<SidechatThreadInfo>('sidechat.info', { childId }),
/** One transcript pull of a Side Chat thread: the thread's OWN events
* (the inherited seed is cut host-side and never crosses the wire).
* `afterSeq` narrows the response to the delta beyond it (poll tail).
* `live` is the thread's in-flight model deltas (DSH 0.1.5 publishes them
* outside the session log) — the CURRENT attempt on every pull, never a
* delta, so the caller replaces its live set instead of appending. */
* `afterSeq` narrows the response to the delta beyond it (poll tail). */
sidechatEvents: (childId: string, afterSeq?: number, signal?: AbortSignal) =>
call<{ events: SidechatLogEvent[]; live: SidechatLiveEvent[] }>('sidechat.events', {
call<{ events: SidechatLogEvent[] }>('sidechat.events', {
childId,
...(afterSeq !== undefined ? { afterSeq } : {}),
}, signal),
+28 -3
View File
@@ -22,7 +22,7 @@ import { SubagentView } from '../SubagentView.tsx'
import { consumeSidechatSeed, SideChatView, sidechatThreadIdOf } from '../SideChatView.tsx'
import { api } from '../api.ts'
import { BrowserView } from '../BrowserView.tsx'
import { IconTerminalOutline16, IconDiffOutline16, IconGlobeOutline16 } from '../icons.tsx'
import { IconTerminalOutline16, IconDiffOutline16, IconGlobeOutline16, IconFloatWindowOutline16, IconPanelBottomOutline16 } from '../icons.tsx'
import { TERMINAL_FONT_SIZE_MAX, TERMINAL_FONT_SIZE_MIN } from '../../prefs-shared.ts'
import type { ComponentType } from 'react'
import type { SessionScope } from '../api.ts'
@@ -72,7 +72,7 @@ function terminalUuid(): string {
/** Count UI-owned terminals (agent:` tabs excluded — they are the model's). */
function uiTerminalCount(state: SidebarState): number {
return allLeaves(state.bottomSplits)
return allLeaves(state.splits)
.flatMap(leaf => leaf.tabs)
.filter(tab => tab.type === 'terminal' && !isAgentTabId(tab.id)).length
}
@@ -155,7 +155,32 @@ export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): read
const count = opCountOf(scope.sessionId)
return count === undefined || count === 0 ? null : count
},
component: ({ ctx, store, scope, tab, visible, onOpenDiff }) => (
// Declarative settings: the diff-open picker (free window vs docked
// pane) renders as an iconed select row under the changes card's gear
// in the Side card settings page.
settings: {
toggles: [{
key: 'changesDiffFloat',
type: 'select',
title: () => t('changesDiffOpenTitle'),
desc: () => t('changesDiffOpenDesc'),
options: [
{
value: true,
icon: (size: number) => <IconFloatWindowOutline16 size={size} />,
title: () => t('changesDiffOpenFloat'),
desc: () => t('changesDiffOpenFloatDesc'),
},
{
value: false,
icon: (size: number) => <IconPanelBottomOutline16 size={size} />,
title: () => t('changesDiffOpenPane'),
desc: () => t('changesDiffOpenPaneDesc'),
},
],
}],
},
component: ({ ctx, store, scope, tab, visible, onOpenFile, onOpenDiff }) => (
<ChangesTab
ctx={ctx}
store={store}
+3 -30
View File
@@ -1,15 +1,5 @@
/**
* Anchor selectors for the DSH AppFrame conversation slot host, newest first:
*
* - DSH 0.1.5-alpha.2 replaced the root `conversation` slot with a root-scoped
* keyed `main` slot, so the conversation renders under `main.conversation`
* (ConversationPanel -> `renderSlot('main.conversation')`);
* - alpha.1 and earlier spell the same host `conversation`.
*
* Both keys are accepted deliberately so one build spans the alpha.1 → alpha.2
* boundary; only one exists in any given host.
*/
const CENTER_COLUMN_SELECTOR = '#root [data-slot="main.conversation"], #root [data-slot="conversation"]'
/** Stable selector for the DSH AppFrame conversation slot. */
const CENTER_COLUMN_SELECTOR = '#root [data-slot="conversation"]'
/** Match Sidebar's existing last-resort retry cadence (issue #248). */
export const CENTER_COLUMN_REVALIDATE_MS = 1500
@@ -37,22 +27,6 @@ const validatedAt = new WeakMap<HTMLElement, number>()
*/
const documentStyleState = new WeakMap<Document, string | null>()
/**
* From the matched conversation slot host, walk up through transparent slot
* hosts to the real column: DSH slot hosts render with `display: contents`,
* so on alpha.2 the `main.conversation` host (and any further keyed slot
* wrappers above it) contributes no box, and the center column is the first
* ancestor whose computed display is NOT `contents`.
*/
function columnOfSlotHost(host: Element): HTMLElement | null {
const view = host.ownerDocument.defaultView
let node = host.parentElement
while (node !== null && (view?.getComputedStyle(node).display ?? '') === 'contents') {
node = node.parentElement
}
return node
}
/**
* Resolve the AppFrame center column while keeping streaming mutations cheap.
*
@@ -100,8 +74,7 @@ export function resolveCenterColumn(
}
const query = options.query ?? (() => doc.querySelector(CENTER_COLUMN_SELECTOR))
const host = query()
const col = host === null || host === undefined ? null : columnOfSlotHost(host)
const col = query()?.parentElement as HTMLElement | null | undefined
if (col === null || col === undefined || !col.isConnected) return undefined
validatedAt.set(col, now)
+20 -16
View File
@@ -3,9 +3,9 @@
* (repository truth: staged/unstaged files, commit box, history) and the
* session round (agent truth: every file the model read, wrote, or edited).
* Both lenses preview their selections in a shared resizable bottom pane
* ({@link DiffPane}); git targets expand into the dedicated diff tab docked
* in the workbench's diff pane. The active lens and the pane height persist
* in the tab's meta, so the tab
* ({@link DiffPane}); git targets expand into the dedicated diff tab —
* docked in a pane, or floated as a free window per the tab's setting.
* The active lens and the pane height persist in the tab's meta, so the tab
* reopens exactly where it was left.
*
* The session events ride the host's `changes.ops` route (the client
@@ -18,8 +18,7 @@ import type { SidebarSessionEvent } from '../../context-types.ts'
import type { TabComponentProps } from '../service.ts'
import { t } from '../locales.ts'
import { api } from '../api.ts'
import { usePolling } from '../use-polling.ts'
import type { SidebarDiffRef } from '../state.ts'
import { floatTab, type SidebarDiffRef } from '../state.ts'
import { GitLens } from './GitLens.tsx'
import { SessionLens } from './SessionLens.tsx'
import { DiffPane, diffTabOf, type ChangesPreview } from './DiffPane.tsx'
@@ -94,9 +93,6 @@ export function ChangesTab({ ctx, store, scope, tab, visible, onOpenFile, onOpen
// only while nothing has ever loaded.
if (generation === pollGen.current) setOpsError(true)
}
// Granular scope fields: the scope object's identity churns, only its
// sessionId / cwd fields gate the poll target.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scope.sessionId, scope.cwd])
useEffect(() => {
pollGen.current += 1
@@ -105,11 +101,12 @@ export function ChangesTab({ ctx, store, scope, tab, visible, onOpenFile, onOpen
seqRef.current = 0
setOpsError(false)
}, [scope.sessionId])
// One pull on every input change — mount, scope change, and visibility
// flip all re-pull (a hidden tab still catches up on the flip); only the
// poll CADENCE below is gated by visibility.
useEffect(() => { void pull() }, [visible, pull])
usePolling(visible, pull, { intervalMs: 2_500 })
useEffect(() => {
void pull()
if (!visible) return
const timer = window.setInterval(() => { void pull() }, 2_500)
return () => { window.clearInterval(timer) }
}, [visible, pull])
// tick only forces the re-render; the fold reads the ref directly.
void tick
const ops = opsRef.current
@@ -137,11 +134,18 @@ export function ChangesTab({ ctx, store, scope, tab, visible, onOpenFile, onOpen
setPreview({ kind: 'op', path, op, prior: knownContentBefore(ops, path, op) })
}
/** Expand the current git preview into the dedicated diff tab, docked
* into the workbench's diff pane. */
/** Expand the current git preview into the dedicated diff tab: docked
* into the shell's diff pane, or floated as a free window centered on
* the viewport when the tab's diff-open setting asks for it (default). */
const expandPreview = (): void => {
if (preview?.kind !== 'git') return
onOpenDiff?.(diffTabOf(preview.ref))
const diffTab = diffTabOf(preview.ref)
onOpenDiff?.(diffTab)
if (store.getPrefs().changesDiffFloat !== false) {
const x = Math.round(window.innerWidth / 2)
const y = Math.round(window.innerHeight / 2)
store.reduce(state => floatTab(state, diffTab.id, x, y))
}
}
const previewKey = (target: ChangesPreview): string => target.kind === 'git'
+9 -293
View File
@@ -9,37 +9,26 @@
* diff tab via the shell.
*/
import { useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react'
import { IconCloseOutline16, IconRefreshOutline16, IconRightUpOutline16, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
import { IconCloseOutline16, IconRefreshOutline16, IconRightUpOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import type { SessionScope } from '../api.ts'
import { api, htmlUrl } from '../api.ts'
import { api } from '../api.ts'
import { t } from '../locales.ts'
import { baseName } from '../paths.ts'
import { resolveSidebarPath } from '../produced-files.ts'
import { HTML_IFRAME_SANDBOX } from '../html-preview.ts'
import type { SidebarDiffRef, SidebarTab } from '../state.ts'
import { DiffRows, ReadRows } from '../diff/DiffRows.tsx'
import { PdfView } from '../PdfView.tsx'
import { DiffFiles } from '../diff/DiffFiles.tsx'
import { langOfPath } from '../diff/highlight.ts'
import { buildDiffSegments, diffLines, diffStats, displayPath, foldRowsFromContents, parseUnifiedDiff, unifiedSegments, type DiffFile, type DiffRow, type FoldSegment } from '../diff/rows.ts'
import { parseReadContent, parseReadLines, type FileOp } from './ops.ts'
import { redactText } from '../redact.ts'
import { rewriteLocalImageUrls } from '../markdown-images.ts'
import { markdownTextProps } from '../markdown-labels.tsx'
import { splitMermaidBlocks } from '../mermaid-blocks.ts'
import { LazyMermaidMarkdown } from '../mermaid-lazy.tsx'
import { buildDiffSegments, diffLines, diffStats, parseUnifiedDiff, unifiedSegments, type DiffRow } from '../diff/rows.ts'
import { parseReadLines, type FileOp } from './ops.ts'
import { createFrameBatcher } from '../frame-batcher.ts'
import css from './changes.module.css'
import diffCss from '../diff/diff.module.css'
/** Drag handle height clamp (px) and keyboard-resize step. */
/** The drag handle height clamp (px) and keyboard-resize step. */
const HEIGHT_MIN = 140
const HEIGHT_STEP = 24
/** The redaction preference, persisted under the repo's sidebar storage
* prefix (see state.ts's `dsh-sidebar:v1`). */
const REDACTION_KEY = 'dsh-sidebar:v1:redaction'
/** What the pane is showing right now. */
export type ChangesPreview =
| { kind: 'git'; ref: SidebarDiffRef }
@@ -66,61 +55,6 @@ function diffOf(op: FileOp, prior: string | undefined): readonly DiffRow[] {
return []
}
/** The render view of one html op target: the route-src iframe. Extracted
* (and exported) so the always-sandboxed contract is pinned directly by the
* sandbox spec — this surface has NO no-sandbox escape hatch. */
export function HtmlRenderPreview(props: { src: string; title: string }) {
return (
<div className={css.htmlPane}>
<iframe
className={css.htmlFrame}
title={props.title}
src={props.src}
sandbox={HTML_IFRAME_SANDBOX}
referrerPolicy="no-referrer"
allow=""
/>
</div>
)
}
/** One header pill toggle — the redaction / reading / render toggles share
* the shape (on-state styling + aria-pressed). */
function PaneToggle(props: { on: boolean; label: string; title?: string; onClick: () => void }) {
return (
<button
type="button"
className={css.mdToggle}
data-on={props.on ? 'true' : undefined}
aria-pressed={props.on}
title={props.title}
onClick={props.onClick}
>
{props.label}
</button>
)
}
/** The reading-mode body of one markdown op target: the shared MarkdownText
* pass (local image destinations already rewritten to the media route by
* the caller). Mermaid fences render through the same chunk-resident
* renderer the editor preview uses (one MarkdownText pass with the fences
* lifted out); the plain path stays byte-for-byte for documents without
* any. */
function MdReadingView(props: { text: string }) {
const codeLabels = { copyLabel: t('copy'), copiedLabel: t('copied') }
const hasMermaid = splitMermaidBlocks(props.text).some((block) => block.kind === 'mermaid')
return (
<div className={css.paneBody}>
<div className={css.mdBody}>
{hasMermaid
? <LazyMermaidMarkdown text={props.text} codeLabels={codeLabels} />
: <MarkdownText {...markdownTextProps(props.text, codeLabels)} />}
</div>
</div>
)
}
/** The diff tab a git preview expands into (the shell owns placement). */
export function diffTabOf(ref: SidebarDiffRef): SidebarTab {
if (ref.kind === 'worktree') {
@@ -158,19 +92,7 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
const [error, setError] = useState<string | null>(null)
const [diffText, setDiffText] = useState<string | null>(null)
const [untracked, setUntracked] = useState<string | undefined>(undefined)
// The staged flag of the side ACTUALLY rendered: when the requested side's
// diff came back empty the load falls back to the other side, and the fold
// expansion must read that side's revisions (else the sliced line numbers
// land on the wrong contents).
const [effectiveStaged, setEffectiveStaged] = useState<boolean | null>(null)
const gitRef = target.kind === 'git' ? target.ref : null
// The scope every git call of this target shares (repoRoot folded in when
// the ref carries one, exactly like the load effect's paneScope).
const gitScope = useMemo<SessionScope>(() => ({
sessionId: scope.sessionId,
cwd: scope.cwd,
...(gitRef?.repoRoot !== undefined ? { repoRoot: gitRef.repoRoot } : {}),
}), [scope.sessionId, scope.cwd, gitRef?.repoRoot])
useEffect(() => {
if (gitRef === null) return
@@ -184,7 +106,6 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
setError(null)
setDiffText(null)
setUntracked(undefined)
setEffectiveStaged(null)
const load = async (): Promise<void> => {
try {
if (gitRef.kind === 'commit') {
@@ -197,10 +118,7 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
// The requested side is empty — try the OTHER side once (the change
// may have moved sides after the preview target was minted).
const other = await api.gitDiff(paneScope, gitRef.path, !gitRef.staged, gitRef.worktree)
if (other.diff !== '') {
result = other
if (!cancelled) setEffectiveStaged(!gitRef.staged)
}
if (other.diff !== '') result = other
}
if (result.diff !== '') {
if (!cancelled) setDiffText(result.diff)
@@ -227,119 +145,10 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
return () => { cancelled = true }
}, [gitRef, scope.sessionId, scope.cwd, tick])
// ── On-demand git fold expansion: a fold's hidden rows come from both
// sides' full contents (git.show / fsRead), fetched ONCE per file so
// sibling folds share the request, then sliced by each fold's line
// ranges. The cache dies with the target or a refresh tick. ───────────
const foldContents = useRef(new Map<string, Promise<{ old: string; new: string }>>())
useEffect(() => { foldContents.current = new Map() }, [gitRef, tick])
const foldLoader = useMemo(() => {
if (gitRef === null) return undefined
const sidesOf = (file: DiffFile): Promise<{ old: string; new: string }> => {
// Both sides empty cannot cover a non-empty fold — treat it as a failed
// fetch so the fold degrades to the unavailable marker instead of
// silently expanding to nothing (the symptom of a bad rev or path
// reading null on both sides).
const ofSides = (oldContent: string | null, newContent: string | null): { old: string; new: string } => {
if ((oldContent ?? '') === '' && (newContent ?? '') === '') throw new Error('no content on either side')
return { old: oldContent ?? '', new: newContent ?? '' }
}
const fetchSides = async (): Promise<{ old: string; new: string }> => {
if (gitRef.kind === 'commit') {
// The patch's -m --first-parent shape: old side from the parent,
// new side from the commit (a root commit's parent read fails → '').
const [oldSide, newSide] = await Promise.all([
file.oldPath === '/dev/null'
? Promise.resolve({ content: null })
: api.gitShow(gitScope, `${gitRef.hashFull}^`, displayPath(file.oldPath), gitRef.worktree),
file.newPath === '/dev/null'
? Promise.resolve({ content: null })
: api.gitShow(gitScope, gitRef.hashFull, displayPath(file.newPath), gitRef.worktree),
])
return ofSides(oldSide.content, newSide.content)
}
// Worktree change: staged is HEAD vs index, unstaged is index vs
// worktree (the worktree side reads the live file).
const staged = effectiveStaged ?? gitRef.staged
if (staged) {
const [oldSide, newSide] = await Promise.all([
file.oldPath === '/dev/null'
? Promise.resolve({ content: null })
: api.gitShow(gitScope, 'HEAD', displayPath(file.oldPath), gitRef.worktree),
file.newPath === '/dev/null'
? Promise.resolve({ content: null })
: api.gitShow(gitScope, ':0', displayPath(file.newPath), gitRef.worktree),
])
return ofSides(oldSide.content, newSide.content)
}
const [oldSide, worktree] = await Promise.all([
file.oldPath === '/dev/null'
? Promise.resolve({ content: null })
: api.gitShow(gitScope, ':0', displayPath(file.oldPath), gitRef.worktree),
api.fsRead(gitScope, resolveSidebarPath(gitRef.repoRoot ?? gitRef.worktree ?? scope.cwd, displayPath(file.newPath))).catch(() => null),
])
return ofSides(oldSide.content, worktree !== null && worktree.kind === 'text' ? worktree.content : null)
}
const path = displayPath(file.newPath === '/dev/null' ? file.oldPath : file.newPath)
let promise = foldContents.current.get(path)
if (promise === undefined) {
promise = fetchSides()
foldContents.current.set(path, promise)
}
return promise
}
return (file: DiffFile, segment: FoldSegment): Promise<readonly DiffRow[]> =>
sidesOf(file).then(sides => foldRowsFromContents(segment, sides.old, sides.new))
}, [gitRef, gitScope, effectiveStaged, scope])
// ── Op target material (pure snapshots; the prior content came with the
// target so a running op shows what is already known). ────────────────
const opRaw = target.kind === 'op' ? target.op : null
const priorRaw = target.kind === 'op' ? target.prior : undefined
// Secret redaction: on by default, toggle persists per browser (localStorage).
// Every op payload consumer below (diff rows, read rows, markdown source,
// error text) renders from the REDACTED shape, so masked payloads are the
// only thing that can reach the DOM while the toggle is on. Display-only:
// session events and the fs layer keep their original bytes.
const [redactionOn, setRedactionOn] = useState((): boolean => {
try { return localStorage.getItem(REDACTION_KEY) !== '0' } catch { return true }
})
const toggleRedaction = (): void => {
setRedactionOn((prev) => {
const next = !prev
try { localStorage.setItem(REDACTION_KEY, next ? '1' : '0') } catch { /* storage unavailable */ }
return next
})
}
const { op, prior, redactionHit } = useMemo(() => {
if (opRaw === null || !redactionOn) return { op: opRaw, prior: priorRaw, redactionHit: false }
const path = target.kind === 'op' ? target.path : ''
// One redactText pass per field: the outcome carries both the masked
// text and whether anything was hit.
const mask = (text: string | undefined): { masked: string | undefined; hit: boolean } => {
if (text === undefined) return { masked: undefined, hit: false }
const outcome = redactText(path, text)
return { masked: outcome.text, hit: outcome.hit }
}
const read = mask(opRaw.read)
const content = mask(opRaw.content)
const editOld = mask(opRaw.edit?.oldString)
const editNew = mask(opRaw.edit?.newString)
const errorText = mask(opRaw.errorText)
const priorMasked = mask(priorRaw)
const hit = read.hit || content.hit || editOld.hit || editNew.hit || errorText.hit || priorMasked.hit
if (!hit) return { op: opRaw, prior: priorRaw, redactionHit: false }
const redacted: FileOp = {
...opRaw,
...(read.masked !== undefined ? { read: read.masked } : {}),
...(content.masked !== undefined ? { content: content.masked } : {}),
...(opRaw.edit !== undefined
? { edit: { oldString: editOld.masked ?? opRaw.edit.oldString, newString: editNew.masked ?? opRaw.edit.newString } }
: {}),
...(errorText.masked !== undefined ? { errorText: errorText.masked } : {}),
}
return { op: redacted, prior: priorMasked.masked, redactionHit: true }
}, [opRaw, priorRaw, target, redactionOn])
const op = target.kind === 'op' ? target.op : null
const prior = target.kind === 'op' ? target.prior : undefined
const opLang = useMemo(() => (target.kind === 'op' ? langOfPath(target.path) : undefined), [target])
const opRows = useMemo(() => (op === null ? [] : diffOf(op, prior)), [op, prior])
const opSegments = useMemo(() => buildDiffSegments(opRows), [opRows])
@@ -349,56 +158,6 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
[op],
)
// ── Markdown reading mode: .md op targets (read/write/edit, non-error)
// toggle between the raw/diff view and the rendered document — the same
// shared MarkdownText pass the editor preview uses, with local image
// destinations rewritten through the /sidebar/file media route. ──────
const mdOp = target.kind === 'op' && !target.op.isError && /\.(md|markdown|mdx)$/i.test(target.path)
const [reading, setReading] = useState(false)
const readingSrc = useMemo(() => {
if (!mdOp || op === null) return ''
if (op.kind === 'read') return parseReadContent(op.read ?? '')
if (op.kind === 'write') return op.content ?? ''
if (op.kind === 'edit' && op.edit !== undefined) {
if (prior !== undefined && prior.includes(op.edit.oldString)) {
return prior.replace(op.edit.oldString, op.edit.newString)
}
return op.edit.newString
}
return ''
}, [mdOp, op, prior])
const readingText = useMemo(
() => (mdOp && reading && readingSrc !== '' && target.kind === 'op'
? rewriteLocalImageUrls(readingSrc, scope, target.path, window.location.origin)
: ''),
[mdOp, reading, readingSrc, scope, target],
)
// ── HTML render mode: .html/.htm op targets (the editor html viewer's
// ext set) load the SAVED file through the same /sidebar/html route the
// editor's html viewer uses — relative assets (./style.css, img/x.png)
// resolve inside the route, and a segmented read still renders the
// whole document (the route serves the file, not the op snapshot). The
// frame is always sandboxed (the attribute plus the route's CSP sandbox
// header); the editor tab owns the warned no-sandbox escape hatch. ──
const htmlOp = target.kind === 'op' && !target.op.isError && /\.(html?)$/i.test(target.path)
const [rendering, setRendering] = useState(false)
const htmlRenderSrc = useMemo(() => {
if (!htmlOp || target.kind !== 'op') return ''
return htmlUrl(scope, resolveSidebarPath(scope.cwd, target.path))
}, [htmlOp, scope, target])
// ── PDF render mode: .pdf op targets (read / write / edit, non-error) reuse
// the editor's PdfView verbatim — media-route bytes wrapped into an
// explicitly-typed Blob so the browser's native PDF viewer opens (a
// direct iframe src can fall back to a download). ──────────────────────
const pdfOp = target.kind === 'op' && !target.op.isError && /\.pdf$/i.test(target.path)
const [renderingPdf, setRenderingPdf] = useState(false)
const pdfRenderPath = useMemo(() => {
if (!pdfOp || target.kind !== 'op') return ''
return resolveSidebarPath(scope.cwd, target.path)
}, [pdfOp, scope, target])
// Header stats for git targets come off the parsed patch text.
const gitStats = useMemo(() => {
if (target.kind !== 'git' || diffText === null || diffText === '') return null
@@ -505,38 +264,6 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
</button>
</>
)}
{redactionHit && (
<span className={css.redactBanner} role="status">{t('changesRedactBanner')}</span>
)}
{target.kind === 'op' && (
<PaneToggle
on={redactionOn}
label={redactionOn ? t('changesRedactOnLabel') : t('changesRedactOffLabel')}
title={redactionOn ? t('changesRedactOff') : t('changesRedactOn')}
onClick={toggleRedaction}
/>
)}
{mdOp && (
<PaneToggle
on={reading}
label={t(reading ? 'changesMdRaw' : 'changesMdReading')}
onClick={() => { setReading(value => !value) }}
/>
)}
{htmlOp && (
<PaneToggle
on={rendering}
label={t(rendering ? 'changesHtmlRaw' : 'changesHtmlRender')}
onClick={() => { setRendering(value => !value) }}
/>
)}
{pdfOp && (
<PaneToggle
on={renderingPdf}
label={t(renderingPdf ? 'changesPdfRaw' : 'changesPdfRender')}
onClick={() => { setRenderingPdf(value => !value) }}
/>
)}
<button
type="button"
className={css.iconButton}
@@ -547,17 +274,7 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
<IconCloseOutline16 size={14} />
</button>
</div>
{target.kind === 'op' && htmlOp && rendering && htmlRenderSrc !== ''
? <HtmlRenderPreview src={htmlRenderSrc} title={target.path} />
: target.kind === 'op' && pdfOp && renderingPdf && pdfRenderPath !== ''
? (
<div className={css.htmlPane}>
<PdfView scope={scope} path={pdfRenderPath} title={target.path} />
</div>
)
: target.kind === 'op' && mdOp && reading && readingText !== ''
? <MdReadingView text={readingText} />
: target.kind === 'op' && op !== null && op.isError
{target.kind === 'op' && op !== null && op.isError
? (
<div className={css.paneBody}>
<div className={css.readError} role="alert">
@@ -589,7 +306,6 @@ export function DiffPane({ target, scope, height, onHeightCommit, onClose, onExp
{diffText !== null && diffText !== '' && (
<DiffFiles
diff={diffText}
resolveFold={foldLoader}
untrackedPath={untracked !== undefined && target.ref.kind === 'worktree' ? target.ref.path : undefined}
untrackedContent={untracked}
/>
+5 -14
View File
@@ -17,7 +17,6 @@ import {
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { GitLogEntry, GitStatusEntry, GitStatusResult, GitWorktree, SessionScope } from '../api.ts'
import { api } from '../api.ts'
import { usePolling } from '../use-polling.ts'
import { baseName, isWithinWorkspace, relativeTo } from '../paths.ts'
import { resolveSidebarPath } from '../produced-files.ts'
import { relativeTime, t } from '../locales.ts'
@@ -169,9 +168,6 @@ export function GitLens(props: GitLensProps) {
} finally {
if (options.loading && options.generation === refreshGeneration.current) setLoading(false)
}
// Granular scope fields: the scope object's identity churns, only its
// sessionId / cwd fields gate the git target.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scope.sessionId, scope.cwd, repoRoot])
const refresh = useCallback(async (silent = false): Promise<void> => {
@@ -235,9 +231,6 @@ export function GitLens(props: GitLensProps) {
} finally {
refreshInFlight.current = false
}
// Granular scope fields: the scope object's identity churns, only its
// sessionId / cwd fields gate the refresh target.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scope.sessionId, scope.cwd, refreshTarget])
useEffect(() => {
@@ -281,11 +274,11 @@ export function GitLens(props: GitLensProps) {
const generation = refreshGeneration.current += 1
void refreshTarget(chosenPathRef.current ?? '', { loading: true, generation })
}
/** The silent poll tick (the status-only fast path between worktree
* re-lists, see refresh) — fixed 2s cadence while visible, no initial
* burst (mount and scope changes already refresh above). */
const pollTick = useCallback((): Promise<void> => refresh(true), [refresh])
usePolling(visible, pollTick, { intervalMs: 2_000 })
useEffect(() => {
if (!visible) return
const timer = window.setInterval(() => { void refresh(true) }, 2_000)
return () => { window.clearInterval(timer) }
}, [visible, refresh])
/** Append the next history page (lazy: only when the user asks for more). */
const loadMoreLog = async (): Promise<void> => {
@@ -671,7 +664,6 @@ export function GitLens(props: GitLensProps) {
if (id === 'absolute') copy(resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, target.entry.path))
}}
portal
compact
align="start"
getAnchorRect={() => (fileMenu === null ? null : new DOMRect(fileMenu.x, fileMenu.y, 0, 0))}
anchor={<span />}
@@ -729,7 +721,6 @@ export function GitLens(props: GitLensProps) {
}
}}
portal
compact
align="start"
getAnchorRect={() => (historyMenu === null ? null : new DOMRect(historyMenu.x, historyMenu.y, 0, 0))}
anchor={<span />}
-8
View File
@@ -342,14 +342,6 @@
/* ── Shared preview pane ───────────────────────────────────────────────── */
.redactBanner { flex: none; font: var(--dsw-font-xxxs-strong-11); padding: 2px 8px; border-radius: 7px; color: var(--dsw-alias-state-warn-primary); background: var(--dsw-alias-interactive-bg-hover); white-space: nowrap; }
.mdToggle { flex: none; border: 1px solid var(--dsw-alias-border-l1); border-radius: 7px; background: transparent; color: var(--dsw-alias-label-secondary); font: var(--dsw-font-xxxs-strong-11); padding: 2px 8px; cursor: pointer; }
.mdToggle:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
.mdToggle[data-on='true'] { border-color: var(--dsw-alias-state-business-primary); color: var(--dsw-alias-state-business-primary); }
.mdBody { padding: 12px 16px 20px; font: var(--dsw-font-xs-regular-13); line-height: 1.6; min-height: 0; overflow-y: auto; }
.htmlPane { flex: 1; min-height: 0; display: flex; }
.htmlFrame { flex: 1; width: 100%; border: none; background: var(--dsw-alias-bg-base); }
.diffPane { border-top: 1px solid var(--dsw-alias-border-l1); display: flex; flex-direction: column; flex: none; min-height: 0; }
.dragHandle { flex: none; height: 8px; cursor: ns-resize; position: relative; background: var(--dsw-alias-bg-base); }
.dragHandle::after { content: ''; position: absolute; left: calc(50% - 18px); top: 3px; width: 36px; height: 2px; border-radius: 1px; background: var(--dsw-alias-border-l2); }
+4 -28
View File
@@ -201,33 +201,6 @@ export function knownContentBefore(ops: readonly FileOp[], path: string, before:
return undefined
}
/**
* The read-tool response's `<content>` body split into lines with the
* "(Showing lines …)" note dropped — the shared envelope core both read
* parsers consume: {@link parseReadLines} recovers line numbers from the
* "<n>: " prefixes, {@link parseReadContent} keeps the raw text.
*/
function contentLines(raw: string): string[] {
const contentMatch = raw.match(/<content>([\s\S]*?)<\/content>/)
const body = contentMatch ? contentMatch[1]! : raw
return body.split('\n').filter((line) => !/^\s*\(Showing lines .*\)\s*$/.test(line))
}
/**
* Strip the DSH read-tool response envelope down to the file's raw content,
* PRESERVING blank lines (unlike {@link parseReadLines}, which drops them):
* drops the <content> wrapper, the "(Showing lines ...)" note, and the
* per-line "<n>: " prefixes. The markdown reading mode needs the blank lines
* because markdown block structure depends on them.
*/
export function parseReadContent(raw: string): string {
return contentLines(raw)
.map((line) => line.replace(/^\s*\d+:\s/, ''))
.join('\n')
.replace(/^[\n]+/, '')
.replace(/\n+$/, '')
}
/** One parsed read line: the file's own line number and its content text. */
export interface ReadLine { readonly line: number; readonly text: string }
@@ -237,9 +210,12 @@ export interface ReadLine { readonly line: number; readonly text: string }
* "<n>: " prefix as the line number, falling back to sequential counting.
*/
export function parseReadLines(raw: string): ReadLine[] {
const contentMatch = raw.match(/<content>([\s\S]*?)<\/content>/)
const body = contentMatch ? contentMatch[1]! : raw
const result: ReadLine[] = []
let fallback = 1
for (const line of contentLines(raw)) {
for (const line of body.split('\n')) {
if (/^\s*\(Showing lines .*\)\s*$/.test(line)) continue
if (line.length === 0) continue
const match = line.match(/^\s*(\d+):\s?(.*)$/)
if (match !== null) {
+12 -10
View File
@@ -23,9 +23,8 @@
* arbitrary file names, so the plugin's own host route serves the chunks),
* 2. read the factory from the global registry,
* 3. call it with a require that resolves the platform externals through
* the injected module system's `import(spec)` (the `ctx.modules` service)
* — the seed-word branch, the one part of the module system that is
* stable across versions.
* `__DSH_MODULES__.import(spec)` — the seed-word branch, the one part of
* the module system that is stable across versions.
*
* Caching contract (three layers, each with a failure path):
* - In-memory: one in-flight promise per chunk, memoized until
@@ -91,7 +90,8 @@ const CHUNK_REVALIDATE_TIMEOUT_MS = 5_000
* The client module system surface this loader needs to resolve externals.
* DSH 0.1.0-rc.8 provides it as the `ctx.modules` service (no page global
* anymore); the plugin injects it at activation via
* {@link setChunkModuleSystem}.
* {@link setChunkModuleSystem}. The rc.7-era `window.__DSH_MODULES__` global
* remains as a fallback so older hosts and the test harness keep working.
*/
export interface ChunkModuleSystem {
import(specifier: string): Promise<unknown>
@@ -123,11 +123,12 @@ export function setChunkModuleSystem(system: ChunkModuleSystem | undefined): voi
}
/** Resolve the shell-installed module system (injected, then the plugin
* global shared with chunk-bundle copies). */
* global shared with chunk-bundle copies, then the rc.7 page global). */
function moduleSystem(): ChunkModuleSystem | undefined {
const g = globalThis as Record<string, unknown>
return injectedModuleSystem
?? g[MODULE_SYSTEM_GLOBAL] as ChunkModuleSystem | undefined
?? (g as { __DSH_MODULES__?: ChunkModuleSystem }).__DSH_MODULES__
}
/** The plugin-owned chunk factory registry the chunk scripts populate. */
@@ -153,7 +154,7 @@ const defaultScriptLoader: ChunkScriptLoader = (src) => new Promise((resolve, re
}, { once: true })
el.addEventListener('error', () => {
el.remove()
reject(new Error(`[dsh-better-sidebar] chunk script ${src} failed to load`))
reject(new Error(`[dsh-apes-sidebar] chunk script ${src} failed to load`))
}, { once: true })
document.head.append(el)
})
@@ -191,7 +192,7 @@ async function buildExternalsRequire(modules: ChunkModuleSystem): Promise<(spec:
if (!table.has(spec)) {
// Single quotes: the bundle-consistency scan regexes for require("...")
// lexical calls — a double-quoted literal here would trip it.
throw new Error(`[dsh-better-sidebar] chunk require('${spec}') missed the module table`)
throw new Error(`[dsh-apes-sidebar] chunk require('${spec}') missed the module table`)
}
return table.get(spec)
}
@@ -243,17 +244,18 @@ export async function loadChunk(name: ChunkName): Promise<ChunkExports> {
if (revalidation !== null) await revalidation
const cached = cache.get(name)
if (cached !== undefined) return cached
const task: Promise<ChunkExports> = (async (): Promise<ChunkExports> => {
let task: Promise<ChunkExports>
task = (async (): Promise<ChunkExports> => {
const test = testLoaders.get(name)
if (test !== undefined) return test()
const modules = moduleSystem()
if (modules === undefined) {
throw new Error(`[dsh-better-sidebar] chunk "${name}": client module system unavailable`)
throw new Error(`[dsh-apes-sidebar] chunk "${name}": client module system unavailable`)
}
await scriptLoader(CHUNK_URL(name))
const factory = chunkRegistry()[name]
if (typeof factory !== 'function') {
throw new Error(`[dsh-better-sidebar] chunk "${name}" script did not register its factory`)
throw new Error(`[dsh-apes-sidebar] chunk "${name}" script did not register its factory`)
}
const require = await buildExternalsRequire(modules)
const exports = factory(require)
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Lazy chunk entry: the code/markdown/html text editor (CodeMirror 6 + the
* language packages). Built as `lib/client-editor.js` and registered under
* `dsh-better-sidebar/editor` — fetched only when a text file is first
* `dsh-apes-sidebar/editor` — fetched only when a text file is first
* opened (see chunk-loader.ts and docs/plans/2026-08-12-lazy-chunks-design.md).
* Never import this module from the core bundle: it pulls CodeMirror into
* the startup path.
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Lazy chunk entry: the interactive terminal (xterm + fit addon). Built as
* `lib/client-terminal.js` and registered under
* `dsh-better-sidebar/terminal` — fetched only when a terminal tab is first
* `dsh-apes-sidebar/terminal` — fetched only when a terminal tab is first
* opened (see chunk-loader.ts and docs/plans/2026-08-12-lazy-chunks-design.md).
* Never import this module from the core bundle: it pulls xterm (and its
* stylesheet) into the startup path.
+42 -44
View File
@@ -2,17 +2,15 @@
* CodeMirror 6 theme pieces for the sidebar editor. The editor surface
* (background, caret, gutter) rides the DSH theme tokens so it blends with
* the panel in both schemes; only the syntax token colors need concrete
* values, and those come from the same designed syntax families the app's
* code surfaces use — one-dark for dark, one-light for light
* (one-dark-palette.ts, shared with the terminal's ANSI palette). The
* scheme flip reconfigures these via a compartment (see TextEditor), so
* the document, undo history and scroll survive re-theming.
* values, and those come from the same designed palettes the app's code
* surfaces use — the one-dark family for dark, the one-light family for
* light. The scheme flip reconfigures these via a compartment (see
* TextEditor), so the document, undo history and scroll survive re-theming.
*/
import { Compartment } from '@codemirror/state'
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
import { tags, type Tag } from '@lezer/highlight'
import { EditorView } from '@codemirror/view'
import { ONE_DARK, ONE_LIGHT } from './one-dark-palette.ts'
/** Token-driven surface shared by both schemes (pure CSS values). */
export const cmSurfaceTheme = EditorView.theme({
@@ -55,52 +53,52 @@ interface HighlightRule {
fontStyle?: string
}
/** one-dark syntax rules (mirrors @codemirror/theme-one-dark; hues from one-dark-palette.ts). */
/** one-dark syntax palette (mirrors @codemirror/theme-one-dark). */
const HIGHLIGHTS_DARK: HighlightRule[] = [
{ tag: tags.comment, color: ONE_DARK.faintGray, fontStyle: 'italic' },
{ tag: tags.keyword, color: ONE_DARK.magenta },
{ tag: tags.string, color: ONE_DARK.green },
{ tag: tags.number, color: ONE_DARK.orange },
{ tag: tags.bool, color: ONE_DARK.orange },
{ tag: tags.atom, color: ONE_DARK.orange },
{ tag: tags.typeName, color: ONE_DARK.yellow },
{ tag: tags.className, color: ONE_DARK.yellow },
{ tag: tags.propertyName, color: ONE_DARK.red },
{ tag: tags.function(tags.variableName), color: ONE_DARK.blue },
{ tag: tags.variableName, color: ONE_DARK.red },
{ tag: tags.operator, color: ONE_DARK.cyan },
{ tag: tags.tagName, color: ONE_DARK.red },
{ tag: tags.attributeName, color: ONE_DARK.orange },
{ tag: tags.heading, color: ONE_DARK.red, fontStyle: 'bold' },
{ tag: tags.comment, color: '#5c6370', fontStyle: 'italic' },
{ tag: tags.keyword, color: '#c678dd' },
{ tag: tags.string, color: '#98c379' },
{ tag: tags.number, color: '#d19a66' },
{ tag: tags.bool, color: '#d19a66' },
{ tag: tags.atom, color: '#d19a66' },
{ tag: tags.typeName, color: '#e5c07b' },
{ tag: tags.className, color: '#e5c07b' },
{ tag: tags.propertyName, color: '#e06c75' },
{ tag: tags.function(tags.variableName), color: '#61afef' },
{ tag: tags.variableName, color: '#e06c75' },
{ tag: tags.operator, color: '#56b6c2' },
{ tag: tags.tagName, color: '#e06c75' },
{ tag: tags.attributeName, color: '#d19a66' },
{ tag: tags.heading, color: '#e06c75', fontStyle: 'bold' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strong, fontStyle: 'bold' },
{ tag: tags.link, color: ONE_DARK.blue, fontStyle: 'underline' },
{ tag: tags.meta, color: ONE_DARK.yellow },
{ tag: tags.invalid, color: ONE_DARK.white, fontStyle: 'bold' },
{ tag: tags.link, color: '#61afef', fontStyle: 'underline' },
{ tag: tags.meta, color: '#e5c07b' },
{ tag: tags.invalid, color: '#ffffff', fontStyle: 'bold' },
]
/** one-light syntax rules (the light counterpart; hues from one-dark-palette.ts). */
/** one-light syntax palette (the light counterpart of one-dark). */
const HIGHLIGHTS_LIGHT: HighlightRule[] = [
{ tag: tags.comment, color: ONE_LIGHT.gray, fontStyle: 'italic' },
{ tag: tags.keyword, color: ONE_LIGHT.magenta },
{ tag: tags.string, color: ONE_LIGHT.green },
{ tag: tags.number, color: ONE_LIGHT.orange },
{ tag: tags.bool, color: ONE_LIGHT.blue },
{ tag: tags.atom, color: ONE_LIGHT.blue },
{ tag: tags.typeName, color: ONE_LIGHT.yellow },
{ tag: tags.className, color: ONE_LIGHT.yellow },
{ tag: tags.propertyName, color: ONE_LIGHT.red },
{ tag: tags.function(tags.variableName), color: ONE_LIGHT.yellow },
{ tag: tags.variableName, color: ONE_LIGHT.red },
{ tag: tags.operator, color: ONE_LIGHT.black },
{ tag: tags.tagName, color: ONE_LIGHT.red },
{ tag: tags.attributeName, color: ONE_LIGHT.orange },
{ tag: tags.heading, color: ONE_LIGHT.red, fontStyle: 'bold' },
{ tag: tags.comment, color: '#a0a1a7', fontStyle: 'italic' },
{ tag: tags.keyword, color: '#a626a4' },
{ tag: tags.string, color: '#50a14f' },
{ tag: tags.number, color: '#986801' },
{ tag: tags.bool, color: '#0184bc' },
{ tag: tags.atom, color: '#0184bc' },
{ tag: tags.typeName, color: '#c18401' },
{ tag: tags.className, color: '#c18401' },
{ tag: tags.propertyName, color: '#e45649' },
{ tag: tags.function(tags.variableName), color: '#c18401' },
{ tag: tags.variableName, color: '#e45649' },
{ tag: tags.operator, color: '#383a42' },
{ tag: tags.tagName, color: '#e45649' },
{ tag: tags.attributeName, color: '#986801' },
{ tag: tags.heading, color: '#e45649', fontStyle: 'bold' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strong, fontStyle: 'bold' },
{ tag: tags.link, color: ONE_LIGHT.link, fontStyle: 'underline' },
{ tag: tags.meta, color: ONE_LIGHT.yellow },
{ tag: tags.invalid, color: ONE_LIGHT.white, fontStyle: 'bold' },
{ tag: tags.link, color: '#4078f2', fontStyle: 'underline' },
{ tag: tags.meta, color: '#c18401' },
{ tag: tags.invalid, color: '#ffffff', fontStyle: 'bold' },
]
/** The scheme-dependent extension pair (surface tint + syntax highlight). */
+4 -5
View File
@@ -170,12 +170,12 @@ export function appendToDraft(ctx: Context, sessionId: string, text: string): bo
try {
const actx = ctx.sessions.scope(sessionId)
if (actx === undefined) {
console.warn('[dsh-better-sidebar] draft insert skipped: no session scope', sessionId)
console.warn('[dsh-apes-sidebar] draft insert skipped: no session scope', sessionId)
return false
}
const conversation = ctx.get('conversation') as SidebarConversation | undefined
if (conversation === undefined) {
console.warn('[dsh-better-sidebar] draft insert skipped: conversation service unavailable')
console.warn('[dsh-apes-sidebar] draft insert skipped: conversation service unavailable')
return false
}
const input = conversation.input.for(actx)
@@ -189,7 +189,7 @@ export function appendToDraft(ctx: Context, sessionId: string, text: string): bo
placeComposerCaretAfterInsert(next, caretAfter)
return true
} catch (error) {
console.warn('[dsh-better-sidebar] draft insert failed:', error)
console.warn('[dsh-apes-sidebar] draft insert failed:', error)
return false
}
}
@@ -203,7 +203,6 @@ export function appendToDraft(ctx: Context, sessionId: string, text: string): bo
*/
export function fileMention(relativePath: string): { mention: string; label: string } | undefined {
const path = relativePath.replace(/[\\/]+$/, '')
// eslint-disable-next-line no-control-regex -- rejecting control characters is the point of this guard
if (/[\u0000-\u001f\u007f-\u009f"]/u.test(path)) return undefined
const mention = /\s/u.test(path) ? `@"${path}"` : `@${path}`
const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
@@ -250,7 +249,7 @@ export function insertFileReference(ctx: Context, sessionId: string, relativePat
const after = input.state.getSnapshot()
return after.draftRev !== before.draftRev
} catch (error) {
console.warn('[dsh-better-sidebar] file-reference insert failed:', error)
console.warn('[dsh-apes-sidebar] file-reference insert failed:', error)
return false
}
}
+4 -29
View File
@@ -7,10 +7,10 @@
* Untracked files produce no `git diff` output; the caller passes their
* content to render as a full-file addition instead.
*/
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
import { useEffect, useMemo, useState, type ReactNode } from 'react'
import clsx from 'clsx'
import { t } from '../locales.ts'
import { diffStats, displayPath, parseUnifiedDiff, unifiedSegments, untrackedFile, type DiffFile, type DiffRow, type FoldSegment } from './rows.ts'
import { diffStats, displayPath, parseUnifiedDiff, unifiedSegments, untrackedFile, type DiffFile } from './rows.ts'
import { langOfPath } from './highlight.ts'
import { DiffRows } from './DiffRows.tsx'
import css from './diff.module.css'
@@ -51,14 +51,9 @@ export interface DiffFilesProps {
/** Untracked-file content: when present, renders as a full-file addition instead of parsing. */
untrackedPath?: string
untrackedContent?: string
/** Fetch a git gap fold's hidden rows on demand (both sides' contents by
* the fold's line ranges); forwarded to every file's DiffRows. Absent
* folds without `rows` stay non-expandable (session-op diffs and
* untracked additions always carry theirs). */
resolveFold?: (file: DiffFile, segment: FoldSegment) => Promise<readonly DiffRow[]>
}
export function DiffFiles({ diff, untrackedPath, untrackedContent, resolveFold }: DiffFilesProps) {
export function DiffFiles({ diff, untrackedPath, untrackedContent }: DiffFilesProps) {
const parsed = useMemo(() => {
if (untrackedPath !== undefined) {
return { files: [untrackedFile(untrackedPath, untrackedContent ?? '')] }
@@ -68,26 +63,6 @@ export function DiffFiles({ diff, untrackedPath, untrackedContent, resolveFold }
const [expandedFiles, setExpandedFiles] = useState<Set<number>>(() => defaultExpandedFiles(parsed.files))
useEffect(() => { setExpandedFiles(defaultExpandedFiles(parsed.files)) }, [parsed])
// One in-flight/resolved promise per file+fold key, so a fold re-clicked
// after a remount (file header collapsed and re-expanded) resolves without
// a second fetch and concurrent clicks join the same request. The cache
// dies with `parsed` (a new diff text); rejected promises are dropped so a
// later retry can go through.
const foldCache = useRef(new Map<string, Promise<readonly DiffRow[]>>())
useEffect(() => { foldCache.current = new Map() }, [parsed])
const loadFold = (file: DiffFile, segment: FoldSegment): Promise<readonly DiffRow[]> => {
if (resolveFold === undefined) return Promise.resolve([])
const path = displayPath(file.newPath === '/dev/null' ? file.oldPath : file.newPath)
const key = `${path}|${String(segment.oldStart)}|${String(segment.newStart)}`
let promise = foldCache.current.get(key)
if (promise === undefined) {
promise = resolveFold(file, segment)
foldCache.current.set(key, promise)
promise.catch(() => { foldCache.current.delete(key) })
}
return promise
}
// Segments and header stats computed once per file.
const files = useMemo(
() => parsed.files.map((file) => {
@@ -132,7 +107,7 @@ export function DiffFiles({ diff, untrackedPath, untrackedContent, resolveFold }
)}
</button>
{expandable && fileExpanded && (
<DiffRows segments={segments} lang={langOfPath(to)} resolveFold={resolveFold !== undefined ? (segment) => loadFold(file, segment) : undefined} />
<DiffRows segments={segments} lang={langOfPath(to)} />
)}
</div>
)
+16 -68
View File
@@ -4,14 +4,11 @@
* line gutters, rewrite (mod) tinting with intra-line character highlights,
* lightweight syntax coloring, and long-line folding. Presentational — the
* segments arrive precomputed (session ops via buildDiffSegments, git diffs
* via unifiedSegments) so both producers render identically; the one
* exception is git gap folds, whose rows git never emitted and a caller
* supplies on demand through resolveFold (first click fetches, then they
* toggle like any fold).
* via unifiedSegments) so both producers render identically.
*/
import { useEffect, useMemo, useRef, useState, type ReactElement, type ReactNode } from 'react'
import { useEffect, useMemo, useState, type ReactElement, type ReactNode } from 'react'
import { t } from '../locales.ts'
import { coalesceInline, diffInline, MIN_FOLD, type DiffRow, type DiffSegment, type FoldSegment, type InlineDiff } from './rows.ts'
import { coalesceInline, diffInline, MIN_FOLD, type DiffRow, type DiffSegment, type InlineDiff } from './rows.ts'
import { hasBlockComment, isColored, scanLine, type CodeToken, type TokenType } from './highlight.ts'
import css from './diff.module.css'
@@ -108,36 +105,21 @@ export interface DiffRowsProps {
segments: readonly DiffSegment[]
/** Syntax language id (langOfPath); undefined renders plain text. */
lang?: string
/** Fetch a git gap fold's hidden rows on demand (both sides' contents
* sliced by the fold's line ranges). Absent folds without `rows` — git's
* unemitted gaps without a resolver — stay non-expandable markers. */
resolveFold?: (segment: FoldSegment) => Promise<readonly DiffRow[]>
}
/** One file's diff rows: fold chips between hunks, highlighted code rows. */
export function DiffRows({ segments, lang, resolveFold }: DiffRowsProps) {
export function DiffRows({ segments, lang }: DiffRowsProps) {
// Long diff lines fold to one ellipsized row; the set holds expanded row keys.
const [expandedLines, setExpandedLines] = useState<ReadonlySet<string>>(new Set())
// Hunk-fold segments expanded by index; default collapsed.
const [expandedFolds, setExpandedFolds] = useState<ReadonlySet<number>>(new Set())
// Row-count cap expanded: a huge file renders head rows plus this button.
const [expandedAll, setExpandedAll] = useState(false)
// On-demand fold expansion (git gaps): resolved rows per segment index,
// the loading/failed markers, and an epoch that invalidates in-flight
// resolves when the segments identity changes.
const [foldData, setFoldData] = useState<ReadonlyMap<number, readonly DiffRow[]>>(new Map())
const [foldLoading, setFoldLoading] = useState<ReadonlySet<number>>(new Set())
const [foldFailed, setFoldFailed] = useState<ReadonlySet<number>>(new Set())
const foldEpoch = useRef(0)
// Reset all folding when the segments identity changes (new target).
useEffect(() => {
setExpandedLines(new Set())
setExpandedFolds(new Set())
setExpandedAll(false)
setFoldData(new Map())
setFoldLoading(new Set())
setFoldFailed(new Set())
foldEpoch.current += 1
}, [segments])
const inlineMap = useMemo(() => buildInlineMap(segments), [segments])
@@ -208,19 +190,7 @@ export function DiffRows({ segments, lang, resolveFold }: DiffRowsProps) {
</div>
)
}
if (foldFailed.has(segIndex)) {
// The on-demand fetch failed: a quiet, non-clickable marker — the
// expansion promise is gone, so no interaction is promised either.
return (
<div key={`fold-${String(segIndex)}`} className={css.foldRow}>
<span className={css.foldMarker} title={t('changesContext')}>
{t('changesFoldUnavailable')}
</span>
</div>
)
}
const resolvedRows = foldData.get(segIndex)
const expandable = (segment.rows !== undefined || resolvedRows !== undefined || resolveFold !== undefined) && segment.count >= MIN_FOLD
const expandable = segment.rows !== undefined && segment.count >= MIN_FOLD
if (!expandable) {
// A tiny fold (or a git gap with no rows to reveal): a quiet marker.
return (
@@ -231,49 +201,27 @@ export function DiffRows({ segments, lang, resolveFold }: DiffRowsProps) {
</div>
)
}
const loading = foldLoading.has(segIndex)
const isExpanded = expandedFolds.has(segIndex)
const revealed = segment.rows ?? resolvedRows
return (
<div
key={`fold-${String(segIndex)}`}
className={css.foldRow}
data-expandable={loading ? undefined : 'true'}
data-expandable="true"
data-expanded={isExpanded ? 'true' : undefined}
onClick={loading ? undefined : () => {
if (revealed !== undefined) {
setExpandedFolds(prev => {
const next = new Set(prev)
if (next.has(segIndex)) next.delete(segIndex)
else next.add(segIndex)
return next
})
return
}
// A git gap fold's first click: fetch its rows, then expand.
if (resolveFold === undefined) return
const epoch = foldEpoch.current
setFoldLoading(prev => new Set(prev).add(segIndex))
resolveFold(segment).then(
(rows) => {
if (foldEpoch.current !== epoch) return
setFoldData(prev => new Map(prev).set(segIndex, rows))
setFoldLoading(prev => { const next = new Set(prev); next.delete(segIndex); return next })
setExpandedFolds(prev => new Set(prev).add(segIndex))
},
() => {
if (foldEpoch.current !== epoch) return
setFoldLoading(prev => { const next = new Set(prev); next.delete(segIndex); return next })
setFoldFailed(prev => new Set(prev).add(segIndex))
},
)
onClick={() => {
setExpandedFolds(prev => {
const next = new Set(prev)
if (next.has(segIndex)) next.delete(segIndex)
else next.add(segIndex)
return next
})
}}
>
{isExpanded && revealed !== undefined
? revealed.map((row, index) => renderDiffRow(row, `${segIndex}-${String(index)}`))
{isExpanded
? segment.rows!.map((row, index) => renderDiffRow(row, `${segIndex}-${String(index)}`))
: (
<span className={css.foldMarker} title={t('changesContext')}>
{loading ? t('changesFoldLoading') : t('changesFold', { count: segment.count })}
{t('changesFold', { count: segment.count })}
</span>
)}
</div>
+25 -85
View File
@@ -132,77 +132,28 @@ const MD_LANG: LangConfig = {
wordBody: WORD,
}
/** JS keyword list, shared by js/jsx/mjs/cjs. */
const JS_WORDS = `async await break case catch class const continue debugger default delete do else
export extends false finally for from function get if implements import in instanceof interface
let new null of return set static super switch this throw true try typeof undefined var void
while with yield`
/** TS keyword list, shared by ts/tsx/mts/cts. */
const TS_WORDS = `abstract any as asserts async await boolean break case catch class const constructor
continue debugger declare default delete do else enum export extends false finally for from
function get if implements import in infer instanceof interface is keyof let module namespace
never new null number object of override private protected public readonly return satisfies set
static string super switch symbol this throw true try type typeof undefined union unknown var
void while with yield`
/** CSS family: block comments delimited by slash-star, hyphenated property words. */
const CSS_LANG: LangConfig = {
lineComments: [],
blockComment: ['/*', '*/'],
strings: ['"', "'"],
keywords: kw(`media import charset keyframes font-face supports page namespace layer scope container
property value at-rule when and not only from to important
background background-color background-image background-position background-size
border border-radius border-color border-width border-style bottom box-shadow box-sizing
color content cursor clip clip-path clear display direction
flex flex-direction flex-wrap flex-flow flex-grow flex-shrink flex-basis
font font-family font-size font-style font-weight float fill
grid grid-area grid-template grid-template-columns grid-template-rows grid-gap gap
height left letter-spacing line-height list-style margin margin-top margin-right
margin-bottom margin-left max-height max-width min-height min-width opacity order
outline overflow padding padding-top padding-right padding-bottom padding-left
position pointer-events right rotate scale translate transform transform-origin
text-align text-decoration text-transform top transition transition-property
user-select vertical-align visibility white-space width word-break word-spacing z-index
align-items align-content align-self justify-content justify-items justify-self
aspect-ratio inset object-fit object-position resize scroll-behavior filter backdrop-filter
animation animation-name animation-duration animation-timing-function animation-delay
will-change
html body p a div span li ul ol table tr td th form input button label select textarea img
section header footer main nav article aside h1 h2 h3 h4 h5 h6 i b em strong small pre code
inherit initial unset auto none fixed absolute relative sticky static flex block inline
inline-block inline-flex grid hidden visible bold normal root var calc env`),
constants: new Set(),
macro: false,
wordStart: /[A-Za-z-]/u,
wordBody: /[A-Za-z0-9-]/u,
}
/** Markup family (HTML/XML/SVG/Vue): markup comments plus common tags. */
const MARKUP_LANG: LangConfig = {
lineComments: [],
blockComment: ['<!--', '-->'],
strings: ['"', "'"],
keywords: kw(`html head body title meta link script style template slot
div span p a img ul ol li table thead tbody tr th td form input button label select
option textarea header footer main nav section article aside figure figcaption
h1 h2 h3 h4 h5 h6 strong em b i u s small br hr pre code blockquote
svg path circle rect line polyline polygon g defs use text symbol
xml doctype class id href src type value name content charset async defer
vue component props setup script-style export import v-if v-for v-bind v-on`),
constants: new Set(),
macro: false,
wordStart: LETTER,
wordBody: WORD,
}
/** Extension → language id, mirroring the read tool's hint table. */
const LANGS: Readonly<Record<string, LangConfig>> = {
ts: C_FAMILY(TS_WORDS),
tsx: C_FAMILY(TS_WORDS),
js: C_FAMILY(JS_WORDS),
jsx: C_FAMILY(JS_WORDS),
ts: C_FAMILY(`abstract any as asserts async await boolean break case catch class const constructor
continue debugger declare default delete do else enum export extends false finally for from
function get if implements import in infer instanceof interface is keyof let module namespace
never new null number object of override private protected public readonly return satisfies set
static string super switch symbol this throw true try type typeof undefined union unknown var
void while with yield`),
tsx: C_FAMILY(`abstract any as asserts async await boolean break case catch class const constructor
continue declare default delete do else enum export extends false finally for from function get
if implements import in infer instanceof interface is keyof let module namespace never new null
number object of override private protected public readonly return satisfies set static string
super switch symbol this throw true try type typeof undefined union unknown var void while with
yield`),
js: C_FAMILY(`async await break case catch class const continue debugger default delete do else
export extends false finally for from function get if implements import in instanceof interface
let new null of return set static super switch this throw true try typeof undefined var void
while with yield`),
jsx: C_FAMILY(`async await break case catch class const continue debugger default delete do else
export extends false finally for from function get if implements import in instanceof interface
let new null of return set static super switch this throw true try typeof undefined var void
while with yield`),
json: CONFIG_LANG,
py: HASH_FAMILY(`and as assert async await break class continue def del elif else except finally
for from global if import in is lambda nonlocal not or pass raise return try while with yield
@@ -274,22 +225,11 @@ const LANGS: Readonly<Record<string, LangConfig>> = {
md: MD_LANG,
markdown: MD_LANG,
mdx: MD_LANG,
mjs: C_FAMILY(JS_WORDS),
cjs: C_FAMILY(JS_WORDS),
mts: C_FAMILY(TS_WORDS),
cts: C_FAMILY(TS_WORDS),
jsonc: CONFIG_LANG,
json5: CONFIG_LANG,
html: MARKUP_LANG,
htm: MARKUP_LANG,
xml: MARKUP_LANG,
svg: MARKUP_LANG,
vue: MARKUP_LANG,
css: CSS_LANG,
scss: CSS_LANG,
less: CSS_LANG,
graphql: HASH_FAMILY('query mutation fragment on directive enum input interface scalar schema type implements'),
gql: HASH_FAMILY('query mutation fragment on directive enum input interface scalar schema type implements'),
html: CONFIG_LANG,
htm: CONFIG_LANG,
css: HASH_FAMILY(''),
scss: HASH_FAMILY(''),
less: HASH_FAMILY(''),
lua: HASH_FAMILY(`and break do else elseif end false for function goto if in local nil not or
repeat return then true until while`),
}
-35
View File
@@ -451,41 +451,6 @@ export function untrackedFile(path: string, content: string): DiffFile {
return { oldPath: '/dev/null', newPath: `b/${path}`, binary: false, hunks: [{ oldStart: 0, newStart: 1, header: '', lines }] }
}
/**
* Materialize a git gap fold's hidden rows from the two sides' full file
* contents, by the fold's known line ranges: the old side drives context
* rows (each mapped onto the new side through the fold's offset — a gap is
* an unchanged run, so the sides align), and new-side lines the old range
* never reaches become pure additions. Line numbers clip to the actual
* content (a no-newline file's ranges can overrun by one); `\r` endings
* survive verbatim, like git's own context lines.
*/
export function foldRowsFromContents(fold: FoldSegment, oldContent: string, newContent: string): DiffRow[] {
const oldLines = oldContent.length === 0 ? [] : oldContent.split('\n')
const newLines = newContent.length === 0 ? [] : newContent.split('\n')
const offset = fold.newStart - fold.oldStart
const rows: DiffRow[] = []
const oldFrom = Math.max(fold.oldStart, 1)
const oldTo = Math.min(fold.oldEnd, oldLines.length)
for (let oldLine = oldFrom; oldLine <= oldTo; oldLine += 1) {
const text = oldLines[oldLine - 1]!
const newLine = oldLine + offset
rows.push(
newLine >= fold.newStart && newLine <= fold.newEnd && newLine <= newLines.length
? { kind: 'context', oldLine, newLine, text }
: { kind: 'context', oldLine, text },
)
}
// New-side lines beyond what the old range reached (a pure-addition gap):
// rows carrying only the new-side number, exactly like added lines.
const newFrom = Math.max(fold.newStart, 1)
const newTo = Math.min(fold.newEnd, newLines.length)
for (let newLine = Math.max(newFrom, oldTo + offset + 1); newLine <= newTo; newLine += 1) {
rows.push({ kind: 'add', newLine, text: newLines[newLine - 1]! })
}
return rows
}
/** Strip the `a/` / `b/` prefix git puts on diff paths (not on /dev/null). */
export function displayPath(path: string): string {
if (path === '/dev/null') return path
-15
View File
@@ -1,15 +0,0 @@
/**
* Shared vocabulary of the sidebar's HTML preview surfaces (the editor's
* html viewer and the changes tab's op render preview): the sandbox tokens
* both preview iframes load with.
*/
/**
* The sandbox tokens of an HTML preview iframe. NO allow-same-origin (the
* preview must stay in an opaque origin — with the route's own origin it
* could read session data) and NO allow-top-navigation (a previewed page
* must not hijack the GUI). The editor viewer can disable the sandbox
* per-feature in the side card settings (warned); the changes tab's op
* render preview always stays sandboxed.
*/
export const HTML_IFRAME_SANDBOX = 'allow-scripts allow-popups allow-downloads allow-modals'
+26 -2
View File
@@ -1,12 +1,24 @@
/**
* Icons the sidebar needs beyond the primitives set: a terminal glyph (the
* icon library has none), a diff glyph, and the bottom-workbench toggle.
* Per-tab icons live on the tab descriptors
* icon library has none), a diff glyph, and the two panel-toggle glyphs for
* the top-right cluster. Per-tab icons live on the tab descriptors
* (`descriptor.icon`), not in a type-keyed switch — the icon mapping was
* registry-ized with the tab types.
*/
import type { IconProps } from '@deepseek-ai/dsh-client-ui-primitives'
/**
* Right-panel toggle glyph (the "侧拉" button): a frame with a filled strip
* along its RIGHT edge, in the app's outline style (1.5px stroke,
* currentColor).
*/
export const IconPanelRightOutline16 = ({ size = 16, className }: IconProps) => (
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.5" y="2" width="13" height="12" rx="2.5" stroke="currentColor" strokeWidth="1.5" />
<rect x="10.5" y="3.25" width="2.75" height="9.5" rx="1" fill="currentColor" stroke="none" />
</svg>
)
/**
* Bottom-panel toggle glyph (the "底栏" button): a frame with a filled strip
* along its BOTTOM edge, in the app's outline style.
@@ -149,3 +161,15 @@ export const IconVscode16 = ({ size = 16, className }: IconProps) => (
<path d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z" />
</svg>
)
/**
* Free-window glyph in the app's outline style (1.5px stroke, currentColor):
* a background frame with a detached rounded mini-window floating over its
* top-right — the changes tab's "diff opens as a free window" setting.
*/
export const IconFloatWindowOutline16 = ({ size = 16, className }: IconProps) => (
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.5" y="4.5" width="10" height="10" rx="2" stroke="currentColor" strokeWidth="1.5" />
<rect x="9.5" y="1.5" width="5" height="5" rx="1.5" stroke="currentColor" strokeWidth="1.5" fill="none" />
</svg>
)
+3 -6
View File
@@ -25,12 +25,9 @@
* legacy signal engines emit without isComposing.
*/
/** The pure decision: is this keyboard event part of an IME composition?
* `isComposing` is optional on the input: React's synthetic KeyboardEvent
* type does not declare it (the DOM event always carries it), and the
* keyCode 229 fallback covers exactly those callers. */
export function isImeComposition(event: { isComposing?: boolean; keyCode: number }): boolean {
return event.isComposing === true || event.keyCode === 229
/** The pure decision: is this keyboard event part of an IME composition? */
export function isImeComposition(event: { isComposing: boolean; keyCode: number }): boolean {
return event.isComposing || event.keyCode === 229
}
/**
+64 -55
View File
@@ -1,5 +1,5 @@
/**
* Client half of dsh-better-sidebar: resolves the user's "Side card"
* Client half of dsh-apes-sidebar: resolves the user's "Side card"
* preferences through the plugin's own fenced settings route, mounts the
* right sidebar portal (inside an error boundary so a rendering failure
* shows an error strip instead of a blank panel), registers the turn-tail
@@ -15,13 +15,11 @@ import { allLeaves, createSidebarStore, isAgentTabId } from './state.ts'
import { createBetterSidebarService, matchUrlTarget } from './service.ts'
import { revalidateChunksOnReactivate, setChunkModuleSystem } from './chunk-loader.ts'
import { registerBuiltins } from './builtins/index.ts'
import { registerApesTabs, APES_LOCALE_NS, apesZh, apesEn, attachApesLocale } from './apes/index.ts'
import { registerApesNativeTabs } from './apes/native-tabs.tsx'
import { Sidebar } from './Sidebar.tsx'
import { RenderBoundary } from './RenderBoundary.tsx'
import { registerTurnTailInterception } from './intercept.tsx'
import { createNativeTabRecords } from './native/tab-adapter.tsx'
import { registerNativeSurface } from './native/index.ts'
import { registerBottomToggle } from './sidebar/bottom-toggle.tsx'
import { createNativeSurface } from './native/surface.ts'
import { registerOpenPathInterception, registerTurnTailInterception } from './intercept.tsx'
import { registerLinkInterception } from './link-intercept.ts'
import { registerImeGuard } from './ime-guard.ts'
import { registerSettingsNavIcon } from './settings-nav-icon.ts'
@@ -61,11 +59,17 @@ export function apply(ctx: Context): void {
// plugin's dictionaries into the shared locale registry. The disposers
// run on fiber disposal, so re-activation (HMR) re-registers cleanly.
attachLocale(ctx.locale)
// The apes module has its own locale namespace (isolated from the upstream
// betterSidebar dicts per D1=B); attach it so apes t() tracks the DSH
// active-locale preference, and register its zh/en dicts under `apes`.
attachApesLocale(ctx.locale)
ctx.effect(() => {
const offZh = ctx.locale.register(LOCALE_NS, 'zh', zh)
const offEn = ctx.locale.register(LOCALE_NS, 'en', en)
return () => { offZh(); offEn() }
}, 'dsh-better-sidebar: dictionaries')
const offApesZh = ctx.locale.register(APES_LOCALE_NS, 'zh', apesZh as Record<string, string>)
const offApesEn = ctx.locale.register(APES_LOCALE_NS, 'en', apesEn as Record<string, string>)
return () => { offZh(); offEn(); offApesZh(); offApesEn() }
}, 'dsh-apes-sidebar: dictionaries')
// Opt-in third-language support through @huanlin/dsh-plugin-better-locale.
// When that plugin is installed, it publishes `ctx.betterLocale` (the
@@ -129,7 +133,7 @@ export function apply(ctx: Context): void {
dispose?.()
attachBetterLocale(undefined)
}
}, 'dsh-better-sidebar: better-locale lazy integration')
}, 'dsh-apes-sidebar: better-locale lazy integration')
// One store instance per activation: production code creates it only here,
// then hands it to the mounted panel and closes over it in the slot
// registrations (the official createXXXStore() factory rule — no
@@ -141,27 +145,6 @@ export function apply(ctx: Context): void {
// are ready by the time the sidebar renders.
const service = createBetterSidebarService(sidebarStore)
ctx.provide('betterSidebar', service)
// The native right-Sidebar surface: the plugin's content is registered as
// DSH tab types (one per descriptor) and every open routes there, so the
// right column belongs to the host and only the bottom workbench stays
// plugin-owned. Both halves live for this fiber's lifetime.
const nativeRecords = createNativeTabRecords()
const nativeSurface = createNativeSurface(ctx, nativeRecords)
service.setSurface(nativeSurface)
ctx.effect(
() => registerNativeSurface({ ctx, store: sidebarStore, service, records: nativeRecords }),
'dsh-better-sidebar: native right-Sidebar registrations',
)
// The bottom workbench's expand/collapse button in DSH's session header
// (the header's corner seat belongs to the native sidebar's own control).
ctx.effect(
() => registerBottomToggle(ctx, sidebarStore),
'dsh-better-sidebar: bottom-workbench toggle',
)
ctx.effect(
() => () => { nativeSurface.dispose(); service.setSurface(undefined) },
'dsh-better-sidebar: native right-Sidebar surface',
)
// Terminal tab titles use the host's effective shell name (e.g. bash/zsh)
// instead of "Terminal 1". Start with a safe fallback and replace it as
// soon as the host shell info resolves. Tabs created before the response
@@ -173,7 +156,8 @@ export function apply(ctx: Context): void {
terminalTitle = name
const snapshot = service.getSnapshot()
if (snapshot.state === undefined) return
const tabs = allLeaves(snapshot.state.bottomSplits)
const tabs = allLeaves(snapshot.state.splits)
.concat(allLeaves(snapshot.state.bottomSplits))
.flatMap(leaf => leaf.tabs)
for (const tab of tabs) {
if (tab.type === 'terminal' && !isAgentTabId(tab.id) && tab.title === fallbackTitle) {
@@ -186,35 +170,48 @@ export function apply(ctx: Context): void {
// fiber disposal (HMR-safe).
ctx.effect(
() => registerBuiltins(ctx, service, { terminalTitle: () => terminalTitle }),
'dsh-better-sidebar: register built-in tabs and viewers',
'dsh-apes-sidebar: register built-in tabs and viewers',
)
// The apes:* information-layer tabs (the fork's own contribution, isolated
// under src/client/apes per D1=B). Registered through the same service as
// the builtins so they get the full sidebar lifecycle; the disposer runs on
// fiber disposal (HMR-safe). A throw here is caught by the outer try and
// routed to fail() — a bad apes registration must never blank the sidebar.
ctx.effect(
() => registerApesTabs(ctx, service),
'dsh-apes-sidebar: register apes tabs',
)
// Phase A spike: also publish apes:profile as an OFFICIAL right-sidebar tab
// type (ctx.sidebarRightTabs + the keyed `sidebar.right.pane.tab` seat), so
// the tab renders inside DSH's own panel instead of only in the fork's
// shell. Both surfaces coexist during the spike; the disposer reverses the
// two-stage registration on fiber disposal (HMR-safe). A throw is caught
// inside the bridge and never reaches this effect.
ctx.effect(
() => registerApesNativeTabs(ctx),
'dsh-apes-sidebar: apes native right-sidebar tabs',
)
// A failure anywhere in the client lifecycle must never take the app down
// silently: log with the plugin prefix and pin a visible diagnostic strip
// to the page so a blank panel is never the only symptom. This strip is
// the last-resort reporter (no CSS module is reachable from here), so its
// colors go through skin token chains with the previous hexes as the
// chain tails — worst case (no skin tokens on the page) it renders
// byte-identical to the old hardcoded bar, and any `--dsw-alias-*` skin
// re-themes it (guide §12: no hardcoded colors).
// to the page so a blank panel is never the only symptom.
const fail = (phase: string, error: unknown): void => {
console.error(`[dsh-better-sidebar] ${phase} error:`, error)
console.error(`[dsh-apes-sidebar] ${phase} error:`, error)
try {
const bar = document.createElement('div')
bar.style.cssText = 'position:fixed;left:8px;bottom:8px;z-index:2147483000;max-width:70vw;padding:8px 12px;'
+ 'font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;'
+ 'color:var(--dsw-alias-state-error-primary,#f2a1a1);'
+ 'background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-base,#1b1b22));'
+ 'border:1px solid var(--dsw-alias-state-error-primary,#f2a1a1);border-radius:8px;white-space:pre-wrap'
bar.textContent = `[dsh-better-sidebar] ${phase} error: ${error instanceof Error ? error.message : String(error)}`
+ 'font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#f2a1a1;background:#1b1b22;'
+ 'border:1px solid #f2a1a1;border-radius:8px;white-space:pre-wrap'
bar.textContent = `[dsh-apes-sidebar] ${phase} error: ${error instanceof Error ? error.message : String(error)}`
document.body.appendChild(bar)
} catch {
// Nothing left to report with.
}
}
try {
// rc.8+ exposes the client module system as the `ctx.modules` service;
// the chunk loader needs it to resolve its externals, so inject it
// before anything can load a lazy chunk.
// rc.8+ exposes the client module system as the `ctx.modules` service
// (no window.__DSH_MODULES__ page global anymore); the chunk loader needs
// it to resolve its externals, so inject it before anything can load a
// lazy chunk. The loader falls back to the rc.7 global when absent.
setChunkModuleSystem(ctx.modules)
// Fresh chunk state for this activation: drop per-test fixtures and
// revalidate loaded chunk scripts against the bundle route's ETags —
@@ -274,7 +271,7 @@ export function apply(ctx: Context): void {
return
}
layer.setAttribute('data-dsh-panel-host-degraded', '')
console.warn('[dsh-better-sidebar] panel host geometry mismatch — a page-level transform was detected; using degraded viewport sync')
console.warn('[dsh-apes-sidebar] panel host geometry mismatch — a page-level transform was detected; using degraded viewport sync')
// Track our own compensating translation so the loop judges the
// UNCORRECTED geometry: clearing degraded mode must wait for the
// ancestor transform to actually disappear — the frame right after
@@ -305,7 +302,7 @@ export function apply(ctx: Context): void {
if (mounted || disposed) return
try {
host = document.createElement('div')
host.setAttribute('data-dsh-better-sidebar', '')
host.setAttribute('data-dsh-apes-sidebar', '')
document.body.appendChild(host)
root = createRoot(host)
root.render(createElement(RenderBoundary, { className: css.boundaryError }, createElement(Sidebar, { ctx, store: sidebarStore })))
@@ -327,7 +324,7 @@ export function apply(ctx: Context): void {
// rides the same fetch, so one round trip covers both decisions).
const decision = await Promise.race([
loadBootDecision(api),
new Promise<null>(resolve => { window.setTimeout(() => resolve(null), 2000) }),
new Promise<null>(resolve => { const timer = window.setTimeout(() => resolve(null), 2000) }),
])
if (disposed) return
if (decision !== null) {
@@ -352,7 +349,7 @@ export function apply(ctx: Context): void {
offRemote?.()
unmount()
}
}, 'dsh-better-sidebar: sidebar mount')
}, 'dsh-apes-sidebar: sidebar mount')
ctx.effect(
() => {
@@ -363,7 +360,19 @@ export function apply(ctx: Context): void {
return () => {}
}
},
'dsh-better-sidebar: turn-tail interception',
'dsh-apes-sidebar: turn-tail interception',
)
ctx.effect(
() => {
try {
return registerOpenPathInterception(ctx, sidebarStore)
} catch (error) {
fail('interception', error)
return () => {}
}
},
'dsh-apes-sidebar: open-path interception',
)
ctx.effect(
@@ -407,7 +416,7 @@ export function apply(ctx: Context): void {
return () => {}
}
},
'dsh-better-sidebar: link interception',
'dsh-apes-sidebar: link interception',
)
// The IME guard: composition keys (candidate arrows, confirm, cancel)
@@ -427,7 +436,7 @@ export function apply(ctx: Context): void {
return () => {}
}
},
'dsh-better-sidebar: IME composition guard',
'dsh-apes-sidebar: IME composition guard',
)
// DSH 0.1.x does not yet carry an icon through the settings.section
@@ -437,7 +446,7 @@ export function apply(ctx: Context): void {
// the marker for HMR / plugin disable.
ctx.effect(
() => registerSettingsNavIcon(() => t('settingsNav')),
'dsh-better-sidebar: settings navigation icon',
'dsh-apes-sidebar: settings navigation icon',
)
// The "Side card" settings section: appears in the DSH Settings shell
+60 -5
View File
@@ -8,9 +8,10 @@
*/
import { IconCodeOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
import type { Context } from '../context-types.ts'
import { revealPaths, type SidebarStore } from './state.ts'
import { firstLeaf, revealPaths, togglePanel, type SidebarStore } from './state.ts'
import { t } from './locales.ts'
import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts'
import { wrapOpenWorkspacePath, type OpenWorkspacePathService } from './openpath-intercept.ts'
import css from './sidebar.module.css'
/** Open a file in the sidebar's editor (used by the intercepted row and the explorer). */
@@ -24,10 +25,18 @@ export function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: st
ctx.get('betterSidebar')?.openTab({ type: 'editor', title, path: absolute, id: `editor:${absolute}` })
}
/**
* The produced files the turn-tail selector last matched for the visible
* session. The "Show in folder" gesture carries no file path of its own
* (`'.'`), so the reveal highlights exactly these rows when available.
*/
let lastProduced: readonly string[] = []
/**
* Reveal the produced files in the sidebar explorer: expand their parent
* directories, highlight the rows, and focus the explorer tab. Unknown
* files fall back to revealing the workspace root itself.
* directories, highlight the rows, and focus the explorer tab (expanding the
* hosting panel when it is collapsed). Unknown files fall back to revealing
* the workspace root itself.
*/
export function revealInExplorer(
ctx: Context,
@@ -44,6 +53,15 @@ export function revealInExplorer(
? files.map(path => resolveSidebarPath(cwd, path))
: cwd === undefined ? [] : [cwd]
store.reduce(state => revealPaths(state, cwd, targets))
// A type-only open never auto-expands the panel (only content opens do,
// see service.openTab) — so a reveal opens the panel itself when it is
// collapsed, exactly like the subagent auto-open flows, or the highlight
// would be set on an invisible panel.
store.reduce(s => (s.panelOpen ? s : togglePanel(s)))
// Pin the landing to the right panel: the files window must appear where
// the panel just expanded, not in a bottom-panel pane the user last
// touched.
store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id }))
// Focus the single-instance editor home tab (the files window) where the
// reveal highlight renders. Read via ctx.get like every other internal
// consumer (#357): the provider is not on this fiber chain, so a direct
@@ -119,13 +137,50 @@ export function registerTurnTailInterception(ctx: Context, store: SidebarStore):
select: (owner) => {
if (store.getSuspended()) return null
if (store.getPrefs().tabsEnabled['editor'] === false) return null
return selectProducedFiles(owner)
const matched = selectProducedFiles(owner)
if (matched !== null) lastProduced = matched
return matched
},
priority: -1,
registrant: 'dsh-better-sidebar',
registrant: 'dsh-apes-sidebar',
inject: (sessionId: string) => ({
openInSidebar: (path: string) => { openSidebarFile(ctx, store, sessionId, path) },
onShowInFolder: (files: readonly string[]) => { revealInExplorer(ctx, store, sessionId, files) },
}),
}, SidebarProducedFiles))
}
/**
* Register the chat file-open interception: shadows
* `remote.session.openWorkspacePath` — the single funnel every chat-side
* file open goes through on alpha hosts (tool-row path links, the
* produced-files row, prose mentions, inline-code paths) — so opens land in
* the sidebar editor instead of the Host OS. The folder-reveal gesture
* ("Show in folder" passes `'.'`) is the one exception: it is routed to the
* explorer. Gated by BOTH the `interceptOpenPath` pref and the editor tab's
* enable switch; declined opens fall through to the original remote call.
*
* The `remote.session` namespace service mounts asynchronously (the gateway
* client creates it when the session-controller contribution arrives) and
* is recreated on contribution remounts, so the wrapper installs through
* `ctx.inject`: the callback runs once the service exists and re-runs after
* every remount, re-applying the shadow on the fresh instance. Returns the
* disposer (disposes the inject fiber, which restores the original method
* descriptor — HMR-safe).
*/
export function registerOpenPathInterception(ctx: Context, store: SidebarStore): () => void {
const fiber = ctx.inject(['remote.session'], (fctx) => {
fctx.effect(() => {
const service = fctx.get('remote.session') as OpenWorkspacePathService
return wrapOpenWorkspacePath(service, {
takeoverEnabled: () => !store.getSuspended()
&& store.getPrefs().interceptOpenPath !== false
&& store.getPrefs().tabsEnabled['editor'] !== false,
currentSessionId: () => ctx.sessions.list.getSnapshot().current,
openInSidebar: (path, sessionId) => { openSidebarFile(ctx, store, sessionId, path) },
revealInExplorer: (_path, sessionId) => { revealInExplorer(ctx, store, sessionId, lastProduced) },
})
}, 'dsh-apes-sidebar: open-path interception wrap')
})
return () => { void fiber.dispose() }
}
+1 -1
View File
@@ -217,7 +217,7 @@ export function languageForPath(path: string): Language | LanguageSupport | null
return FACTORIES[key]!()
} catch (error) {
// A broken factory degrades to plain text, never crashes the editor.
console.warn(`[dsh-better-sidebar] language factory "${key}" failed:`, error)
console.warn(`[dsh-apes-sidebar] language factory "${key}" failed:`, error)
return null
}
}
+25 -16
View File
@@ -1,28 +1,37 @@
/**
* Size written to `--dsh-sidebar-height`. The conversation column (output +
* composer) must keep at least {@link CONVERSATION_MIN} of the viewport after
* the bottom workbench claims height; a closed workbench pushes 0. The right
* column belongs to DSH's native Sidebar, so this shell pushes no width.
* Size written to `--dsh-sidebar-width` / `--dsh-sidebar-height`.
* The conversation column (output + composer) must keep at least
* {@link PANEL_MIN} of the viewport after the bottom panel claims height.
*/
import { CONVERSATION_MIN } from './state.ts'
import { PANEL_MIN } from './state.ts'
export interface BottomPushInput {
/** Whether the bottom workbench is expanded. */
open: boolean
/** Committed (or mid-drag) bottom height in px. */
height: number
/** Viewport height the cap is measured against (visual viewport when known). */
export interface LayoutPushInput {
narrow: boolean
panelOpen: boolean
bottomOpen: boolean
width: number
bottomHeight: number
viewportWidth: number
viewportHeight: number
}
export interface LayoutPushSize {
width: number
height: number
}
function finiteNonNegative(value: number): number {
return Number.isFinite(value) ? Math.max(0, value) : 0
}
/** Compute the live layout-push height for the bottom workbench. */
export function bottomPushHeight(input: BottomPushInput): number {
if (!input.open) return 0
/** Compute the live layout-push size. Narrow drawers float and push 0. */
export function layoutPushSize(input: LayoutPushInput): LayoutPushSize {
if (input.narrow) return { width: 0, height: 0 }
const viewportWidth = finiteNonNegative(input.viewportWidth)
const viewportHeight = finiteNonNegative(input.viewportHeight)
const maxHeight = Math.max(0, viewportHeight - Math.min(CONVERSATION_MIN, viewportHeight))
return Math.min(finiteNonNegative(input.height), maxHeight)
const maxHeight = Math.max(0, viewportHeight - Math.min(PANEL_MIN, viewportHeight))
return {
width: input.panelOpen ? Math.min(finiteNonNegative(input.width), viewportWidth) : 0,
height: input.bottomOpen ? Math.min(finiteNonNegative(input.bottomHeight), maxHeight) : 0,
}
}
+45 -73
View File
@@ -1,25 +1,47 @@
/**
* Layout push: the bottom workbench occupies layout instead of floating over
* the conversation. The right column belongs to DSH's native Sidebar, so this
* stylesheet pushes no width — only the workbench's height.
* Layout push: an open panel occupies layout instead of floating over it.
* The right panel reserves padding inside the AppFrame three-column grid,
* so the app sidebar, conversation, and details column all remain to its
* left while the frame's border-box stays viewport-wide. Harness measures
* that border-box to choose desktop versus narrow presentation; preserving
* it prevents a desktop window from entering narrow mode merely because the
* plugin panel opened.
*
* The workbench squeezes only the conversation column. The anchor is the
* AppFrame positions its details drag handle from the measured border-box.
* Move that handle left by the same reserved width so it stays on the details
* column edge. The ordinary sidebar handle remains at its native coordinate.
*
* The bottom panel squeezes only the conversation column. The anchor is the
* [data-dsh-center-col] tag the Sidebar shell's locator writes on the
* frame's measured center grid item (Sidebar.tsx locate — not a positional
* path: the frame also contains an overlay layer and drag handles). A
* stretched grid item shrinks by its bottom margin, so the conversation
* output and composer lift without touching either sidebar.
*
* The size rides a CSS variable updated by the Sidebar shell (0 while
* collapsed). Expand/collapse uses the host theme duration; drags disable
* transitions so the reserved edge tracks the pointer.
* Sizes ride CSS variables updated by the Sidebar shell (0 while collapsed).
* Expand/collapse uses the host theme duration; drags disable transitions so
* every reserved edge tracks the pointer.
*/
/* Current shells expose [data-dsh-frame]; rc.8-era shells expose the same
AppFrame only as the root slot's direct child. */
#root [data-dsh-frame],
#root > [data-slot="root"] > div {
box-sizing: border-box;
padding-right: var(--dsh-sidebar-width, 0px);
transition: padding-right var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
#root [data-dsh-frame] > [data-side="details"],
#root > [data-slot="root"] > div > [data-side="details"] {
transform: translateX(calc(0px - var(--dsh-sidebar-width, 0px)));
transition:
left var(--ds-transition-duration-slow) var(--ds-ease-in-out),
transform var(--ds-transition-duration-slow) var(--ds-ease-in-out);
}
/* The AppFrame's center column. The Sidebar shell's locator (Sidebar.tsx
locate/measureCenter, resolving via center-column.ts) tags the measured
column node with [data-dsh-center-col] — one attribute write per node
lifetime. The locator anchors on the conversation slot host
(`main.conversation` on DSH 0.1.5-alpha.2+, `conversation` on alpha.1)
and skips the transparent display:contents slot hosts above it. The rule
locate/measureCenter) tags the measured column node with
[data-dsh-center-col] — one attribute write per node lifetime. The rule
used to anchor on a structural has()-selector over the conversation slot
wrapper, whose match cache invalidates on every #root subtree mutation
(the streaming chat mutates it constantly), re-evaluating the selector
@@ -51,7 +73,13 @@ body[data-dsh-sidebar-collapsed] [data-slot="conversation.session.header"] > hea
padding-right: 78px;
}
body[data-dsh-sidebar-dragging] #root [data-dsh-center-col] {
body[data-dsh-sidebar-dragging] #root [data-dsh-frame],
body[data-dsh-sidebar-dragging] #root > [data-slot="root"] > div,
body[data-dsh-sidebar-dragging] #root [data-dsh-frame] > [data-side="details"],
body[data-dsh-sidebar-dragging] #root > [data-slot="root"] > div > [data-side="details"],
body[data-dsh-sidebar-dragging] #root [data-dsh-center-col],
#root [data-dsh-frame][data-dragging] > [data-side="details"],
#root > [data-slot="root"] > div[data-dragging] > [data-side="details"] {
transition: none;
}
@@ -75,67 +103,11 @@ body[data-dsh-sidebar-dragging] #root [data-dsh-center-col] {
}
@media (prefers-reduced-motion: reduce) {
#root [data-dsh-frame],
#root > [data-slot="root"] > div,
#root [data-dsh-frame] > [data-side="details"],
#root > [data-slot="root"] > div > [data-side="details"],
#root [data-dsh-center-col] {
transition: none;
}
}
/* Context-menu submenu flip (viewport clamping for the portaled menus).
*
* The ui-primitives Menu clamps its main portal list into the viewport but
* positions nested submenus with static CSS only — anchored at the parent
* row's bottom, growing UP and to the RIGHT — with no clamp whatsoever.
* Both defaults are the worst case for our menus: a tall "open with" submenu
* escapes past the viewport top when the row menu opens near the top, and
* past the right edge in a narrow workbench pane (a pane can be far narrower
* than the card 218 + gap + submenu 165 a submenu needs).
*
* menu-flip.ts publishes `data-dsh-sidebar-submenu` on <body> — with the
* words "down" and/or "left" — for exactly the lifetime of one of OUR
* submenu-bearing menus (FileTree row menu, TabBar tab menu). The tokens
* flip the growth direction; the attribute's absence restores the
* primitive's own geometry untouched, and host-owned menus are never
* affected because they never open while the attribute exists (each side
* closes on the other's outside pointerdown).
*
* No max-height/overflow scroll cap here on purpose: overflow clipping
* would cut the submenu's ::before hover bridge (the corridor that keeps
* itemWrap's mouseleave from closing the card mid-crossing). */
/* Visual tether, part 1 — the parent row stays lit while its submenu is
* open. The primitive only styles :hover, so once the pointer crossed into
* the side card the parent row's fill cleared and the floating card read as
* unrelated. The Menu marks the open parent row with aria-expanded="true"
* (host-rendered, stable), so the same hover token re-applies for the
* submenu's whole lifetime. */
body[data-dsh-sidebar-submenu] div[role="menu"] button[aria-expanded="true"] {
background: var(--dsw-alias-interactive-bg-hover);
}
/* Visual tether, part 2 — the side card hugs its parent (10px → 4px gap),
* with the ::before hover corridor narrowed to match. Ordered BEFORE the
* flip overrides below so their left/right flips still win. */
body[data-dsh-sidebar-submenu] div[role="menu"] div[role="menu"] {
left: calc(100% + 4px);
}
body[data-dsh-sidebar-submenu] div[role="menu"] div[role="menu"]::before {
left: -4px;
width: 4px;
}
body[data-dsh-sidebar-submenu~="down"] div[role="menu"] div[role="menu"] {
top: -4px;
bottom: auto;
}
body[data-dsh-sidebar-submenu~="left"] div[role="menu"] div[role="menu"] {
left: auto;
right: calc(100% + 4px);
}
/* The hover bridge follows the card to the left side of the parent row. */
body[data-dsh-sidebar-submenu~="left"] div[role="menu"] div[role="menu"]::before {
left: auto;
right: -4px;
}
+3 -3
View File
@@ -22,12 +22,12 @@ interface LazyChunkViewProps<P> {
props: P
}
function LazyChunkView<P extends object>({ chunk, pick, props }: LazyChunkViewProps<P>): ReactNode {
function LazyChunkView<P>({ chunk, pick, props }: LazyChunkViewProps<P>): ReactNode {
const [attempt, setAttempt] = useState(0)
const [state, setState] = useState<
| { status: 'loading' }
| { status: 'error'; message: string }
| { status: 'ready'; Comp: ComponentType<P> }
| { status: 'ready'; Comp: ComponentType<any> }
>({ status: 'loading' })
useEffect(() => {
@@ -37,7 +37,7 @@ function LazyChunkView<P extends object>({ chunk, pick, props }: LazyChunkViewPr
if (cancelled) return
const Comp = pick(mod)
if (Comp === undefined) {
setState({ status: 'error', message: `[dsh-better-sidebar] chunk "${chunk}" is missing its component` })
setState({ status: 'error', message: `[dsh-apes-sidebar] chunk "${chunk}" is missing its component` })
return
}
setState({ status: 'ready', Comp })
+18 -43
View File
@@ -20,19 +20,11 @@ export const ar: Record<string, string> = {
files: 'الملفات',
changesSessionEmpty: 'لا توجد عمليات ملفات في هذه الجلسة بعد',
changesRead: 'قراءة',
changesMdReading: 'قراءة',
changesHtmlRender: "عرض",
changesHtmlRaw: "المصدر",
changesPdfRender: "عرض",
changesPdfRaw: "المصدر",
changesMdRaw: 'الخام',
changesWrite: 'كتابة',
changesEdit: 'تحرير',
changesRunning: 'قيد التنفيذ',
changesError: 'خطأ',
changesFold: '{count} أسطر… انقر للتوسيع',
changesFoldLoading: 'جارٍ التوسيع…',
changesFoldUnavailable: 'السياق غير متوفر',
changesContext: 'سياق',
changesPriorUnknown: 'المحتوى السابق خارج النافذة؛ يُعرض كما لو أُضيف بالكامل',
explorer: 'المستكشف',
@@ -82,6 +74,7 @@ export const ar: Record<string, string> = {
closeOtherTabs: 'إغلاق التبويبات الأخرى',
closeLeftTabs: 'إغلاق التبويبات على اليسار',
closeRightTabs: 'إغلاق التبويبات على اليمين',
moveToFreeWindow: 'النقل إلى نافذة حرة',
floatDropHint: 'حرّك للفتح في نافذة حرة',
dockToSidebar: 'العودة إلى الشريط الجانبي',
pinTerminal: 'تثبيت الطرفية',
@@ -96,6 +89,8 @@ export const ar: Record<string, string> = {
pinnedTerminalScopeGlobal: 'مثبتة عام',
pinnedRailLabel: 'الطرفيات المثبتة',
closePinnedTerminal: 'إغلاق الطرفية',
collapse: 'طي الشريط الجانبي',
expand: 'توسيع الشريط الجانبي',
collapseBottomPanel: 'طي اللوحة السفلية',
expandBottomPanel: 'توسيع اللوحة السفلية',
terminalError: 'فشل اتصال الطرفية',
@@ -104,7 +99,6 @@ export const ar: Record<string, string> = {
terminalDepsFailed: 'فشل تحميل تبعية الطرفية node-pty',
terminalDepsHint: 'شغّل الأمر التالي في طرفية أو cmd على جهاز DSH للإصلاح، ثم أعد المحاولة (يبقى node-pty متزامناً مع إصدار نواة DSH):',
terminalDepsProfile: ' (الملف الشخصي المكتشف: {profile})',
terminalShellNotFound: 'لم يتم العثور على الصدفة المُعدَّة: {name} — تحقق من مسار الصدفة في الإعدادات → Side card → Terminal',
preview: 'معاينة',
toc: 'الفهرس',
edit: 'تحرير',
@@ -151,6 +145,12 @@ export const ar: Record<string, string> = {
changesOpenDiffTab: 'فتح في تبويب diff منفصل',
changesClosePreview: 'إغلاق المعاينة',
changesResizePreview: 'تغيير حجم المعاينة',
changesDiffOpenTitle: 'يفتح الفرق كـ',
changesDiffOpenDesc: 'أين يضع إجراء «التوسيع إلى تبويب diff» الفرق',
changesDiffOpenFloat: 'نافذة عائمة',
changesDiffOpenFloatDesc: 'نافذة عائمة في وسط الشاشة — سحب وتحجيم وتثبيت فوق',
changesDiffOpenPane: 'لوحة',
changesDiffOpenPaneDesc: 'مثبتة أسفل اللوحة المصدر (تقسيم diff بنمط VSCode)',
changesLoadError: 'سجلات ملفات الجلسة غير متاحة الآن',
staged: 'مُدرج',
unstaged: 'غير مُدرج',
@@ -199,13 +199,6 @@ export const ar: Record<string, string> = {
addToConversation: 'إضافة إلى المحادثة',
copyRelative: 'نسخ المسار النسبي',
copyAbsolute: 'نسخ المسار المطلق',
rename: 'إعادة تسمية',
renameInvalid: 'لا يمكن أن يكون الاسم فارغًا أو يحتوي على فواصل مسار.',
delete: 'حذف',
deleteTitle: 'حذف «{name}»؟',
deleteDescFile: 'سيتم حذف هذا الملف نهائيًا. لا يمكن التراجع عن هذا الإجراء.',
deleteDescDir: 'سيتم حذف هذا الدليل وكل محتوياته نهائيًا. لا يمكن التراجع عن هذا الإجراء.',
dismiss: 'إغلاق',
download: 'تنزيل',
uploadFiles: 'رفع ملفات',
uploadFolder: 'رفع مجلد',
@@ -224,6 +217,13 @@ export const ar: Record<string, string> = {
settingsIntro: 'إدارة ما تعرضه البطاقة الجانبية وكيف تتصرف',
settingsPopupDesc: 'تكوين خيارات ذات صلة بـ {feature}',
settingsDone: 'تم',
settingsOpenTitle: 'فتح افتراضياً للمحادثات الجديدة',
settingsOpenDesc: 'توسيع البطاقة الجانبية تلقائياً للمحادثات الجديدة تماماً؛ المحادثات الموجودة تحتفظ بتخطيطاتها',
settingsWidthTitle: 'الحصة الافتراضية للعرض',
settingsWidthDesc: 'الحصة الافتراضية للبطاقة الجانبية من عرض النافذة للمحادثات الجديدة (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'فتح ملفات المحادثة في الشريط الجانبي',
settingsOpenPathDesc: 'فتح روابط الملفات في المحادثة (صفوف الأدوات، الملفات الناتجة، الإشارات) في محرّر الشريط الجانبي بدلاً من التطبيق الافتراضي للنظام',
settingsOpenToolsTitle: 'حقن أداة فتح الشريط الجانبي للنموذج',
settingsOpenToolsDesc: 'عند التفعيل، يمكن للنموذج فتح الملفات والمجلدات وصفحات HTTP(S) في الشريط الجانبي عبر أداة sidebar_open (معطّل افتراضياً)',
settingsTitleBarTitle: 'وضع توافق الموضع',
@@ -396,7 +396,7 @@ export const ar: Record<string, string> = {
addPluginsViewerCardDesc: 'تسجيل معاينة نوع ملف',
addPluginsTabDesc: 'يمكن توسيع صفحات الشريط الجانبي (التبويبات) بواسطة الإضافات. تُسجَّل الإضافات عبر خدمة ctx.betterSidebar؛ النقر على «تثبيت» ينسخ أمر التثبيت — الصقه في طرفية حيث يقيم ملف DSH الشخصي وشغّله.',
addPluginsViewerDesc: 'يمكن توسيع عارضات الملفات بواسطة الإضافات. تُسجَّل الإضافات عبر خدمة ctx.betterSidebar؛ النقر على «تثبيت» ينسخ أمر التثبيت — الصقه في طرفية حيث يقيم ملف DSH الشخصي وشغّله.',
addPluginsBrowseMore: 'تصفّح المزيد من الإضافات على GitHub (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'تصفّح المزيد من الإضافات على GitHub (topic: dsh-apes-sidebar)',
addPluginsSearch: 'البحث باسم الإضافة / الوصف…',
addPluginsNoMatch: 'لا إضافات مطابقة',
addPluginsRecommended: 'إضافات موصى بها',
@@ -409,7 +409,7 @@ export const ar: Record<string, string> = {
pluginGithubWorkbenchDesc: 'تبويب GitHub Workbench: شجرة المستودع البعيد + Issues / Pull requests / Actions مع دعم كامل للكتابة — إنشاء Issue/PR، والتعليق، والتعديل، والإغلاق/إعادة الفتح، والدمج عبر squash·merge·rebase (بتأكيد قوي)، وإعادة تشغيل CI أو إلغائها؛ يعرض مبدّل المستودعات تلقائيًا المستودعات المتاحة ويتيح البحث في المستودعات العامة؛ وبدون better-sidebar يتحول تلقائيًا إلى لوحة جانبية مستقلة على اليمين',
pluginMdExportDesc: 'يضيف زر «تصدير» إلى شريط أدوات Markdown في better-sidebar: يعرض ملف .md الحالي بنقرة واحدة كملف HTML مستقل (الجداول/كتل الأكواد/مخططات Mermaid مدمجة، والتخطيط يتبع سمة المعاينة) ويحفظه بجوار الملف، أو يصدّره PDF عبر نافذة الطباعة',
pluginCodeNavDesc: 'تنقل معاينة الكود: يكتشف اللغة تلقائياً من نوع الملف ويبرز الصياغة، مخطط الرموز (تصفية الفئات/الطرق/المتغيرات + قفزة بنقرة واحدة)، والبحث داخل الملف (إبراز كل التطابقات، السابق/التالي، مطابقة حالة الأحرف) — يتولى معاينة ملفات الكود في محرر better-sidebar',
pluginSuhuangScrollDesc: 'يربط بيئة Suhuang Scroll المحلية بإعدادات DSH وbetter-sidebar لإعداد النموذج واختبار الاتصال والتحكم في التصحيح المتواصل؛ يتطلب Suhuang Scroll Runtime وdsh-better-sidebar',
pluginSuhuangScrollDesc: 'يربط بيئة Suhuang Scroll المحلية بإعدادات DSH وbetter-sidebar لإعداد النموذج واختبار الاتصال والتحكم في التصحيح المتواصل؛ يتطلب Suhuang Scroll Runtime وdsh-apes-sidebar',
pluginBetterOverleafDesc: 'تبويب Overleaf لـ better-sidebar: تسجيل دخول مباشر عبر متصفح CDP (يدعم Chromium من جهات أخرى)، قائمة المشاريع/التبديل، نسخ git محلية ضمن <workspace>/overleaf/، مزامنة git ثنائية الاتجاه مع تراجع إلى API للقراءة فقط، ومعاينة الملفات عبر مساحة عمل الشريط الجانبي',
pluginGitRemotesDesc: 'تبويب Git Remotes: فرع/أصل/أمام-خلف، fetch (مع prune اختياري)، ff-only pull، والدفع فقط بعد تأكيد داخل التبويب. لا يستبدل تبويب Git المدمج للإدراج/التثبيت، ولا يقدّم force-push أو أداة دفع آلي للنموذج',
pluginSentinelDesc: 'نظام إيقاظ وكيل يحرّكه تحقّق الشروط: مستشعرات ملف/عملية/منفذ/HTTP/أمر/webhook توقظ الجلسات الخاملة عند تحقّق الشرط؛ يُسجِّل تبويب «Sentinel» مع جدول مراقبة شامل للخادم',
@@ -422,29 +422,4 @@ export const ar: Record<string, string> = {
pluginDocsPanelDesc: '«وثائق عامة» في شريط DSH الجانبي: ملاحظات Markdown عامة، قابلة للقراءة من أي مساحة عمل — قائمة ملفات، مخطط تفصيلي، فتح في Chrome / VS Code، وأزرار نسخ؛ دليل الوثائق قابل للتكوين (الافتراضي ~/.dsh/docs)',
pluginEgoBrowserDesc: 'متصفح الوكيل لـ DeepSeek Harness: 32 أداة ego_* تقود متصفح Chromium حقيقي، مع تبويب «متصفح ego» أصلي في الشريط الجانبي يعرض مباشرة كل صفحة يزورها الوكيل — يمكنك النقر والسحب والكتابة لتولي التحكم. يسجّل التبويب تلقائيًا عند وجود better-sidebar، وإلا يظهر كفقاعة عائمة',
pluginBilingualReaderDesc: 'اقرأ ملفات PDF في الشريط الجانبي لـ DSH: عرض PDF أصلي، حدد نصًا لترجمته بواسطة النموذج اللغوي مع السياق، معزولًا تمامًا عن المحادثة الرئيسية',
changesRedactOn: "تنقيع مُفعّل: تظهر الأسرار كـ [REDACTED]؛ انقر للتعطيل (على مسؤوليتك)",
changesRedactOff: "تنقيع مُعطّل: سيُعرض المحتوى كما هو؛ انقر للتفعيل",
changesRedactOnLabel: "تنقيع",
changesRedactOffLabel: "بدون تنقيع",
changesRedactBanner: "مُنقّى",
pluginSentinelName: 'dsh-sentinel نظام الاستيقاظ',
pluginEgoBrowserName: 'ego-browser متصفح الوكيل',
pluginBetterOverleafName: 'dsh-better-overleaf تبويب Overleaf',
pluginDocsPanelName: 'dsh-docs-panel مستندات عامة',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge بيانات اعتماد Git',
pluginGitRemotesName: 'dsh-git-remotes أجهزة Git البعيدة',
pluginGithubWorkbenchName: 'dsh-github-workbench منصة GitHub',
pluginSidebarQaName: 'dsh-sidebar-qa تحديد وسؤال',
pluginSidenoteName: 'dsh-sidenote محادثة جانبية',
pluginServerDeckName: 'dsh-server-deck لوحة الخوادم',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel نفق SSH',
pluginTurnReviewName: 'dsh-turn-review مراجعة الجولة',
pluginBilingualReaderName: 'dsh-bilingual-reader قارئ ثنائي اللغة',
pluginOfficeName: 'معاينة Office',
pluginMdExportName: 'تصدير Markdown',
pluginCodeNavName: 'مستكشف معاينة الكود',
pluginVideoPreviewName: 'معاينة الفيديو',
presetDshDesktopDesc: 'وضع Electron المتقدم (بلا إطار): يحجز macOS شريطًا علويًا بمقدار 20px، ويحجز Windows بمقدار 32px لشريط العنوان عند غياب WCO',
}
+18 -43
View File
@@ -5,19 +5,11 @@ export const de: Record<string, string> = {
files: 'Dateien',
changesSessionEmpty: 'Noch keine Dateioperationen in dieser Sitzung',
changesRead: 'Lesen',
changesMdReading: 'Lesen',
changesHtmlRender: "Rendern",
changesHtmlRaw: "Quelltext",
changesPdfRender: "Rendern",
changesPdfRaw: "Quelltext",
changesMdRaw: 'Rohfassung',
changesWrite: 'Schreiben',
changesEdit: 'Bearbeiten',
changesRunning: 'läuft',
changesError: 'Fehler',
changesFold: '{count} Zeilen… zum Aufklappen klicken',
changesFoldLoading: 'Wird aufgeklappt…',
changesFoldUnavailable: 'Kontext nicht verfügbar',
changesContext: 'Kontext',
changesPriorUnknown: 'Inhalt vor der Änderung liegt außerhalb des Fensters; als vollständig neu angezeigt',
explorer: 'Explorer',
@@ -67,6 +59,7 @@ export const de: Record<string, string> = {
closeOtherTabs: 'Andere Tabs schließen',
closeLeftTabs: 'Tabs links schließen',
closeRightTabs: 'Tabs rechts schließen',
moveToFreeWindow: 'In freies Fenster verschieben',
floatDropHint: 'Loslassen, um in einem freien Fenster zu öffnen',
dockToSidebar: 'Zurück zur Seitenleiste',
pinTerminal: 'Terminal anheften',
@@ -81,6 +74,8 @@ export const de: Record<string, string> = {
pinnedTerminalScopeGlobal: 'Global angeheftet',
pinnedRailLabel: 'Angeheftete Terminals',
closePinnedTerminal: 'Terminal schließen',
collapse: 'Seitenleiste einklappen',
expand: 'Seitenleiste ausklappen',
collapseBottomPanel: 'Unteres Panel einklappen',
expandBottomPanel: 'Unteres Panel ausklappen',
terminalError: 'Terminalverbindung fehlgeschlagen',
@@ -89,7 +84,6 @@ export const de: Record<string, string> = {
terminalDepsFailed: 'Die Terminal-Abhängigkeit node-pty konnte nicht geladen werden',
terminalDepsHint: 'Führen Sie den folgenden Befehl in einem Terminal oder in cmd auf dem DSH-System aus, um dies zu beheben, und klicken Sie dann auf „Erneut versuchen“ (node-pty bleibt mit der DSH-Core-Version synchron):',
terminalDepsProfile: ' (erkanntes Profil: {profile})',
terminalShellNotFound: 'Konfigurierte Shell nicht gefunden: {name} — Shell-Pfad unter Einstellungen → Side card → Terminal prüfen',
preview: 'Vorschau',
toc: 'Inhaltsverzeichnis',
edit: 'Bearbeiten',
@@ -136,6 +130,12 @@ export const de: Record<string, string> = {
changesOpenDiffTab: 'In eigenem Tab öffnen',
changesClosePreview: 'Vorschau schließen',
changesResizePreview: 'Vorschauhöhe anpassen',
changesDiffOpenTitle: 'Diff öffnet als',
changesDiffOpenDesc: 'Wo die Aktion „In eigenem Tab öffnen“ den Diff platziert',
changesDiffOpenFloat: 'Freies Fenster',
changesDiffOpenFloatDesc: 'Ein schwebendes Fenster mittig im Viewport — verschieben, skalieren, oben halten',
changesDiffOpenPane: 'Bereich',
changesDiffOpenPaneDesc: 'Unter dem Quellbereich angedockt (VSCode-artiger Diff-Split)',
changesLoadError: 'Sitzungsdatei-Aufzeichnungen sind derzeit nicht verfügbar',
staged: 'Gestaged',
unstaged: 'Nicht gestaged',
@@ -184,13 +184,6 @@ export const de: Record<string, string> = {
addToConversation: 'Zur Unterhaltung hinzufügen',
copyRelative: 'Relativen Pfad kopieren',
copyAbsolute: 'Absoluten Pfad kopieren',
rename: 'Umbenennen',
renameInvalid: 'Der Name darf nicht leer sein und keine Pfadtrenner enthalten.',
delete: 'Löschen',
deleteTitle: '„{name}“ löschen?',
deleteDescFile: 'Dadurch wird die Datei dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.',
deleteDescDir: 'Dadurch werden das Verzeichnis und dessen gesamter Inhalt dauerhaft gelöscht. Dies kann nicht rückgängig gemacht werden.',
dismiss: 'Schließen',
download: 'Herunterladen',
uploadFiles: 'Dateien hochladen',
uploadFolder: 'Ordner hochladen',
@@ -209,6 +202,13 @@ export const de: Record<string, string> = {
settingsIntro: 'Verwalten Sie, was die Seitenkarte anzeigt und wie sie sich verhält',
settingsPopupDesc: 'Optionen für „{feature}“ konfigurieren',
settingsDone: 'Fertig',
settingsOpenTitle: 'Für neue Unterhaltungen standardmäßig geöffnet',
settingsOpenDesc: 'Die Seitenkarte wird für brandneue Unterhaltungen automatisch ausgeklappt; bestehende Unterhaltungen behalten ihr eigenes Layout',
settingsWidthTitle: 'Standardbreite',
settingsWidthDesc: 'Standardanteil der Seitenkarte an der Fensterbreite für neue Unterhaltungen (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Chat-Dateien in der Seitenleiste öffnen',
settingsOpenPathDesc: 'Dateilinks im Chat (Werkzeugzeilen, erstellte Dateien, Erwähnungen) werden im Seitenleisten-Editor geöffnet statt in der System-Standardanwendung',
settingsOpenToolsTitle: 'Seitenleisten-Öffnungswerkzeug für das Modell bereitstellen',
settingsOpenToolsDesc: 'Wenn aktiviert, kann das Modell über das sidebar_open-Werkzeug Dateien, Ordner und HTTP(S)-Seiten in der Seitenleiste öffnen (standardmäßig deaktiviert)',
settingsTitleBarTitle: 'Kompatibilitätsmodus der Position',
@@ -381,7 +381,7 @@ export const de: Record<string, string> = {
addPluginsViewerCardDesc: 'Eine Dateityp-Vorschau registrieren',
addPluginsTabDesc: 'Seitenleisten-Seiten (Tabs) können durch Plugins erweitert werden. Plugins registrieren sich über den ctx.betterSidebar-Dienst; ein Klick auf „Installieren“ kopiert den Installationsbefehl – fügen Sie ihn in einem Terminal ein, in dem Ihr DSH-Profil liegt, und führen Sie ihn aus.',
addPluginsViewerDesc: 'Datei-Viewer können durch Plugins erweitert werden. Plugins registrieren sich über den ctx.betterSidebar-Dienst; ein Klick auf „Installieren“ kopiert den Installationsbefehl – fügen Sie ihn in einem Terminal ein, in dem Ihr DSH-Profil liegt, und führen Sie ihn aus.',
addPluginsBrowseMore: 'Weitere Plugins auf GitHub durchsuchen (Topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'Weitere Plugins auf GitHub durchsuchen (Topic: dsh-apes-sidebar)',
addPluginsSearch: 'Nach Plugin-Name oder -Beschreibung suchen…',
addPluginsNoMatch: 'Keine passenden Plugins',
addPluginsRecommended: 'Empfohlene Plugins',
@@ -394,7 +394,7 @@ export const de: Record<string, string> = {
pluginGithubWorkbenchDesc: '„GitHub Workbench“-Tab: Repository-Baum + Issues / Pull requests / Actions mit vollen Schreibrechten — Issue/PR erstellen, kommentieren, bearbeiten, schließen/wieder öffnen, squash·merge·rebase-Merge (mit starker Bestätigung), CI erneut ausführen/abbrechen; der Repo-Umschalter listet automatisch zugreifbare Repos auf und durchsucht öffentliche; ohne better-sidebar fällt er auf ein eigenständiges rechtes Panel zurück',
pluginMdExportDesc: 'Fügt einen „Export"-Button zur Markdown-Symbolleiste in better-sidebar hinzu: rendert die aktuelle .md per Klick in eigenständiges HTML (Tabellen/Codeblöcke/Mermaid-Diagramme inline, Layout folgt dem Vorschau-Theme) und speichert es neben der .md, oder exportiert über den Druckdialog als PDF',
pluginCodeNavDesc: 'Code-Vorschau-Navigation: erkennt die Sprache am Dateityp und hebt die Syntax hervor, Symbol-Gliederung (Filter für Klassen/Methoden/Variablen + Sprung mit einem Klick) und Suche in der Datei (alle Treffer hervorgehoben, vor/zurück, Groß-/Kleinschreibung) — übernimmt die Code-Dateivorschau im better-sidebar-Editor',
pluginSuhuangScrollDesc: 'Verbindet die lokale Suhuang Scroll Runtime mit den DSH-Einstellungen und better-sidebar für Modellkonfiguration, Verbindungstests und fortlaufende Bewertung; benötigt Suhuang Scroll Runtime und dsh-better-sidebar',
pluginSuhuangScrollDesc: 'Verbindet die lokale Suhuang Scroll Runtime mit den DSH-Einstellungen und better-sidebar für Modellkonfiguration, Verbindungstests und fortlaufende Bewertung; benötigt Suhuang Scroll Runtime und dsh-apes-sidebar',
pluginBetterOverleafDesc: 'Overleaf-Tab für better-sidebar: Login direkt über CDP-Browser (Chromium von Drittanbietern unterstützt), Projekliste/-wechsel, lokale git-Spiegel unter <workspace>/overleaf/, bidirektionaler git-Abgleich mit Read-only-API-Fallback und Dateivorschau über die Sidebar-Workbench',
pluginGitRemotesDesc: '„Git-Remotes“-Tab: Branches/Upstream/ahead-behind ansehen, fetch (optional prune), ff-only pull und push erst nach Bestätigung im Tab. Ersetzt nicht den eingebauten Stagen/Commit-Tab und bietet weder force-push noch automatisches Push-Werkzeug für das Modell',
pluginSentinelDesc: 'Bedingungsgesteuertes Wecksystem für Agenten: Datei-/Prozess-/Port-/HTTP-/Befehls-/Webhook-Sensoren wecken ruhende Sitzungen, wenn Bedingungen zutreffen; registriert einen „Sentinel“-Tab mit der serverweiten Überwachungstabelle',
@@ -407,29 +407,4 @@ export const de: Record<string, string> = {
pluginDocsPanelDesc: '„Globale Dokumente“ in der DSH-Seitenleiste: eigene Markdown-Notizen aus jedem Arbeitsbereich lesen – Dateiliste, Gliederung, Öffnen in Chrome / VS Code und Kopieren-Buttons; das Dokumentenverzeichnis ist konfigurierbar (Standard ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Der Agent-Browser für DeepSeek Harness: 32 ego_*-Tools steuern ein echtes Chromium; ein nativer „ego-Browser“-Tab in der Seitenleiste zeigt live jede Seite, die der Agent besucht – Klicken, Ziehen und Tippen zum Übernehmen. Registriert den Tab automatisch, wenn better-sidebar vorhanden ist, sonst eine schwebende Blase',
pluginBilingualReaderDesc: 'PDFs im DSH-Seitenbereich lesen: native PDF-Anzeige, Text auswählen und mit dem LLM übersetzen — mit Kontext, vollständig vom Hauptgespräch isoliert',
changesRedactOn: "Schwärzung aktiv: Geheimnisse als [REDACTED]; Klicken zum Deaktivieren (auf eigenes Risiko)",
changesRedactOff: "Schwärzung aus: Inhalte werden unverändert angezeigt; Klicken zum Aktivieren",
changesRedactOnLabel: "Schwärzung",
changesRedactOffLabel: "Ohne Schwärzung",
changesRedactBanner: "Geschwärzt",
pluginSentinelName: 'dsh-sentinel Wecksystem',
pluginEgoBrowserName: 'ego-browser Agent-Browser',
pluginBetterOverleafName: 'dsh-better-overleaf Overleaf-Tab',
pluginDocsPanelName: 'dsh-docs-panel Globale Dokumente',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Git-Zugangsdaten',
pluginGitRemotesName: 'dsh-git-remotes Git-Remotes',
pluginGithubWorkbenchName: 'dsh-github-workbench GitHub-Workbench',
pluginSidebarQaName: 'dsh-sidebar-qa Auswählen & Fragen',
pluginSidenoteName: 'dsh-sidenote Seiten-Chat',
pluginServerDeckName: 'dsh-server-deck Server-Deck',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel SSH-Tunnel',
pluginTurnReviewName: 'dsh-turn-review Runden-Review',
pluginBilingualReaderName: 'dsh-bilingual-reader Zweisprachiger Reader',
pluginOfficeName: 'Office-Vorschau',
pluginMdExportName: 'Markdown-Export',
pluginCodeNavName: 'Code-Vorschau-Navigation',
pluginVideoPreviewName: 'Video-Vorschau',
presetDshDesktopDesc: 'Elektronischer Erweiterte-Modus (rahmenlos): macOS reserviert oben 20px; Windows reserviert ohne WCO 32px für die Titelleiste',
}
+18 -43
View File
@@ -12,19 +12,11 @@ export const fr: Record<string, string> = {
files: 'Fichiers',
changesSessionEmpty: 'Aucune opération de fichier dans cette session',
changesRead: 'Lecture',
changesMdReading: 'Lecture',
changesHtmlRender: "Rendu",
changesHtmlRaw: "Source",
changesPdfRender: "Rendu",
changesPdfRaw: "Source",
changesMdRaw: 'Brut',
changesWrite: 'Écriture',
changesEdit: 'Modification',
changesRunning: 'en cours',
changesError: 'erreur',
changesFold: '{count} lignes… cliquer pour déplier',
changesFoldLoading: 'Dépliage…',
changesFoldUnavailable: 'Contexte indisponible',
changesContext: 'contexte',
changesPriorUnknown: 'Le contenu antérieur est hors de la fenêtre ; affiché comme entièrement ajouté',
explorer: 'Explorateur',
@@ -74,6 +66,7 @@ export const fr: Record<string, string> = {
closeOtherTabs: 'Fermer les autres onglets',
closeLeftTabs: 'Fermer les onglets à gauche',
closeRightTabs: 'Fermer les onglets à droite',
moveToFreeWindow: 'Déplacer vers la fenêtre flottante',
floatDropHint: 'Relâchez pour ouvrir dans une fenêtre flottante',
dockToSidebar: 'Revenir à la barre latérale',
pinTerminal: 'Épingler le terminal',
@@ -88,6 +81,8 @@ export const fr: Record<string, string> = {
pinnedTerminalScopeGlobal: 'Épinglé globalement',
pinnedRailLabel: 'Terminaux épinglés',
closePinnedTerminal: 'Fermer le terminal',
collapse: 'Réduire la barre latérale',
expand: 'Déployer la barre latérale',
collapseBottomPanel: 'Réduire le panneau inférieur',
expandBottomPanel: 'Déployer le panneau inférieur',
terminalError: 'Échec de la connexion au terminal',
@@ -96,7 +91,6 @@ export const fr: Record<string, string> = {
terminalDepsFailed: 'Échec du chargement de la dépendance terminal node-pty',
terminalDepsHint: 'Exécutez la commande suivante dans un terminal ou cmd de l’environnement DSH pour réparer, puis cliquez sur Réessayer (node-pty reste à la même version que le cœur DSH) :',
terminalDepsProfile: ' (profil détecté : {profile})',
terminalShellNotFound: 'Shell configuré introuvable : {name} — vérifiez le chemin du shell dans Réglages → Side card → Terminal',
preview: 'Aperçu',
toc: 'Sommaire',
edit: 'Modifier',
@@ -143,6 +137,12 @@ export const fr: Record<string, string> = {
changesOpenDiffTab: 'Ouvrir dans un onglet diff',
changesClosePreview: 'Fermer l\'aperçu',
changesResizePreview: 'Redimensionner l\'aperçu',
changesDiffOpenTitle: 'Le diff s\'ouvre en',
changesDiffOpenDesc: 'Où l\'action « Ouvrir dans un onglet diff » place le diff',
changesDiffOpenFloat: 'Fenêtre flottante',
changesDiffOpenFloatDesc: 'Une fenêtre flottante centrée sur la fenêtre — déplacer, redimensionner, garder au premier plan',
changesDiffOpenPane: 'Panneau',
changesDiffOpenPaneDesc: 'Ancré sous le panneau source (division de diff style VSCode)',
changesLoadError: 'Les enregistrements de fichiers de la session sont indisponibles',
staged: 'En scène',
unstaged: 'Hors scène',
@@ -191,13 +191,6 @@ export const fr: Record<string, string> = {
addToConversation: 'Ajouter à la discussion',
copyRelative: 'Copier le chemin relatif',
copyAbsolute: 'Copier le chemin absolu',
rename: 'Renommer',
renameInvalid: 'Le nom ne peut pas être vide ni contenir de séparateurs de chemin.',
delete: 'Supprimer',
deleteTitle: 'Supprimer « {name} » ?',
deleteDescFile: 'Ce fichier sera définitivement supprimé. Cette action est irréversible.',
deleteDescDir: 'Ce répertoire et tout son contenu seront définitivement supprimés. Cette action est irréversible.',
dismiss: 'Fermer',
download: 'Télécharger',
uploadFiles: 'Importer des fichiers',
uploadFolder: 'Importer un dossier',
@@ -216,6 +209,13 @@ export const fr: Record<string, string> = {
settingsIntro: 'Gérer le contenu affiché par la carte latérale et son comportement par défaut',
settingsPopupDesc: 'Configurer les options liées à « {feature} »',
settingsDone: 'Terminé',
settingsOpenTitle: 'Ouvert par défaut pour les nouvelles sessions',
settingsOpenDesc: 'Déployer automatiquement la carte latérale lors de la création d’une session ; les sessions existantes conservent chacune leur disposition',
settingsWidthTitle: 'Part de largeur par défaut',
settingsWidthDesc: 'Pourcentage de la largeur de la fenêtre occupé par la carte latérale pour les nouvelles sessions (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Ouvrir les fichiers de discussion dans la barre latérale',
settingsOpenPathDesc: 'À la place de l’application par défaut du système, ouvrir dans l’éditeur de la barre latérale les liens vers les fichiers du chat (lignes d’outils, listes de produits, mentions de fichiers)',
settingsOpenToolsTitle: 'Injecter l\'outil d\'ouverture latérale pour le modèle',
settingsOpenToolsDesc: 'Une fois activé, le modèle peut ouvrir des fichiers, dossiers et pages HTTP(S) dans la barre latérale via l\'outil sidebar_open (désactivé par défaut)',
settingsTitleBarTitle: 'Mode de compatibilité de position',
@@ -388,7 +388,7 @@ export const fr: Record<string, string> = {
addPluginsViewerCardDesc: 'Enregistrer un aperçu d’un type de fichier',
addPluginsTabDesc: 'Les pages de la barre latérale (onglets) peuvent être étendues par des plugins. Les plugins s’enregistrent via le service ctx.betterSidebar ; cliquez sur « Installer » pour copier la commande d’installation, collez-la dans un terminal de l’environnement DSH et exécutez-la.',
addPluginsViewerDesc: 'Les aperçus de fichiers peuvent être étendus par des plugins. Les plugins s’enregistrent via le service ctx.betterSidebar ; cliquez sur « Installer » pour copier la commande d’installation, collez-la dans un terminal de l’environnement DSH et exécutez-la.',
addPluginsBrowseMore: 'Parcourir plus de plugins sur GitHub (topic : dsh-better-sidebar)',
addPluginsBrowseMore: 'Parcourir plus de plugins sur GitHub (topic : dsh-apes-sidebar)',
addPluginsSearch: 'Rechercher par nom / description de plugin…',
addPluginsNoMatch: 'Aucun plugin correspondant',
addPluginsRecommended: 'Plugins recommandés',
@@ -401,7 +401,7 @@ export const fr: Record<string, string> = {
pluginGithubWorkbenchDesc: 'Onglet « GitHub Workbench » de better-sidebar : arborescence du dépôt distant + Issues / Pull requests / Actions avec prise en charge complète en écriture — création d’issue/PR, commentaire, édition, fermeture/réouverture, merge squash·merge·rebase (confirmation renforcée), relance/annulation de la CI ; le sélecteur de dépôt liste automatiquement les dépôts accessibles et recherche les dépôts publics ; sans better-sidebar, il bascule sur un panneau latéral droit autonome',
pluginMdExportDesc: 'Ajoute un bouton « Exporter » à la barre d’outils Markdown de better-sidebar : rend en un clic le .md courant en HTML autonome (tableaux/blocs de code/diagrammes Mermaid en ligne, mise en page fidèle au thème d’aperçu) et l’enregistre à côté du .md, ou exporte en PDF via la boîte de dialogue d’impression',
pluginCodeNavDesc: 'Navigation de prévisualisation de code : détecte le langage selon le type de fichier et colore la syntaxe, plan des symboles (filtres classes/méthodes/variables + saut en un clic) et recherche dans le fichier (toutes les correspondances surlignées, précédent/suivant, respect de la casse) — prend en charge l’aperçu des fichiers de code dans l’éditeur better-sidebar',
pluginSuhuangScrollDesc: 'Connecte le Runtime Suhuang Scroll local aux réglages DSH et à better-sidebar pour configurer le modèle, tester la connexion et contrôler la correction continue ; nécessite Suhuang Scroll Runtime et dsh-better-sidebar',
pluginSuhuangScrollDesc: 'Connecte le Runtime Suhuang Scroll local aux réglages DSH et à better-sidebar pour configurer le modèle, tester la connexion et contrôler la correction continue ; nécessite Suhuang Scroll Runtime et dsh-apes-sidebar',
pluginBetterOverleafDesc: 'Onglet Overleaf pour better-sidebar : connexion directe via navigateur CDP (Chromium tiers pris en charge), liste/sélection de projets, miroirs git locaux sous <workspace>/overleaf/, synchronisation git bidirectionnelle avec repli API en lecture seule, et aperçu des fichiers via l’atelier de la barre latérale',
pluginGitRemotesDesc: 'Onglet Git Remotes de better-sidebar : branches/amonts/ahead-behind, fetch (avec prune possible), pull en ff-only, push seulement après confirmation. Ne remplace pas la mise en scène/les commits du Git intégré et ne fournit pas de force-push ni de push automatique par le modèle',
pluginSentinelDesc: 'Système de réveil d’agent piloté par conditions : capteurs fichier/processus/port/HTTP/commande/webhook, les sessions en veille se réveillent automatiquement lorsque les conditions sont remplies ; enregistre l’onglet « Sentinel » affichant le tableau de surveillance global du serveur',
@@ -414,29 +414,4 @@ export const fr: Record<string, string> = {
pluginDocsPanelDesc: '« Documentation globale » dans la barre latérale DSH : notes Markdown globales, lisibles depuis n’importe quel espace de travail — sélection dans la liste, saut via le plan flottant, ouverture externe Chrome / VS Code, copie de code, répertoire configurable (défaut ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Le navigateur d’agent pour DeepSeek Harness : 32 outils ego_* pilotent un vrai Chromium ; un onglet natif « ego browser » dans la barre latérale montre en direct chaque page visitée par l’agent — cliquez, glissez et tapez pour reprendre la main. Enregistre l’onglet automatiquement si better-sidebar est présent, sinon une bulle flottante',
pluginBilingualReaderDesc: 'Lire des PDF dans la barre latérale DSH : affichage PDF natif, sélectionnez du texte pour le traduire avec le LLM, avec contexte et totalement isolé de la conversation principale',
changesRedactOn: "Masquage actif : les secrets s'affichent en [REDACTED] ; cliquez pour désactiver (à vos risques)",
changesRedactOff: "Masquage désactivé : le contenu s'affiche tel quel ; cliquez pour activer",
changesRedactOnLabel: "Masquage",
changesRedactOffLabel: "Sans masquage",
changesRedactBanner: "Masqué",
pluginSentinelName: 'dsh-sentinel Système de réveil',
pluginEgoBrowserName: 'ego-browser Navigateur d’agents',
pluginBetterOverleafName: 'dsh-better-overleaf Onglet Overleaf',
pluginDocsPanelName: 'dsh-docs-panel Docs globales',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Identifiants Git',
pluginGitRemotesName: 'dsh-git-remotes Dépôts distants Git',
pluginGithubWorkbenchName: 'dsh-github-workbench Atelier GitHub',
pluginSidebarQaName: 'dsh-sidebar-qa Sélectionner & demander',
pluginSidenoteName: 'dsh-sidenote Chat latéral',
pluginServerDeckName: 'dsh-server-deck Tableau de bord serveurs',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel Tunnel SSH',
pluginTurnReviewName: 'dsh-turn-review Revue du tour',
pluginBilingualReaderName: 'dsh-bilingual-reader Lecteur bilingue',
pluginOfficeName: 'Aperçu Office',
pluginMdExportName: 'Exportation Markdown',
pluginCodeNavName: 'Navigateur d’aperçu de code',
pluginVideoPreviewName: 'Aperçu vidéo',
presetDshDesktopDesc: 'Mode avancé Electron (sans bordure) : macOS réserve 20px en haut ; Windows réserve 32px pour la barre de titre sans WCO',
}
+18 -43
View File
@@ -19,19 +19,11 @@ export const hi: Record<string, string> = {
files: 'फ़ाइलें',
changesSessionEmpty: 'इस सत्र में अभी कोई फ़ाइल ऑपरेशन नहीं',
changesRead: 'पढ़ना',
changesMdReading: 'पठन',
changesHtmlRender: "रेंडर",
changesHtmlRaw: "मूल",
changesPdfRender: "रेंडर",
changesPdfRaw: "मूल",
changesMdRaw: 'मूल पाठ',
changesWrite: 'लिखना',
changesEdit: 'संपादन',
changesRunning: 'चल रहा है',
changesError: 'त्रुटि',
changesFold: '{count} पंक्तियाँ… विस्तार के लिए क्लिक करें',
changesFoldLoading: 'विस्तार हो रहा है…',
changesFoldUnavailable: 'संदर्भ अनुपलब्ध',
changesContext: 'संदर्भ',
changesPriorUnknown: 'बदलाव से पहले की सामग्री विंडो में नहीं है; पूरी नई जोड़ी गई दिखाई जाती है',
explorer: 'एक्सप्लोरर',
@@ -81,6 +73,7 @@ export const hi: Record<string, string> = {
closeOtherTabs: 'अन्य टैब बंद करें',
closeLeftTabs: 'बाएँ टैब बंद करें',
closeRightTabs: 'दाएँ टैब बंद करें',
moveToFreeWindow: 'फ़्री विंडो में ले जाएँ',
floatDropHint: 'फ़्री विंडो में खोलने के लिए छोड़ें',
dockToSidebar: 'साइडबार पर वापस जाएँ',
pinTerminal: 'टर्मिनल पिन करें',
@@ -95,6 +88,8 @@ export const hi: Record<string, string> = {
pinnedTerminalScopeGlobal: 'वैश्विक रूप से पिन किया गया',
pinnedRailLabel: 'पिन किए गए टर्मिनल',
closePinnedTerminal: 'टर्मिनल बंद करें',
collapse: 'साइडबार संक्षिप्त करें',
expand: 'साइडबार विस्तृत करें',
collapseBottomPanel: 'निचला पैनल संक्षिप्त करें',
expandBottomPanel: 'निचला पैनल विस्तृत करें',
terminalError: 'टर्मिनल कनेक्शन विफल',
@@ -103,7 +98,6 @@ export const hi: Record<string, string> = {
terminalDepsFailed: 'टर्मिनल निर्भरता node-pty लोड विफल',
terminalDepsHint: 'DSH मशीन पर एक टर्मिनल या cmd में नीचे दिया गया कमांड चलाएँ, फिर पुनः प्रयास करें (node-pty DSH कोर संस्करण के साथ सिंक रहता है):',
terminalDepsProfile: ' (पहचाना गया प्रोफ़ाइल: {profile})',
terminalShellNotFound: 'कॉन्फ़िगर किया गया shell नहीं मिला: {name} — Settings → Side card → Terminal में shell पथ जाँचें',
preview: 'पूर्वावलोकन',
toc: 'विषय-सूची',
edit: 'संपादित करें',
@@ -150,6 +144,12 @@ export const hi: Record<string, string> = {
changesOpenDiffTab: 'अलग diff टैब में खोलें',
changesClosePreview: 'पूर्वावलोकन बंद करें',
changesResizePreview: 'पूर्वावलोकन का आकार बदलें',
changesDiffOpenTitle: 'diff खुलता है',
changesDiffOpenDesc: '«अलग diff टैब में खोलें» क्रिया diff को कहाँ रखती है',
changesDiffOpenFloat: 'मुक्त विंडो',
changesDiffOpenFloatDesc: 'व्यूपोर्ट के केंद्र में एक तैरती विंडो — खींचें, आकार बदलें, ऊपर रखें',
changesDiffOpenPane: 'पैनल',
changesDiffOpenPaneDesc: 'सोर्स पैनल के नीचे डॉक किया (VSCode-शैली diff स्प्लिट)',
changesLoadError: 'सेशन फ़ाइल रिकॉर्ड अभी अनुपलब्ध हैं',
staged: 'स्टेज किया',
unstaged: 'अनस्टेज',
@@ -198,13 +198,6 @@ export const hi: Record<string, string> = {
addToConversation: 'वार्तालाप में जोड़ें',
copyRelative: 'सापेक्ष पथ कॉपी',
copyAbsolute: 'निरपेक्ष पथ कॉपी',
rename: 'नाम बदलें',
renameInvalid: 'नाम खाली नहीं हो सकता और उसमें पथ विभाजक नहीं हो सकते।',
delete: 'हटाएँ',
deleteTitle: '“{name}” हटाएँ?',
deleteDescFile: 'यह फ़ाइल स्थायी रूप से हटा दी जाएगी। इसे पूर्ववत नहीं किया जा सकता।',
deleteDescDir: 'यह निर्देशिका और उसकी सारी सामग्री स्थायी रूप से हटा दी जाएगी। इसे पूर्ववत नहीं किया जा सकता।',
dismiss: 'बंद करें',
download: 'डाउनलोड',
uploadFiles: 'फ़ाइलें अपलोड करें',
uploadFolder: 'फ़ोल्डर अपलोड करें',
@@ -223,6 +216,13 @@ export const hi: Record<string, string> = {
settingsIntro: 'साइड कार्ड क्या दिखाता है और कैसे व्यवहार करता है प्रबंधित करें',
settingsPopupDesc: '{feature} के लिए संबंधित विकल्प कॉन्फ़िगर करें',
settingsDone: 'पूर्ण',
settingsOpenTitle: 'नए वार्तालाप के लिए डिफ़ॉल्ट रूप से खोलें',
settingsOpenDesc: 'बिल्कुल नए वार्तालाप के लिए साइड कार्ड स्वतः विस्तृत करें; मौजूदा वार्तालाप अपना लेआउट रखते हैं',
settingsWidthTitle: 'डिफ़ॉल्ट चौड़ाई हिस्सा',
settingsWidthDesc: 'नए वार्तालाप के लिए साइड कार्ड की विंडो चौड़ाई का डिफ़ॉल्ट हिस्सा (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'चैट फ़ाइलें साइडबार में खोलें',
settingsOpenPathDesc: 'चैट में फ़ाइल लिंक (टूल पंक्ति, उत्पादित फ़ाइलें, उल्लेख) क्लिक करने पर सिस्टम डिफ़ॉल्ट ऐप के बजाय साइडबार एडिटर में खोलें',
settingsOpenToolsTitle: 'मॉडल के लिए साइडबार ओपन टूल इंजेक्ट करें',
settingsOpenToolsDesc: 'चालू होने पर, मॉडल sidebar_open टूल से साइडबार में फ़ाइलें, फ़ोल्डर और HTTP(S) पेज खोल सकता है (डिफ़ॉल्ट रूप से बंद)',
settingsTitleBarTitle: 'स्थिति संगतता मोड',
@@ -395,7 +395,7 @@ export const hi: Record<string, string> = {
addPluginsViewerCardDesc: 'फ़ाइल-प्रकार पूर्वावलोकन पंजीकृत करें',
addPluginsTabDesc: 'साइडबार पृष्ठ (टैब) प्लगइन्स द्वारा विस्तारित हो सकते हैं। प्लगइन्स ctx.betterSidebar सेवा के माध्यम से पंजीकृत होते हैं; इंस्टॉल क्लिक करने पर इंस्टॉल कमांड कॉपी होता है — इसे उस टर्मिनल में पेस्ट करें जहाँ आपका DSH प्रोफ़ाइल है और चलाएँ।',
addPluginsViewerDesc: 'फ़ाइल पूर्वावलोकक प्लगइन्स द्वारा विस्तारित हो सकते हैं। प्लगइन्स ctx.betterSidebar सेवा के माध्यम से पंजीकृत होते हैं; इंस्टॉल क्लिक करने पर इंस्टॉल कमांड कॉपी होता है — इसे उस टर्मिनल में पेस्ट करें जहाँ आपका DSH प्रोफ़ाइल है और चलाएँ।',
addPluginsBrowseMore: 'GitHub पर और प्लगइन्स देखें (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'GitHub पर और प्लगइन्स देखें (topic: dsh-apes-sidebar)',
addPluginsSearch: 'प्लगइन नाम या विवरण से खोजें…',
addPluginsNoMatch: 'कोई प्लगइन मेल नहीं खाता',
addPluginsRecommended: 'अनुशंसित प्लगइन',
@@ -408,7 +408,7 @@ export const hi: Record<string, string> = {
pluginGithubWorkbenchDesc: 'GitHub Workbench टैब: रिमोट रिपॉज़िटरी ट्री + Issues / Pull requests / Actions, पढ़ने के साथ-साथ पूर्ण लेखन समर्थन — Issue/PR बनाना, टिप्पणी करना, संपादन, बंद करना/पुनः खोलना, squash·merge·rebase मर्ज (पुष्टि अनिवार्य), CI पुनः चलाना/रद्द करना; रिपॉज़िटरी स्विचर स्वतः सुलभ रिपॉज़िटरी सूचीबद्ध करता है और सार्वजनिक रिपॉज़िटरी खोजता है; better-sidebar न होने पर स्वतंत्र दाईं ओर पैनल में फ़ॉलबैक होता है',
pluginMdExportDesc: 'better-sidebar के Markdown टूलबार में "एक्सपोर्ट" बटन जोड़ता है: एक क्लिक में वर्तमान .md को स्टैंडअलोन HTML (टेबल/कोड ब्लॉक/Mermaid आरेख इनलाइन, लेआउट प्रीव्यू थीम का अनुसरण करता है) में रेंडर करके उसी फ़ोल्डर में सहेजता है, या प्रिंट डायलॉग से PDF निर्यात करता है',
pluginCodeNavDesc: 'कोड प्रीव्यू नेविगेशन: फ़ाइल प्रकार से भाषा पहचानकर सिंटैक्स हाइलाइट, सिंबल आउटलाइन (क्लास/मेथड/वेरिएबल फ़िल्टर + एक-क्लिक जंप), फ़ाइल में खोज (सभी मैच हाइलाइट, पिछला/अगला, केस मैच) — better-sidebar एडिटर में कोड फ़ाइल प्रीव्यू संभालता है',
pluginSuhuangScrollDesc: 'स्थानीय Suhuang Scroll Runtime को DSH सेटिंग्स और better-sidebar से जोड़ता है, ताकि मॉडल कॉन्फ़िगरेशन, कनेक्शन परीक्षण और निरंतर ग्रेडिंग नियंत्रण उपलब्ध हों; Suhuang Scroll Runtime और dsh-better-sidebar आवश्यक हैं',
pluginSuhuangScrollDesc: 'स्थानीय Suhuang Scroll Runtime को DSH सेटिंग्स और better-sidebar से जोड़ता है, ताकि मॉडल कॉन्फ़िगरेशन, कनेक्शन परीक्षण और निरंतर ग्रेडिंग नियंत्रण उपलब्ध हों; Suhuang Scroll Runtime और dsh-apes-sidebar आवश्यक हैं',
pluginBetterOverleafDesc: 'better-sidebar के लिए Overleaf टैब: सीधे CDP ब्राउज़र से लॉगिन (तीसरे पक्ष का Chromium समर्थित), प्रोजेक्ट सूची/स्विच, <workspace>/overleaf/ में स्थानीय git मिरर, रीड-ओनली API फ़ॉलबैक के साथ द्विदिश git सिंक, और फ़ाइल प्रीव्यू साइडबार वर्कबेंच से',
pluginGitRemotesDesc: 'Git Remotes टैब: ब्रांच/अपस्ट्रीम/ahead-behind, fetch (वैकल्पिक prune), ff-only pull, और केवल टैब-में पुष्टि के बाद push। अंतर्निहित Git स्टेज/कमिट टैब को प्रतिस्थापित नहीं करता, और force-push या मॉडल ऑटो-push टूल नहीं देता',
pluginSentinelDesc: 'स्थिति-संचालित एजेंट जागरूकता: फ़ाइल/प्रक्रिया/पोर्ट/http/कमांड/webhook सेंसर स्थितियाँ पूरी होने पर निष्क्रिय सत्र जगाते हैं; सर्वर-व्यापी वॉच टेबल के साथ "Sentinel" टैब पंजीकृत करता है',
@@ -421,29 +421,4 @@ export const hi: Record<string, string> = {
pluginDocsPanelDesc: 'DSH साइडबार में ग्लोबल डॉक्स: किसी भी वर्कस्पेस से अपने Markdown नोट्स पढ़ें — फ़ाइल सूची, रूपरेखा, Chrome / VS Code में खोलें, और कॉपी बटन; docs डायरेक्टरी कॉन्फ़िगर करने योग्य (डिफ़ॉल्ट ~/.dsh/docs)',
pluginEgoBrowserDesc: 'DeepSeek Harness के लिए एजेंट ब्राउज़र: 32 ego_* टूल असली Chromium चलाते हैं; साइडबार में नेटिव «ego browser» टैब एजेंट के हर विज़िट किए गए पेज को लाइव दिखाता है — आप क्लिक, ड्रैग और टाइप करके नियंत्रण ले सकते हैं। better-sidebar मौजूद होने पर टैब स्वतः रजिस्टर होता है, अन्यथा फ्लोटिंग बबल के रूप में दिखता है',
pluginBilingualReaderDesc: 'DSH साइडबार में PDF पढ़ें: मूल PDF प्रदर्शन, टेक्स्ट चुनकर LLM से अनुवाद करें, संदर्भ के साथ और मुख्य संवाद से पूरी तरह अलग',
changesRedactOn: "रिडैक्शन चालू: संवेदनशील मान [REDACTED] दिखेंगे; बंद करने के लिए क्लिक करें (अपने जोखिम पर)",
changesRedactOff: "रिडैक्शन बंद: सामग्री जैसी है वैसी दिखेगी; चालू करने के लिए क्लिक करें",
changesRedactOnLabel: "रिडैक्शन",
changesRedactOffLabel: "बिना रिडैक्शन",
changesRedactBanner: "रिडैक्टेड",
pluginSentinelName: 'dsh-sentinel वेक-अप सिस्टम',
pluginEgoBrowserName: 'ego-browser एजेंट ब्राउज़र',
pluginBetterOverleafName: 'dsh-better-overleaf Overleaf टैब',
pluginDocsPanelName: 'dsh-docs-panel ग्लोबल डॉक्स',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Git क्रेडेंशियल',
pluginGitRemotesName: 'dsh-git-remotes Git रिमोट',
pluginGithubWorkbenchName: 'dsh-github-workbench GitHub वर्कबेंच',
pluginSidebarQaName: 'dsh-sidebar-qa चुनें और पूछें',
pluginSidenoteName: 'dsh-sidenote साइड चैट',
pluginServerDeckName: 'dsh-server-deck सर्वर डेक',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel SSH टनल',
pluginTurnReviewName: 'dsh-turn-review टर्न समीक्षा',
pluginBilingualReaderName: 'dsh-bilingual-reader द्विभाषी रीडर',
pluginOfficeName: 'Office प्रीव्यू',
pluginMdExportName: 'Markdown एक्सपोर्ट',
pluginCodeNavName: 'कोड प्रीव्यू नेविगेटर',
pluginVideoPreviewName: 'वीडियो प्रीव्यू',
presetDshDesktopDesc: 'Electron एडवांस्ड (फ्रेमलेस) मोड: macOS ऊपर 20px सुरक्षित रखता है; Windows WCO अनुपलब्ध होने पर टाइटल बार के लिए 32px रखता है',
}
+18 -43
View File
@@ -17,19 +17,11 @@ export const id: Record<string, string> = {
files: 'Berkas',
changesSessionEmpty: 'Belum ada operasi file di sesi ini',
changesRead: 'Baca',
changesMdReading: 'Baca',
changesHtmlRender: "Render",
changesHtmlRaw: "Sumber",
changesPdfRender: "Render",
changesPdfRaw: "Sumber",
changesMdRaw: 'Mentah',
changesWrite: 'Tulis',
changesEdit: 'Edit',
changesRunning: 'berjalan',
changesError: 'error',
changesFold: '{count} baris… klik untuk meluaskan',
changesFoldLoading: 'Meluaskan…',
changesFoldUnavailable: 'Konteks tidak tersedia',
changesContext: 'konteks',
changesPriorUnknown: 'Konten sebelum perubahan berada di luar jendela; ditampilkan sebagai sepenuhnya baru',
explorer: 'Explorer',
@@ -79,6 +71,7 @@ export const id: Record<string, string> = {
closeOtherTabs: 'Tutup Tab Lain',
closeLeftTabs: 'Tutup Tab di Kiri',
closeRightTabs: 'Tutup Tab di Kanan',
moveToFreeWindow: 'Pindahkan ke jendela bebas',
floatDropHint: 'Lepaskan untuk membuka di jendela bebas',
dockToSidebar: 'Kembali ke bilah samping',
pinTerminal: 'Sematkan Terminal',
@@ -93,6 +86,8 @@ export const id: Record<string, string> = {
pinnedTerminalScopeGlobal: 'Disematkan global',
pinnedRailLabel: 'Terminal Tersemat',
closePinnedTerminal: 'Tutup Terminal',
collapse: 'Lipat sidebar',
expand: 'Bentangkan sidebar',
collapseBottomPanel: 'Lipat panel bawah',
expandBottomPanel: 'Bentangkan panel bawah',
terminalError: 'Koneksi terminal gagal',
@@ -101,7 +96,6 @@ export const id: Record<string, string> = {
terminalDepsFailed: 'Dependensi terminal node-pty gagal dimuat',
terminalDepsHint: 'Jalankan perintah di bawah ini di terminal atau cmd pada mesin DSH untuk memperbaikinya, lalu coba lagi (node-pty tetap sinkron dengan versi inti DSH):',
terminalDepsProfile: ' (profil terdeteksi: {profile})',
terminalShellNotFound: 'Shell yang dikonfigurasi tidak ditemukan: {name} — periksa path shell di Pengaturan → Side card → Terminal',
preview: 'Pratinjau',
toc: 'Daftar Isi',
edit: 'Edit',
@@ -148,6 +142,12 @@ export const id: Record<string, string> = {
changesOpenDiffTab: 'Buka di tab diff terpisah',
changesClosePreview: 'Tutup pratinjau',
changesResizePreview: 'Ubah ukuran pratinjau',
changesDiffOpenTitle: 'Diff terbuka sebagai',
changesDiffOpenDesc: 'Di mana aksi "buka di tab diff terpisah" menempatkan diff',
changesDiffOpenFloat: 'Jendela bebas',
changesDiffOpenFloatDesc: 'Jendela mengambang di tengah viewport — geser, ubah ukuran, selalu di atas',
changesDiffOpenPane: 'Panel',
changesDiffOpenPaneDesc: 'Ditambat di bawah panel sumber (split diff gaya VSCode)',
changesLoadError: 'Catatan berkas sesi sedang tidak tersedia',
staged: 'Staged',
unstaged: 'Unstaged',
@@ -196,13 +196,6 @@ export const id: Record<string, string> = {
addToConversation: 'Tambahkan ke obrolan',
copyRelative: 'Salin path relatif',
copyAbsolute: 'Salin path absolut',
rename: 'Ganti nama',
renameInvalid: 'Nama tidak boleh kosong atau mengandung pemisah jalur.',
delete: 'Hapus',
deleteTitle: 'Hapus "{name}"?',
deleteDescFile: 'Ini menghapus file secara permanen. Tidak dapat dibatalkan.',
deleteDescDir: 'Ini menghapus direktori dan seluruh isinya secara permanen. Tidak dapat dibatalkan.',
dismiss: 'Tutup',
download: 'Unduh',
uploadFiles: 'Unggah berkas',
uploadFolder: 'Unggah folder',
@@ -221,6 +214,13 @@ export const id: Record<string, string> = {
settingsIntro: 'Kelola apa yang ditampilkan kartu samping dan perilakunya',
settingsPopupDesc: 'Konfigurasikan opsi terkait untuk {feature}',
settingsDone: 'Selesai',
settingsOpenTitle: 'Buka secara default untuk obrolan baru',
settingsOpenDesc: 'Bentangkan kartu samping secara otomatis untuk obrolan baru; obrolan yang sudah ada mempertahankan tata letaknya',
settingsWidthTitle: 'Porsi lebar default',
settingsWidthDesc: 'Porsi lebar jendela default kartu samping untuk obrolan baru (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Buka berkas obrolan di sidebar',
settingsOpenPathDesc: 'Buka tautan berkas di obrolan (baris alat, berkas yang dihasilkan, mention) di editor sidebar alih-alih aplikasi default sistem',
settingsOpenToolsTitle: 'Suntik alat buka sidebar untuk model',
settingsOpenToolsDesc: 'Saat diaktifkan, model dapat membuka file, folder, dan halaman HTTP(S) di sidebar melalui alat sidebar_open (nonaktif secara default)',
settingsTitleBarTitle: 'Mode kompatibilitas posisi',
@@ -393,7 +393,7 @@ export const id: Record<string, string> = {
addPluginsViewerCardDesc: 'Daftarkan pratinjau tipe berkas',
addPluginsTabDesc: 'Halaman sidebar (tab) dapat diperluas oleh plugin. Plugin mendaftar melalui layanan ctx.betterSidebar; klik Install menyalin perintah instalasi — tempel ke terminal tempat profil DSH Anda berada dan jalankan.',
addPluginsViewerDesc: 'Pratinjau berkas dapat diperluas oleh plugin. Plugin mendaftar melalui layanan ctx.betterSidebar; klik Install menyalin perintah instalasi — tempel ke terminal tempat profil DSH Anda berada dan jalankan.',
addPluginsBrowseMore: 'Jelajahi lebih banyak plugin di GitHub (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'Jelajahi lebih banyak plugin di GitHub (topic: dsh-apes-sidebar)',
addPluginsSearch: 'Cari berdasarkan nama / deskripsi plugin…',
addPluginsNoMatch: 'Tidak ada plugin yang cocok',
addPluginsRecommended: 'Plugin yang direkomendasikan',
@@ -406,7 +406,7 @@ export const id: Record<string, string> = {
pluginGithubWorkbenchDesc: 'Tab GitHub Workbench: pohon repo remote + Issues / Pull requests / Actions dengan dukungan tulis penuh — buat Issue/PR, komentar, edit, tutup/buka ulang, merge squash·merge·rebase (konfirmasi kuat), jalankan ulang/batalkan CI; pengalih repo otomatis mencantumkan repo yang dapat diakses dan mencari repo publik; tanpa better-sidebar otomatis beralih ke panel kanan mandiri',
pluginMdExportDesc: 'Menambahkan tombol "Ekspor" ke bilah alat Markdown di better-sidebar: merender .md saat ini menjadi HTML mandiri dalam satu klik (tabel/blok kode/diagram Mermaid inline, tata letak mengikuti tema pratinjau) dan menyimpannya di folder yang sama, atau mengekspor ke PDF lewat dialog cetak',
pluginCodeNavDesc: 'Navigasi pratinjau kode: mengenali bahasa dari jenis berkas dan menyorot sintaks, kerangka simbol (filter kelas/metode/variabel + lompat sekali klik), serta pencarian dalam berkas (sorot semua kecocokan, sebelum/berikutnya, peka huruf besar-kecil) — mengambil alih pratinjau berkas kode di editor better-sidebar',
pluginSuhuangScrollDesc: 'Menghubungkan Suhuang Scroll Runtime lokal ke pengaturan DSH dan better-sidebar untuk konfigurasi model, uji koneksi, dan kontrol penilaian berkelanjutan; memerlukan Suhuang Scroll Runtime dan dsh-better-sidebar',
pluginSuhuangScrollDesc: 'Menghubungkan Suhuang Scroll Runtime lokal ke pengaturan DSH dan better-sidebar untuk konfigurasi model, uji koneksi, dan kontrol penilaian berkelanjutan; memerlukan Suhuang Scroll Runtime dan dsh-apes-sidebar',
pluginBetterOverleafDesc: 'Tab Overleaf untuk better-sidebar: login langsung lewat peramban CDP (mendukung Chromium pihak ketiga), daftar/pergantian proyek, mirror git lokal di <workspace>/overleaf/, sinkronisasi git dua arah dengan fallback API baca-saja, dan pratinjau berkas melalui workbench sidebar',
pluginGitRemotesDesc: 'Tab Git Remotes: branch/upstream/ahead-behind, fetch (prune opsional), ff-only pull, dan push hanya setelah konfirmasi dalam tab. Tidak menggantikan tab stage/commit Git bawaan, dan tidak menawarkan force-push atau alat auto-push model',
pluginSentinelDesc: 'Sistem bangun agen berbasis kondisi: sensor berkas/proses/port/http/perintah/webhook membangunkan sesi yang tidur saat kondisi terpicu; mendaftarkan tab "Sentinel" dengan tabel pengawasan server-wide',
@@ -419,29 +419,4 @@ export const id: Record<string, string> = {
pluginDocsPanelDesc: 'Dokumen global di sidebar DSH: baca catatan Markdown Anda sendiri dari ruang kerja apa pun — daftar berkas, kerangka, buka di Chrome / VS Code, dan tombol salin; direktori docs dapat dikonfigurasi (default ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Browser agen untuk DeepSeek Harness: 32 alat ego_* menggerakkan Chromium sungguhan; tab «ego browser» native di bilah samping menampilkan langsung setiap halaman yang dikunjungi agen — Anda bisa mengklik, menyeret, dan mengetik untuk mengambil alih. Tab terdaftar otomatis jika better-sidebar terpasang; jika tidak, jatuh ke gelembung mengambang',
pluginBilingualReaderDesc: 'Baca PDF di sidebar DSH: tampilan PDF asli, pilih teks untuk diterjemahkan dengan LLM, dengan konteks dan sepenuhnya terisolasi dari percakapan utama',
changesRedactOn: "Redaksi aktif: rahasia tampil sebagai [REDACTED]; klik untuk mematikan (risiko Anda sendiri)",
changesRedactOff: "Redaksi mati: konten tampil apa adanya; klik untuk mengaktifkan",
changesRedactOnLabel: "Redaksi",
changesRedactOffLabel: "Tanpa redaksi",
changesRedactBanner: "Teredaksi",
pluginSentinelName: 'dsh-sentinel Sistem bangun',
pluginEgoBrowserName: 'ego-browser Peramban agen',
pluginBetterOverleafName: 'dsh-better-overleaf Tab Overleaf',
pluginDocsPanelName: 'dsh-docs-panel Dokumen global',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Kredensial Git',
pluginGitRemotesName: 'dsh-git-remotes Remote Git',
pluginGithubWorkbenchName: 'dsh-github-workbench Meja kerja GitHub',
pluginSidebarQaName: 'dsh-sidebar-qa Pilih & tanya',
pluginSidenoteName: 'dsh-sidenote Obrolan samping',
pluginServerDeckName: 'dsh-server-deck Dek server',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel Terowongan SSH',
pluginTurnReviewName: 'dsh-turn-review Tinjauan giliran',
pluginBilingualReaderName: 'dsh-bilingual-reader Pembaca dwibahasa',
pluginOfficeName: 'Pratinjau Office',
pluginMdExportName: 'Ekspor Markdown',
pluginCodeNavName: 'Navigator pratinjau kode',
pluginVideoPreviewName: 'Pratinjau video',
presetDshDesktopDesc: 'Mode Electron lanjutan (tanpa bingkai): macOS mencadangkan 20px di atas; Windows mencadangkan 32px untuk bilah judul saat WCO tidak tersedia',
}
+19 -44
View File
@@ -3,26 +3,18 @@
* consumed by the better-locale override store (see `locales.ts` for the
* zh/en/ja lookup chain and the `attachBetterLocale` contract).
*
* @module dsh-better-sidebar/client/locales-it
* @module dsh-apes-sidebar/client/locales-it
*/
export const it: Record<string, string> = {
files: 'File',
changesSessionEmpty: 'Ancora nessuna operazione sui file in questa sessione',
changesRead: 'Lettura',
changesMdReading: 'Lettura',
changesHtmlRender: "Render",
changesHtmlRaw: "Sorgente",
changesPdfRender: "Render",
changesPdfRaw: "Sorgente",
changesMdRaw: 'Grezzo',
changesWrite: 'Scrittura',
changesEdit: 'Modifica',
changesRunning: 'in esecuzione',
changesError: 'errore',
changesFold: '{count} righe… clicca per espandere',
changesFoldLoading: 'Espansione…',
changesFoldUnavailable: 'Contesto non disponibile',
changesContext: 'contesto',
changesPriorUnknown: 'Il contenuto precedente è fuori dalla finestra; mostrato come interamente aggiunto',
explorer: 'Esplora risorse',
@@ -72,6 +64,7 @@ export const it: Record<string, string> = {
closeOtherTabs: 'Chiudi le altre schede',
closeLeftTabs: 'Chiudi le schede a sinistra',
closeRightTabs: 'Chiudi le schede a destra',
moveToFreeWindow: 'Sposta in finestra libera',
floatDropHint: 'Rilascia per aprire in una finestra libera',
dockToSidebar: 'Torna alla barra laterale',
pinTerminal: 'Blocca Terminale',
@@ -86,6 +79,8 @@ export const it: Record<string, string> = {
pinnedTerminalScopeGlobal: 'Bloccato globalmente',
pinnedRailLabel: 'Terminali Bloccati',
closePinnedTerminal: 'Chiudi Terminale',
collapse: 'Comprimi barra laterale',
expand: 'Espandi barra laterale',
collapseBottomPanel: 'Comprimi pannello inferiore',
expandBottomPanel: 'Espandi pannello inferiore',
terminalError: 'Connessione del terminale non riuscita',
@@ -94,7 +89,6 @@ export const it: Record<string, string> = {
terminalDepsFailed: 'Caricamento della dipendenza del terminale node-pty non riuscito',
terminalDepsHint: 'Esegua il comando seguente in un terminale o cmd sul computer DSH per ripristinarlo, poi riprovi (node-pty resta sincronizzato con la versione del core DSH):',
terminalDepsProfile: ' (rilevato profilo: {profile})',
terminalShellNotFound: 'Shell configurata non trovata: {name} — verifica il percorso della shell in Impostazioni → Side card → Terminal',
preview: 'Anteprima',
toc: 'Indice',
edit: 'Modifica',
@@ -141,6 +135,12 @@ export const it: Record<string, string> = {
changesOpenDiffTab: 'Apri in una scheda diff',
changesClosePreview: 'Ridimensiona l\'anteprima',
changesResizePreview: 'Ridimensiona anteprima',
changesDiffOpenTitle: 'Il diff si apre come',
changesDiffOpenDesc: 'Dove l\'azione "apri in una scheda diff" colloca il diff',
changesDiffOpenFloat: 'Finestra libera',
changesDiffOpenFloatDesc: 'Una finestra flottante centrata sul viewport — trascina, ridimensiona, sempre in primo piano',
changesDiffOpenPane: 'Pannello',
changesDiffOpenPaneDesc: 'Ancorato sotto il pannello sorgente (divisione diff in stile VSCode)',
changesLoadError: 'I record dei file della sessione non sono disponibili',
staged: 'In stage',
unstaged: 'Non in stage',
@@ -189,13 +189,6 @@ export const it: Record<string, string> = {
addToConversation: 'Aggiungi alla conversazione',
copyRelative: 'Copia percorso relativo',
copyAbsolute: 'Copia percorso assoluto',
rename: 'Rinomina',
renameInvalid: 'Il nome non può essere vuoto o contenere separatori di percorso.',
delete: 'Elimina',
deleteTitle: 'Eliminare "{name}"?',
deleteDescFile: 'Il file verrà eliminato definitivamente. Questa azione non può essere annullata.',
deleteDescDir: 'La directory e tutto il suo contenuto verranno eliminati definitivamente. Questa azione non può essere annullata.',
dismiss: 'Chiudi',
download: 'Scarica',
uploadFiles: 'Carica file',
uploadFolder: 'Carica cartella',
@@ -214,6 +207,13 @@ export const it: Record<string, string> = {
settingsIntro: 'Gestisca cosa mostra la scheda laterale e come si comporta',
settingsPopupDesc: 'Configuri le opzioni relative a {feature}',
settingsDone: 'Fatto',
settingsOpenTitle: 'Apri per impostazione predefinita per le nuove conversazioni',
settingsOpenDesc: 'Espande automaticamente la scheda laterale per le conversazioni nuove; le conversazioni esistenti mantengono il proprio layout',
settingsWidthTitle: 'Quota di larghezza predefinita',
settingsWidthDesc: 'La quota predefinita della scheda laterale sulla larghezza della finestra per le nuove conversazioni (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Apri i file della chat nella barra laterale',
settingsOpenPathDesc: 'Apre i collegamenti ai file nella chat (righe di strumenti, file prodotti, menzioni) nell’editor della barra laterale invece dell’app predefinita di sistema',
settingsOpenToolsTitle: 'Iniettare lo strumento di apertura della barra laterale per il modello',
settingsOpenToolsDesc: 'Se attivato, il modello può aprire file, cartelle e pagine HTTP(S) nella barra laterale tramite lo strumento sidebar_open (disattivato per impostazione predefinita)',
settingsTitleBarTitle: 'Modalità di compatibilità della posizione',
@@ -386,7 +386,7 @@ export const it: Record<string, string> = {
addPluginsViewerCardDesc: 'Registri un’anteprima di tipo di file',
addPluginsTabDesc: 'Le pagine della barra laterale (schede) possono essere estese dai plugin. I plugin si registrano tramite il servizio ctx.betterSidebar; cliccando Installa viene copiato il comando di installazione — lo incolli in un terminale dove risiede il profilo DSH e lo esegua.',
addPluginsViewerDesc: 'I visualizzatori di file possono essere estesi dai plugin. I plugin si registrano tramite il servizio ctx.betterSidebar; cliccando Installa viene copiato il comando di installazione — lo incolli in un terminale dove risiede il profilo DSH e lo esegua.',
addPluginsBrowseMore: 'Sfogli altri plugin su GitHub (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'Sfogli altri plugin su GitHub (topic: dsh-apes-sidebar)',
addPluginsSearch: 'Cerca per nome / descrizione del plugin…',
addPluginsNoMatch: 'Nessun plugin corrispondente',
addPluginsRecommended: 'Plugin consigliati',
@@ -399,7 +399,7 @@ export const it: Record<string, string> = {
pluginGithubWorkbenchDesc: 'Scheda GitHub Workbench: albero del repository remoto + Issues / Pull requests / Actions con pieno supporto in scrittura — creazione di issue/PR, commenti, modifica, chiusura/riapertura, merge squash·merge·rebase (conferma rafforzata), riavvio/annullamento della CI; il selettore dei repository elenca automaticamente quelli accessibili e cerca tra quelli pubblici; senza better-sidebar esegue il fallback a un pannello laterale destro indipendente',
pluginMdExportDesc: 'Aggiunge un pulsante «Esporta» alla barra degli strumenti Markdown di better-sidebar: con un clic esegue il rendering del .md corrente in HTML autonomo (tabelle/blocchi di codice/diagrammi Mermaid in linea, layout fedele al tema di anteprima) e lo salva accanto al .md, oppure esporta in PDF tramite la finestra di stampa',
pluginCodeNavDesc: 'Navigazione dell’anteprima del codice: rileva il linguaggio dal tipo di file ed evidenzia la sintassi, struttura dei simboli (filtri classi/metodi/variabili + salto con un clic) e ricerca nel file (tutte le corrispondenze evidenziate, precedente/successiva, maiuscole/minuscole) — gestisce l’anteprima dei file di codice nell’editor better-sidebar',
pluginSuhuangScrollDesc: 'Collega il Runtime Suhuang Scroll locale alle impostazioni DSH e a better-sidebar per configurare il modello, testare la connessione e controllare la correzione continua; richiede Suhuang Scroll Runtime e dsh-better-sidebar',
pluginSuhuangScrollDesc: 'Collega il Runtime Suhuang Scroll locale alle impostazioni DSH e a better-sidebar per configurare il modello, testare la connessione e controllare la correzione continua; richiede Suhuang Scroll Runtime e dsh-apes-sidebar',
pluginBetterOverleafDesc: 'Tab Overleaf per better-sidebar: accesso diretto tramite browser CDP (supporta Chromium di terze parti), elenco/cambio progetti, mirror git locali in <workspace>/overleaf/, sincronizzazione git bidirezionale con fallback API di sola lettura e anteprima dei file tramite la workbench della barra laterale',
pluginGitRemotesDesc: 'Scheda Git Remotes: rami/upstream/ahead-behind, fetch (prune opzionale), pull ff-only e push solo dopo una conferma nella scheda. Non sostituisce la scheda stage/commit integrata di Git, e non offre force-push o uno strumento di push automatico del modello',
pluginSentinelDesc: 'Sistema di risveglio dell’agente guidato da condizioni: sensori file/processo/porta/HTTP/comando/webhook risvegliano le sessioni dormienti quando le condizioni si verificano; registra una scheda «Sentinella» con la tabella di monitoraggio globale del server',
@@ -412,29 +412,4 @@ export const it: Record<string, string> = {
pluginDocsPanelDesc: 'Documenti globali nella barra laterale di DSH: legga le sue note Markdown da qualsiasi spazio di lavoro — una lista di file, una struttura, apertura in Chrome / VS Code e pulsanti di copia; la directory dei documenti è configurabile (predefinita ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Il browser agente per DeepSeek Harness: 32 strumenti ego_* pilotano un vero Chromium; una scheda nativa «ego browser» nella barra laterale mostra in tempo reale ogni pagina visitata dall’agente — puoi cliccare, trascinare e digitare per prendere il controllo. Registra la scheda automaticamente se better-sidebar è presente, altrimenti una bolla flottante',
pluginBilingualReaderDesc: 'Leggi PDF nella barra laterale DSH: visualizzazione PDF nativa, seleziona il testo per tradurlo con l’LLM, con contesto e completamente isolato dalla conversazione principale',
changesRedactOn: "Mascheramento attivo: i segreti appaiono come [REDACTED]; clicca per disattivare (a tuo rischio)",
changesRedactOff: "Mascheramento disattivo: il contenuto appare invariato; clicca per attivare",
changesRedactOnLabel: "Mascheramento",
changesRedactOffLabel: "Senza mascheramento",
changesRedactBanner: "Mascherato",
pluginSentinelName: 'dsh-sentinel Sistema di riattivazione',
pluginEgoBrowserName: 'ego-browser Browser dell’agente',
pluginBetterOverleafName: 'dsh-better-overleaf Scheda Overleaf',
pluginDocsPanelName: 'dsh-docs-panel Documenti globali',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Credenziali Git',
pluginGitRemotesName: 'dsh-git-remotes Remote Git',
pluginGithubWorkbenchName: 'dsh-github-workbench Banco di lavoro GitHub',
pluginSidebarQaName: 'dsh-sidebar-qa Seleziona e chiedi',
pluginSidenoteName: 'dsh-sidenote Chat laterale',
pluginServerDeckName: 'dsh-server-deck Ponte server',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel Tunnel SSH',
pluginTurnReviewName: 'dsh-turn-review Revisione del turno',
pluginBilingualReaderName: 'dsh-bilingual-reader Lettore bilingue',
pluginOfficeName: 'Anteprima Office',
pluginMdExportName: 'Esportazione Markdown',
pluginCodeNavName: 'Navigatore anteprima codice',
pluginVideoPreviewName: 'Anteprima video',
presetDshDesktopDesc: 'Modalità avanzata Electron (senza bordi): macOS riserva 20px in alto; Windows riserva 32px per la barra del titolo quando WCO non è disponibile',
}
+18 -43
View File
@@ -19,19 +19,11 @@ export const ja: Record<string, string> = {
files: 'ファイル',
changesSessionEmpty: 'このセッションにはファイル操作がまだありません',
changesRead: '読み取り',
changesMdReading: '閲覧',
changesHtmlRender: "レンダリング",
changesHtmlRaw: "原文",
changesPdfRender: "レンダリング",
changesPdfRaw: "原文",
changesMdRaw: '原文',
changesWrite: '書き込み',
changesEdit: '編集',
changesRunning: '実行中',
changesError: 'エラー',
changesFold: '{count} 行…クリックで展開',
changesFoldLoading: '展開中…',
changesFoldUnavailable: 'コンテキスト未読み込み',
changesContext: 'コンテキスト',
changesPriorUnknown: '変更前の内容がウィンドウ内になく、すべて新規追加として表示されます',
explorer: 'エクスプローラー',
@@ -81,6 +73,7 @@ export const ja: Record<string, string> = {
closeOtherTabs: '他のタブを閉じる',
closeLeftTabs: '左のタブを閉じる',
closeRightTabs: '右のタブを閉じる',
moveToFreeWindow: 'フリーウィンドウへ移動',
floatDropHint: '離すとフリーウィンドウで開きます',
dockToSidebar: 'サイドバーへ戻す',
pinTerminal: 'ターミナルを固定',
@@ -95,6 +88,8 @@ export const ja: Record<string, string> = {
pinnedTerminalScopeGlobal: 'グローバルに固定',
pinnedRailLabel: '固定されたターミナル',
closePinnedTerminal: 'ターミナルを閉じる',
collapse: 'サイドバーを折りたたむ',
expand: 'サイドバーを展開',
collapseBottomPanel: '下パネルを折りたたむ',
expandBottomPanel: '下パネルを展開',
terminalError: 'ターミナル接続に失敗',
@@ -103,7 +98,6 @@ export const ja: Record<string, string> = {
terminalDepsFailed: 'ターミナル依存関係 node-pty の読み込みに失敗',
terminalDepsHint: 'DSH 環境のターミナルまたは cmd で以下のコマンドを実行して修復し、再試行してください(node-pty は DSH コアと同じバージョンを維持):',
terminalDepsProfile: '(検出された profile:{profile})',
terminalShellNotFound: '設定されたシェルが見つかりません:{name}(設定 → サイドカード → ターミナル のシェルパスを確認してください)',
preview: 'プレビュー',
toc: '目次',
edit: '編集',
@@ -150,6 +144,12 @@ export const ja: Record<string, string> = {
changesOpenDiffTab: '独立タブで開く',
changesClosePreview: 'プレビューを閉じる',
changesResizePreview: 'プレビューの高さを変更',
changesDiffOpenTitle: 'diff の展開先',
changesDiffOpenDesc: '「独立タブで開く」操作が diff をどこに配置するか',
changesDiffOpenFloat: 'フリーウィンドウ',
changesDiffOpenFloatDesc: 'ビューポート中央に浮かぶウィンドウ — ドラッグ・リサイズ・最前面固定',
changesDiffOpenPane: 'ペイン',
changesDiffOpenPaneDesc: 'ソースペインの下にドッキング(VSCode 式 diff 分割)',
changesLoadError: 'セッションのファイル記録は現在利用できません',
staged: 'ステージ済み',
unstaged: '未ステージ',
@@ -198,13 +198,6 @@ export const ja: Record<string, string> = {
addToConversation: '会話に追加',
copyRelative: '相対パスをコピー',
copyAbsolute: '絶対パスをコピー',
rename: '名前の変更',
renameInvalid: '名前は空にできず、パス区切り文字を含められません。',
delete: '削除',
deleteTitle: '「{name}」を削除しますか?',
deleteDescFile: 'このファイルを完全に削除します。元に戻せません。',
deleteDescDir: 'このディレクトリと内容物をすべて完全に削除します。元に戻せません。',
dismiss: '閉じる',
download: 'ダウンロード',
uploadFiles: 'ファイルをアップロード',
uploadFolder: 'フォルダーをアップロード',
@@ -223,6 +216,13 @@ export const ja: Record<string, string> = {
settingsIntro: 'サイドカードの表示内容とデフォルト挙動を管理',
settingsPopupDesc: '「{feature}」の関連オプションを設定',
settingsDone: '完了',
settingsOpenTitle: '新規会話でデフォルト展開',
settingsOpenDesc: '新規会話時にサイドカードを自動展開。既存の会話はそれぞれのレイアウトを維持',
settingsWidthTitle: 'デフォルト幅の比率',
settingsWidthDesc: '新規会話時のサイドカードがウィンドウ幅に占める割合 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'チャット内ファイルをサイドバーで開く',
settingsOpenPathDesc: 'チャット内のファイルリンク(ツール行、産物リスト、ファイル言及)クリック時に、システム既定アプリではなくサイドバーエディターで開く',
settingsOpenToolsTitle: 'モデルにサイドバー開くツールを注入',
settingsOpenToolsDesc: 'オンにすると、モデルは sidebar_open ツールでサイドバーにファイル・フォルダー・HTTP(S) ページを開ける(デフォルトオフ)',
settingsTitleBarTitle: '位置互換モード',
@@ -395,7 +395,7 @@ export const ja: Record<string, string> = {
addPluginsViewerCardDesc: '新しいファイル種別プレビューを登録',
addPluginsTabDesc: 'サイドバーページ(Tab)はプラグインで拡張可能。プラグインは ctx.betterSidebar サービス経由で登録。「インストール」をクリックしてインストールコマンドをコピー、DSH 環境のターミナルに貼り付けて実行。',
addPluginsViewerDesc: 'ファイルプレビューアーはプラグインで拡張可能。プラグインは ctx.betterSidebar サービス経由で登録。「インストール」をクリックしてインストールコマンドをコピー、DSH 環境のターミナルに貼り付けて実行。',
addPluginsBrowseMore: 'GitHub でプラグинをさらに閲覧(topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'GitHub でプラグинをさらに閲覧(topic: dsh-apes-sidebar)',
addPluginsSearch: 'プラグイン名 / 説明で検索…',
addPluginsNoMatch: '一致するプラグインなし',
addPluginsRecommended: '推奨プラグイン',
@@ -407,7 +407,7 @@ export const ja: Record<string, string> = {
pluginFlowglassDesc: 'ライブセッションフローグラフ:ユーザー、アシスタント、ツール呼び出しの 3 レーン泳道、並列グループ、サブエージェント支線、ドリルダウン、ライブ状態をサポート。better-sidebar インストール時にネイティブ「Flowglass」Tab を登録、未インストール時は独立ドロワーを保持',
pluginGitForgeDesc: 'better-sidebar「Git Forge」Tab:GitHub/Gitea 等 Forge アカウントライブラリ + プロジェクト単位の認可 + push ポリシー硬核拒絶。token はローカル secrets のみ、モデルコンテキストに入らない。読み取り専用 GitForge ツールと agent HTTPS credential helper を提供',
pluginGithubWorkbenchDesc: 'better-sidebar「GitHub ワークベンチ」タブ:リモートリポジトリのツリー + Issues / Pull requests / Actions タブ。読み取りに加え、Issue・PR の作成、コメント、編集、クローズ/再オープン、squash・merge・rebase マージ(強確認)、CI の再実行/キャンセルに対応。リポジトリ切替ポップアップはアクセス可能なリポジトリを自動取得し、公開リポジトリの検索にも対応。better-sidebar 未導入時は独立右パネルへ自動フォールバック',
pluginSuhuangScrollDesc: 'ローカルの Suhuang Scroll Runtime を DSH 設定と better-sidebar に接続し、モデル設定、接続テスト、連続採点の操作を提供。Suhuang Scroll Runtime と dsh-better-sidebar が必要',
pluginSuhuangScrollDesc: 'ローカルの Suhuang Scroll Runtime を DSH 設定と better-sidebar に接続し、モデル設定、接続テスト、連続採点の操作を提供。Suhuang Scroll Runtime と dsh-apes-sidebar が必要',
pluginBetterOverleafDesc: 'better-sidebar の Overleaf タブ:CDP ブラウザーに直接接続してログイン(サードパーティー Chromium 対応)、プロジェクト一覧/切替、<workspace>/overleaf/ のローカル git ミラー、双方向 git 同期(API 読み取り専用フォールバック)、ファイルプレビューはサイドバーのワークベンチを利用',
pluginGitRemotesDesc: 'better-sidebar Git リモート Tab:ブランチ/アップストリーム/ahead-behind 表示、fetch(prune 可)、ff-only pull、確認後のみ push。内蔵 Git のステージ/コミットは置き換えず、force-push やモデル自動 push ツールは提供しない',
pluginSentinelDesc: '条件駆動の agent 起動システム:ファイル/プロセス/ポート/HTTP/コマンド/webhook センサーが条件達成時に休眠会話を自動起動。「Sentinel」Tab を登録しサーバー全体の監視テーブルを表示',
@@ -421,29 +421,4 @@ export const ja: Record<string, string> = {
pluginDocsPanelDesc: 'DSH サイドバー内の「グローバルドキュメント」:任意のワークスペースから読めるグローバル Markdown ノート。リストで選んで閲覧、ホバーでアウトラインジャンプ、Chrome / VS Code で外部オープン、コードコピー。ディレクトリは設定可(デフォルト ~/.dsh/docs)',
pluginEgoBrowserDesc: 'DeepSeek Harness 用エージェントブラウザ:32 個の ego_* ツールが本物の Chromium を操作し、サイドバーのネイティブ「ego ブラウザ」Tab がエージェントの訪れる全ページをライブ表示。クリック・ドラッグ・入力で操作を引き継げます。better-sidebar があれば Tab を自動登録、なければフローティングバブルにフォールバック',
pluginBilingualReaderDesc: 'DSH サイドバーで論文 PDF を読む:ネイティブ PDF 表示、テキストを選択して LLM で翻訳。コンテキストを使い、メイン会話から完全に分離',
changesRedactOn: "マスキング有効:機密値は [REDACTED] で表示。クリックで無効化(自己責任)",
changesRedactOff: "マスキング無効:内容がそのまま表示されます。クリックで有効化",
changesRedactOnLabel: "マスキング中",
changesRedactOffLabel: "マスキング解除",
changesRedactBanner: "マスク済み",
pluginSentinelName: 'dsh-sentinel ウェイクアップシステム',
pluginEgoBrowserName: 'ego-browser エージェントブラウザー',
pluginBetterOverleafName: 'dsh-better-overleaf Overleaf タブ',
pluginDocsPanelName: 'dsh-docs-panel グローバルドキュメント',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Git 認証情報',
pluginGitRemotesName: 'dsh-git-remotes Git リモート',
pluginGithubWorkbenchName: 'dsh-github-workbench GitHub ワークベンチ',
pluginSidebarQaName: 'dsh-sidebar-qa 選択して質問',
pluginSidenoteName: 'dsh-sidenote サイドチャット',
pluginServerDeckName: 'dsh-server-deck サーバーデッキ',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel SSH トンネル',
pluginTurnReviewName: 'dsh-turn-review ターンレビュー',
pluginBilingualReaderName: 'dsh-bilingual-reader バイリンガルリーダー',
pluginOfficeName: 'Office プレビュー',
pluginMdExportName: 'Markdown エクスポート',
pluginCodeNavName: 'コードプレビューナビゲーター',
pluginVideoPreviewName: '動画プレビュー',
presetDshDesktopDesc: 'Electron 高度モード(枠なし):macOS は上部 20px、Windows は WCO 未提供時に 32px のタイトルバー分を確保',
}
+18 -43
View File
@@ -11,19 +11,11 @@ export const ko: Record<string, string> = {
files: '파일',
changesSessionEmpty: '이 세션에 아직 파일 작업이 없습니다',
changesRead: '읽기',
changesMdReading: '읽기',
changesHtmlRender: "렌더링",
changesHtmlRaw: "원문",
changesPdfRender: "렌더링",
changesPdfRaw: "원문",
changesMdRaw: '원문',
changesWrite: '쓰기',
changesEdit: '편집',
changesRunning: '실행 중',
changesError: '오류',
changesFold: '{count}줄… 클릭하여 펼치기',
changesFoldLoading: '펼치는 중…',
changesFoldUnavailable: '컨텍스트를 불러올 수 없음',
changesContext: '컨텍스트',
changesPriorUnknown: '변경 전 내용이 창에 없어 모두 새로 추가된 것으로 표시됩니다',
explorer: '탐색기',
@@ -73,6 +65,7 @@ export const ko: Record<string, string> = {
closeOtherTabs: '다른 탭 닫기',
closeLeftTabs: '왼쪽 탭 닫기',
closeRightTabs: '오른쪽 탭 닫기',
moveToFreeWindow: '자유 창으로 이동',
floatDropHint: '놓으면 자유 창에서 열립니다',
dockToSidebar: '사이드바로 돌아가기',
pinTerminal: '터미널 고정',
@@ -87,6 +80,8 @@ export const ko: Record<string, string> = {
pinnedTerminalScopeGlobal: '전역에 고정됨',
pinnedRailLabel: '고정된 터미널',
closePinnedTerminal: '터미널 닫기',
collapse: '사이드바 접기',
expand: '사이드바 펼치기',
collapseBottomPanel: '하단 패널 접기',
expandBottomPanel: '하단 패널 펼치기',
terminalError: '터미널 연결 실패',
@@ -95,7 +90,6 @@ export const ko: Record<string, string> = {
terminalDepsFailed: '터미널 의존성 node-pty 로드 실패',
terminalDepsHint: 'DSH가 설치된 환경의 터미널 또는 cmd에서 아래 명령을 실행하여 복구한 후 다시 시도를 클릭하세요(node-pty는 DSH 코어와 동일한 버전을 유지합니다):',
terminalDepsProfile: ' (profile 감지됨: {profile})',
terminalShellNotFound: '설정된 셸을 찾을 수 없습니다: {name} — 설정 → 사이드 카드 → 터미널에서 셸 경로를 확인하세요',
preview: '미리보기',
toc: '목차',
edit: '편집',
@@ -142,6 +136,12 @@ export const ko: Record<string, string> = {
changesOpenDiffTab: '별도 탭에서 열기',
changesClosePreview: '미리보기 닫기',
changesResizePreview: '미리보기 높이 조정',
changesDiffOpenTitle: 'diff 열기 위치',
changesDiffOpenDesc: '「별도 탭에서 열기」 작업이 diff를 배치하는 곳',
changesDiffOpenFloat: '자유 창',
changesDiffOpenFloatDesc: '뷰포트 중앙의 떠 있는 창 — 드래그, 크기 조절, 항상 위',
changesDiffOpenPane: '패널',
changesDiffOpenPaneDesc: '소스 패널 아래에 도킹(VSCode 스타일 diff 분할)',
changesLoadError: '세션 파일 기록을 지금 사용할 수 없습니다',
staged: '스테이징됨',
unstaged: '스테이징 안 됨',
@@ -190,13 +190,6 @@ export const ko: Record<string, string> = {
addToConversation: '대화에 추가',
copyRelative: '상대 경로 복사',
copyAbsolute: '절대 경로 복사',
rename: '이름 바꾸기',
renameInvalid: '이름은 비워 둘 수 없으며 경로 구분 기호를 포함할 수 없습니다.',
delete: '삭제',
deleteTitle: '"{name}"을(를) 삭제할까요?',
deleteDescFile: '이 파일이 영구적으로 삭제됩니다. 되돌릴 수 없습니다.',
deleteDescDir: '이 디렉터리와 모든 내용이 영구적으로 삭제됩니다. 되돌릴 수 없습니다.',
dismiss: '닫기',
download: '다운로드',
uploadFiles: '파일 업로드',
uploadFolder: '폴더 업로드',
@@ -215,6 +208,13 @@ export const ko: Record<string, string> = {
settingsIntro: '사이드 카드의 표시 내용과 기본 동작을 관리합니다',
settingsPopupDesc: '"{feature}" 관련 옵션을 구성합니다',
settingsDone: '완료',
settingsOpenTitle: '새 대화 기본 열림',
settingsOpenDesc: '새 대화를 만들 때 사이드 카드를 자동으로 펼칩니다. 기존 대화는 각자의 레이아웃을 유지합니다',
settingsWidthTitle: '기본 너비 비율',
settingsWidthDesc: '새 대화에서 사이드 카드가 창 너비에서 차지하는 비율(%) (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '채팅 영역 파일을 사이드바에서 열기',
settingsOpenPathDesc: '채팅에서 파일 링크(도구 행, 산출물 목록, 파일 언급)를 클릭하면 사이드바 편집기에서 열고, 시스템 기본 앱은 호출하지 않습니다',
settingsOpenToolsTitle: '모델에 사이드바 열기 도구 주입',
settingsOpenToolsDesc: '켜면 모델이 sidebar_open 도구로 사이드바에서 파일·폴더·HTTP(S) 페이지를 열 수 있습니다(기본 꺼짐)',
settingsTitleBarTitle: '위치 호환 모드',
@@ -387,7 +387,7 @@ export const ko: Record<string, string> = {
addPluginsViewerCardDesc: '새 파일 형식 미리보기 등록',
addPluginsTabDesc: '사이드바 페이지(Tab)는 플러그인으로 확장할 수 있습니다. 플러그인은 ctx.betterSidebar 서비스를 통해 등록합니다. "설치"를 클릭하면 설치 명령이 복사되며, DSH가 설치된 환경의 터미널에 붙여넣어 실행하세요.',
addPluginsViewerDesc: '파일 미리보기는 플러그인으로 확장할 수 있습니다. 플러그인은 ctx.betterSidebar 서비스를 통해 등록합니다. "설치"를 클릭하면 설치 명령이 복사되며, DSH가 설치된 환경의 터미널에 붙여넣어 실행하세요.',
addPluginsBrowseMore: 'GitHub에서 더 많은 플러그인 보기(topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'GitHub에서 더 많은 플러그인 보기(topic: dsh-apes-sidebar)',
addPluginsSearch: '플러그인 이름 / 설명 검색…',
addPluginsNoMatch: '일치하는 플러그인 없음',
addPluginsRecommended: '추천 플러그인',
@@ -400,7 +400,7 @@ export const ko: Record<string, string> = {
pluginGithubWorkbenchDesc: 'better-sidebar "GitHub Workbench" Tab: 원격 저장소 트리 + Issues / Pull requests / Actions 탭. 읽기 외에 Issue·PR 생성, 댓글, 편집, 닫기/다시 열기, squash·merge·rebase 병합(강한 확인), CI 재실행/취소를 지원합니다. 저장소 전환 팝업은 접근 가능한 저장소를 자동으로 가져오고 공개 저장소 검색도 지원합니다. better-sidebar 미설치 시 독립 우측 패널로 자동 폴백합니다',
pluginMdExportDesc: 'better-sidebar의 Markdown 툴바에 "내보내기" 버튼을 추가: 클릭 한 번으로 현재 .md를 독립 실행형 HTML(표/코드 블록/Mermaid 다이어그램 인라인, 레이아웃은 미리보기 테마 따름)로 렌더링해 같은 폴더에 저장하거나, 인쇄 대화상자로 PDF를 내보냅니다',
pluginCodeNavDesc: '코드 미리보기 내비게이션: 파일 형식으로 언어를 자동 인식해 구문 강조, 심볼 아웃라인(클래스/메서드/변수 필터 + 원클릭 이동), 파일 내 검색(모든 일치 강조, 이전/다음, 대소문자 구분) — better-sidebar 편집기의 코드 파일 미리보기를 대신 맡습니다',
pluginSuhuangScrollDesc: '로컬 Suhuang Scroll Runtime을 DSH 설정과 better-sidebar에 연결하여 모델 구성, 연결 테스트, 연속 채점 제어를 제공합니다. Suhuang Scroll Runtime과 dsh-better-sidebar가 필요합니다',
pluginSuhuangScrollDesc: '로컬 Suhuang Scroll Runtime을 DSH 설정과 better-sidebar에 연결하여 모델 구성, 연결 테스트, 연속 채점 제어를 제공합니다. Suhuang Scroll Runtime과 dsh-apes-sidebar가 필요합니다',
pluginBetterOverleafDesc: 'better-sidebar의 Overleaf 탭: CDP 브라우저 직접 로그인(서드파티 Chromium 지원), 프로젝트 목록/전환, <workspace>/overleaf/ 로컬 git 미러, 읽기 전용 API 폴백이 있는 양방향 git 동기화, 파일 미리보기는 사이드바 워크벤치 사용',
pluginGitRemotesDesc: 'better-sidebar Git 원격 Tab: 브랜치/업스트림/ahead-behind 확인, fetch(prune 가능), ff-only pull, 확인 후에만 push. 내장 Git의 스테이징/커밋을 대체하지 않으며, force-push나 모델 자동 push도 제공하지 않습니다',
pluginSentinelDesc: '조건 기반 agent 웨이크업 시스템: 파일/프로세스/포트/HTTP/명령/webhook 센서가 조건을 충족하면 휴면 세션을 자동으로 깨웁니다. "센티널(哨兵)" Tab을 등록하여 서버 전역 모니터링 표를 보여줍니다',
@@ -413,29 +413,4 @@ export const ko: Record<string, string> = {
pluginDocsPanelDesc: 'DSH 사이드바의 "전역 문서": 전역 Markdown 메모로, 어떤 작업 공간에서든 언제든 읽을 수 있습니다. 목록을 클릭해 읽기, 마우스를 올리면 개요로 이동, Chrome / VS Code에서 외부 열기, 코드 복사. 디렉터리를 설정할 수 있습니다(기본 ~/.dsh/docs)',
pluginEgoBrowserDesc: 'DeepSeek Harness용 에이전트 브라우저: 32개의 ego_* 도구가 실제 Chromium을 구동하며, 사이드바의 네이티브 «ego 브라우저» 탭에서 에이전트가 방문하는 모든 페이지를 실시간으로 볼 수 있습니다 — 클릭, 드래그, 타이핑으로 직접 조작할 수 있습니다. better-sidebar가 있으면 탭을 자동 등록하고, 없으면 플로팅 버블로 대체합니다',
pluginBilingualReaderDesc: 'DSH 사이드바에서 PDF 읽기: 네이티브 PDF 표시, 텍스트를 선택해 LLM으로 번역, 컨텍스트 사용 및 메인 대화와 완전 격리',
changesRedactOn: "마스킹 켜짐: 민감 값이 [REDACTED]로 표시됩니다. 클릭하면 꺼집니다(책임 본인)",
changesRedactOff: "마스킹 꺼짐: 내용이 그대로 표시됩니다. 클릭하면 켜집니다",
changesRedactOnLabel: "마스킹 중",
changesRedactOffLabel: "마스킹 해제",
changesRedactBanner: "마스킹됨",
pluginSentinelName: 'dsh-sentinel 웨이크업 시스템',
pluginEgoBrowserName: 'ego-browser 에이전트 브라우저',
pluginBetterOverleafName: 'dsh-better-overleaf Overleaf 탭',
pluginDocsPanelName: 'dsh-docs-panel 전역 문서',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Git 자격 증명',
pluginGitRemotesName: 'dsh-git-remotes Git 리모트',
pluginGithubWorkbenchName: 'dsh-github-workbench GitHub 워크벤치',
pluginSidebarQaName: 'dsh-sidebar-qa 선택 후 질문',
pluginSidenoteName: 'dsh-sidenote 사이드 채팅',
pluginServerDeckName: 'dsh-server-deck 서버 데크',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel SSH 터널',
pluginTurnReviewName: 'dsh-turn-review 턴 리뷰',
pluginBilingualReaderName: 'dsh-bilingual-reader 이중언어 리더',
pluginOfficeName: 'Office 미리보기',
pluginMdExportName: 'Markdown 내보내기',
pluginCodeNavName: '코드 미리보기 내비게이터',
pluginVideoPreviewName: '동영상 미리보기',
presetDshDesktopDesc: 'Electron 고급 모드(테두리 없음): macOS는 상단에 20px, Windows는 WCO가 없을 때 타이틀 바에 32px를 확보',
}
+18 -43
View File
@@ -17,19 +17,11 @@ export const nl: Record<string, string> = {
files: 'Bestanden',
changesSessionEmpty: 'Nog geen bestandsbewerkingen in deze sessie',
changesRead: 'Lezen',
changesMdReading: 'Lezen',
changesHtmlRender: "Renderen",
changesHtmlRaw: "Bron",
changesPdfRender: "Renderen",
changesPdfRaw: "Bron",
changesMdRaw: 'Brontekst',
changesWrite: 'Schrijven',
changesEdit: 'Bewerken',
changesRunning: 'actief',
changesError: 'fout',
changesFold: '{count} regels… klik om uit te klappen',
changesFoldLoading: 'Uitklappen…',
changesFoldUnavailable: 'Context niet beschikbaar',
changesContext: 'context',
changesPriorUnknown: 'Eerdere inhoud valt buiten het venster; getoond als volledig nieuw',
explorer: 'Verkenner',
@@ -79,6 +71,7 @@ export const nl: Record<string, string> = {
closeOtherTabs: 'Andere tabbladen sluiten',
closeLeftTabs: 'Tabbladen links sluiten',
closeRightTabs: 'Tabbladen rechts sluiten',
moveToFreeWindow: 'Naar vrij venster verplaatsen',
floatDropHint: 'Loslaten om in een vrij venster te openen',
dockToSidebar: 'Terug naar de zijbalk',
pinTerminal: 'Terminal vastmaken',
@@ -93,6 +86,8 @@ export const nl: Record<string, string> = {
pinnedTerminalScopeGlobal: 'Wereldwijd vastgemaakt',
pinnedRailLabel: 'Vastgemaakte terminals',
closePinnedTerminal: 'Terminal sluiten',
collapse: 'Zijbalk inklappen',
expand: 'Zijbalk uitklappen',
collapseBottomPanel: 'Onderpaneel inklappen',
expandBottomPanel: 'Onderpaneel uitklappen',
terminalError: 'Terminalverbinding mislukt',
@@ -101,7 +96,6 @@ export const nl: Record<string, string> = {
terminalDepsFailed: 'Terminalafhankelijkheid node-pty kon niet worden geladen',
terminalDepsHint: 'Voer het onderstaande commando uit in een terminal of cmd op de DSH-machine om het te herstellen en probeer het opnieuw (node-pty blijft in sync met de DSH-coreversie):',
terminalDepsProfile: ' (gedetecteerde profile: {profile})',
terminalShellNotFound: 'Geconfigureerde shell niet gevonden: {name} — controleer het shell-pad onder Instellingen → Side card → Terminal',
preview: 'Voorbeeld',
toc: 'Inhoudsopgave',
edit: 'Bewerken',
@@ -148,6 +142,12 @@ export const nl: Record<string, string> = {
changesOpenDiffTab: 'In eigen diff-tabblad openen',
changesClosePreview: 'Voorbeeld sluiten',
changesResizePreview: 'Voorbeeld vergroten/verkleinen',
changesDiffOpenTitle: 'Diff opent als',
changesDiffOpenDesc: 'Waar de actie „in eigen diff-tabblad openen” de diff plaatst',
changesDiffOpenFloat: 'Vrij venster',
changesDiffOpenFloatDesc: 'Een zwevend venster in het midden — slepen, schalen, bovenop houden',
changesDiffOpenPane: 'Paneel',
changesDiffOpenPaneDesc: 'Vastgemaakt onder het bronpaneel (VSCode-achtige diff-split)',
changesLoadError: 'Sessiebestandsregistraties zijn nu niet beschikbaar',
staged: 'Gestaged',
unstaged: 'Niet gestaged',
@@ -196,13 +196,6 @@ export const nl: Record<string, string> = {
addToConversation: 'Aan conversatie toevoegen',
copyRelative: 'Relatief pad kopiëren',
copyAbsolute: 'Absoluut pad kopiëren',
rename: 'Naam wijzigen',
renameInvalid: 'De naam mag niet leeg zijn of padscheidingstekens bevatten.',
delete: 'Verwijderen',
deleteTitle: '"{name}" verwijderen?',
deleteDescFile: 'Dit verwijdert het bestand permanent. Dit kan niet ongedaan worden gemaakt.',
deleteDescDir: 'Dit verwijdert de map en alles erin permanent. Dit kan niet ongedaan worden gemaakt.',
dismiss: 'Sluiten',
download: 'Downloaden',
uploadFiles: 'Bestanden uploaden',
uploadFolder: 'Map uploaden',
@@ -221,6 +214,13 @@ export const nl: Record<string, string> = {
settingsIntro: 'Beheer wat de zijkaart toont en hoe deze zich gedraagt',
settingsPopupDesc: 'Gerelateerde opties voor {feature} configureren',
settingsDone: 'Klaar',
settingsOpenTitle: 'Standaard openen voor nieuwe conversaties',
settingsOpenDesc: 'De zijkaart automatisch uitklappen voor gloednieuwe conversaties; bestaande conversaties behouden hun eigen layout',
settingsWidthTitle: 'Standaard breedte-aandeel',
settingsWidthDesc: 'Het standaard breedte-aandeel van de zijkaart voor nieuwe conversaties (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Chatbestanden in de zijbalk openen',
settingsOpenPathDesc: 'Bestandslinks in de chat (toolrijen, geproduceerde bestanden, vermeldingen) openen in de zijbalk-editor in plaats van de standaard systeemapp',
settingsOpenToolsTitle: 'Zijbalk-openen-tool voor het model injecteren',
settingsOpenToolsDesc: 'Indien ingeschakeld kan het model bestanden, mappen en HTTP(S)-pagina\'s in de zijbalk openen via de sidebar_open-tool (standaard uit)',
settingsTitleBarTitle: 'Positiecompatibiliteitsmodus',
@@ -393,7 +393,7 @@ export const nl: Record<string, string> = {
addPluginsViewerCardDesc: 'Een bestandstype-preview registreren',
addPluginsTabDesc: 'Zijbalkpagina\'s (tabbladen) kunnen door plugins worden uitgebreid. Plugins registreren via de ctx.betterSidebar-service; op Installeren klikken kopieert het installatiecommando — plak het in een terminal waar uw DSH-profiel zich bevindt en voer het uit.',
addPluginsViewerDesc: 'Bestandsviewers kunnen door plugins worden uitgebreid. Plugins registreren via de ctx.betterSidebar-service; op Installeren klikken kopieert het installatiecommando — plak het in een terminal waar uw DSH-profiel zich bevindt en voer het uit.',
addPluginsBrowseMore: 'Meer plugins bekijken op GitHub (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'Meer plugins bekijken op GitHub (topic: dsh-apes-sidebar)',
addPluginsSearch: 'Zoek op plugin-naam of -beschrijving…',
addPluginsNoMatch: 'Geen plugins komen overeen',
addPluginsRecommended: 'Aanbevolen plugins',
@@ -406,7 +406,7 @@ export const nl: Record<string, string> = {
pluginGithubWorkbenchDesc: 'GitHub Workbench-tab: externe repositoryboom + Issues / Pull requests / Actions met volledige schrijfontsteuning — issue/PR aanmaken, reageren, bewerken, sluiten/heropenen, squash·merge·rebase-merge (sterke bevestiging), CI opnieuw uitvoeren/annuleren; de repo-wisselaar toont automatisch toegankelijke repo’s en doorzoekt openbare; zonder better-sidebar valt het terug op een zelfstandig rechterpaneel',
pluginMdExportDesc: 'Voegt een knop «Exporteren» toe aan de Markdown-werkbalk van better-sidebar: rendert de huidige .md in één klik naar zelfstandige HTML (tabellen/codeblokken/Mermaid-diagrammen inline, lay-out volgt het voorbeeldthema) en slaat die naast het .md op, of exporteert via het afdrukvenster naar PDF',
pluginCodeNavDesc: 'Codevoorbeeldnavigatie: detecteert de taal op bestandstype en kleurt de syntaxis, symbooloverzicht (filters voor klassen/methoden/variabelen + sprong met één klik) en zoeken in het bestand (alle treffers gemarkeerd, vorige/volgende, hoofdlettergevoelig) — neemt het codevoorbeeld in de better-sidebar-editor over',
pluginSuhuangScrollDesc: 'Verbindt de lokale Suhuang Scroll Runtime met DSH-instellingen en better-sidebar voor modelconfiguratie, verbindingstests en doorlopende beoordelingsbediening; vereist Suhuang Scroll Runtime en dsh-better-sidebar',
pluginSuhuangScrollDesc: 'Verbindt de lokale Suhuang Scroll Runtime met DSH-instellingen en better-sidebar voor modelconfiguratie, verbindingstests en doorlopende beoordelingsbediening; vereist Suhuang Scroll Runtime en dsh-apes-sidebar',
pluginBetterOverleafDesc: 'Overleaf-tabblad voor better-sidebar: directe browserlogin via CDP (ondersteunt Chromium van derden), projectlijst/wisselen, lokale git-spiegels onder <workspace>/overleaf/, tweerichtingssynchronisatie met read-only-API-terugval en bestandsvoorbeeld via de sidebar-werkbank',
pluginGitRemotesDesc: 'Git Remotes-tabblad: branch/upstream/ahead-behind, fetch (optioneel prune), ff-only pull, en push pas na een bevestiging in het tabblad. Vervangt het ingebouwde Git stage/commit-tabblad niet, en biedt geen force-push of een model-auto-push-tool',
pluginSentinelDesc: 'Conditie-gedreven agent-wakeupsysteem: bestands/proces/poort/http/command/webhook-sensoren wekken slapende sessies wanneer condities af gaan; registreert een "Sentinel"-tabblad met de serverbrede watch-tabel',
@@ -419,29 +419,4 @@ export const nl: Record<string, string> = {
pluginDocsPanelDesc: 'Globale docs in de DSH-zijbalk: lees uw eigen Markdown-notities vanuit elke werkruimte — een bestandslijst, een outline, openen in Chrome / VS Code, en kopieerknoppen; de docs-map is configureerbaar (standaard ~/.dsh/docs)',
pluginEgoBrowserDesc: 'De agentbrowser voor DeepSeek Harness: 32 ego_*-tools besturen een echt Chromium; een native «ego-browser»-tab in de zijbalk toont live elke pagina die de agent bezoekt — u kunt klikken, slepen en typen om over te nemen. Registreert de tab automatisch als better-sidebar aanwezig is, anders een zwevende bel',
pluginBilingualReaderDesc: 'Lees PDF’s in de DSH-zijbalk: native PDF-weergave, selecteer tekst om te vertalen met de LLM, met context en volledig geïsoleerd van het hoofdgesprek',
changesRedactOn: "Redactie aan: geheimen worden als [REDACTED] getoond; klik om uit te schakelen (op eigen risico)",
changesRedactOff: "Redactie uit: inhoud wordt ongewijzigd getoond; klik om in te schakelen",
changesRedactOnLabel: "Redactie",
changesRedactOffLabel: "Zonder redactie",
changesRedactBanner: "Geredigeerd",
pluginSentinelName: 'dsh-sentinel Weksysteem',
pluginEgoBrowserName: 'ego-browser Agentbrowser',
pluginBetterOverleafName: 'dsh-better-overleaf Overleaf-tab',
pluginDocsPanelName: 'dsh-docs-panel Globale documenten',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Git-inloggegevens',
pluginGitRemotesName: 'dsh-git-remotes Git-remotes',
pluginGithubWorkbenchName: 'dsh-github-workbench GitHub-workbench',
pluginSidebarQaName: 'dsh-sidebar-qa Selecteren & vragen',
pluginSidenoteName: 'dsh-sidenote Zijchat',
pluginServerDeckName: 'dsh-server-deck Serverdeck',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel SSH-tunnel',
pluginTurnReviewName: 'dsh-turn-review Beurtreview',
pluginBilingualReaderName: 'dsh-bilingual-reader Tweetalige lezer',
pluginOfficeName: 'Office-voorvertoning',
pluginMdExportName: 'Markdown-export',
pluginCodeNavName: 'Codevoorvertoning-navigator',
pluginVideoPreviewName: 'Videovoorvertoning',
presetDshDesktopDesc: 'Geavanceerde Electronmodus (zonder rand): macOS reserveert 20px bovenaan; Windows reserveert 32px voor de titelbalk wanneer WCO niet beschikbaar is',
}
+18 -43
View File
@@ -21,19 +21,11 @@ export const pl: Record<string, string> = {
files: 'Pliki',
changesSessionEmpty: 'Brak operacji na plikach w tej sesji',
changesRead: 'Odczyt',
changesMdReading: 'Czytanie',
changesHtmlRender: "Renderuj",
changesHtmlRaw: "Źródło",
changesPdfRender: "Renderuj",
changesPdfRaw: "Źródło",
changesMdRaw: 'Źródło',
changesWrite: 'Zapis',
changesEdit: 'Edycja',
changesRunning: 'w toku',
changesError: 'błąd',
changesFold: '{count} wierszy… kliknij, aby rozwinąć',
changesFoldLoading: 'Rozwijanie…',
changesFoldUnavailable: 'Kontekst niedostępny',
changesContext: 'kontekst',
changesPriorUnknown: 'Zawartość sprzed zmiany jest poza oknem; pokazana jako w całości nowa',
explorer: 'Eksplorator',
@@ -83,6 +75,7 @@ export const pl: Record<string, string> = {
closeOtherTabs: 'Zamknij inne karty',
closeLeftTabs: 'Zamknij karty po lewej',
closeRightTabs: 'Zamknij karty po prawej',
moveToFreeWindow: 'Przenieś do wolnego okna',
floatDropHint: 'Puść, aby otworzyć w wolnym oknie',
dockToSidebar: 'Wróć do panelu bocznego',
pinTerminal: 'Przypnij Terminal',
@@ -97,6 +90,8 @@ export const pl: Record<string, string> = {
pinnedTerminalScopeGlobal: 'Przypięty globalnie',
pinnedRailLabel: 'Przypięte terminale',
closePinnedTerminal: 'Zamknij terminal',
collapse: 'Zwiń panel boczny',
expand: 'Rozwiń panel boczny',
collapseBottomPanel: 'Zwiń panel dolny',
expandBottomPanel: 'Rozwiń panel dolny',
terminalError: 'Połączenie z terminalem nie powiodło się',
@@ -105,7 +100,6 @@ export const pl: Record<string, string> = {
terminalDepsFailed: 'Zależność terminala node-pty nie mogła się załadować',
terminalDepsHint: 'Uruchom poniższe polecenie w terminalu lub cmd na maszynie DSH, aby to naprawić, a następnie ponów (node-pty pozostaje w synchronizacji z wersją rdzenia DSH):',
terminalDepsProfile: ' (wykryty profil: {profile})',
terminalShellNotFound: 'Nie znaleziono skonfigurowanej powłoki: {name} — sprawdź ścieżkę powłoki w Ustawienia → Side card → Terminal',
preview: 'Podgląd',
toc: 'Spis treści',
edit: 'Edytuj',
@@ -152,6 +146,12 @@ export const pl: Record<string, string> = {
changesOpenDiffTab: 'Otwórz w osobnej karcie',
changesClosePreview: 'Zamknij podgląd',
changesResizePreview: 'Zmień wysokość podglądu',
changesDiffOpenTitle: 'Diff otwiera się jako',
changesDiffOpenDesc: 'Gdzie akcja „otwórz w osobnej karcie” umieszcza diff',
changesDiffOpenFloat: 'Odrębne okno',
changesDiffOpenFloatDesc: 'Pływające okno wyśrodkowane na ekranie — przeciągaj, skaluj, trzymaj na wierzchu',
changesDiffOpenPane: 'Panel',
changesDiffOpenPaneDesc: 'Zadokowane pod panelem źródłowym (podział diff w stylu VSCode)',
changesLoadError: 'Zapisy plików sesji są teraz niedostępne',
staged: 'W indeksie',
unstaged: 'Poza indeksem',
@@ -200,13 +200,6 @@ export const pl: Record<string, string> = {
addToConversation: 'Dodaj do rozmowy',
copyRelative: 'Kopiuj ścieżkę względną',
copyAbsolute: 'Kopiuj ścieżkę bezwzględną',
rename: 'Zmień nazwę',
renameInvalid: 'Nazwa nie może być pusta ani zawierać separatorów ścieżki.',
delete: 'Usuń',
deleteTitle: 'Usunąć „{name}”?',
deleteDescFile: 'Spowoduje to trwałe usunięcie pliku. Tej operacji nie można cofnąć.',
deleteDescDir: 'Spowoduje to trwałe usunięcie katalogu i całej jego zawartości. Tej operacji nie można cofnąć.',
dismiss: 'Zamknij',
download: 'Pobierz',
uploadFiles: 'Wgraj pliki',
uploadFolder: 'Wgraj folder',
@@ -225,6 +218,13 @@ export const pl: Record<string, string> = {
settingsIntro: 'Zarządzaj tym, co pokazuje karta boczna i jak się zachowuje',
settingsPopupDesc: 'Skonfiguruj opcje powiązane z: {feature}',
settingsDone: 'Gotowe',
settingsOpenTitle: 'Otwórz domyślnie dla nowych rozmów',
settingsOpenDesc: 'Rozwiń kartę boczną automatycznie dla zupełnie nowych rozmów; istniejące rozmowy zachowują własne układy',
settingsWidthTitle: 'Domyślny udział szerokości',
settingsWidthDesc: 'Domyślny udział szerokości okna dla karty bocznej w nowych rozmowach (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Otwieraj pliki czatu w panelu bocznym',
settingsOpenPathDesc: 'Otwieraj linki plików na czacie (wiersze narzędzi, wyprodukowane pliki, wzmianki) w edytorze panelu bocznego zamiast w domyślnej aplikacji systemu',
settingsOpenToolsTitle: 'Wstrzyknij narzędzie otwierania panelu bocznego dla modelu',
settingsOpenToolsDesc: 'Po włączeniu model może otwierać pliki, foldery i strony HTTP(S) w panelu bocznym za pomocą narzędzia sidebar_open (domyślnie wyłączone)',
settingsTitleBarTitle: 'Tryb zgodności pozycji',
@@ -397,7 +397,7 @@ export const pl: Record<string, string> = {
addPluginsViewerCardDesc: 'Zarejestruj podgląd typu pliku',
addPluginsTabDesc: 'Strony panelu bocznego (karty) mogą być rozszerzane przez wtyczki. Wtyczki rejestrują się przez usługę ctx.betterSidebar; kliknięcie „Zainstaluj” kopiuje polecenie instalacji — wklej je do terminala tam, gdzie żyje Twój profil DSH, i uruchom.',
addPluginsViewerDesc: 'Podglądy plików mogą być rozszerzane przez wtyczki. Wtyczki rejestrują się przez usługę ctx.betterSidebar; kliknięcie „Zainstaluj” kopiuje polecenie instalacji — wklej je do terminala tam, gdzie żyje Twój profil DSH, i uruchom.',
addPluginsBrowseMore: 'Przeglądaj więcej wtyczek na GitHub (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'Przeglądaj więcej wtyczek na GitHub (topic: dsh-apes-sidebar)',
addPluginsSearch: 'Szukaj po nazwie lub opisie wtyczki…',
addPluginsNoMatch: 'Brak pasujących wtyczek',
addPluginsRecommended: 'Zalecane wtyczki',
@@ -410,7 +410,7 @@ export const pl: Record<string, string> = {
pluginGithubWorkbenchDesc: 'Karta GitHub Workbench: drzewo zdalnego repozytorium + Issues / Pull requests / Actions z pełną obsługą zapisu — tworzenie issue/PR, komentowanie, edycja, zamykanie/ponowne otwieranie, scalanie squash·merge·rebase (mocne potwierdzenie), ponowne uruchomienie/anulowanie CI; przełącznik repozytoriów automatycznie wyświetla dostępne repozytoria i wyszukuje publiczne; bez better-sidebar następuje przejście na samodzielny panel po prawej stronie',
pluginMdExportDesc: 'Dodaje przycisk „Eksportuj" do paska narzędzi Markdown w better-sidebar: jednym kliknięciem renderuje bieżący .md do samodzielnego HTML (tabele/bloki kodu/diagramy Mermaid w treści, układ zgodny z motywem podglądu) i zapisuje obok pliku .md albo eksportuje do PDF przez okno drukowania',
pluginCodeNavDesc: 'Nawigacja podglądu kodu: rozpoznaje język po typie pliku i koloruje składnię, konspekt symboli (filtry klas/metod/zmiennych + skok jednym kliknięciem) oraz wyszukiwanie w pliku (podświetlanie wszystkich trafień, poprzedni/następny, rozróżnianie wielkości liter) — przejmuje podgląd plików kodu w edytorze better-sidebar',
pluginSuhuangScrollDesc: 'Łączy lokalny Suhuang Scroll Runtime z ustawieniami DSH i better-sidebar, zapewniając konfigurację modelu, test połączenia i sterowanie ciągłym ocenianiem; wymaga Suhuang Scroll Runtime i dsh-better-sidebar',
pluginSuhuangScrollDesc: 'Łączy lokalny Suhuang Scroll Runtime z ustawieniami DSH i better-sidebar, zapewniając konfigurację modelu, test połączenia i sterowanie ciągłym ocenianiem; wymaga Suhuang Scroll Runtime i dsh-apes-sidebar',
pluginBetterOverleafDesc: 'Karta Overleaf dla better-sidebar: logowanie bezpośrednio przez przeglądarkę CDP (obsługa Chromium innych firm), lista/przełączanie projektów, lokalne kopie lustrzane git w <workspace>/overleaf/, dwukierunkowa synchronizacja git z awaryjnym API tylko do odczytu oraz podgląd plików przez warsztat panelu bocznego',
pluginGitRemotesDesc: 'Karta Git Remotes: gałąź/nadrzędne/ahead-behind, fetch (z opcjonalnym prune), ff-only pull i push dopiero po potwierdzeniu w karcie. Nie zastępuje wbudowanej karty Git stage/commit i nie oferuje force-push ani narzędzia auto-push modelu',
pluginSentinelDesc: 'Budzenie agenta sterowane warunkami: czujniki plików/procesów/portów/HTTP/poleceń/webhooków wybudzają uśpione sesje, gdy warunek zajdzie; rejestruje kartę „Sentinel” z ogólnoserwerową tabelą obserwacji',
@@ -423,29 +423,4 @@ export const pl: Record<string, string> = {
pluginDocsPanelDesc: 'Globalne dokumenty w panelu bocznym DSH: globalne notatki Markdown, czytelne z dowolnego obszaru roboczego — lista z kliknięciem, kontur z przejściami, otwieranie w Chrome / VS Code i przyciski kopiowania kodu; katalog konfigurowalny (domyślnie ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Przeglądarka agenta dla DeepSeek Harness: 32 narzędzia ego_* sterują prawdziwym Chromium; natywna karta «ego browser» na pasku bocznym pokazuje na żywo każdą stronę odwiedzaną przez agenta — możesz klikać, przeciągać i pisać, aby przejąć kontrolę. Karta rejestruje się automatycznie, gdy better-sidebar jest zainstalowany; w przeciwnym razie pływający dymek',
pluginBilingualReaderDesc: 'Czytaj PDF-y w panelu bocznym DSH: natywny podgląd PDF, zaznacz tekst, aby przetłumaczyć go LLM-em, z kontekstem i całkowicie izolowany od głównej rozmowy',
changesRedactOn: "Redakcja włączona: tajne dane pokazywane jako [REDACTED]; kliknij, aby wyłączyć (na własne ryzyko)",
changesRedactOff: "Redakcja wyłączona: treść pokazywana bez zmian; kliknij, aby włączyć",
changesRedactOnLabel: "Redakcja",
changesRedactOffLabel: "Bez redakcji",
changesRedactBanner: "Zredagowano",
pluginSentinelName: 'dsh-sentinel System wybudzania',
pluginEgoBrowserName: 'ego-browser Przeglądarka agenta',
pluginBetterOverleafName: 'dsh-better-overleaf Karta Overleaf',
pluginDocsPanelName: 'dsh-docs-panel Dokumenty globalne',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Dane uwierzytelniające Git',
pluginGitRemotesName: 'dsh-git-remotes Zdalne repozytoria Git',
pluginGithubWorkbenchName: 'dsh-github-workbench Warsztat GitHub',
pluginSidebarQaName: 'dsh-sidebar-qa Zaznacz i zapytaj',
pluginSidenoteName: 'dsh-sidenote Czat boczny',
pluginServerDeckName: 'dsh-server-deck Panel serwerów',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel Tunel SSH',
pluginTurnReviewName: 'dsh-turn-review Rewizja tury',
pluginBilingualReaderName: 'dsh-bilingual-reader Czytnik dwujęzyczny',
pluginOfficeName: 'Podgląd Office',
pluginMdExportName: 'Eksport Markdown',
pluginCodeNavName: 'Nawigator podglądu kodu',
pluginVideoPreviewName: 'Podgląd wideo',
presetDshDesktopDesc: 'Tryb zaawansowany Electron (bez ramki): macOS rezerwuje 20px u góry; Windows bez WCO rezerwuje 32px na pasek tytułu',
}
+18 -43
View File
@@ -2,19 +2,11 @@ export const pt: Record<string, string> = {
files: 'Arquivos',
changesSessionEmpty: 'Ainda sem operações de arquivo nesta sessão',
changesRead: 'Leitura',
changesMdReading: 'Leitura',
changesHtmlRender: "Renderizar",
changesHtmlRaw: "Fonte",
changesPdfRender: "Renderizar",
changesPdfRaw: "Fonte",
changesMdRaw: 'Bruto',
changesWrite: 'Escrita',
changesEdit: 'Edição',
changesRunning: 'em execução',
changesError: 'erro',
changesFold: '{count} linhas… clique para expandir',
changesFoldLoading: 'Expandindo…',
changesFoldUnavailable: 'Contexto indisponível',
changesContext: 'contexto',
changesPriorUnknown: 'O conteúdo anterior está fora da janela; mostrado como totalmente adicionado',
explorer: 'Explorador',
@@ -64,6 +56,7 @@ export const pt: Record<string, string> = {
closeOtherTabs: 'Fechar outras abas',
closeLeftTabs: 'Fechar abas à esquerda',
closeRightTabs: 'Fechar abas à direita',
moveToFreeWindow: 'Mover para janela flutuante',
floatDropHint: 'Solte para abrir em uma janela flutuante',
dockToSidebar: 'Voltar para a barra lateral',
pinTerminal: 'Fixar Terminal',
@@ -78,6 +71,8 @@ export const pt: Record<string, string> = {
pinnedTerminalScopeGlobal: 'Fixado globalmente',
pinnedRailLabel: 'Terminais Fixados',
closePinnedTerminal: 'Fechar Terminal',
collapse: 'Recolher barra lateral',
expand: 'Expandir barra lateral',
collapseBottomPanel: 'Recolher painel inferior',
expandBottomPanel: 'Expandir painel inferior',
terminalError: 'Falha na conexão do terminal',
@@ -86,7 +81,6 @@ export const pt: Record<string, string> = {
terminalDepsFailed: 'A dependência do terminal node-pty falhou ao carregar',
terminalDepsHint: 'Execute o comando abaixo em um terminal ou cmd na máquina do DSH para reparar e tente novamente (node-pty permanece em sincronia com a versão do núcleo do DSH):',
terminalDepsProfile: ' (perfil detectado: {profile})',
terminalShellNotFound: 'Shell configurado não encontrado: {name} — verifique o caminho do shell em Configurações → Side card → Terminal',
preview: 'Pré-visualizar',
toc: 'Sumário',
edit: 'Editar',
@@ -133,6 +127,12 @@ export const pt: Record<string, string> = {
changesOpenDiffTab: 'Abrir em uma aba de diff',
changesClosePreview: 'Fechar prévia',
changesResizePreview: 'Ajustar altura da prévia',
changesDiffOpenTitle: 'O diff abre como',
changesDiffOpenDesc: 'Onde a ação "abrir em uma aba de diff" coloca o diff',
changesDiffOpenFloat: 'Janela livre',
changesDiffOpenFloatDesc: 'Uma janela flutuante centralizada — arraste, redimensione, mantenha no topo',
changesDiffOpenPane: 'Painel',
changesDiffOpenPaneDesc: 'Ancorado abaixo do painel de origem (divisão de diff estilo VSCode)',
changesLoadError: 'Os registros de arquivos da sessão estão indisponíveis no momento',
staged: 'Preparadas',
unstaged: 'Não preparadas',
@@ -181,13 +181,6 @@ export const pt: Record<string, string> = {
addToConversation: 'Adicionar à conversa',
copyRelative: 'Copiar caminho relativo',
copyAbsolute: 'Copiar caminho absoluto',
rename: 'Renomear',
renameInvalid: 'O nome não pode ser vazio nem conter separadores de caminho.',
delete: 'Excluir',
deleteTitle: 'Excluir "{name}"?',
deleteDescFile: 'Isso exclui o arquivo permanentemente. Esta ação não pode ser desfeita.',
deleteDescDir: 'Isso exclui o diretório e todo o seu conteúdo permanentemente. Esta ação não pode ser desfeita.',
dismiss: 'Fechar',
download: 'Baixar',
uploadFiles: 'Enviar arquivos',
uploadFolder: 'Enviar pasta',
@@ -206,6 +199,13 @@ export const pt: Record<string, string> = {
settingsIntro: 'Gerencie o que o cartão lateral mostra e como ele se comporta',
settingsPopupDesc: 'Configurar opções relacionadas para {feature}',
settingsDone: 'Concluído',
settingsOpenTitle: 'Abrir por padrão em novas conversas',
settingsOpenDesc: 'Expandir o cartão lateral automaticamente em conversas totalmente novas; conversas existentes mantêm seus próprios layouts',
settingsWidthTitle: 'Participação padrão na largura',
settingsWidthDesc: 'Participação padrão do cartão lateral na largura da janela em novas conversas (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Abrir arquivos do chat na barra lateral',
settingsOpenPathDesc: 'Abrir links de arquivos no chat (linhas de ferramentas, arquivos produzidos, menções) no editor da barra lateral em vez do aplicativo padrão do sistema',
settingsOpenToolsTitle: 'Injetar a ferramenta de abertura na barra lateral para o modelo',
settingsOpenToolsDesc: 'Quando ativado, o modelo pode abrir arquivos, pastas e páginas HTTP(S) na barra lateral pela ferramenta sidebar_open (desativado por padrão)',
settingsTitleBarTitle: 'Modo de compatibilidade de posição',
@@ -378,7 +378,7 @@ export const pt: Record<string, string> = {
addPluginsViewerCardDesc: 'Registrar uma pré-visualização de tipo de arquivo',
addPluginsTabDesc: 'As páginas da barra lateral (abas) podem ser estendidas por plugins. Os plugins registram pelo serviço ctx.betterSidebar; clicar em Instalar copia o comando de instalação — cole-o em um terminal onde seu perfil do DSH vive e execute.',
addPluginsViewerDesc: 'Os visualizadores de arquivos podem ser estendidos por plugins. Os plugins registram pelo serviço ctx.betterSidebar; clicar em Instalar copia o comando de instalação — cole-o em um terminal onde seu perfil do DSH vive e execute.',
addPluginsBrowseMore: 'Navegue por mais plugins no GitHub (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'Navegue por mais plugins no GitHub (topic: dsh-apes-sidebar)',
addPluginsSearch: 'Pesquisar por nome ou descrição do plugin…',
addPluginsNoMatch: 'Nenhum plugin corresponde',
addPluginsRecommended: 'Plugins recomendados',
@@ -391,7 +391,7 @@ export const pt: Record<string, string> = {
pluginGithubWorkbenchDesc: 'Aba GitHub Workbench: árvore do repositório remoto + Issues / Pull requests / Actions com suporte completo de escrita — criar Issue/PR, comentar, editar, fechar/reabrir, merge squash·merge·rebase (confirmação reforçada), reexecutar/cancelar CI; o seletor de repositórios lista automaticamente os repositórios acessíveis e busca nos públicos; sem better-sidebar, faz fallback para um painel lateral direito independente',
pluginMdExportDesc: 'Adiciona um botão «Exportar» à barra de ferramentas do Markdown no better-sidebar: com um clique, renderiza o .md atual em HTML autônomo (tabelas/blocos de código/diagramas Mermaid embutidos, layout seguindo o tema de pré-visualização) e salva na mesma pasta, ou exporta para PDF pela caixa de impressão',
pluginCodeNavDesc: 'Navegação de pré-visualização de código: detecta a linguagem pelo tipo de arquivo e destaca a sintaxe, contorno de símbolos (filtros de classes/métodos/variáveis + salto em um clique) e busca no arquivo (todas as ocorrências destacadas, anterior/próxima, diferenciando maiúsculas) — assume a pré-visualização de arquivos de código no editor do better-sidebar',
pluginSuhuangScrollDesc: 'Conecta o Suhuang Scroll Runtime local às configurações do DSH e ao better-sidebar para configurar o modelo, testar a conexão e controlar a correção contínua; requer Suhuang Scroll Runtime e dsh-better-sidebar',
pluginSuhuangScrollDesc: 'Conecta o Suhuang Scroll Runtime local às configurações do DSH e ao better-sidebar para configurar o modelo, testar a conexão e controlar a correção contínua; requer Suhuang Scroll Runtime e dsh-apes-sidebar',
pluginBetterOverleafDesc: 'Aba Overleaf para better-sidebar: login direto pelo navegador via CDP (compatível com Chromium de terceiros), lista/troca de projetos, espelhos git locais em <workspace>/overleaf/, sincronização git bidirecional com fallback de API somente leitura e pré-visualização de arquivos pela bancada da barra lateral',
pluginGitRemotesDesc: 'Aba de remotos Git: branch/upstream/ahead-behind, fetch (prune opcional), pull ff-only e push apenas após confirmar na aba. Não substitui a aba interna de stage/commit do Git e não oferece force-push nem push automático pelo modelo',
pluginSentinelDesc: 'Despertar de agentes dirigido por condições: sensores de arquivo/processo/porta/HTTP/comando/webhook acordam sessões adormecidas quando as condições disparam; registra uma aba "Sentinelas" com a tabela de monitoramento global do servidor',
@@ -404,29 +404,4 @@ export const pt: Record<string, string> = {
pluginDocsPanelDesc: 'Documentos globais na barra lateral do DSH: leia suas próprias notas Markdown de qualquer workspace — uma lista de arquivos, um sumário, abrir no Chrome / VS Code e botões de copiar; o diretório de documentos é configurável (padrão ~/.dsh/docs)',
pluginEgoBrowserDesc: 'O navegador de agente para o DeepSeek Harness: 32 ferramentas ego_* controlam um Chromium real; uma aba nativa «ego browser» na barra lateral mostra ao vivo cada página visitada pelo agente — você pode clicar, arrastar e digitar para assumir. Registra a aba automaticamente quando o better-sidebar está presente; caso contrário, uma bolha flutuante',
pluginBilingualReaderDesc: 'Leia PDFs na barra lateral do DSH: visualização nativa de PDF, selecione o texto para traduzir com o LLM, com contexto e totalmente isolado da conversa principal',
changesRedactOn: "Máscara ativa: segredos exibidos como [REDACTED]; clique para desativar (por sua conta)",
changesRedactOff: "Máscara desativada: conteúdo exibido como está; clique para ativar",
changesRedactOnLabel: "Máscara",
changesRedactOffLabel: "Sem máscara",
changesRedactBanner: "Mascarado",
pluginSentinelName: 'dsh-sentinel Sistema de despertar',
pluginEgoBrowserName: 'ego-browser Navegador de agentes',
pluginBetterOverleafName: 'dsh-better-overleaf Aba Overleaf',
pluginDocsPanelName: 'dsh-docs-panel Documentos globais',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Credenciais Git',
pluginGitRemotesName: 'dsh-git-remotes Remotos Git',
pluginGithubWorkbenchName: 'dsh-github-workbench Bancada GitHub',
pluginSidebarQaName: 'dsh-sidebar-qa Selecionar e perguntar',
pluginSidenoteName: 'dsh-sidenote Chat lateral',
pluginServerDeckName: 'dsh-server-deck Deck de servidores',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel Túnel SSH',
pluginTurnReviewName: 'dsh-turn-review Revisão do turno',
pluginBilingualReaderName: 'dsh-bilingual-reader Leitor bilíngue',
pluginOfficeName: 'Pré-visualização Office',
pluginMdExportName: 'Exportação Markdown',
pluginCodeNavName: 'Navegador de pré-visualização de código',
pluginVideoPreviewName: 'Pré-visualização de vídeo',
presetDshDesktopDesc: 'Modo avançado Electron (sem moldura): o macOS reserva 20px no topo; o Windows reserva 32px para a barra de título sem WCO',
}
+18 -43
View File
@@ -15,19 +15,11 @@ export const ru: Record<string, string> = {
files: 'Файлы',
changesSessionEmpty: 'В этой сессии пока нет файловых операций',
changesRead: 'Чтение',
changesMdReading: 'Чтение',
changesHtmlRender: "Рендер",
changesHtmlRaw: "Исходник",
changesPdfRender: "Рендер",
changesPdfRaw: "Исходник",
changesMdRaw: 'Исходный текст',
changesWrite: 'Запись',
changesEdit: 'Изменение',
changesRunning: 'выполняется',
changesError: 'ошибка',
changesFold: '{count} строк… нажмите, чтобы развернуть',
changesFoldLoading: 'Разворачивание…',
changesFoldUnavailable: 'Контекст недоступен',
changesContext: 'контекст',
changesPriorUnknown: 'Содержимое до изменения вне окна; показано как полностью добавленное',
explorer: 'Проводник',
@@ -77,6 +69,7 @@ export const ru: Record<string, string> = {
closeOtherTabs: 'Закрыть остальные вкладки',
closeLeftTabs: 'Закрыть вкладки слева',
closeRightTabs: 'Закрыть вкладки справа',
moveToFreeWindow: 'Переместить в свободное окно',
floatDropHint: 'Отпустите, чтобы открыть в свободном окне',
dockToSidebar: 'Вернуть на боковую панель',
pinTerminal: 'Закрепить терминал',
@@ -91,6 +84,8 @@ export const ru: Record<string, string> = {
pinnedTerminalScopeGlobal: 'Закреплён глобально',
pinnedRailLabel: 'Закреплённые терминалы',
closePinnedTerminal: 'Закрыть терминал',
collapse: 'Свернуть боковую панель',
expand: 'Развернуть боковую панель',
collapseBottomPanel: 'Свернуть нижнюю панель',
expandBottomPanel: 'Развернуть нижнюю панель',
terminalError: 'Сбой подключения терминала',
@@ -99,7 +94,6 @@ export const ru: Record<string, string> = {
terminalDepsFailed: 'Не удалось загрузить зависимость терминала node-pty',
terminalDepsHint: 'Выполните приведённую ниже команду в терминале или cmd на машине DSH, затем нажмите «Повторить» (node-pty остаётся синхронизированным с версией ядра DSH):',
terminalDepsProfile: ' (обнаружен profile: {profile})',
terminalShellNotFound: 'Настроенная оболочка не найдена: {name} — проверьте путь к оболочке в Настройки → Side card → Terminal',
preview: 'Предпросмотр',
toc: 'Оглавление',
edit: 'Изменить',
@@ -146,6 +140,12 @@ export const ru: Record<string, string> = {
changesOpenDiffTab: 'Открыть в отдельной вкладке',
changesClosePreview: 'Закрыть просмотр',
changesResizePreview: 'Изменить высоту просмотра',
changesDiffOpenTitle: 'Diff открывается как',
changesDiffOpenDesc: 'Куда действие «открыть в отдельной вкладке» помещает diff',
changesDiffOpenFloat: 'Свободное окно',
changesDiffOpenFloatDesc: 'Плавающее окно по центру экрана — перетаскивание, масштаб, поверх всех',
changesDiffOpenPane: 'Панель',
changesDiffOpenPaneDesc: 'Прикреплено под исходной панелью (разделение diff в стиле VSCode)',
changesLoadError: 'Записи файлов сессии сейчас недоступны',
staged: 'В индексе',
unstaged: 'Не в индексе',
@@ -194,13 +194,6 @@ export const ru: Record<string, string> = {
addToConversation: 'Добавить в диалог',
copyRelative: 'Копировать относительный путь',
copyAbsolute: 'Копировать абсолютный путь',
rename: 'Переименовать',
renameInvalid: 'Имя не может быть пустым или содержать разделители пути.',
delete: 'Удалить',
deleteTitle: 'Удалить «{name}»?',
deleteDescFile: 'Файл будет удалён безвозвратно. Это действие нельзя отменить.',
deleteDescDir: 'Каталог и всё его содержимое будут удалены безвозвратно. Это действие нельзя отменить.',
dismiss: 'Закрыть',
download: 'Скачать',
uploadFiles: 'Загрузить файлы',
uploadFolder: 'Загрузить папку',
@@ -220,6 +213,13 @@ export const ru: Record<string, string> = {
settingsIntro: 'Управление содержимым и поведением боковой карточки по умолчанию',
settingsPopupDesc: 'Настройка параметров «{feature}»',
settingsDone: 'Готово',
settingsOpenTitle: 'Открывать по умолчанию для новых сессий',
settingsOpenDesc: 'Автоматически разворачивать боковую карточку для новых сессий; у существующих сессий сохраняется их собственная раскладка',
settingsWidthTitle: 'Доля ширины по умолчанию',
settingsWidthDesc: 'Доля боковой карточки в ширине окна для новых сессий, в процентах (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Открывать файлы чата в боковой панели',
settingsOpenPathDesc: 'При щелчке по ссылке на файл в чате (строки инструментов, списки результатов, упоминания) файл открывается в редакторе боковой панели, а не в системном приложении по умолчанию',
settingsOpenToolsTitle: 'Внедрить инструмент открытия в боковой панели для модели',
settingsOpenToolsDesc: 'Если включено, модель может открывать файлы, папки и HTTP(S)-страницы в боковой панели с помощью инструмента sidebar_open (по умолчанию выключено)',
settingsTitleBarTitle: 'Режим совместимости заголовка',
@@ -393,7 +393,7 @@ export const ru: Record<string, string> = {
addPluginsViewerCardDesc: 'Зарегистрировать предпросмотр типа файлов',
addPluginsTabDesc: 'Страницы боковой панели (вкладки) расширяются плагинами. Плагины регистрируются через службу ctx.betterSidebar; щелчок по «Установить» копирует команду — вставьте её в терминал, где находится профиль DSH, и выполните.',
addPluginsViewerDesc: 'Просмотрщики файлов расширяются плагинами. Плагины регистрируются через службу ctx.betterSidebar; щелчок по «Установить» копирует команду — вставьте её в терминал, где находится профиль DSH, и выполните.',
addPluginsBrowseMore: 'Найти больше плагинов на GitHub (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'Найти больше плагинов на GitHub (topic: dsh-apes-sidebar)',
addPluginsSearch: 'Поиск по имени или описанию плагина…',
addPluginsNoMatch: 'Нет совпадающих плагинов',
addPluginsRecommended: 'Рекомендуемые плагины',
@@ -406,7 +406,7 @@ export const ru: Record<string, string> = {
pluginGithubWorkbenchDesc: 'Вкладка «GitHub Workbench»: дерево удалённого репозитория + Issues / Pull requests / Actions с полной поддержкой записи — создание Issue/PR, комментарии, редактирование, закрытие/повторное открытие, merge squash·merge·rebase (с сильным подтверждением), перезапуск/отмена CI; переключатель репозиториев автоматически показывает доступные репозитории и ищет по публичным; без better-sidebar используется отдельная правая панель',
pluginMdExportDesc: 'Добавляет кнопку «Экспорт» на панель инструментов Markdown в better-sidebar: одним кликом рендерит текущий .md в самостоятельный HTML (таблицы/блоки кода/Mermaid-диаграммы встроены, вёрстка следует теме предпросмотра) и сохраняет рядом с .md либо экспортирует в PDF через диалог печати',
pluginCodeNavDesc: 'Навигация по предпросмотру кода: определяет язык по типу файла и подсвечивает синтаксис, схема символов (фильтры классов/методов/переменных + переход в один клик) и поиск по файлу (подсветка всех совпадений, назад/вперёд, учёт регистра) — берёт на себя предпросмотр файлов кода в редакторе better-sidebar',
pluginSuhuangScrollDesc: 'Подключает локальный Suhuang Scroll Runtime к настройкам DSH и better-sidebar для настройки модели, проверки соединения и управления непрерывной проверкой; требуются Suhuang Scroll Runtime и dsh-better-sidebar',
pluginSuhuangScrollDesc: 'Подключает локальный Suhuang Scroll Runtime к настройкам DSH и better-sidebar для настройки модели, проверки соединения и управления непрерывной проверкой; требуются Suhuang Scroll Runtime и dsh-apes-sidebar',
pluginBetterOverleafDesc: 'Вкладка Overleaf для better-sidebar: вход напрямую через CDP-браузер (поддержка стороннего Chromium), список/переключение проектов, локальные git-зеркала в <workspace>/overleaf/, двунаправленная синхронизация git с откатом к API только для чтения, просмотр файлов через рабочую панель сайдбара',
pluginGitRemotesDesc: 'Вкладка Git Remotes: просмотр веток/апстрима/ahead-behind, fetch (с возможностью prune), ff-only pull и push только после подтверждения во вкладке. Не заменяет встроенную вкладку Git (индексирование/коммит), не предоставляет force-push и инструмент авто-push для модели',
pluginSentinelDesc: 'Система пробуждения агента по условиям: сенсоры файлов/процессов/портов/HTTP/команд/webhook будят спящие сессии при срабатывании; регистрирует вкладку «Sentinel» с глобальной таблицей наблюдений сервера',
@@ -419,29 +419,4 @@ export const ru: Record<string, string> = {
pluginDocsPanelDesc: '«Глобальные документы» в боковой панели DSH: глобальные Markdown-заметки, доступные из любой рабочей области — список для чтения, навигация по плавающему оглавлению, открытие в Chrome / VS Code, копирование кода; каталог настраивается (по умолчанию ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Браузер агента для DeepSeek Harness: 32 инструмента ego_* управляют настоящим Chromium; встроенная вкладка «ego browser» в боковой панели показывает в реальном времени каждую страницу, которую посещает агент — кликайте, перетаскивайте и вводите текст, чтобы перехватить управление. Вкладка регистрируется автоматически при наличии better-sidebar, иначе используется плавающий пузырёк',
pluginBilingualReaderDesc: 'Читайте PDF в боковой панели DSH: нативный просмотр PDF, выделите текст для перевода с помощью LLM, используя контекст и полностью изолированно от основного разговора',
changesRedactOn: "Маскировка включена: секреты показываются как [REDACTED]; нажмите, чтобы отключить (на свой риск)",
changesRedactOff: "Маскировка выключена: содержимое показывается как есть; нажмите, чтобы включить",
changesRedactOnLabel: "Маскировка",
changesRedactOffLabel: "Без маскировки",
changesRedactBanner: "Замаскировано",
pluginSentinelName: 'dsh-sentinel Система пробуждения',
pluginEgoBrowserName: 'ego-browser Браузер агента',
pluginBetterOverleafName: 'dsh-better-overleaf Вкладка Overleaf',
pluginDocsPanelName: 'dsh-docs-panel Глобальные документы',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Учётные данные Git',
pluginGitRemotesName: 'dsh-git-remotes Удалённые репозитории Git',
pluginGithubWorkbenchName: 'dsh-github-workbench Рабочая среда GitHub',
pluginSidebarQaName: 'dsh-sidebar-qa Выделить и спросить',
pluginSidenoteName: 'dsh-sidenote Боковой чат',
pluginServerDeckName: 'dsh-server-deck Панель серверов',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel SSH-туннель',
pluginTurnReviewName: 'dsh-turn-review Ревью раунда',
pluginBilingualReaderName: 'dsh-bilingual-reader Двуязычный ридер',
pluginOfficeName: 'Предпросмотр Office',
pluginMdExportName: 'Экспорт Markdown',
pluginCodeNavName: 'Навигатор предпросмотра кода',
pluginVideoPreviewName: 'Предпросмотр видео',
presetDshDesktopDesc: 'Расширенный режим Electron (без рамки): macOS резервирует 20px сверху; Windows без WCO резервирует 32px под строку заголовка',
}
+18 -43
View File
@@ -2,19 +2,11 @@ export const sv: Record<string, string> = {
files: 'Filer',
changesSessionEmpty: 'Inga filoperationer i den här sessionen ännu',
changesRead: 'Läs',
changesMdReading: 'Läsning',
changesHtmlRender: "Rendera",
changesHtmlRaw: "Källa",
changesPdfRender: "Rendera",
changesPdfRaw: "Källa",
changesMdRaw: 'Råtext',
changesWrite: 'Skriv',
changesEdit: 'Redigera',
changesRunning: 'körs',
changesError: 'fel',
changesFold: '{count} rader… klicka för att expandera',
changesFoldLoading: 'Expanderar…',
changesFoldUnavailable: 'Kontext ej tillgänglig',
changesContext: 'kontext',
changesPriorUnknown: 'Innehållet före ändringen ligger utanför fönstret; visas som helt nytt',
explorer: 'Utforskare',
@@ -64,6 +56,7 @@ export const sv: Record<string, string> = {
closeOtherTabs: 'Stäng andra flikar',
closeLeftTabs: 'Stäng flikar till vänster',
closeRightTabs: 'Stäng flikar till höger',
moveToFreeWindow: 'Flytta till fritt fönster',
floatDropHint: 'Släpp för att öppna i ett fritt fönster',
dockToSidebar: 'Tillbaka till sidopanelen',
pinTerminal: 'Fäst Terminal',
@@ -78,6 +71,8 @@ export const sv: Record<string, string> = {
pinnedTerminalScopeGlobal: 'Fäst globalt',
pinnedRailLabel: 'Fästa terminaler',
closePinnedTerminal: 'Stäng terminal',
collapse: 'Komprimera sidopanel',
expand: 'Expandera sidopanel',
collapseBottomPanel: 'Komprimera bottenpanel',
expandBottomPanel: 'Expandera bottenpanel',
terminalError: 'Terminalanslutning misslyckades',
@@ -86,7 +81,6 @@ export const sv: Record<string, string> = {
terminalDepsFailed: 'Terminalberoendet node-pty kunde inte laddas',
terminalDepsHint: 'Kör kommandot nedan i en terminal eller cmd på DSH-maskinen för att reparera det, försök sedan igen (node-pty hålls synkad med DSH-kärnversionen):',
terminalDepsProfile: ' (upptäckt profil: {profile})',
terminalShellNotFound: 'Konfigurerat skal hittades inte: {name} — kontrollera skalsökvägen under Inställningar → Side card → Terminal',
preview: 'Förhandsgranska',
toc: 'Innehållsförteckning',
edit: 'Redigera',
@@ -133,6 +127,12 @@ export const sv: Record<string, string> = {
changesOpenDiffTab: 'Öppna i egen diff-flik',
changesClosePreview: 'Stäng förhandsvisning',
changesResizePreview: 'Ändra storlek på förhandsvisningen',
changesDiffOpenTitle: 'Diff öppnas som',
changesDiffOpenDesc: 'Var åtgärden "öppna i egen diff-flik" placerar diffen',
changesDiffOpenFloat: 'Fritt fönster',
changesDiffOpenFloatDesc: 'Ett flytande fönster centrerat i vyn — dra, ändra storlek, håll överst',
changesDiffOpenPane: 'Panel',
changesDiffOpenPaneDesc: 'Dockad under källpanelen (VSCode-liknande diff-delning)',
changesLoadError: 'Sessionens filposter är inte tillgängliga just nu',
staged: 'Köade',
unstaged: 'Avköade',
@@ -181,13 +181,6 @@ export const sv: Record<string, string> = {
addToConversation: 'Lägg till i konversation',
copyRelative: 'Kopiera relativ sökväg',
copyAbsolute: 'Kopiera absolut sökväg',
rename: 'Byt namn',
renameInvalid: 'Namnet får inte vara tomt eller innehålla sökvägsavgränsare.',
delete: 'Ta bort',
deleteTitle: 'Ta bort "{name}"?',
deleteDescFile: 'Detta tar bort filen permanent. Det går inte att ångra.',
deleteDescDir: 'Detta tar bort katalogen och allt i den permanent. Det går inte att ångra.',
dismiss: 'Stäng',
download: 'Ladda ner',
uploadFiles: 'Ladda upp filer',
uploadFolder: 'Ladda upp mapp',
@@ -206,6 +199,13 @@ export const sv: Record<string, string> = {
settingsIntro: 'Hantera vad sidokortet visar och hur det beter sig',
settingsPopupDesc: 'Konfigurera relaterade alternativ för {feature}',
settingsDone: 'Klar',
settingsOpenTitle: 'Öppna som standard för nya konversationer',
settingsOpenDesc: 'Expandera sidokortet automatiskt för helt nya konversationer; befintliga konversationer behåller sina egna layouter',
settingsWidthTitle: 'Standardbreddandel',
settingsWidthDesc: 'Sidokortets standardandel av fönsterbredden för nya konversationer (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Öppna chattfiler i sidopanelen',
settingsOpenPathDesc: 'Öppna fillänkar i chatten (verktygsrader, producerade filer, omnämnanden) i sidopanelens editor i stället för systemets standardapp',
settingsOpenToolsTitle: 'Injicera sidopanel-öppningsverktyg för modellen',
settingsOpenToolsDesc: 'När aktiverat kan modellen öppna filer, mappar och HTTP(S)-sidor i sidopanelen via sidebar_open-verktyget (av som standard)',
settingsTitleBarTitle: 'Positions kompatibilitetsläge',
@@ -378,7 +378,7 @@ export const sv: Record<string, string> = {
addPluginsViewerCardDesc: 'Registrera en filtypsförhandsgranskning',
addPluginsTabDesc: 'Sidopanelens sidor (flikar) kan utökas av plugins. Plugins registreras via ctx.betterSidebar-tjänsten; att klicka på Installera kopierar installationskommandot — klistra in det i en terminal där er DSH-profil lever och kör det.',
addPluginsViewerDesc: 'Filförhandsgranskare kan utökas av plugins. Plugins registreras via ctx.betterSidebar-tjänsten; att klicka på Installera kopierar installationskommandot — klistra in det i en terminal där er DSH-profil lever och kör det.',
addPluginsBrowseMore: 'Bläddra bland fler plugins på GitHub (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'Bläddra bland fler plugins på GitHub (topic: dsh-apes-sidebar)',
addPluginsSearch: 'Sök på plugin-namn eller beskrivning…',
addPluginsNoMatch: 'Inga plugins matchar',
addPluginsRecommended: 'Rekommenderade plugins',
@@ -391,7 +391,7 @@ export const sv: Record<string, string> = {
pluginGithubWorkbenchDesc: 'GitHub Workbench-flik: fjärrrepo-träd + Issues / Pull requests / Actions med fullt skrivstöd — skapa issue/PR, kommentera, redigera, stäng/öppna igen, squash·merge·rebase-merge (stark bekräftelse), kör om/avbryt CI; repoväxlaren listar automatiskt tillgängliga repon och söker i publika; utan better-sidebar växlar den till en fristående höger panel',
pluginMdExportDesc: 'Lägger till en «Exportera»-knapp i Markdown-verktygsfältet i better-sidebar: renderar nuvarande .md till fristående HTML i ett klick (tabeller/kodblock/Mermaid-diagram inline, layout följer förhandsvisningstemat) och sparar bredvid .md-filen, eller exporterar till PDF via utskriftsdialogen',
pluginCodeNavDesc: 'Kodförhandsvisningsnavigering: känner automatiskt av språket från filtypen och färglägger syntaxen, symbolöversikt (filter för klasser/metoder/variabler + hopp med ett klick) och filsökning (markera alla träffar, föregående/nästa, skiftlägeskänsligt) — tar över kodfilförhandsvisning i better-sidebar-redigeraren',
pluginSuhuangScrollDesc: 'Ansluter den lokala Suhuang Scroll Runtime till DSH-inställningar och better-sidebar för modellkonfiguration, anslutningstest och styrning av kontinuerlig rättning; kräver Suhuang Scroll Runtime och dsh-better-sidebar',
pluginSuhuangScrollDesc: 'Ansluter den lokala Suhuang Scroll Runtime till DSH-inställningar och better-sidebar för modellkonfiguration, anslutningstest och styrning av kontinuerlig rättning; kräver Suhuang Scroll Runtime och dsh-apes-sidebar',
pluginBetterOverleafDesc: 'Overleaf-flik för better-sidebar: direktinloggning via CDP-webbläsare (stöd för tredjeparts-Chromium), projektlista/byte, lokala git-speglar under <workspace>/overleaf/, dubbelriktad git-synk med read-only-API-fallback och förhandsvisning av filer via sidofältets workbench',
pluginGitRemotesDesc: 'Git Remotes-flik: gren/upstream/ahead-behind, fetch (valbar prune), ff-only pull och push endast efter bekräftelse i fliken. Ersätter inte den inbyggda Git-köa/commit-fliken, och erbjuder inte force-push eller ett model-auto-push-verktyg',
pluginSentinelDesc: 'Villkorsdriven agentväckning: fil-/process-/port-/http-/kommando-/webhook-sensorer väcker vilande sessioner när villkor utlöses; registrerar en "Sentinel"-flik med serverns globala bevakningstabell',
@@ -404,29 +404,4 @@ export const sv: Record<string, string> = {
pluginDocsPanelDesc: 'Globala dokument i DSH-sidopanelen: läs era egna Markdown-anteckningar från valfri arbetsyta — en fillista, en översikt, öppna i Chrome / VS Code och kopieringsknappar; dokumentkatalogen är konfigurerbar (standard ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Agentwebbläsaren för DeepSeek Harness: 32 ego_*-verktyg styr en riktig Chromium; en inbyggd «ego browser»-flik i sidopanelen visar varje sida agenten besöker i realtid — du kan klicka, dra och skriva för att ta över. Registrerar fliken automatiskt när better-sidebar finns, annars en flytande bubbla',
pluginBilingualReaderDesc: 'Läs PDF-filer i DSH-sidopanelen: inbyggd PDF-visning, markera text för att översätta med LLM, med kontext och helt isolerad från huvudkonversationen',
changesRedactOn: "Maskning på: hemligheter visas som [REDACTED]; klicka för att stänga av (på egen risk)",
changesRedactOff: "Maskning av: innehåll visas som det är; klicka för att aktivera",
changesRedactOnLabel: "Maskning",
changesRedactOffLabel: "Utan maskning",
changesRedactBanner: "Maskerat",
pluginSentinelName: 'dsh-sentinel Väcksystem',
pluginEgoBrowserName: 'ego-browser Agentwebbläsare',
pluginBetterOverleafName: 'dsh-better-overleaf Overleaf-flik',
pluginDocsPanelName: 'dsh-docs-panel Globala dokument',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Git-autentiseringsuppgifter',
pluginGitRemotesName: 'dsh-git-remotes Git-fjärrarkiv',
pluginGithubWorkbenchName: 'dsh-github-workbench GitHub-arbetsbänk',
pluginSidebarQaName: 'dsh-sidebar-qa Markera och fråga',
pluginSidenoteName: 'dsh-sidenote Sidchatt',
pluginServerDeckName: 'dsh-server-deck Serverdäck',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel SSH-tunnel',
pluginTurnReviewName: 'dsh-turn-review Varvgranskning',
pluginBilingualReaderName: 'dsh-bilingual-reader Tvåspråkig läsare',
pluginOfficeName: 'Office-förhandsvisning',
pluginMdExportName: 'Markdown-export',
pluginCodeNavName: 'Kodförhandsvisningsnavigatör',
pluginVideoPreviewName: 'Videoförhandsvisning',
presetDshDesktopDesc: 'Electrons avancerade läge (ramlöst): macOS reserverar 20px högst upp; Windows reserverar 32px för namnlisten när WCO inte är tillgängligt',
}
+18 -43
View File
@@ -19,19 +19,11 @@ export const th: Record<string, string> = {
files: 'ไฟล์',
changesSessionEmpty: 'ยังไม่มีการดำเนินการไฟล์ในเซสชันนี้',
changesRead: 'อ่าน',
changesMdReading: 'อ่าน',
changesHtmlRender: "เรนเดอร์",
changesHtmlRaw: "ต้นฉบับ",
changesPdfRender: "เรนเดอร์",
changesPdfRaw: "ต้นฉบับ",
changesMdRaw: 'ดิบ',
changesWrite: 'เขียน',
changesEdit: 'แก้ไข',
changesRunning: 'กำลังทำงาน',
changesError: 'ข้อผิดพลาด',
changesFold: '{count} บรรทัด… คลิกเพื่อขยาย',
changesFoldLoading: 'กำลังขยาย…',
changesFoldUnavailable: 'ไม่สามารถโหลดบริบทได้',
changesContext: 'บริบท',
changesPriorUnknown: 'เนื้อหาก่อนแก้ไขอยู่นอกหน้าต่าง แสดงเป็นเพิ่มใหม่ทั้งหมด',
explorer: 'ตัวสำรวจ',
@@ -81,6 +73,7 @@ export const th: Record<string, string> = {
closeOtherTabs: 'ปิด Tab อื่น',
closeLeftTabs: 'ปิด Tab ด้านซ้าย',
closeRightTabs: 'ปิด Tab ด้านขวา',
moveToFreeWindow: 'ย้ายไปยังหน้าต่างอิสระ',
floatDropHint: 'ปล่อยเพื่อเปิดในหน้าต่างอิสระ',
dockToSidebar: 'กลับไปที่แถบข้าง',
pinTerminal: 'ปักหมุดเทอร์มินัล',
@@ -95,6 +88,8 @@ export const th: Record<string, string> = {
pinnedTerminalScopeGlobal: 'ปักหมุดแบบ Global',
pinnedRailLabel: 'เทอร์มินัลที่ปักหมุด',
closePinnedTerminal: 'ปิดเทอร์มินัล',
collapse: 'ย่อแถบด้านข้าง',
expand: 'ขยายแถบด้านข้าง',
collapseBottomPanel: 'ย่อพาเนลด้านล่าง',
expandBottomPanel: 'ขยายพาเนลด้านล่าง',
terminalError: 'การเชื่อมต่อเทอร์มินัลล้มเหลว',
@@ -103,7 +98,6 @@ export const th: Record<string, string> = {
terminalDepsFailed: 'การอ้างอิงเทอร์มินัล node-pty โหลดล้มเหลว',
terminalDepsHint: 'เรียกใช้คำสั่งด้านล่างในเทอร์มินัลหรือ cmd บนเครื่อง DSH เพื่อซ่อมแซม แล้วกดลองอีกครั้ง (node-pty ต้องซิงค์กับเวอร์ชันหลักของ DSH):',
terminalDepsProfile: ' (ตรวจพบ profile: {profile})',
terminalShellNotFound: 'ไม่พบ shell ที่กำหนดไว้: {name} — ตรวจสอบพาธ shell ใน การตั้งค่า → Side card → Terminal',
preview: 'พรีวิว',
toc: 'สารบัญ',
edit: 'แก้ไข',
@@ -150,6 +144,12 @@ export const th: Record<string, string> = {
changesOpenDiffTab: 'เปิดในแท็บ diff แยก',
changesClosePreview: 'ปิดตัวอย่าง',
changesResizePreview: 'ปรับขนาดตัวอย่าง',
changesDiffOpenTitle: 'diff เปิดเป็น',
changesDiffOpenDesc: 'การกระทำ «เปิดในแท็บ diff แยก» วาง diff ไว้ที่ใด',
changesDiffOpenFloat: 'หน้าต่างลอย',
changesDiffOpenFloatDesc: 'หน้าต่างลอยกลางจอ — ลาก ย่อขยาย ปักหมุดไว้บนสุด',
changesDiffOpenPane: 'แพเนล',
changesDiffOpenPaneDesc: 'จอดใต้แพเนลต้นทาง (แยก diff สไตล์ VSCode)',
changesLoadError: 'บันทึกไฟล์ของเซสชันยังใช้ไม่ได้ตอนนี้',
staged: 'Staged',
unstaged: 'Unstaged',
@@ -198,13 +198,6 @@ export const th: Record<string, string> = {
addToConversation: 'เพิ่มไปยังแชท',
copyRelative: 'คัดลอกพาธสัมพัทธ์',
copyAbsolute: 'คัดลอกพาธ absolute',
rename: 'เปลี่ยนชื่อ',
renameInvalid: 'ชื่อต้องไม่ว่างเปล่าและต้องไม่มีตัวคั่นเส้นทาง',
delete: 'ลบ',
deleteTitle: 'ลบ "{name}" หรือไม่',
deleteDescFile: 'การดำเนินการนี้จะลบไฟล์อย่างถาวร และไม่สามารถย้อนกลับได้',
deleteDescDir: 'การดำเนินการนี้จะลบไดเรกทอรีและเนื้อหาทั้งหมดอย่างถาวร และไม่สามารถย้อนกลับได้',
dismiss: 'ปิด',
download: 'ดาวน์โหลด',
uploadFiles: 'อัปโหลดไฟล์',
uploadFolder: 'อัปโหลดโฟลเดอร์',
@@ -223,6 +216,13 @@ export const th: Record<string, string> = {
settingsIntro: 'จัดการสิ่งที่การ์ดด้านข้างแสดงและพฤติกรรมของมัน',
settingsPopupDesc: 'กำหนดค่าตัวเลือกที่เกี่ยวข้องสำหรับ {feature}',
settingsDone: 'เสร็จสิ้น',
settingsOpenTitle: 'เปิดโดยค่าเริ่มต้นสำหรับแชทใหม่',
settingsOpenDesc: 'ขยายการ์ดด้านข้างโดยอัตโนมัติสำหรับแชทใหม่; แชทที่มีอยู่แล้วยังคงเค้าโครงของตัวเอง',
settingsWidthTitle: 'สัดส่วนความกว้างเริ่มต้น',
settingsWidthDesc: 'สัดส่วนความกว้างเริ่มต้นของการ์ดด้านข้างจากความกว้างหน้าต่างสำหรับแชทใหม่ (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'เปิดไฟล์แชทในแถบด้านข้าง',
settingsOpenPathDesc: 'เปิดลิงก์ไฟล์ในแชท (แถวเครื่องมือ, ไฟล์ที่สร้าง, การกล่าวถึง) ในตัวแก้ไขแถบด้านข้างแทนแอปเริ่มต้นของระบบ',
settingsOpenToolsTitle: 'ฉีดเครื่องมือเปิดแถบด้านข้างสำหรับโมเดล',
settingsOpenToolsDesc: 'เมื่อเปิดใช้ โมเดลสามารถเปิดไฟล์ โฟลเดอร์ และหน้า HTTP(S) ในแถบด้านข้างผ่านเครื่องมือ sidebar_open (ปิดเป็นค่าเริ่มต้น)',
settingsTitleBarTitle: 'โหมดความเข้ากันได้ของตำแหน่ง',
@@ -395,7 +395,7 @@ export const th: Record<string, string> = {
addPluginsViewerCardDesc: 'ลงทะเบียนพรีวิวประเภทไฟล์',
addPluginsTabDesc: 'หน้าแถบด้านข้าง (tab) สามารถขยายได้ด้วยปลั๊กอิน ปลั๊กอินลงทะเบียนผ่านบริการ ctx.betterSidebar; การคลิก Install จะคัดลอกคำสั่งติดตั้ง — วางลงในเทอร์มินัลที่ DSH profile ของคุณอยู่แล้วเรียกใช้',
addPluginsViewerDesc: 'ตัวพรีวิวไฟล์สามารถขยายได้ด้วยปลั๊กอิน ปลั๊กอินลงทะเบียนผ่านบริการ ctx.betterSidebar; การคลิก Install จะคัดลอกคำสั่งติดตั้ง — วางลงในเทอร์มินัลที่ DSH profile ของคุณอยู่แล้วเรียกใช้',
addPluginsBrowseMore: 'เรียกดูปลั๊กอินเพิ่มเติมบน GitHub (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'เรียกดูปลั๊กอินเพิ่มเติมบน GitHub (topic: dsh-apes-sidebar)',
addPluginsSearch: 'ค้นหาตามชื่อหรือคำอธิบายปลั๊กอิน…',
addPluginsNoMatch: 'ไม่มีปลั๊กอินที่ตรง',
addPluginsRecommended: 'ปลั๊กอินแนะนำ',
@@ -408,7 +408,7 @@ export const th: Record<string, string> = {
pluginGithubWorkbenchDesc: 'แท็บ GitHub Workbench: โครงสร้าง repo รีโมต + Issues / Pull requests / Actions พร้อมรองรับการเขียนครบวงจร — สร้าง Issue/PR, แสดงความคิดเห็น, แก้ไข, ปิด/เปิดใหม่, รวมแบบ squash·merge·rebase (ยืนยันก่อนเสมอ), รัน/ยกเลิก CI ใหม่; ตัวสลับ repo จะดึงรายการ repo ที่มีสิทธิ์เข้าถึงโดยอัตโนมัติและค้นหา repo สาธารณะได้; หากไม่ได้ติดตั้ง better-sidebar จะสลับไปใช้แผงด้านขวาแบบสแตนด์อโลนอัตโนมัติ',
pluginMdExportDesc: 'เพิ่มปุ่ม "ส่งออก" ในแถบเครื่องมือ Markdown ของ better-sidebar: เรนเดอร์ .md ปัจจุบันเป็น HTML แบบสแตนด์อโลนในคลิกเดียว (ตาราง/บล็อกโค้ด/ไดอะแกรม Mermaid แบบอินไลน์ เลย์เอาต์ตามธีมพรีวิว) แล้วบันทึกไว้โฟลเดอร์เดียวกัน หรือส่งออกเป็น PDF ผ่านกล่องโต้ตอบการพิมพ์',
pluginCodeNavDesc: 'การนำทางพรีวิวโค้ด: ระบุภาษาอัตโนมัติจากประเภทไฟล์และไฮไลต์ไวยากรณ์ โครงร่างสัญลักษณ์ (กรองคลาส/เมธอด/ตัวแปร + กระโดดคลิกเดียว) และค้นหาในไฟล์ (ไฮไลต์ทุกจุดที่ตรง ก่อนหน้า/ถัดไป แยกตัวพิมพ์ใหญ่เล็ก) — รับช่วงพรีวิวไฟล์โค้ดในตัวแก้ไข better-sidebar',
pluginSuhuangScrollDesc: 'เชื่อมต่อ Suhuang Scroll Runtime ในเครื่องกับการตั้งค่า DSH และ better-sidebar สำหรับการกำหนดค่าโมเดล การทดสอบการเชื่อมต่อ และการควบคุมการตรวจงานต่อเนื่อง โดยต้องติดตั้ง Suhuang Scroll Runtime และ dsh-better-sidebar',
pluginSuhuangScrollDesc: 'เชื่อมต่อ Suhuang Scroll Runtime ในเครื่องกับการตั้งค่า DSH และ better-sidebar สำหรับการกำหนดค่าโมเดล การทดสอบการเชื่อมต่อ และการควบคุมการตรวจงานต่อเนื่อง โดยต้องติดตั้ง Suhuang Scroll Runtime และ dsh-apes-sidebar',
pluginBetterOverleafDesc: 'แท็บ Overleaf สำหรับ better-sidebar: เข้าสู่ระบบผ่านเบราว์เซอร์ CDP โดยตรง (รองรับ Chromium ของบุคคลที่สาม) รายการ/สลับโปรเจกต์ มิเรอร์ git ภายในเครื่องที่ <workspace>/overleaf/ ซิงก์ git สองทางพร้อมสำรองแบบ API อ่านอย่างเดียว และดูตัวอย่างไฟล์ผ่านเวิร์กเบนช์ของแถบข้าง',
pluginGitRemotesDesc: 'tab Git Remotes: branch/upstream/ahead-behind, fetch (optional prune), ff-only pull และ push หลังการยืนยันใน tab เท่านั้น ไม่แทนที่ tab stage/commit ของ Git แบบ built-in และไม่เสนอ force-push หรือเครื่องมือ auto-push ของโมเดล',
pluginSentinelDesc: 'การปลุกตัวแทนที่ขับเคลื่อนด้วยเงื่อนไข: เซนเซอร์ file/process/port/http/command/webhook ปลุกเซสชันที่หลับเมื่อเงื่อนไขเกิดขึ้น; ลงทะเบียน tab "Sentinel" พร้อมตารางเฝ้าระวังทั่วทั้งเซิร์ฟเวอร์',
@@ -421,29 +421,4 @@ export const th: Record<string, string> = {
pluginDocsPanelDesc: 'เอกสารส่วนกลางในแถบด้านข้าง DSH: อ่านบันทึก Markdown ของคุณเองจากพื้นที่ทำงานใดๆ — รายการไฟล์, เค้าโครง, เปิดใน Chrome / VS Code และปุ่มคัดลอก; ไดเรกทอรีเอกสารสามารถกำหนดค่าได้ (ค่าเริ่มต้น ~/.dsh/docs)',
pluginEgoBrowserDesc: 'เบราว์เซอร์เอเจนต์สำหรับ DeepSeek Harness: เครื่องมือ ego_* 32 ตัวขับเคลื่อน Chromium จริง แท็บ «ego browser» เนทีฟในแถบด้านข้างแสดงทุกหน้าที่เอเจนต์เยี่ยมชมแบบสด — คลิก ลาก และพิมพ์เพื่อเข้าไปควบคุมแทนได้ ลงทะเบียนแท็บอัตโนมัติเมื่อมี better-sidebar มิฉะนั้นจะใช้บับเบิลลอย',
pluginBilingualReaderDesc: 'อ่าน PDF ในแถบด้านข้าง DSH: แสดง PDF ต้นฉบับ เลือกข้อความเพื่อแปลด้วย LLM พร้อมบริบทและแยกจากการสนทนาหลักโดยสิ้นเชิง',
changesRedactOn: "ซ่อนข้อมูลอยู่: ค่าลับแสดงเป็น [REDACTED]; คลิกเพื่อปิด (ยอมรับความเสี่ยงเอง)",
changesRedactOff: "ปิดการซ่อนข้อมูล: เนื้อหาจะแสดงตามจริง; คลิกเพื่อเปิด",
changesRedactOnLabel: "กำลังซ่อน",
changesRedactOffLabel: "ไม่ซ่อน",
changesRedactBanner: "ถูกซ่อนแล้ว",
pluginSentinelName: 'dsh-sentinel ระบบปลุกเตือน',
pluginEgoBrowserName: 'ego-browser เบราว์เซอร์เอเจนต์',
pluginBetterOverleafName: 'dsh-better-overleaf แท็บ Overleaf',
pluginDocsPanelName: 'dsh-docs-panel เอกสารส่วนกลาง',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge ข้อมูลรับรอง Git',
pluginGitRemotesName: 'dsh-git-remotes Remote ของ Git',
pluginGithubWorkbenchName: 'dsh-github-workbench เวิร์กเบนช์ GitHub',
pluginSidebarQaName: 'dsh-sidebar-qa เลือกแล้วถาม',
pluginSidenoteName: 'dsh-sidenote แชทข้างเคียง',
pluginServerDeckName: 'dsh-server-deck เด็คเซิร์ฟเวอร์',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel อุโมงค์ SSH',
pluginTurnReviewName: 'dsh-turn-review รีวิวรอบ',
pluginBilingualReaderName: 'dsh-bilingual-reader ตัวอ่านสองภาษา',
pluginOfficeName: 'พรีวิว Office',
pluginMdExportName: 'ส่งออก Markdown',
pluginCodeNavName: 'ตัวนำทางพรีวิวโค้ด',
pluginVideoPreviewName: 'พรีวิววิดีโอ',
presetDshDesktopDesc: 'โหมดขั้นสูงของ Electron (ไร้กรอบ): macOS จองแถบด้านบน 20px; Windows จอง 32px ให้แถบหัวเรื่องเมื่อไม่มี WCO',
}
+18 -43
View File
@@ -19,19 +19,11 @@ export const tr: Record<string, string> = {
files: 'Dosyalar',
changesSessionEmpty: 'Bu oturumda henüz dosya işlemi yok',
changesRead: 'Okuma',
changesMdReading: 'Okuma',
changesHtmlRender: "Render",
changesHtmlRaw: "Kaynak",
changesPdfRender: "Render",
changesPdfRaw: "Kaynak",
changesMdRaw: 'Ham',
changesWrite: 'Yazma',
changesEdit: 'Düzenleme',
changesRunning: 'çalışıyor',
changesError: 'hata',
changesFold: '{count} satır… genişletmek için tıklayın',
changesFoldLoading: 'Genişletiliyor…',
changesFoldUnavailable: 'Bağlam yüklenemedi',
changesContext: 'bağlam',
changesPriorUnknown: 'Değişiklik öncesi içerik pencere dışında; tamamen yeni eklenmiş olarak gösteriliyor',
explorer: 'Gezgin',
@@ -81,6 +73,7 @@ export const tr: Record<string, string> = {
closeOtherTabs: 'Diğer sekmeleri kapat',
closeLeftTabs: 'Soldaki sekmeleri kapat',
closeRightTabs: 'Sağdaki sekmeleri kapat',
moveToFreeWindow: 'Serbest pencereye taşı',
floatDropHint: 'Serbest pencerede açmak için bırakın',
dockToSidebar: 'Kenar çubuğuna geri dön',
pinTerminal: 'Terminali Sabitle',
@@ -95,6 +88,8 @@ export const tr: Record<string, string> = {
pinnedTerminalScopeGlobal: 'Genel olarak sabitlendi',
pinnedRailLabel: 'Sabitlenmiş Terminaller',
closePinnedTerminal: 'Terminali Kapat',
collapse: 'Kenar çubuğunu daralt',
expand: 'Kenar çubuğunu genişlet',
collapseBottomPanel: 'Alt paneli daralt',
expandBottomPanel: 'Alt paneli genişlet',
terminalError: 'Terminal bağlantısı başarısız',
@@ -103,7 +98,6 @@ export const tr: Record<string, string> = {
terminalDepsFailed: 'Terminal bağımlılığı node-pty yüklenemedi',
terminalDepsHint: 'Onarmak için DSH makinesindeki bir terminalde veya cmd’de aşağıdaki komutu çalıştırın, sonra yeniden deneyin (node-pty, DSH çekirdek sürümüyle senkron kalır):',
terminalDepsProfile: ' (algılanan profil: {profile})',
terminalShellNotFound: 'Yapılandırılan kabuk bulunamadı: {name} — Ayarlar → Side card → Terminal altındaki kabuk yolunu denetleyin',
preview: 'Önizleme',
toc: 'İçindekiler',
edit: 'Düzenle',
@@ -150,6 +144,12 @@ export const tr: Record<string, string> = {
changesOpenDiffTab: 'Ayrı bir diff sekmesinde aç',
changesClosePreview: 'Önizlemeyi kapat',
changesResizePreview: 'Önizlemeyi boyutlandır',
changesDiffOpenTitle: 'diff açılır',
changesDiffOpenDesc: '"Ayrı bir diff sekmesinde aç" eylemi diff\'i nereye koyar',
changesDiffOpenFloat: 'Serbest pencere',
changesDiffOpenFloatDesc: 'Görünümün ortasında yüzen pencere — sürükle, boyutlandır, üstte tut',
changesDiffOpenPane: 'Panel',
changesDiffOpenPaneDesc: 'Kaynak panelin altına sabitlenmiş (VSCode tarzı diff bölmesi)',
changesLoadError: 'Oturum dosya kayıtları şu anda kullanılamıyor',
staged: 'Sahnelendi',
unstaged: 'Sahnelenmedi',
@@ -198,13 +198,6 @@ export const tr: Record<string, string> = {
addToConversation: 'Sohbete ekle',
copyRelative: 'Göreli yolu kopyala',
copyAbsolute: 'Mutlak yolu kopyala',
rename: 'Yeniden adlandır',
renameInvalid: 'Ad boş olamaz veya yol ayırıcıları içeremez.',
delete: 'Sil',
deleteTitle: '"{name}" silinsin mi?',
deleteDescFile: 'Bu, dosyayı kalıcı olarak siler. Bu işlem geri alınamaz.',
deleteDescDir: 'Bu, dizini ve tüm içeriğini kalıcı olarak siler. Bu işlem geri alınamaz.',
dismiss: 'Kapat',
download: 'İndir',
uploadFiles: 'Dosyaları yükle',
uploadFolder: 'Klasör yükle',
@@ -223,6 +216,13 @@ export const tr: Record<string, string> = {
settingsIntro: 'Yan kartın ne gösterdiğini ve nasıl davrandığını yönetin',
settingsPopupDesc: '«{feature}» için ilgili seçenekleri yapılandırın',
settingsDone: 'Bitti',
settingsOpenTitle: 'Yeni sohbetlerde varsayılan olarak aç',
settingsOpenDesc: 'Yepyeni sohbetler için yan kartı otomatik genişlet; var olan sohbetler kendi düzenlerini korur',
settingsWidthTitle: 'Varsayılan genişlik payı',
settingsWidthDesc: 'Yeni sohbetler için yan kartın pencere genişliğindeki varsayılan payı (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Sohbet dosyalarını kenar çubuğunda aç',
settingsOpenPathDesc: 'Sohbetteki dosya bağlantılarını (araç satırları, üretilen dosyalar, anılmalar) sistem varsayılan uygulaması yerine kenar çubuğu düzenleyicisinde aç',
settingsOpenToolsTitle: 'Model için kenar çubuğu açma aracı enjekte et',
settingsOpenToolsDesc: 'Etkinleştirildiğinde model, sidebar_open aracıyla kenar çubuğunda dosyaları, klasörleri ve HTTP(S) sayfalarını açabilir (varsayılan kapalı)',
settingsTitleBarTitle: 'Konum uyumluluk modu',
@@ -395,7 +395,7 @@ export const tr: Record<string, string> = {
addPluginsViewerCardDesc: 'Yeni bir dosya türü önizlemesi kaydet',
addPluginsTabDesc: 'Kenar çubuğu sayfaları (sekmeler) eklentilerce genişletilebilir. Eklentiler ctx.betterSidebar hizmeti aracılığıyla kaydolur; Yükle’ye tıklamak kurulum komutunu kopyalar — DSH profilinizin yaşadığı bir terminale yapıştırın ve çalıştırın.',
addPluginsViewerDesc: 'Dosya önizleyicileri eklentilerce genişletilebilir. Eklentiler ctx.betterSidebar hizmeti aracılığıyla kaydolur; Yükle’ye tıklamak kurulum komutunu kopyalar — DSH profilinizin yaşadığı bir terminale yapıştırın ve çalıştırın.',
addPluginsBrowseMore: 'GitHub’da daha fazla eklentiye göz atın (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'GitHub’da daha fazla eklentiye göz atın (topic: dsh-apes-sidebar)',
addPluginsSearch: 'Eklenti adı / açıklamasına göre ara…',
addPluginsNoMatch: 'Eşleşen eklenti yok',
addPluginsRecommended: 'Önerilen eklentiler',
@@ -408,7 +408,7 @@ export const tr: Record<string, string> = {
pluginGithubWorkbenchDesc: 'GitHub Workbench sekmesi: uzak depo ağacı + Issues / Pull requests / Actions tam yazma desteğiyle — Issue/PR oluşturma, yorum yapma, düzenleme, kapatma/yeniden açma, squash·merge·rebase birleştirme (güçlü onay), CI’yi yeniden çalıştırma/iptal etme; repo değiştirici erişilebilir depoları otomatik listeler ve genel depolarda arama yapar; better-sidebar yoksa bağımsız sağ panele geri düşer',
pluginMdExportDesc: 'better-sidebar Markdown araç çubuğuna «Dışa aktar» düğmesi ekler: geçerli .md dosyasını tek tıkla bağımsız HTML biçimine dönüştürür (tablolar/kod blokları/Mermaid diyagramları gömülü, düzen önizleme temasını izler) ve .md dosyasının yanına kaydeder ya da yazdırma penceresinden PDF dışa aktarır',
pluginCodeNavDesc: 'Kod önizleme gezinmesi: dosya türünden dili otomatik algılayıp söz dizimini vurgular, simge şeması (sınıf/yöntem/değişken filtreleri + tek tıkla atlama) ve dosya içi arama (tüm eşleşmeleri vurgula, önceki/sonraki, büyük/küçük harf duyarlı) — better-sidebar düzenleyicisinde kod dosyası önizlemesini devralır',
pluginSuhuangScrollDesc: 'Yerel Suhuang Scroll Runtime ortamını model yapılandırması, bağlantı testi ve kesintisiz değerlendirme denetimi için DSH ayarlarına ve better-sidebar öğesine bağlar; Suhuang Scroll Runtime ve dsh-better-sidebar gerektirir',
pluginSuhuangScrollDesc: 'Yerel Suhuang Scroll Runtime ortamını model yapılandırması, bağlantı testi ve kesintisiz değerlendirme denetimi için DSH ayarlarına ve better-sidebar öğesine bağlar; Suhuang Scroll Runtime ve dsh-apes-sidebar gerektirir',
pluginBetterOverleafDesc: 'better-sidebar için Overleaf sekmesi: doğrudan CDP tarayıcı girişi (üçüncü taraf Chromium destekli), proje listesi/geçiş, <workspace>/overleaf/ altında yerel git aynaları, salt okunur API yedekli çift yönlü git senkronizasyonu ve dosya önizlemesi kenar çubuğu workbench üzerinden',
pluginGitRemotesDesc: 'Git Remotes sekmesi: dal/yukarı akış/ahead-behind, fetch (isteğe bağlı prune), ff-only pull ve yalnızca sekme içi onaydan sonra push. Yerleşik Git sahnele/işle sekmesini değiştirmez; force-push veya model otomatik push aracı sunmaz',
pluginSentinelDesc: 'Koşul odaklı aracı uyandırma: dosya/süreç/port/HTTP/komut/web algılayıcılar koşullar tetiklendiğinde uyuyan oturumları uyandırır; sunucu geneli izleme tablosuyla «Sentinel» sekmesi kaydeder',
@@ -421,29 +421,4 @@ export const tr: Record<string, string> = {
pluginDocsPanelDesc: 'DSH kenar çubuğunda «Genel belgeler»: herhangi bir çalışma alanından kendi Markdown notlarınızı okuyun — dosya listesi, ana hat, Chrome / VS Code’da açma ve kopyalama düğmeleri; belgeler dizini yapılandırılabilir (varsayılan ~/.dsh/docs)',
pluginEgoBrowserDesc: 'DeepSeek Harness için aracı tarayıcı: 32 ego_* aracı gerçek bir Chromium’u yönetir; kenar çubuğundaki yerel «ego tarayıcı» sekmesi aracının ziyaret ettiği her sayfayı canlı gösterir — tıklayarak, sürükleyerek ve yazarak kontrolü devralabilirsiniz. better-sidebar varsa sekme otomatik kaydedilir, yoksa yüzen baloncuğa düşer',
pluginBilingualReaderDesc: 'DSH kenar çubuğunda PDF okuyun: yerel PDF görüntüleme, LLM ile çevirmek için metin seçin, bağlamı kullanarak ve ana sohbetten tamamen izole',
changesRedactOn: "Maskeleme açık: gizli değerler [REDACTED] olarak görünür; kapatmak için tıklayın (kendiniz sorumlusunuz)",
changesRedactOff: "Maskeleme kapalı: içerik olduğu gibi görünür; açmak için tıklayın",
changesRedactOnLabel: "Maskeleme",
changesRedactOffLabel: "Maskeleme yok",
changesRedactBanner: "Maskelendi",
pluginSentinelName: 'dsh-sentinel Uyandırma sistemi',
pluginEgoBrowserName: 'ego-browser Ajan tarayıcısı',
pluginBetterOverleafName: 'dsh-better-overleaf Overleaf sekmesi',
pluginDocsPanelName: 'dsh-docs-panel Genel belgeler',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Git kimlik bilgileri',
pluginGitRemotesName: 'dsh-git-remotes Git uzak depoları',
pluginGithubWorkbenchName: 'dsh-github-workbench GitHub çalışma tezgâhı',
pluginSidebarQaName: 'dsh-sidebar-qa Seç ve sor',
pluginSidenoteName: 'dsh-sidenote Yan sohbet',
pluginServerDeckName: 'dsh-server-deck Sunucu paneli',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel SSH tüneli',
pluginTurnReviewName: 'dsh-turn-review Tur incelemesi',
pluginBilingualReaderName: 'dsh-bilingual-reader İki dilli okuyucu',
pluginOfficeName: 'Office önizlemesi',
pluginMdExportName: 'Markdown dışa aktarma',
pluginCodeNavName: 'Kod önizleme gezgini',
pluginVideoPreviewName: 'Video önizlemesi',
presetDshDesktopDesc: 'Electron gelişmiş (çerçevesiz) mod: macOS üstte 20px ayırır; Windows, WCO yokken başlık çubuğu için 32px ayırır',
}
+18 -43
View File
@@ -19,19 +19,11 @@ export const vi: Record<string, string> = {
files: 'Tệp',
changesSessionEmpty: 'Chưa có thao tác tệp nào trong phiên này',
changesRead: 'Đọc',
changesMdReading: 'Đọc',
changesHtmlRender: "Kết xuất",
changesHtmlRaw: "Nguyên văn",
changesPdfRender: "Kết xuất",
changesPdfRaw: "Nguyên văn",
changesMdRaw: 'Gốc',
changesWrite: 'Ghi',
changesEdit: 'Chỉnh sửa',
changesRunning: 'đang chạy',
changesError: 'lỗi',
changesFold: '{count} dòng… bấm để mở rộng',
changesFoldLoading: 'Đang mở rộng…',
changesFoldUnavailable: 'Không tải được ngữ cảnh',
changesContext: 'ngữ cảnh',
changesPriorUnknown: 'Nội dung trước thay đổi nằm ngoài cửa sổ; hiển thị như toàn bộ mới thêm',
explorer: 'Trình khám phá',
@@ -81,6 +73,7 @@ export const vi: Record<string, string> = {
closeOtherTabs: 'Đóng các tab khác',
closeLeftTabs: 'Đóng tab bên trái',
closeRightTabs: 'Đóng tab bên phải',
moveToFreeWindow: 'Chuyển sang cửa sổ tự do',
floatDropHint: 'Thả để mở trong cửa sổ tự do',
dockToSidebar: 'Quay lại thanh bên',
pinTerminal: 'Ghim Terminal',
@@ -95,6 +88,8 @@ export const vi: Record<string, string> = {
pinnedTerminalScopeGlobal: 'Đã ghim toàn cục',
pinnedRailLabel: 'Terminal đã ghim',
closePinnedTerminal: 'Đóng Terminal',
collapse: 'Thu gọn thanh bên',
expand: 'Mở rộng thanh bên',
collapseBottomPanel: 'Thu gọn panel dưới',
expandBottomPanel: 'Mở rộng panel dưới',
terminalError: 'Kết nối terminal thất bại',
@@ -103,7 +98,6 @@ export const vi: Record<string, string> = {
terminalDepsFailed: 'Dependency terminal node-pty tải thất bại',
terminalDepsHint: 'Chạy lệnh sau trong terminal hoặc cmd trên máy DSH để sửa, rồi nhấn thử lại (node-pty đồng bộ phiên bản với lõi DSH):',
terminalDepsProfile: ' (phát hiện profile: {profile})',
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',
preview: 'Xem trước',
toc: 'Mục lục',
edit: 'Chỉnh sửa',
@@ -150,6 +144,12 @@ export const vi: Record<string, string> = {
changesOpenDiffTab: 'Mở trong tab diff riêng',
changesClosePreview: 'Đóng xem trước',
changesResizePreview: 'Điều chỉnh kích thước xem trước',
changesDiffOpenTitle: 'diff mở thành',
changesDiffOpenDesc: 'Hành động «mở trong tab diff riêng» đặt diff ở đâu',
changesDiffOpenFloat: 'Cửa sổ tự do',
changesDiffOpenFloatDesc: 'Cửa sổ nổi giữa khung nhìn — kéo, đổi kích thước, ghim trên cùng',
changesDiffOpenPane: 'Bảng',
changesDiffOpenPaneDesc: 'Gắn dưới bảng nguồn (chia diff kiểu VSCode)',
changesLoadError: 'Bản ghi tệp phiên hiện không khả dụng',
staged: 'Đã stage',
unstaged: 'Chưa stage',
@@ -198,13 +198,6 @@ export const vi: Record<string, string> = {
addToConversation: 'Thêm vào cuộc trò chuyện',
copyRelative: 'Sao chép đường dẫn tương đối',
copyAbsolute: 'Sao chép đường dẫn tuyệt đối',
rename: 'Đổi tên',
renameInvalid: 'Tên không được để trống hoặc chứa dấu phân tách đường dẫn.',
delete: 'Xóa',
deleteTitle: 'Xóa "{name}"?',
deleteDescFile: 'Thao tác này sẽ xóa vĩnh viễn tệp. Không thể hoàn tác.',
deleteDescDir: 'Thao tác này sẽ xóa vĩnh viễn thư mục và toàn bộ nội dung của nó. Không thể hoàn tác.',
dismiss: 'Đóng',
download: 'Tải xuống',
uploadFiles: 'Tải tệp lên',
uploadFolder: 'Tải thư mục lên',
@@ -223,6 +216,13 @@ export const vi: Record<string, string> = {
settingsIntro: 'Quản lý nội dung hiển thị và hành vi mặc định của thẻ bên',
settingsPopupDesc: 'Cấu hình tùy chọn cho «{feature}»',
settingsDone: 'Xong',
settingsOpenTitle: 'Mở mặc định cho phiên mới',
settingsOpenDesc: 'Tự động mở rộng thẻ bên cho phiên mới; phiên đã có giữ nguyên bố cục',
settingsWidthTitle: 'Tỷ lệ chiều rộng mặc định',
settingsWidthDesc: 'Tỷ lệ phần trăm chiều rộng cửa sổ mà thẻ bên chiếm cho phiên mới (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: 'Mở tệp chat trong thanh bên',
settingsOpenPathDesc: 'Khi nhấp link tệp trong chat (dòng công cụ, danh sách sản phẩm, nhắc tệp), mở trong trình soạn thảo thanh bên thay vì ứng dụng mặc định hệ thống',
settingsOpenToolsTitle: 'Tiêm công cụ mở thanh bên cho mô hình',
settingsOpenToolsDesc: 'Khi bật, mô hình có thể mở tệp, thư mục và trang HTTP(S) trong thanh bên qua công cụ sidebar_open (mặc định tắt)',
settingsTitleBarTitle: 'Chế độ tương thích thanh tiêu đề',
@@ -395,7 +395,7 @@ export const vi: Record<string, string> = {
addPluginsViewerCardDesc: 'Đăng ký xem trước loại tệp mới',
addPluginsTabDesc: 'Trang thanh bên (tab) có thể mở rộng qua plugin. Plugin đăng ký qua service ctx.betterSidebar; nhấp «Cài đặt» để sao chép lệnh cài, dán vào terminal nơi DSH chạy rồi thực thi.',
addPluginsViewerDesc: 'Trình xem tệp có thể mở rộng qua plugin. Plugin đăng ký qua service ctx.betterSidebar; nhấp «Cài đặt» để sao chép lệnh cài, dán vào terminal nơi DSH chạy rồi thực thi.',
addPluginsBrowseMore: 'Duyệt thêm plugin trên GitHub (topic: dsh-better-sidebar)',
addPluginsBrowseMore: 'Duyệt thêm plugin trên GitHub (topic: dsh-apes-sidebar)',
addPluginsSearch: 'Tìm theo tên / mô tả plugin…',
addPluginsNoMatch: 'Không có plugin phù hợp',
addPluginsRecommended: 'Plugin đề xuất',
@@ -408,7 +408,7 @@ export const vi: Record<string, string> = {
pluginGithubWorkbenchDesc: 'Tab GitHub Workbench: cây repo từ xa + Issues / Pull requests / Actions với hỗ trợ ghi đầy đủ — tạo Issue/PR, bình luận, chỉnh sửa, đóng/mở lại, merge squash·merge·rebase (xác nhận mạnh), chạy lại/hủy CI; bộ chuyển repo tự liệt kê các repo có quyền truy cập và tìm kiếm repo công khai; khi chưa cài better-sidebar sẽ tự động chuyển sang panel bên phải độc lập',
pluginMdExportDesc: 'Thêm nút «Xuất» vào thanh công cụ Markdown của better-sidebar: một cú nhấp chuyển .md hiện tại thành HTML độc lập (bảng/khối mã/sơ đồ Mermaid nhúng, bố cục theo theme xem trước) và lưu cạnh tệp .md, hoặc xuất PDF qua hộp thoại in',
pluginCodeNavDesc: 'Điều hướng xem trước mã: tự nhận diện ngôn ngữ theo loại tệp và tô sáng cú pháp, phác thảo ký hiệu (lọc lớp/phương thức/biến + nhảy một cú nhấp), tìm kiếm trong tệp (tô sáng mọi kết quả, trước/sau, phân biệt hoa thường) — đảm nhận xem trước tệp mã trong trình soạn thảo better-sidebar',
pluginSuhuangScrollDesc: 'Kết nối Suhuang Scroll Runtime cục bộ với phần cài đặt DSH và better-sidebar để cấu hình mô hình, kiểm tra kết nối và điều khiển chấm bài liên tục; yêu cầu Suhuang Scroll Runtime và dsh-better-sidebar',
pluginSuhuangScrollDesc: 'Kết nối Suhuang Scroll Runtime cục bộ với phần cài đặt DSH và better-sidebar để cấu hình mô hình, kiểm tra kết nối và điều khiển chấm bài liên tục; yêu cầu Suhuang Scroll Runtime và dsh-apes-sidebar',
pluginBetterOverleafDesc: 'Tab Overleaf cho better-sidebar: đăng nhập trực tiếp qua trình duyệt CDP (hỗ trợ Chromium bên thứ ba), danh sách/chuyển dự án, mirror git cục bộ trong <workspace>/overleaf/, đồng bộ git hai chiều với dự phòng API chỉ đọc, xem trước tệp qua workbench của thanh bên',
pluginGitRemotesDesc: 'Tab Git Remotes của better-sidebar: xem branch/upstream/ahead-behind, fetch (có thể prune), pull ff-only, push chỉ sau khi xác nhận trong tab. Không thay thế tab stage/commit Git built-in, cũng không cung cấp force-push hay công cụ auto-push cho mô hình',
pluginSentinelDesc: 'Hệ thống đánh thức agent theo điều kiện: cảm biến file/process/port/HTTP/command/webhook, khi điều kiện đạt sẽ tự đánh thức phiên ngủ; đăng ký tab «Sentinel» hiển thị bảng giám sát toàn server',
@@ -421,29 +421,4 @@ export const vi: Record<string, string> = {
pluginDocsPanelDesc: '«Tài liệu toàn cục» trong thanh bên DSH: ghi chú Markdown toàn cục, đọc từ bất kỳ workspace nào — danh sách chọn để đọc, outline hover để nhảy, mở ngoài trong Chrome / VS Code, sao chép code, thư mục cấu hình được (mặc định ~/.dsh/docs)',
pluginEgoBrowserDesc: 'Trình duyệt agent cho DeepSeek Harness: 32 công cụ ego_* điều khiển Chromium thật; tab «ego browser» gốc trong thanh bên hiển thị trực tiếp mọi trang agent truy cập — bạn có thể bấm, kéo và gõ để tiếp quản. Tự động đăng ký tab khi có better-sidebar, nếu không thì rơi về bong bóng nổi',
pluginBilingualReaderDesc: 'Đọc PDF trong thanh bên DSH: hiển thị PDF gốc, chọn văn bản để dịch bằng LLM, dùng ngữ cảnh và hoàn toàn tách biệt khỏi cuộc trò chuyện chính',
changesRedactOn: "Đang làm mờ: giá trị nhạy cảm hiển thị [REDACTED]; nhấp để tắt (tự chịu rủi ro)",
changesRedactOff: "Đã tắt làm mờ: nội dung hiển thị nguyên bản; nhấp để bật",
changesRedactOnLabel: "Đang làm mờ",
changesRedactOffLabel: "Không làm mờ",
changesRedactBanner: "Đã làm mờ",
pluginSentinelName: 'dsh-sentinel Hệ thống đánh thức',
pluginEgoBrowserName: 'ego-browser Trình duyệt agent',
pluginBetterOverleafName: 'dsh-better-overleaf Tab Overleaf',
pluginDocsPanelName: 'dsh-docs-panel Tài liệu toàn cục',
pluginFlowglassName: 'dsh-flowglass Flowglass',
pluginGitForgeName: 'dsh-git-forge Thông tin đăng nhập Git',
pluginGitRemotesName: 'dsh-git-remotes Remote Git',
pluginGithubWorkbenchName: 'dsh-github-workbench Bàn làm việc GitHub',
pluginSidebarQaName: 'dsh-sidebar-qa Chọn & hỏi',
pluginSidenoteName: 'dsh-sidenote Trò chuyện bên',
pluginServerDeckName: 'dsh-server-deck Bảng máy chủ',
pluginSuhuangScrollName: 'dsh-suhuang-scroll Suhuang Scroll',
pluginSshTunnelName: 'dsh-ssh-tunnel Đường hầm SSH',
pluginTurnReviewName: 'dsh-turn-review Duyệt vòng',
pluginBilingualReaderName: 'dsh-bilingual-reader Trình đọc song ngữ',
pluginOfficeName: 'Xem trước Office',
pluginMdExportName: 'Xuất Markdown',
pluginCodeNavName: 'Trình điều hướng xem trước mã',
pluginVideoPreviewName: 'Xem trước video',
presetDshDesktopDesc: 'Chế độ Electron nâng cao (không viền): macOS dành 20px ở trên cùng; Windows dành 32px cho thanh tiêu đề khi không có WCO',
}
+18 -43
View File
@@ -34,19 +34,11 @@ export const zhHK: Record<string, string> = {
files: '檔案',
changesSessionEmpty: '本工作階段還沒有檔案操作',
changesRead: '讀取',
changesMdReading: '閱讀',
changesHtmlRender: "渲染",
changesHtmlRaw: "原文",
changesPdfRender: "渲染",
changesPdfRaw: "原文",
changesMdRaw: '原文',
changesWrite: '寫入',
changesEdit: '編輯',
changesRunning: '執行中',
changesError: '出錯',
changesFold: '{count} 行…點擊展開',
changesFoldLoading: '展開中…',
changesFoldUnavailable: '上下文未載入',
changesContext: '上下文',
changesPriorUnknown: '變更前的內容不在視窗內,顯示為全部新增',
explorer: '檔案總管',
@@ -96,6 +88,7 @@ export const zhHK: Record<string, string> = {
closeOtherTabs: '關閉其他標籤',
closeLeftTabs: '關閉左側標籤',
closeRightTabs: '關閉右側標籤',
moveToFreeWindow: '移動到自由視窗',
floatDropHint: '放開以在自由視窗中開啟',
dockToSidebar: '返回側邊欄',
pinTerminal: '固定終端',
@@ -110,6 +103,8 @@ export const zhHK: Record<string, string> = {
pinnedTerminalScopeGlobal: '固定到全域',
pinnedRailLabel: '固定終端',
closePinnedTerminal: '關閉終端',
collapse: '收起側邊欄',
expand: '展開側邊欄',
collapseBottomPanel: '收起底部面板',
expandBottomPanel: '展開底部面板',
terminalError: '終端連線失敗',
@@ -118,7 +113,6 @@ export const zhHK: Record<string, string> = {
terminalDepsFailed: '終端依賴 node-pty 載入失敗',
terminalDepsHint: '在 DSH 所在環境的終端或 cmd 中執行以下命令修復,然後點重試(node-pty 與 DSH 核心保持同一版本):',
terminalDepsProfile: '(偵測到 profile:{profile})',
terminalShellNotFound: '未找到配置的 Shell:{name},請到 設定 → 側邊卡片 → 終端 檢查 Shell 路徑',
preview: '預覽',
toc: '目錄',
edit: '編輯',
@@ -165,6 +159,12 @@ export const zhHK: Record<string, string> = {
changesOpenDiffTab: '在獨立分頁中開啟',
changesClosePreview: '關閉預覽',
changesResizePreview: '調整預覽高度',
changesDiffOpenTitle: '差異展開方式',
changesDiffOpenDesc: '「展開為獨立分頁」把 diff 放到哪裡',
changesDiffOpenFloat: '浮動視窗',
changesDiffOpenFloatDesc: '作為自由視窗置中開啟,可拖拽、縮放、置頂',
changesDiffOpenPane: '面板',
changesDiffOpenPaneDesc: '停駐在來源面板下方(VSCode 式 diff 分欄)',
changesLoadError: '工作階段檔案記錄暫不可用',
staged: '已暫存',
unstaged: '未暫存',
@@ -213,13 +213,6 @@ export const zhHK: Record<string, string> = {
addToConversation: '新增到對話',
copyRelative: '複製相對位址',
copyAbsolute: '複製絕對位址',
rename: '重新命名',
renameInvalid: '名稱不可為空,且不可包含路徑分隔符。',
delete: '刪除',
deleteTitle: '刪除「{name}」?',
deleteDescFile: '將永久刪除該檔案,此操作無法復原。',
deleteDescDir: '將永久刪除該目錄及其全部內容,此操作無法復原。',
dismiss: '關閉',
download: '下載',
uploadFiles: '上傳檔案',
uploadFolder: '上傳資料夾',
@@ -238,6 +231,13 @@ export const zhHK: Record<string, string> = {
settingsIntro: '管理側邊卡片的顯示內容與預設行為',
settingsPopupDesc: '為「{feature}」設定相關選項',
settingsDone: '完成',
settingsOpenTitle: '新工作階段預設開啟',
settingsOpenDesc: '新增工作階段時自動展開側邊卡片;已存在的工作階段保持各自佈局',
settingsWidthTitle: '預設寬度佔比',
settingsWidthDesc: '新增工作階段時側邊卡片佔視窗寬度的百分比 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '聊天區檔案在側邊欄開啟',
settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式',
settingsOpenToolsTitle: '為模型注入側邊欄開啟工具',
settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)',
settingsTitleBarTitle: '位置相容模式',
@@ -410,7 +410,7 @@ export const zhHK: Record<string, string> = {
addPluginsViewerCardDesc: '註冊新的檔案類型預覽',
addPluginsTabDesc: '側邊欄頁面(Tab)可以由插件擴展。插件透過 ctx.betterSidebar 服務註冊;點擊「安裝」複製安裝命令,貼到 DSH 所在環境的終端執行。',
addPluginsViewerDesc: '檔案預覽器可以由插件擴展。插件透過 ctx.betterSidebar 服務註冊;點擊「安裝」複製安裝命令,貼到 DSH 所在環境的終端執行。',
addPluginsBrowseMore: '在 GitHub 上瀏覽更多插件(topic: dsh-better-sidebar)',
addPluginsBrowseMore: '在 GitHub 上瀏覽更多插件(topic: dsh-apes-sidebar)',
addPluginsSearch: '搜尋插件名稱 / 描述…',
addPluginsNoMatch: '沒有匹配的插件',
addPluginsRecommended: '推薦插件',
@@ -423,7 +423,7 @@ export const zhHK: Record<string, string> = {
pluginGithubWorkbenchDesc: 'better-sidebar「GitHub 工作台」Tab:遠端儲存庫目錄樹 + Issues / Pull requests / Actions 頁簽,讀取之外支援新建 Issue/PR、評論、編輯、關閉重開、squash·merge·rebase 合併(強確認)與重跑/取消 CI;儲存庫彈出層自動拉取有權限列表並支援公開儲存庫搜尋;未安裝 better-sidebar 時自動降級為獨立右側面板',
pluginMdExportDesc: '在 better-sidebar 的 Markdown 工具列上新增「匯出」按鈕:一鍵把目前 .md 渲染為獨立 HTML(表格/程式碼區塊/Mermaid 圖表內嵌,版面跟隨預覽主題)直接儲存到同目錄,或透過列印對話框匯出為 PDF',
pluginCodeNavDesc: '程式碼預覽導覽:按檔案類型自動識別語言並高亮語法,符號大綱(類別/方法/變數篩選 + 一鍵跳轉),檔案內搜尋(全部符合高亮、上/下一處、區分大小寫),接管 better-sidebar 的程式碼檔案預覽',
pluginSuhuangScrollDesc: '把本機蘇黃共閱 Runtime 接入 DSH 設定與 better-sidebar,支援模型設定、連線測試及連續閱卷控制;使用前須安裝 Suhuang Scroll Runtime 與 dsh-better-sidebar',
pluginSuhuangScrollDesc: '把本機蘇黃共閱 Runtime 接入 DSH 設定與 better-sidebar,支援模型設定、連線測試及連續閱卷控制;使用前須安裝 Suhuang Scroll Runtime 與 dsh-apes-sidebar',
pluginBetterOverleafDesc: 'better-sidebar 的 Overleaf 分頁:直連 CDP 瀏覽器登入(支援第三方 Chromium),專案列表/切換,<workspace>/overleaf/ 本地 git 映像,git 雙向同步(API 唯讀兜底),檔案預覽走側邊欄工作台',
pluginGitRemotesDesc: 'better-sidebar Git 遠端 Tab:看分支/上游/ahead-behind,fetch(可 prune)、ff-only pull、確認後才 push。不替換內建 Git 的暫存/提交,也不提供 force-push 或模型自動推送',
pluginSentinelDesc: '條件驅動的 agent 喚醒系統:檔案/程序/連接埠/HTTP/命令/webhook 感測器,條件達成自動喚醒休眠工作階段;註冊「哨兵」Tab 展示伺服器全域監控表',
@@ -436,29 +436,4 @@ export const zhHK: Record<string, string> = {
pluginDocsPanelDesc: 'DSH 側邊欄裡的「全域文件」:全域 Markdown 筆記,任何工作區隨時可讀——列表點選閱讀、懸浮大綱跳轉、Chrome / VS Code 外部開啟、程式碼複製,目錄可設定(預設 ~/.dsh/docs)',
pluginEgoBrowserDesc: '將 CitroLabs/ego-lite 接進 DeepSeek Harness 的 agent 瀏覽器:32 個 ego_* 工具驅動真實 Chromium,側邊欄原生「ego 瀏覽器」Tab 實時觀察 agent 逛的每個頁面,可直接點擊/拖拽/輸入接管;裝 better-sidebar 時自動註冊 Tab,沒裝則退回浮動浮窗',
pluginBilingualReaderDesc: '喺 DSH 側邊欄讀論文 PDF:原生 PDF 顯示,揀一段文字就用大模型劃詞翻譯,結合上下文、完全隔離主對話,只做閱讀輔助',
changesRedactOn: "已開啟脫敏:敏感內容以 [REDACTED] 顯示;點擊關閉(自擔風險)",
changesRedactOff: "脫敏已關閉:將原樣顯示文件內容;點擊開啟",
changesRedactOnLabel: "脫敏中",
changesRedactOffLabel: "脫敏關",
changesRedactBanner: "已脫敏",
pluginSentinelName: 'dsh-sentinel 喚醒系統',
pluginEgoBrowserName: 'ego-browser Agent 瀏覽器',
pluginBetterOverleafName: 'dsh-better-overleaf Overleaf 標籤',
pluginDocsPanelName: 'dsh-docs-panel 全域文件',
pluginFlowglassName: 'dsh-flowglass 流鏡',
pluginGitForgeName: 'dsh-git-forge Git 憑證',
pluginGitRemotesName: 'dsh-git-remotes Git 遠端',
pluginGithubWorkbenchName: 'dsh-github-workbench GitHub 工作台',
pluginSidebarQaName: 'dsh-sidebar-qa 劃選追問',
pluginSidenoteName: 'dsh-sidenote 側邊對話',
pluginServerDeckName: 'dsh-server-deck 伺服器甲板',
pluginSuhuangScrollName: 'dsh-suhuang-scroll 蘇黃共閱',
pluginSshTunnelName: 'dsh-ssh-tunnel SSH 隧道',
pluginTurnReviewName: 'dsh-turn-review 本輪審查',
pluginBilingualReaderName: 'dsh-bilingual-reader 雙語閱讀',
pluginOfficeName: 'Office 預覽插件',
pluginMdExportName: 'Markdown 匯出插件',
pluginCodeNavName: '程式碼預覽導航',
pluginVideoPreviewName: '影片預覽插件',
presetDshDesktopDesc: 'Electron 進階模式(無邊框):macOS 頂欄 20px、Windows 無 WCO 時 32px 標題欄讓位',
}
+18 -43
View File
@@ -34,19 +34,11 @@ export const zhMO: Record<string, string> = {
files: '檔案',
changesSessionEmpty: '本工作階段還沒有檔案操作',
changesRead: '讀取',
changesMdReading: '閱讀',
changesHtmlRender: "渲染",
changesHtmlRaw: "原文",
changesPdfRender: "渲染",
changesPdfRaw: "原文",
changesMdRaw: '原文',
changesWrite: '寫入',
changesEdit: '編輯',
changesRunning: '執行中',
changesError: '出錯',
changesFold: '{count} 行…點擊展開',
changesFoldLoading: '展開中…',
changesFoldUnavailable: '上下文未載入',
changesContext: '上下文',
changesPriorUnknown: '變更前的內容不在視窗內,顯示為全部新增',
explorer: '檔案總管',
@@ -96,6 +88,7 @@ export const zhMO: Record<string, string> = {
closeOtherTabs: '關閉其他標籤',
closeLeftTabs: '關閉左側標籤',
closeRightTabs: '關閉右側標籤',
moveToFreeWindow: '移動到自由視窗',
floatDropHint: '放開以在自由視窗中開啟',
dockToSidebar: '返回側邊欄',
pinTerminal: '固定終端',
@@ -110,6 +103,8 @@ export const zhMO: Record<string, string> = {
pinnedTerminalScopeGlobal: '固定到全域',
pinnedRailLabel: '固定終端',
closePinnedTerminal: '關閉終端',
collapse: '收起側邊欄',
expand: '展開側邊欄',
collapseBottomPanel: '收起底部面板',
expandBottomPanel: '展開底部面板',
terminalError: '終端連線失敗',
@@ -118,7 +113,6 @@ export const zhMO: Record<string, string> = {
terminalDepsFailed: '終端依賴 node-pty 載入失敗',
terminalDepsHint: '在 DSH 所在環境的終端或 cmd 中執行以下命令修復,然後點重試(node-pty 與 DSH 核心保持同一版本):',
terminalDepsProfile: '(偵測到 profile:{profile})',
terminalShellNotFound: '未找到配置的 Shell:{name},請到 設定 → 側邊卡片 → 終端 檢查 Shell 路徑',
preview: '預覽',
toc: '目錄',
edit: '編輯',
@@ -165,6 +159,12 @@ export const zhMO: Record<string, string> = {
changesOpenDiffTab: '在獨立分頁中開啟',
changesClosePreview: '關閉預覽',
changesResizePreview: '調整預覽高度',
changesDiffOpenTitle: '差異展開方式',
changesDiffOpenDesc: '「展開為獨立分頁」把 diff 放到哪裡',
changesDiffOpenFloat: '浮動視窗',
changesDiffOpenFloatDesc: '作為自由視窗置中開啟,可拖拽、縮放、置頂',
changesDiffOpenPane: '面板',
changesDiffOpenPaneDesc: '停駐在來源面板下方(VSCode 式 diff 分欄)',
changesLoadError: '工作階段檔案記錄暫不可用',
staged: '已暫存',
unstaged: '未暫存',
@@ -213,13 +213,6 @@ export const zhMO: Record<string, string> = {
addToConversation: '新增到對話',
copyRelative: '複製相對位址',
copyAbsolute: '複製絕對位址',
rename: '重新命名',
renameInvalid: '名稱不可為空,且不可包含路徑分隔符。',
delete: '刪除',
deleteTitle: '刪除「{name}」?',
deleteDescFile: '將永久刪除該檔案,此操作無法復原。',
deleteDescDir: '將永久刪除該目錄及其全部內容,此操作無法復原。',
dismiss: '關閉',
download: '下載',
uploadFiles: '上傳檔案',
uploadFolder: '上傳資料夾',
@@ -238,6 +231,13 @@ export const zhMO: Record<string, string> = {
settingsIntro: '管理側邊卡片的顯示內容與預設行為',
settingsPopupDesc: '為「{feature}」設定相關選項',
settingsDone: '完成',
settingsOpenTitle: '新工作階段預設開啟',
settingsOpenDesc: '新增工作階段時自動展開側邊卡片;已存在的工作階段保持各自佈局',
settingsWidthTitle: '預設寬度佔比',
settingsWidthDesc: '新增工作階段時側邊卡片佔視窗寬度的百分比 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '聊天區檔案在側邊欄開啟',
settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式',
settingsOpenToolsTitle: '為模型注入側邊欄開啟工具',
settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)',
settingsTitleBarTitle: '位置相容模式',
@@ -410,7 +410,7 @@ export const zhMO: Record<string, string> = {
addPluginsViewerCardDesc: '註冊新的檔案類型預覽',
addPluginsTabDesc: '側邊欄頁面(Tab)可以由插件擴展。插件透過 ctx.betterSidebar 服務註冊;點擊「安裝」複製安裝命令,貼到 DSH 所在環境的終端執行。',
addPluginsViewerDesc: '檔案預覽器可以由插件擴展。插件透過 ctx.betterSidebar 服務註冊;點擊「安裝」複製安裝命令,貼到 DSH 所在環境的終端執行。',
addPluginsBrowseMore: '在 GitHub 上瀏覽更多插件(topic: dsh-better-sidebar)',
addPluginsBrowseMore: '在 GitHub 上瀏覽更多插件(topic: dsh-apes-sidebar)',
addPluginsSearch: '搜尋插件名稱 / 描述…',
addPluginsNoMatch: '沒有匹配的插件',
addPluginsRecommended: '推薦插件',
@@ -423,7 +423,7 @@ export const zhMO: Record<string, string> = {
pluginGithubWorkbenchDesc: 'better-sidebar「GitHub 工作台」Tab:遠端儲存庫目錄樹 + Issues / Pull requests / Actions 頁簽,讀取之外支援新建 Issue/PR、評論、編輯、關閉重開、squash·merge·rebase 合併(強確認)與重跑/取消 CI;儲存庫彈層自動拉取有權限清單並支援公開儲存庫搜尋;未安裝 better-sidebar 時自動降級為獨立右側面板',
pluginMdExportDesc: '在 better-sidebar 的 Markdown 工具列上新增「匯出」按鈕:一鍵把目前 .md 渲染為獨立 HTML(表格/程式碼區塊/Mermaid 圖表內嵌,版面跟隨預覽主題)直接儲存到同目錄,或透過列印對話框匯出為 PDF',
pluginCodeNavDesc: '程式碼預覽導覽:按檔案類型自動識別語言並高亮語法,符號大綱(類別/方法/變數篩選 + 一鍵跳轉),檔案內搜尋(全部符合高亮、上/下一處、區分大小寫),接管 better-sidebar 的程式碼檔案預覽',
pluginSuhuangScrollDesc: '把本機蘇黃共閱 Runtime 接入 DSH 設定與 better-sidebar,支援模型設定、連線測試及連續閱卷控制;使用前須安裝 Suhuang Scroll Runtime 與 dsh-better-sidebar',
pluginSuhuangScrollDesc: '把本機蘇黃共閱 Runtime 接入 DSH 設定與 better-sidebar,支援模型設定、連線測試及連續閱卷控制;使用前須安裝 Suhuang Scroll Runtime 與 dsh-apes-sidebar',
pluginBetterOverleafDesc: 'better-sidebar 的 Overleaf 分頁:直連 CDP 瀏覽器登入(支援第三方 Chromium),專案列表/切換,<workspace>/overleaf/ 本地 git 映像,git 雙向同步(API 唯讀兜底),檔案預覽走側邊欄工作台',
pluginGitRemotesDesc: 'better-sidebar Git 遠端 Tab:看分支/上游/ahead-behind,fetch(可 prune)、ff-only pull、確認後才 push。不替換內建 Git 的暫存/提交,也不提供 force-push 或模型自動推送',
pluginSentinelDesc: '條件驅動的 agent 喚醒系統:檔案/程序/連接埠/HTTP/命令/webhook 感測器,條件達成自動喚醒休眠工作階段;註冊「哨兵」Tab 展示伺服器全域監控表',
@@ -436,29 +436,4 @@ export const zhMO: Record<string, string> = {
pluginDocsPanelDesc: 'DSH 側邊欄裡的「全域文件」:全域 Markdown 筆記,任何工作區隨時可讀——列表點選閱讀、懸浮大綱跳轉、Chrome / VS Code 外部開啟、程式碼複製,目錄可設定(預設 ~/.dsh/docs)',
pluginEgoBrowserDesc: '將 CitroLabs/ego-lite 接進 DeepSeek Harness 的 agent 瀏覽器:32 個 ego_* 工具驅動真實 Chromium,側邊欄原生「ego 瀏覽器」Tab 實時觀察 agent 逛的每個頁面,可直接點擊/拖拽/輸入接管;裝 better-sidebar 時自動註冊 Tab,沒裝則退回浮動浮窗',
pluginBilingualReaderDesc: '喺 DSH 側邊欄讀論文 PDF:原生 PDF 顯示,揀一段文字就用大模型劃詞翻譯,結合上下文、完全隔離主對話,只做閱讀輔助',
changesRedactOn: "已開啟脫敏:敏感內容以 [REDACTED] 顯示;點擊關閉(自擔風險)",
changesRedactOff: "脫敏已關閉:將原樣顯示文件內容;點擊開啟",
changesRedactOnLabel: "脫敏中",
changesRedactOffLabel: "脫敏關",
changesRedactBanner: "已脫敏",
pluginSentinelName: 'dsh-sentinel 喚醒系統',
pluginEgoBrowserName: 'ego-browser Agent 瀏覽器',
pluginBetterOverleafName: 'dsh-better-overleaf Overleaf 標籤',
pluginDocsPanelName: 'dsh-docs-panel 全域文件',
pluginFlowglassName: 'dsh-flowglass 流鏡',
pluginGitForgeName: 'dsh-git-forge Git 憑證',
pluginGitRemotesName: 'dsh-git-remotes Git 遠端',
pluginGithubWorkbenchName: 'dsh-github-workbench GitHub 工作台',
pluginSidebarQaName: 'dsh-sidebar-qa 劃選追問',
pluginSidenoteName: 'dsh-sidenote 側邊對話',
pluginServerDeckName: 'dsh-server-deck 伺服器甲板',
pluginSuhuangScrollName: 'dsh-suhuang-scroll 蘇黃共閱',
pluginSshTunnelName: 'dsh-ssh-tunnel SSH 隧道',
pluginTurnReviewName: 'dsh-turn-review 本輪審查',
pluginBilingualReaderName: 'dsh-bilingual-reader 雙語閱讀',
pluginOfficeName: 'Office 預覽插件',
pluginMdExportName: 'Markdown 匯出插件',
pluginCodeNavName: '代碼預覽導航',
pluginVideoPreviewName: '影片預覽插件',
presetDshDesktopDesc: 'Electron 進階模式(無邊框):macOS 頂欄 20px、Windows 無 WCO 時 32px 標題欄讓位',
}
+18 -43
View File
@@ -34,19 +34,11 @@ export const zhTW: Record<string, string> = {
files: '檔案',
changesSessionEmpty: '本作業階段還沒有檔案操作',
changesRead: '讀取',
changesMdReading: '閱讀',
changesHtmlRender: "渲染",
changesHtmlRaw: "原文",
changesPdfRender: "渲染",
changesPdfRaw: "原文",
changesMdRaw: '原文',
changesWrite: '寫入',
changesEdit: '編輯',
changesRunning: '執行中',
changesError: '出錯',
changesFold: '{count} 行…點擊展開',
changesFoldLoading: '展開中…',
changesFoldUnavailable: '上下文未載入',
changesContext: '上下文',
changesPriorUnknown: '變更前的內容不在視窗內,顯示為全部新增',
explorer: '檔案總管',
@@ -96,6 +88,7 @@ export const zhTW: Record<string, string> = {
closeOtherTabs: '關閉其他標籤',
closeLeftTabs: '關閉左側標籤',
closeRightTabs: '關閉右側標籤',
moveToFreeWindow: '移動到自由視窗',
floatDropHint: '放開以在自由視窗中開啟',
dockToSidebar: '返回側邊欄',
pinTerminal: '固定終端',
@@ -110,6 +103,8 @@ export const zhTW: Record<string, string> = {
pinnedTerminalScopeGlobal: '固定到全域',
pinnedRailLabel: '固定終端',
closePinnedTerminal: '關閉終端',
collapse: '收起側邊欄',
expand: '展開側邊欄',
collapseBottomPanel: '收起底部面板',
expandBottomPanel: '展開底部面板',
terminalError: '終端連線失敗',
@@ -118,7 +113,6 @@ export const zhTW: Record<string, string> = {
terminalDepsFailed: '終端依賴 node-pty 載入失敗',
terminalDepsHint: '在 DSH 所在環境的終端或 cmd 中執行以下命令修復,然後點重試(node-pty 與 DSH 核心保持同一版本):',
terminalDepsProfile: '(偵測到 profile:{profile})',
terminalShellNotFound: '未找到配置的 Shell:{name},請到 設定 → 側邊卡片 → 終端 檢查 Shell 路徑',
preview: '預覽',
toc: '目錄',
edit: '編輯',
@@ -165,6 +159,12 @@ export const zhTW: Record<string, string> = {
changesOpenDiffTab: '在獨立頁籤中開啟',
changesClosePreview: '關閉預覽',
changesResizePreview: '調整預覽高度',
changesDiffOpenTitle: '差異展開方式',
changesDiffOpenDesc: '「展開為獨立頁籤」把 diff 放到哪裡',
changesDiffOpenFloat: '浮動視窗',
changesDiffOpenFloatDesc: '作為自由視窗置中開啟,可拖拽、縮放、置頂',
changesDiffOpenPane: '面板',
changesDiffOpenPaneDesc: '停駐在來源面板下方(VSCode 式 diff 分欄)',
changesLoadError: '工作階段檔案記錄暫不可用',
staged: '已暫存',
unstaged: '未暫存',
@@ -213,13 +213,6 @@ export const zhTW: Record<string, string> = {
addToConversation: '新增到對話',
copyRelative: '複製相對位址',
copyAbsolute: '複製絕對位址',
rename: '重新命名',
renameInvalid: '名稱不可為空,且不可包含路徑分隔符。',
delete: '刪除',
deleteTitle: '刪除「{name}」?',
deleteDescFile: '將永久刪除該檔案,此操作無法復原。',
deleteDescDir: '將永久刪除該目錄及其全部內容,此操作無法復原。',
dismiss: '關閉',
download: '下載',
uploadFiles: '上傳檔案',
uploadFolder: '上傳資料夾',
@@ -238,6 +231,13 @@ export const zhTW: Record<string, string> = {
settingsIntro: '管理側邊卡片的顯示內容與預設行為',
settingsPopupDesc: '為「{feature}」設定相關選項',
settingsDone: '完成',
settingsOpenTitle: '新工作階段預設開啟',
settingsOpenDesc: '新增工作階段時自動展開側邊卡片;已存在的工作階段保持各自佈局',
settingsWidthTitle: '預設寬度佔比',
settingsWidthDesc: '新增工作階段時側邊卡片佔視窗寬度的百分比 (20–60)',
settingsWidthSuffix: '%',
settingsOpenPathTitle: '聊天區檔案在側邊欄開啟',
settingsOpenPathDesc: '在聊天裡點擊檔案連結(工具行、產物列表、檔案提及)時,在側邊欄編輯器中開啟,不再呼叫系統預設應用程式',
settingsOpenToolsTitle: '為模型注入側邊欄開啟工具',
settingsOpenToolsDesc: '開啟後,模型可透過 sidebar_open 工具在側邊欄主動開啟檔案、資料夾和 HTTP(S) 網頁(預設關閉)',
settingsTitleBarTitle: '位置相容模式',
@@ -410,7 +410,7 @@ export const zhTW: Record<string, string> = {
addPluginsViewerCardDesc: '註冊新的檔案類型預覽',
addPluginsTabDesc: '側邊欄頁面(Tab)可以由插件擴展。插件透過 ctx.betterSidebar 服務註冊;點擊「安裝」複製安裝命令,貼到 DSH 所在環境的終端執行。',
addPluginsViewerDesc: '檔案預覽器可以由插件擴展。插件透過 ctx.betterSidebar 服務註冊;點擊「安裝」複製安裝命令,貼到 DSH 所在環境的終端執行。',
addPluginsBrowseMore: '在 GitHub 上瀏覽更多插件(topic: dsh-better-sidebar)',
addPluginsBrowseMore: '在 GitHub 上瀏覽更多插件(topic: dsh-apes-sidebar)',
addPluginsSearch: '搜尋插件名稱 / 描述…',
addPluginsNoMatch: '沒有匹配的插件',
addPluginsRecommended: '推薦插件',
@@ -423,7 +423,7 @@ export const zhTW: Record<string, string> = {
pluginGithubWorkbenchDesc: 'better-sidebar「GitHub 工作台」Tab:遠端儲存庫目錄樹 + Issues / Pull requests / Actions 頁籤,讀取之外支援建立 Issue/PR、留言、編輯、關閉重開、squash·merge·rebase 合併(強確認)與重跑/取消 CI;儲存庫彈出視窗自動載入有權限清單並支援公開儲存庫搜尋;未安裝 better-sidebar 時自動降級為獨立右側面板',
pluginMdExportDesc: '在 better-sidebar 的 Markdown 工具列上新增「匯出」按鈕:一鍵把目前 .md 渲染為獨立 HTML(表格/程式碼區塊/Mermaid 圖表內嵌,版面跟隨預覽主題)直接儲存到同目錄,或透過列印對話框匯出為 PDF',
pluginCodeNavDesc: '程式碼預覽導覽:按檔案類型自動識別語言並高亮語法,符號大綱(類別/方法/變數篩選 + 一鍵跳轉),檔案內搜尋(全部符合高亮、上/下一處、區分大小寫),接管 better-sidebar 的程式碼檔案預覽',
pluginSuhuangScrollDesc: '把本機蘇黃共閱 Runtime 接入 DSH 設定與 better-sidebar,支援模型設定、連線測試及連續閱卷控制;使用前須安裝 Suhuang Scroll Runtime 與 dsh-better-sidebar',
pluginSuhuangScrollDesc: '把本機蘇黃共閱 Runtime 接入 DSH 設定與 better-sidebar,支援模型設定、連線測試及連續閱卷控制;使用前須安裝 Suhuang Scroll Runtime 與 dsh-apes-sidebar',
pluginBetterOverleafDesc: 'better-sidebar 的 Overleaf 分頁:直連 CDP 瀏覽器登入(支援第三方 Chromium),專案列表/切換,<workspace>/overleaf/ 本地 git 映像,git 雙向同步(API 唯讀兜底),檔案預覽走側邊欄工作台',
pluginGitRemotesDesc: 'better-sidebar Git 遠端 Tab:看分支/上游/ahead-behind,fetch(可 prune)、ff-only pull、確認後才 push。不替換內建 Git 的暫存/提交,也不提供 force-push 或模型自動推送',
pluginSentinelDesc: '條件驅動的 agent 喚醒系統:檔案/程序/連接埠/HTTP/命令/webhook 感測器,條件達成自動喚醒休眠工作階段;註冊「哨兵」Tab 展示伺服器全域監控表',
@@ -436,29 +436,4 @@ export const zhTW: Record<string, string> = {
pluginDocsPanelDesc: 'DSH 側邊欄裡的「全域文件」:全域 Markdown 筆記,任何工作區隨時可讀——列表點選閱讀、懸浮大綱跳轉、Chrome / VS Code 外部開啟、程式碼複製,目錄可設定(預設 ~/.dsh/docs)',
pluginEgoBrowserDesc: '將 CitroLabs/ego-lite 接進 DeepSeek Harness 的 agent 瀏覽器:32 個 ego_* 工具驅動真實 Chromium,側邊欄原生「ego 瀏覽器」Tab 實時觀察 agent 逛的每個頁面,可直接點擊/拖拽/輸入接管;裝 better-sidebar 時自動註冊 Tab,沒裝則退回浮動浮窗',
pluginBilingualReaderDesc: '在 DSH 側邊欄讀論文 PDF:原生 PDF 顯示,選取一段文字即用大模型劃詞翻譯,結合上下文、完全隔離主對話,只作閱讀輔助',
changesRedactOn: "已開啟脫敏:敏感內容以 [REDACTED] 顯示;點擊關閉(自擔風險)",
changesRedactOff: "脫敏已關閉:將原樣顯示文件內容;點擊開啟",
changesRedactOnLabel: "脫敏中",
changesRedactOffLabel: "脫敏關",
changesRedactBanner: "已脫敏",
pluginSentinelName: 'dsh-sentinel 喚醒系統',
pluginEgoBrowserName: 'ego-browser Agent 瀏覽器',
pluginBetterOverleafName: 'dsh-better-overleaf Overleaf 標籤',
pluginDocsPanelName: 'dsh-docs-panel 全域文件',
pluginFlowglassName: 'dsh-flowglass 流鏡',
pluginGitForgeName: 'dsh-git-forge Git 憑證',
pluginGitRemotesName: 'dsh-git-remotes Git 遠端',
pluginGithubWorkbenchName: 'dsh-github-workbench GitHub 工作台',
pluginSidebarQaName: 'dsh-sidebar-qa 劃選追問',
pluginSidenoteName: 'dsh-sidenote 側邊對話',
pluginServerDeckName: 'dsh-server-deck 伺服器甲板',
pluginSuhuangScrollName: 'dsh-suhuang-scroll 蘇黃共閱',
pluginSshTunnelName: 'dsh-ssh-tunnel SSH 隧道',
pluginTurnReviewName: 'dsh-turn-review 本輪審查',
pluginBilingualReaderName: 'dsh-bilingual-reader 雙語閱讀',
pluginOfficeName: 'Office 預覽插件',
pluginMdExportName: 'Markdown 匯出插件',
pluginCodeNavName: '程式碼預覽導覽',
pluginVideoPreviewName: '影片預覽插件',
presetDshDesktopDesc: 'Electron 進階模式(無邊框):macOS 頂欄 20px、Windows 無 WCO 時 32px 標題列讓位',
}

Some files were not shown because too many files have changed in this diff Show More