Skip to content

fix(market): install plugins from the registry the market read them from - #352

Merged
yaojin3616 merged 4 commits into
mainfrom
fix/generation-install-registry-337
Sep 10, 2026
Merged

yaojin3616 merged 4 commits into
mainfrom
fix/generation-install-registry-337

Conversation

@yaojin3616

Copy link
Copy Markdown
Contributor

Fixes #337

问题

决定「装哪个版本」的组件和「实际去下载」的组件用的是两个不同的 registry,中间没有任何通道能把前者告诉后者。

报告人指出的三处,在当前 main 上逐一核实成立:

位置 现状
index.js:484 边界只从 args 里挑包名,其余 flag 静默丢弃,调用方无法用 --registry 绕过
index.js:429 → installer.mjs:140 environment 是服务创建时的闭包,不接受调用方传入
installer.mjs:222 staging 的 .npmrc 只写 node-linker / side-effects-cache,没有 registry

而市场那边确实算出了正确的 registry(dshmarket/src/dsh-cli.ts:139,china → mirrors.cloud.tencent.com/npm),但那份 env 是给 spawn dsh CLI 用的;Desktop 把 add 改道到进程内边界后,这个值从来没到过 pnpm。

于是 pnpm 只剩用户级 ~/.npmrc 可读。两个源在任何一次发布之后都会分叉几分钟到几小时,分叉期间:

  • 精确版本 → ERR_PNPM_NO_MATCHING_VERSION,更新 100% 失败;
  • @latest → 静默解析成旧版本、exit 0、装了等于没装,最后被报成 stale 或 RELEASE_TOO_FRESH。

后一种正是 #350 讨论串里那个「点『立即更新』还是弹同一句话」的死循环:跳过冷却期只解开了年龄闸,registry 还是那个滞后的镜像,所以版本依然不动,于是又判回 release-age。

修复

1. 把 staging 钉到市场自己用的那个 registry。

新增 market-registry.mjs,按优先级解析:

  1. 命令行显式指定(--registry <url> / --registry=<url> / --config.registry=<url>)
  2. 已设置的 npm_config_registry → 返回 null,不写(调用方已经表态)
  3. DSHM_NPM_MIRROR(市场自己的镜像 override,同进程可见)
  4. 市场持久化的 region(<profile>/.dsh-market/state.json)→ 区域表
  5. 解析结果是默认 registry → 也不写

然后写进 staging 的 .npmrc。选 .npmrc 而不是 env 有两个原因:项目级 .npmrc 压过用户 ~/.npmrc;而且不依赖 pnpm 大版本还认不认 npm_config_*(报告人实测 pnpm 11/12 已改认 PNPM_CONFIG_*,我们 pin 的 10.34.5 还认,这是个定时炸弹)。

为什么用「推断」而不是让市场传进来:dshmarket 是从 npm 装的、按自己的节奏升级,修复不能等一个新的 dshmarket 发版。所以区域表在这里复制了一份(注释里标了要和 dshmarket/src/regions.ts 同步),同时打开了 --registry= 这条通道并让它优先级最高——将来市场可以直接说,不必再被猜。

刻意不动的两种情况:已经设了 npm_config_registry(沿用 dsh-cli.ts 的既有策略:填补沉默,不覆盖发言),以及 region 就是默认 registry 的用户(此时 ~/.npmrc 是唯一的 registry 声明,强行钉到 npmjs.org 会打断今天正常工作的私有/企业源)。

三个安装入口都接上了:runExternalMarketPluginInstall、plugin-upgrade.ts、generation-migration.ts。

2. 报错保留 pnpm 的错误码。

diagnosticLine 原本取 named.at(-1),而结尾那句 This error happened while installing a direct dependency of <staging 路径> 同样匹配 /error/i,必然盖掉首行的 ERR_PNPM_NO_MATCHING_VERSION。改为优先取带 ERR_* 码的那一行。这也正是本 issue 只能靠报告人手工复现才定位得出来的直接原因。

测试

新增 test/market-registry.test.js(9 条,覆盖解析优先级的每个分支);generation-installer / generation-boundary 各加 3 条与 1 条:

  • staging .npmrc 带上 registry,且 node-linker / side-effects-cache 不受影响;
  • 没解析出 registry 时保持不写(今天的行为不变);
  • 失败输出里 ERR_PNPM_NO_MATCHING_VERSION 和镜像域名都保留,尾部那句 staging 路径不再顶替它;
  • 端到端:region=china 的 profile 走边界安装,pnpm 实际看到的 .npmrc 指向腾讯镜像。

去掉本 PR 的 src 改动后其中 3 条失败,加上后全绿。

npm run typecheck   ✅
vitest run          745 passed / 1 failed

失败的 3 个文件(ppt-activation、ppt-fonts、safe-mode-runtime)在未改动的 main 上同样失败(本地缺 PPT 运行时产物),与本 PR 无关。

未覆盖 / 后续

  • issue 里的次要问题 A(装成功却报 RELEASE_TOO_FRESH)和 B(回滚提示必现)没在本 PR 处理,建议另开。
  • 残留缺口:region 为 global 但用户 ~/.npmrc 指着滞后镜像的组合仍会分叉。上面说明了为什么这里选择保守——要收掉它,得先想清楚怎么和私有 registry 共存。

A generation install had no way to learn a registry. The boundary keeps
only the package spec out of its arguments, its environment is the
service's closure rather than anything the caller passes, and the staging
`.npmrc` was written with no registry line — so pnpm fell back to the
user's `~/.npmrc`.

The market meanwhile picks WHICH version to install by reading metadata
from its region's registry (Tencent's mirror in the china region). Those
two registries diverge for minutes to hours after any publish, and while
they do, every update fails with ERR_PNPM_NO_MATCHING_VERSION or silently
resolves `@latest` to the older build and reports a stale update.

Resolve the market's own registry and pin the staging workspace to it, so
the half that chooses a version and the half that fetches it agree. A
project `.npmrc` outranks `~/.npmrc`, and unlike `npm_config_*` it does
not depend on a pnpm major still reading that env prefix.

The value is inferred from the market's persisted region because dshmarket
ships from npm on its own schedule and the fix cannot wait for a release
that passes it in — but `--registry=` is now read from the boundary's
arguments and wins, so a later market can state it outright. An already
named `npm_config_registry`, and a region on the default registry, are
both left alone: pinning npmjs.org over a private registry would break
installs that work today.

Also report pnpm's error CODE instead of the line after it. The trailing
"This error happened while installing a direct dependency of <path>" also
matches /error/i and, being last, displaced ERR_PNPM_NO_MATCHING_VERSION
from the log — which is why this could only be diagnosed by reproducing
the install by hand.

Fixes #337
@yaojin3616
yaojin3616 merged commit f7aebb5 into main Sep 10, 2026
8 of 12 checks passed
@yaojin3616
yaojin3616 deleted the fix/generation-install-registry-337 branch September 10, 2026 10:34
yaojin3616 added a commit that referenced this pull request Sep 11, 2026
#352 replaced the market install boundary's "staged for next restart"
publish step with publishInstalledGeneration(), which switches the profile
node_modules entry synchronously while Harness is running. For any entry
that is already in use that recreates the Windows locked-rename conflict
the generation model exists to avoid:

  - dshmarket is a core bundle kept in the shared tree, so its entry is a
    real directory. publishInstalledGeneration() refuses a non-link entry:
    every market self-update failed with "Cannot switch a non-link plugin
    directory", and the market's rollback check then reported the previous
    build could not be verified.
  - a loaded generation junction cannot be renamed on Windows, so a plugin
    update mid-session hangs on the rename retry — the market UI sits on
    "in progress" until the registry lock times out.

Restore the pre-#352 behavior: install the new generation, point
desired.json at it, expose only a brand-new link for the market's add
validation, and let the next cold start's projection switch the entry
while Harness is stopped. The #337 registry resolution added by #352 is
kept — only the immediate-switch step is reverted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
yaojin3616 added a commit that referenced this pull request Sep 11, 2026
… paths (#380)

* fix(market): update dshmarket in the shared tree, not via link switch

`dshmarket` is a core bundle the profile migration deliberately keeps in
the shared hoisted tree (`KEEP_IN_SHARED_TREE`), so its entry under
`profiles/web/node_modules` is a real directory, never a generation link.

Since #352, `runExternalMarketPluginInstall` routes every `add` — market
self-updates included — through `installGeneration` + the immediate
`publishInstalledGeneration`, which refuses a non-link directory by design:

    Cannot switch a non-link plugin directory: .../node_modules/dshmarket

So every `dshmarket` self-update failed, and because it throws before any
move, the market's rollback check also reports the previous build could
not be verified. Before #352 the boundary staged the switch for the next
cold start (`exposeMissingGenerationLinks` no-op'd on an existing entry),
so the update eventually landed; #352 turned that into a hard failure.

Branch on the spec: a `dshmarket` package goes through the ordinary
shared-tree `add --workspace-root`, which updates the directory in place —
the same asymmetry #351 already fixed on the uninstall side. The market's
registry is still resolved and pinned (#337), and any stale dshmarket
generation id is pruned from `desired.json`.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(startup): preserve plugin ownership and select compatible updates

* fix(market): stage dshmarket/loaded-bundle updates for next restart

#352 replaced the market install boundary's "staged for next restart"
publish step with publishInstalledGeneration(), which switches the profile
node_modules entry synchronously while Harness is running. For any entry
that is already in use that recreates the Windows locked-rename conflict
the generation model exists to avoid:

  - dshmarket is a core bundle kept in the shared tree, so its entry is a
    real directory. publishInstalledGeneration() refuses a non-link entry:
    every market self-update failed with "Cannot switch a non-link plugin
    directory", and the market's rollback check then reported the previous
    build could not be verified.
  - a loaded generation junction cannot be renamed on Windows, so a plugin
    update mid-session hangs on the rename retry — the market UI sits on
    "in progress" until the registry lock times out.

Restore the pre-#352 behavior: install the new generation, point
desired.json at it, expose only a brand-new link for the market's add
validation, and let the next cold start's projection switch the entry
while Harness is stopped. The #337 registry resolution added by #352 is
kept — only the immediate-switch step is reverted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(market): enforce verified baseline before normal startup

* fix(market): preserve shared-tree and generation update contracts

* fix(market): keep dshmarket out of the generation system entirely

generation-migration.ts already keeps dshmarket in the shared profile
tree forever (KEEP_IN_SHARED_TREE) — it is a core bundle, never meant to
be a generation. Two recent paths broke that invariant by installing it
as one anyway:

  - runExternalMarketPluginInstall only routed dshmarket to the shared-tree
    updateSharedMarket() when its node_modules entry was NOT currently a
    symlink; once anything made it one, later "add" calls fell through to
    installGeneration()/publishInstalledGeneration() like any third-party
    plugin.
  - ensureMarketBaseline's default upgrader (upgradePluginToGeneration)
    installed the verified baseline as a generation and published it as a
    link, for dshmarket specifically.

Once dshmarket became a generation, whether it stayed a real directory or
a symlink depended on whichever code path touched it last — and a build
that reached the live profile that way (dshmarket 1.38.0, missing a host
`dsh-settings` export) could not be detected or rolled back afterward:
plugin-recovery excludes core bundles from its candidate list by design,
so the crash showed "plugin recovery detection: unresolved" with no way
to fix it short of hand-editing the profile.

Close both paths:

  - runExternalMarketPluginInstall always routes a dshmarket spec to
    updateSharedMarket(), which now also drops a stale generation symlink
    (leaving the generation directory itself untouched) before running
    pnpm, so a previously-projected Market collapses back to a real
    directory instead of being treated as an ordinary generation update.
  - plugin-upgrade.ts gains upgradeMarketInSharedTree(), which cleans up
    any stale generation ownership/override, drops a stale symlink, and
    reinstalls the whole shared profile tree via
    installProfileDependenciesWithDsh() (Harness already stopped at this
    point in startup) instead of installGeneration(). market-baseline.ts
    now defaults to it, and also repairs a dshmarket entry that is a
    generation link even when its version already meets the baseline —
    the version check alone can't see that shape and would otherwise leave
    it a generation forever.

upgradePluginToGeneration() itself is unchanged and still correctly used
for ordinary third-party plugin upgrades (recovery-page and Safe Mode
paths), which are unaffected — isThirdPartyPackageName()/CORE_BUNDLES
already exclude dshmarket from those candidate lists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(market): stop projection from resurrecting a dshmarket generation

38be336 closed the two writers that could install dshmarket as a
generation, but not the projector that recreated one on every launch.
`desired.json` still held a `dshmarket+1.38.0` pointer, and
`resolveEnabledGenerations` enables whatever it lists — so
`projectGenerations` re-linked the broken build each boot, ahead of any
repair. The symlink's mtime after installing the previous fix is the
proof: it was rewritten minutes later, by the fixed build itself.

Two failure modes compounded:

  - projection ran before the market repair, so the repair could only
    ever clean up after it; and
  - the repair sat behind `shouldDeferProfileMaintenance`, which is true
    while any plugin removal lacks `bootVerifiedAt` — a flag only a
    successful boot sets. A market that cannot load is what stops the
    boot, so the gate could never clear itself: the repair needed a boot,
    the boot needed the repair.

Enforce the invariant where it cannot be outrun, and let the repair run
early enough to matter:

  - `resolveEnabledGenerations` skips shared-tree-only packages, so a
    stray pointer is inert instead of a link. Nothing can project
    dshmarket, whatever `desired.json` says.
  - `demoteMarketGeneration` undoes a legacy projection before projection
    runs: link, pointer, ownership marker and override all go, while the
    `dependencies` declaration and `bundles` entry stay — dropping those
    reads as "the market was uninstalled" and every later repair declines
    to reinstall it.
  - both that and `ensureMarketBaseline` now run ahead of projection and
    ahead of the removal-verification gate. A frozen migration still
    blocks them; that path keeps the profile byte-for-byte for rollback.
  - `pnpmRunnerPath` reaches `installProfileDependenciesWithDsh`. Without
    it `ensureProfilePnpmShim` refuses to run a plain pnpm against a
    profile that still projects ordinary plugins, which failed the
    install with "the generation-aware pnpm runner is unavailable". The
    option is required rather than optional so the compiler catches the
    next omission.

Verified on a profile in exactly the reported state — a 1.38.0 market
generation plus an unverified removal — which now self-heals to a real
1.45.1 directory in the shared tree on the next launch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): check and handle blocking plugins individually

* fix(recovery): batch upgrades and invalidate repaired plugin failures

* fix(recovery): automatically combine plugin upgrades and removals

* fix(market): execute pnpm runner through linked package paths

* fix(recovery): use Electron networking and retry failed market checks

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Kunstderfug added a commit to Kunstderfug/dsh-desktop that referenced this pull request Sep 16, 2026
…d escapable

Keep the pnpm side-effects cache DISABLED at both spawn sites: bundled pnpm 10.34.5 keys side-effect entries on ENGINE_NAME (${platform};${arch};node<MAJOR>), which is ABI-blind while pnpm runs under ELECTRON_RUN_AS_NODE against native modules (node-pty conpty failure family, dataelement#183). The disable is a deliberate opt-out introduced in dataelement#126 and preserved through dataelement#204/dataelement#220/dataelement#352 (profile .npmrc seed and generations staging pin), not inheritance.

Both buildHarnessSpawnOptions and buildProfilePluginCommandEnvironment now compute npm_config_side_effects_cache and PNPM_CONFIG_SIDE_EFFECTS_CACHE from one exported helper, pnpmSideEffectsCacheAllowed(): default 'false', overwriting any stray parent-shell npm_config value. DSH_PNPM_SIDE_EFFECTS_CACHE=1 (exact token) opts a diagnostic run back in; the two market-installer disable sites are cross-referenced but untouched so generation installs never see the hatch. Windows note documented: env names are case-preserved, so the hatch must be exported with exact casing.

Table-driven tests pin default-off, stray-parent override, near-miss token rejection, and exact '1' enablement for both builders.
yaojin3616 added a commit that referenced this pull request Sep 19, 2026
Harness resolves every profile bundle from its own installation before the
profile (`resolveBundleDir` in dsh-app-boot). Since #352 pinned dshmarket
1.45.1 as a runtime dependency, the packaged copy won that lookup: upgrading
the market wrote 1.48.0 into the profile, the market and the baseline check
both read 1.48.0 back, and Harness kept loading 1.45.1 after every restart.

Nothing in the app imports dshmarket; the profile copy is installed from npm
by the market installer and `ensureMarketBaseline`. It stays a
devDependency for the tests that exercise its real profile reader, which
keeps it out of the packaged app.

`ensureMarketBaseline` now also notes when the copy Harness would load is not
the profile's, so a shadowed market shows up in harness.log instead of as a
version the UI reports but never runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
yaojin3616 added a commit that referenced this pull request Sep 19, 2026
…e mode (#431)

* feat(safe-mode): add floating repair agent widget in recovery and safe mode

- Add floating repair agent FAB widget on plugin recovery and safe mode overlay
- Provide pre-configured quick action prompts with error diagnostics and logs
- Support switching models and uploading/pasting screenshots for vision models
- Connect with isolated safe mode Harness session and stream responses via IPC

* feat(safe-mode): temporarily disable floating repair agent chat widget

* feat(safe-mode): enable repair agent with 0.9.0 incident knowledge and offline diagnostics

* feat(release): expand release notes generator with performance section and full changelog

* feat(window): persist window bounds, maximized state, and zoom level

* fix(repair-agent): handle unconfigured models with api key drawer and prevent silent stream failures

* fix(dev): strip inherited ELECTRON_RUN_AS_NODE to prevent electron launch crash

* feat(dev): forward runtime logs to console in development and log single-instance conflicts

* fix(repair-agent): inherit working default model route and add dual-track history polling

* refactor(repair-agent): rely purely on websocket stream without redundant history polling

* feat(repair-agent): replace floating widget with seamless native Harness UI diagnostic cards

* fix(windows): stop GPU fallback ladder from degrading on TDR device-loss recoveries, recover the menu view from a lost renderer

gcp3 crash data for v0.9.0 showed 23/30 Windows gpu-crash reports at exitCode=34
— Chromium's own exit code when it detects a lost D3D11 device (typically a
driver TDR reset) and exits the GPU process on purpose so it can restart it.
The fallback ladder was counting this self-recovery as evidence the sandbox is
broken and would degrade hardware acceleration after 3 of these on an
otherwise healthy machine. isGpuLossFatal now takes the exit code and treats
exitCode 34 as non-fatal, only logging a breadcrumb.

Separately, ~6 renderer-crash reports were on windows-menu.html: that view runs
in its own WebContentsView outside installMainWindowRendererRecovery, which
only hooks the main window's webContents, so a lost renderer there left a dead
menu until the whole app restarted. It now reloads itself on render-process-gone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(session): skip unreadable session logs instead of failing the whole listing

One corrupt JSONL/zstd session log made sessionPersistence.list() throw, which
failed dsh-workspace and stopped the Harness entirely (0.9.0 startup-failure
reports: "corrupt Zstandard session log", corrupt header id). The log is now
skipped with a warning naming its path and left on disk untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(startup): stop reporting attributed plugin failures, clear stale writer locks, fix repair diagnosis

- Harness startup failures that plugin recovery attributes to a user plugin
  are handed to the user and their pending crash report is discarded; only
  the frontend recovery path did this before, so they were still uploaded.
- Remove dsh-atomic-write locks in DSH_HOME and DSH_HOME/profiles whose owner
  pid is gone before launching; a killed Harness left .credentials.yaml.lock
  behind and the next launch timed out on it.
- Repair Agent diagnosis uses loader provenance and the recovery-resolved
  plugins, and only reports a startup timeout when the runtime says so, since
  "waiting for Harness" + SIGTERM matched ordinary failure cleanup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): ensure safe mode uses hoisted node-linker and hide diagnostics when plugins are identified

* feat(runtime): add preset YAML validation step before launch

* refactor(runtime): remove redundant preset check in favor of native Cordis loader logging

* feat(repair-agent): unify diagnostic cards into a single entry with verification and reporting instructions

* fix(repair-agent): restore the 3 fine-grained diagnostic cards in safe mode

* fix(safe-mode): unify safe mode diagnostic cards into a single AI agent entry

* fix(web-import): never replace a desktop home that already holds plugins or credentials

A 0.9.0-rc1 home with plugins or .credentials.yaml but no settings.yaml or
sessions was treated as unused, so the web import deleted it wholesale. It now
counts as used. An unused home is also moved aside before deletion so
delete-pending entries on Windows cannot block links created at the same path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): don't remove plugins a deferred migration has not installed yet

When the generation migration failed (e.g. pnpm EPERM right after install),
imported plugins stayed manifest-only, failed to prepare, and plugin recovery
removed them as incompatible. Deferred migrations now report the pending
plugins and recovery excludes them. Transient EPERM/EBUSY/EACCES failures no
longer freeze the same profile for six hours, so the next launch retries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): stop the titlebar drag region from swallowing clicks and churning

The drag region now ignores pointer events, and only semantic dialog markers
hide it: class-name guesses like [class*="modal"] matched permanent elements
and hid it for good. Visibility checks run at most once a frame, since
streaming output mutates the DOM continuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(diagnostics): send crashes without an event id right away

Only a plugin-attributed startup failure should wait for recovery to decide
whether to discard it. A crash with no event id compared equal to an unset
pending id and was held back too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(market): pin the exact version a declared dshmarket range names

The installer pins and verifies an exact version, so a declared range such as
^1.47.0 is reduced to the version it names instead of being passed through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: ignore the local pnpm content store

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): resolve Safe Mode plugins from the installation, not the module fallback

Safe Mode loads only installation-owned bundles, yet it still went through
the shared $DSH_HOME/profiles/node_modules fallback, whose junctions Windows
can refuse to recreate (EPERM) for minutes — the top startup failure of
v0.9.0. The desktop now sets DSH_DESKTOP_HOST_RESOLVED for the Safe Mode
profile only; the patched Harness then skips healing the fallback and
resolves bare plugin names, including entries created at runtime through
ctx.loader.create, from its own installation. A flag leaked from the parent
environment never reaches a normal profile.

This replaces the hoisted node-linker workaround for Safe Mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(runtime): mirror Harness logs to the console only in development

A packaged app's stdout may be a closed pipe; writing every Harness log line
to it there buys nothing and can fail. harness.log still receives every line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): still blame a migration-pending plugin whose legacy copy is installed

f475ce5 exempted every plugin with a pending deferred migration from
recovery. Only a plugin the migration left manifest-only is not broken; one
whose legacy copy is installed still loads and can be the real culprit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(logging): record Harness runtime warnings and errors in harness.log

Cordis's ctx.logger keeps messages only in an in-memory ring, and its default
level filters out warnings; nothing in the shipped composition exported them.
Session activation failures (a preset that cannot be found or mounted) never
reach the logger at all — they are only pushed to the client. So errors after
startup left no trace for a person or the Repair Agent once the process was
gone.

A new desktop plugin, dsh-desktop-log-bridge, is composed first into both the
normal and the Safe Mode profile. It writes warn and error messages, the
errors already in the ring, and api-session/error events to stderr, which
the desktop records into harness.log. It rate-limits floods and writes one
ready line per launch, so a log without it is known to predate the bridge.

Every bridged line carries a [harness-log] prefix, and latestHarnessAttemptLogs
drops such lines: runtime warnings never become recovery's failure cause or
the plugin it blames.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(safe-mode): disable plugins instead of removing them

Safe Mode now switches a plugin off the way the plugin market's own toggle
does, so it can come back without a reinstall: `disabled: true` rows for the
loader entries the package inserts in the user patch layer, which the loader
re-applies on every boot, plus the market's .dsh-market/state.json list,
which is also the only switch for client-only packages. Nothing is deleted.

Disabled plugins show as disabled with a Re-enable action instead of a
selection. Compatibility issues that call for disabling a plugin use the same
path; the old approach of dropping it from dsh.profile.bundles was silently
undone, because the generation projection rewrites that list on every launch.
Compatibility inspection skips disabled plugins, since they never load.

A disable-carrier (a bundle whose patch disables another plugin) cannot be
switched off on its own without stranding the plugin it replaces, so it
keeps the backed-up removal. Startup recovery keeps removal too: a package
that is itself broken fails before the patch layer's disable applies. When
recovery removes a plugin it also clears its market disable entry, as the
market's own uninstall does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(repair-agent): start a fresh, well-briefed repair session from one card

Recovery and Safe Mode now offer a single "start the repair agent" card. Each
click opens a new session in the Safe Mode Harness and makes it the session
the Harness UI shows: the UI restores its current session from desktop
storage when the page loads, so the Recovery path opens the session before
its page loads and the Safe Mode path reloads the already open page. The
floating repair widget is gone.

The session's workspace is the Harness home (profiles, plugins, patch layer)
instead of the empty launch root, and the first turn of each session carries
the diagnosis. The system prompt is rebuilt around real incidents:

- a directory map that separates the normal profile it repairs from the Safe
  Mode profile it runs in;
- the path to harness.log with how to find the failed launch in it, plus a
  short excerpt, instead of pasting logs; Safe Mode's own logs are no longer
  used when no failed launch was captured;
- one bilingual playbook source, so the Chinese and English prompts can no
  longer drift: plugin load failures, broken packages (which disabling cannot
  fix), the Windows module-fallback EPERM (no Developer Mode advice), a
  corrupt patch layer, startup timeouts, the code -> ptc preset rename, and
  schema errors in copied user presets;
- actions named after the real UI, and a confirm-and-back-up rule before any
  file change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(market): stop shipping dshmarket so market upgrades take effect

Harness resolves every profile bundle from its own installation before the
profile (`resolveBundleDir` in dsh-app-boot). Since #352 pinned dshmarket
1.45.1 as a runtime dependency, the packaged copy won that lookup: upgrading
the market wrote 1.48.0 into the profile, the market and the baseline check
both read 1.48.0 back, and Harness kept loading 1.45.1 after every restart.

Nothing in the app imports dshmarket; the profile copy is installed from npm
by the market installer and `ensureMarketBaseline`. It stays a
devDependency for the tests that exercise its real profile reader, which
keeps it out of the packaged app.

`ensureMarketBaseline` now also notes when the copy Harness would load is not
the profile's, so a shadowed market shows up in harness.log instead of as a
version the UI reports but never runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(recovery): let startup recovery repair a plugin market that breaks startup

The market is a core bundle, so startup-failure attribution never named it
and the Recovery page, finding no culprit, could only offer Safe Mode — which
does not load the market and has nothing to act on. A market release that
stopped Harness from booting therefore failed every launch the same way.
Until now the packaged dshmarket shadowed the profile copy and hid this;
with it gone the profile copy really loads, so the loop is live.

When the failed launch's log (or loader provenance) names dshmarket, the
Recovery page shows it as its own row, apart from the third-party plugin
list whose removal path refuses core bundles:

- Upgrade to the newest release the market check finds compatible with this
  Harness, for a market too old for it; a release already tried here is not
  offered again.
- Install the verified version (VERIFIED_MARKET_BASELINE). That moves down
  from a broken release, up from a stale one, or reinstalls a damaged copy.
  It is the primary action when the market is the only culprit.
- Remove the market; installed community plugins are kept.

Versions come from the main process's own check, never from the page. Each
change stops Harness first, as the shared-tree installer requires, pins the
exact version so the next baseline check does not reinstall the broken
release, then relaunches and returns to recovery if startup still fails.
The market removal behind the settings page is shared as `removeMarket`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): render the plugin market row like any other plugin row

The market row added its own touches: a version after the name, a green
button for restoring the verified version (which can be a downgrade), and a
confirmation dialog plus its own wording for removal. Third-party rows have
none of these, so the market read as a different kind of item.

Show the name only, keep green for the upgrade alone with every other action
in the neutral style, and remove it the way the page removes any plugin —
"Remove this plugin", no extra confirmation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): offer the plugin market only upgrade and removal

Restoring the verified market version was an action no other plugin row
has. Drop it, so the market row is a plugin row like the rest: upgrade to a
compatible release when the market check finds one, and removal.

When the market alone blocks startup, the page now uses the same buttons as
a lone third-party plugin: "Upgrade plugin and restart" with "Uninstall
plugin" beside it when an upgrade exists, otherwise "Remove this plugin and
continue".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: stop tracking the local pnpm content store

0d083d5 committed the whole `.pnpm-store/` (13,357 files, ~235 MB) along
with its four intended changes, before 7a2cb73 added it to .gitignore. An
ignore rule does not untrack files already committed, so they stayed in the
branch and buried the PR's real diff.

Remove them from the index only; the local store is untouched and stays
ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: drop tests that only grep our own source code

About 40 tests read src/main/index.ts, the preload, build/*.html, the NSIS
script or our own packages' client.js as text and asserted that particular
lines were present — `toContain("ipcMain.handle('safe-mode:action'")`,
`indexOf(a) < indexOf(b)` and the like. They execute nothing, so they cannot
catch a behavior regression, yet they fail on any rename or reformat, and on
Windows, where the checkout has CRLF line endings, any expected string that
spans a newline never matches. Two of them failed CI that way on PR #431.

Removed: every test block whose assertions were such text matches, and the
source-text lines inside otherwise behavioral tests (windows-titlebar
allowlist, runtime Node mode, release dev channel, branding postinstall,
harness-node-entry). The dev-channel check now reads electron-builder.dev.cjs
as a config object instead of matching its text.

Kept: tests that execute code, parse package.json/lockfile/YAML/JSON
config, or check the installed third-party bundles in node_modules (patch
and upstream-contract checks), since those inspect what actually ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(repair-agent): drop tests that only check the prompt's wording

Two repair-prompt tests asserted fixed sentences of the generated prompt:
exact Chinese phrases with hard-coded POSIX paths, and a fixed count of
seven playbooks plus two error strings. Any edit to the copy broke them
without saying anything about how the agent behaves, and the path one
failed on Windows, where path.join correctly yields backslashes.

Keep the test of real prompt logic: only the tail of the log is included,
and none at all when no failed launch was captured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(repair-agent): drop the remaining repair-prompt test

It asserted which log lines and which sentence end up in the generated
prompt text. Like the other prompt tests, that pins the copy rather than
anything the agent does with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): offer the repair agent as one line under the summary

The repair agent sat in its own section below the actions — a heading, a
divider and a card, about 140px. In a 1280x800 window that pushed the plugin
list into a scrollbar, which the native Windows recovery UI check rejects:
every plugin has to be visible at once. Its text was also Chinese-only.

Show it as a single link under the summary instead, in both languages. It
sends the same diagnostic request as the card did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): offer the repair agent as a button beside Safe Mode

The recovery page showed the repair agent in its own section under the
technical details — a heading and a card, in Chinese only. Make it a
secondary "Repair agent" button next to "Enter Safe Mode", in both
languages. As before, it appears only when no plugin or market was
identified; a named culprit has its own repair above.

The native Windows UI check expected Safe Mode to be the only action when
nothing is identified; it now expects the agent beside it, and nowhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(runtime): keep a clean launch off stderr

The packaged Windows smoke fails any launch that writes to stderr once a
workspace and session exist. Two lines did on every launch, and neither
reported a problem:

- The log bridge's "recording from here on" marker. It is a notice, so it
  now goes to stdout; stderr carries only bridged warnings and errors.
- `patch: entry dsh-market not found`. The desktop patch configured the
  market's entry (restart off, wait for desktopProfiles) unconditionally,
  but the market is optional, and a patch row whose entry is absent makes the
  loader warn. It was always there; the bridge only made it visible. The row
  now lives in dsh-desktop-market.patch.yml, passed as a second --patch only
  when the profile boots dshmarket, so a market profile behaves as before.

Checked by booting a fresh web profile (no market overlay, no bridged
warning) and one that boots dshmarket (overlay applied, no warning).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
yaojin3616 added a commit that referenced this pull request Sep 21, 2026
* feat(release): configure initial 5% rollout on crash service (#401)

- Add configure-rollout.mjs script to manage platform rollout rules on crash service

- Integrate initial 5% rollout step into release workflow

- Add unit tests covering creation, update, and race condition conflict recovery

* fix(llm): send Harness session headers through pi-ai (#329)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* fix(llm): recover missing terminal content (#400)

Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: import the local web Harness home on first launch (#397)

* feat: import the local web Harness home on first launch

Copy ~/.dsh into an unused desktop home so same-machine web users are not treated as a fresh install.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(market-baseline): treat pnpm isolated-store symlinks as valid installs

In dev mode (dsh-desktop-dev), profiles have no nodeLinker:hoisted config
so pnpm installs dshmarket as a symlink into .pnpm/…. The previous
isSymbolicLink() check treated any symlink as a stray generation link,
deleted it, then called pnpm to reinstall — which recreated the same
symlink. The post-install guard rejected it and the app entered Safe Mode
on every dev launch.

Fix: read the symlink target and only treat paths containing `.generations`
as generation links. pnpm isolated-store symlinks (.pnpm/…) are left alone
in demoteMarketGeneration and ensureMarketBaseline, and are accepted by
upgradeMarketInSharedTree's post-install verification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(test): correct patch indentation in provider-error-patch.test.ts (#433)

* fix(test): correct patch indentation in provider-error-patch.test.ts

* fix(types): add type declarations for configure-rollout.mjs and resolve typecheck errors

* fix(diagnostics): 减少误报故障反馈、优化插件自愈免报与修复 Windows 启动缺陷 (#418)

* fix(diagnostics): reduce false-positive crash reports and fix Windows startup issues

- Suppress false-positive unclean-exit reports when previous session log was healthy
- Mark clean exit synchronously on before-quit and handle Windows session-end
- Defer plugin failure reports during startup and discard them when recovery succeeds
- Fix Windows EPERM on junction removal in ensureSymlink (@deepseek-ai/dsh-app-boot)
- Use pathToFileURL in loadDesktopResource to avoid ERR_FAILED (-2) on non-C drives
- Enrich render-process-gone logs in harness.log with URL and GPU feature status
- Enable V8 compile cache (NODE_COMPILE_CACHE) for faster Node.js restarts

* fix(windows): declare asInvoker, enable long paths, and defer profile inspection

* feat(runtime): prefix harness.log lines with ISO date timestamp

* feat(release): sign inner Windows binaries and repackage NSIS installer with SafeNet UKey

* fix(release): harden Windows inner signing with version sync and fallback

* fix(runtime): use platform-specific path join for NODE_COMPILE_CACHE

* fix(types): add type declarations for configure-rollout.mjs and resolve typecheck errors

* test: serialize suites on Windows CI and set global timeout to prevent hangs

* fix(release): verify PE header for .node files to skip non-Windows prebuilds in Jsign

* perf(windows): exclude build-tool deps and add Defender exclusions on install

- Exclude ~2,000 build-tool-only transitive files from the Windows package:
    caniuse-lite (838 JS files, browserslist/babel only)
    lodash (1,048 JS files, only used by @malept/flatpak-bundler linux tool)
    browserslist / electron-to-chromium / update-browserslist-db (15 files)
    @malept/* (flatpak bundler, Linux-only build tool)
  Excluded packages are verified to have no runtime dependency chain:
    - @mixmark-io/domino kept (dsh-tool-web → turndown → domino)
    - jsdom kept (dsh-ppt runtime dep)
    - dsh-ppt's nested typescript kept (only top-level typescript is absent)

- Add Windows Defender exclusions in NSIS installer (customInstall macro):
  Add-MpPreference for $INSTDIR and $APPDATA\dsh-desktop via PowerShell.
  Uses -ErrorAction SilentlyContinue so non-admin installs fail silently.
  Without this exclusion Defender scans 44k files on first launch
  (observed: 121-123 s until Harness ready on a fresh install). With the
  exclusion, first launch should complete in under 20 s.

* fix(release): align package.json version with package-lock.json (0.1.1)

* fix(test): correct patch indentation in provider-error-patch.test.ts

* fix(windows): optimize titlebar layout, integrate more actions into app menu, and fix sidebar overlap (#409)

* perf(windows): cut 20k non-runtime files, elevate defender exclusion, and relax startup timeout (#435)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite in… (#437)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite install

An in-app update can be force-killed before will-quit, and a leftover session marker from another version is a reinstall, not a crash.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(diagnostics): suppress startup failure report when frontend identifies incompatible plugins

* fix(build): preserve runtime skill markdown files in build output and update ppt artifacts

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* fix(windows): clear session marker on overwrite install (#440)

NSIS force-kills the running app, so will-quit never clears session.json and a same-version overwrite looks like an unclean exit.

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix/suppress upgrade unclean exit (#441)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite install

An in-app update can be force-killed before will-quit, and a leftover session marker from another version is a reinstall, not a crash.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(diagnostics): suppress startup failure report when frontend identifies incompatible plugins

* fix(build): preserve runtime skill markdown files in build output and update ppt artifacts

* update

* update

* update

* update

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: stop tracking plugin-management and keep it local only (#443)

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: 建立仓库级 AGENTS.md 开发约束与治理规范 (#428)

* docs: establish repository-wide AGENTS development standards and governance contracts

- Add root AGENTS.md defining project boundaries, TypeScript code style, dependency/data constraints, and verification runbooks.
- Add directory-scoped AGENTS.md rules for src/main, src/preload, packages, patches, and build.
- Add supporting design specifications:
  - docs/code-standards.md (design rationale and phased tool enforcement)
  - docs/patch-plugin-contract.md (patch vs plugin selection and slot compatibility)
  - docs/source-build-contract.md (single authoritative source and build artifact boundaries)

* docs: 补齐 AGENTS 前端硬约束与企业/编译型包覆盖

把主题 token、locale、preload 与 slot 边界写成可执行规则,并标出 enterprise 存量与 ppt-runtime 的维护入口。

---------

Co-authored-by: fairy <3054158379@qq.com>

* fix(security): allow notifications permission from the harness window (#461)

The allowlist only covered clipboard-sanitized-write, so notification
permission requests from the harness page could not be granted. The
loopback main-frame checks are unchanged, so the trust boundary is the
same.

* fix(release): honor platform target and add signed-only dispatch mode (#468)

A filled prerelease tag previously forced a Windows build even when
macos was selected. Dispatch now separates development, signed, and
prerelease so production-identity packages can be signed without publishing.

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(settings): keep long navigation menus scrollable (#456)

* fix(settings): keep long navigation menus scrollable

* fix(settings): add navigation overflow patch

* feat(safe-mode): add floating repair agent widget in recovery and safe mode (#431)

* feat(safe-mode): add floating repair agent widget in recovery and safe mode

- Add floating repair agent FAB widget on plugin recovery and safe mode overlay
- Provide pre-configured quick action prompts with error diagnostics and logs
- Support switching models and uploading/pasting screenshots for vision models
- Connect with isolated safe mode Harness session and stream responses via IPC

* feat(safe-mode): temporarily disable floating repair agent chat widget

* feat(safe-mode): enable repair agent with 0.9.0 incident knowledge and offline diagnostics

* feat(release): expand release notes generator with performance section and full changelog

* feat(window): persist window bounds, maximized state, and zoom level

* fix(repair-agent): handle unconfigured models with api key drawer and prevent silent stream failures

* fix(dev): strip inherited ELECTRON_RUN_AS_NODE to prevent electron launch crash

* feat(dev): forward runtime logs to console in development and log single-instance conflicts

* fix(repair-agent): inherit working default model route and add dual-track history polling

* refactor(repair-agent): rely purely on websocket stream without redundant history polling

* feat(repair-agent): replace floating widget with seamless native Harness UI diagnostic cards

* fix(windows): stop GPU fallback ladder from degrading on TDR device-loss recoveries, recover the menu view from a lost renderer

gcp3 crash data for v0.9.0 showed 23/30 Windows gpu-crash reports at exitCode=34
— Chromium's own exit code when it detects a lost D3D11 device (typically a
driver TDR reset) and exits the GPU process on purpose so it can restart it.
The fallback ladder was counting this self-recovery as evidence the sandbox is
broken and would degrade hardware acceleration after 3 of these on an
otherwise healthy machine. isGpuLossFatal now takes the exit code and treats
exitCode 34 as non-fatal, only logging a breadcrumb.

Separately, ~6 renderer-crash reports were on windows-menu.html: that view runs
in its own WebContentsView outside installMainWindowRendererRecovery, which
only hooks the main window's webContents, so a lost renderer there left a dead
menu until the whole app restarted. It now reloads itself on render-process-gone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(session): skip unreadable session logs instead of failing the whole listing

One corrupt JSONL/zstd session log made sessionPersistence.list() throw, which
failed dsh-workspace and stopped the Harness entirely (0.9.0 startup-failure
reports: "corrupt Zstandard session log", corrupt header id). The log is now
skipped with a warning naming its path and left on disk untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(startup): stop reporting attributed plugin failures, clear stale writer locks, fix repair diagnosis

- Harness startup failures that plugin recovery attributes to a user plugin
  are handed to the user and their pending crash report is discarded; only
  the frontend recovery path did this before, so they were still uploaded.
- Remove dsh-atomic-write locks in DSH_HOME and DSH_HOME/profiles whose owner
  pid is gone before launching; a killed Harness left .credentials.yaml.lock
  behind and the next launch timed out on it.
- Repair Agent diagnosis uses loader provenance and the recovery-resolved
  plugins, and only reports a startup timeout when the runtime says so, since
  "waiting for Harness" + SIGTERM matched ordinary failure cleanup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): ensure safe mode uses hoisted node-linker and hide diagnostics when plugins are identified

* feat(runtime): add preset YAML validation step before launch

* refactor(runtime): remove redundant preset check in favor of native Cordis loader logging

* feat(repair-agent): unify diagnostic cards into a single entry with verification and reporting instructions

* fix(repair-agent): restore the 3 fine-grained diagnostic cards in safe mode

* fix(safe-mode): unify safe mode diagnostic cards into a single AI agent entry

* fix(web-import): never replace a desktop home that already holds plugins or credentials

A 0.9.0-rc1 home with plugins or .credentials.yaml but no settings.yaml or
sessions was treated as unused, so the web import deleted it wholesale. It now
counts as used. An unused home is also moved aside before deletion so
delete-pending entries on Windows cannot block links created at the same path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): don't remove plugins a deferred migration has not installed yet

When the generation migration failed (e.g. pnpm EPERM right after install),
imported plugins stayed manifest-only, failed to prepare, and plugin recovery
removed them as incompatible. Deferred migrations now report the pending
plugins and recovery excludes them. Transient EPERM/EBUSY/EACCES failures no
longer freeze the same profile for six hours, so the next launch retries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): stop the titlebar drag region from swallowing clicks and churning

The drag region now ignores pointer events, and only semantic dialog markers
hide it: class-name guesses like [class*="modal"] matched permanent elements
and hid it for good. Visibility checks run at most once a frame, since
streaming output mutates the DOM continuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(diagnostics): send crashes without an event id right away

Only a plugin-attributed startup failure should wait for recovery to decide
whether to discard it. A crash with no event id compared equal to an unset
pending id and was held back too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(market): pin the exact version a declared dshmarket range names

The installer pins and verifies an exact version, so a declared range such as
^1.47.0 is reduced to the version it names instead of being passed through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: ignore the local pnpm content store

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): resolve Safe Mode plugins from the installation, not the module fallback

Safe Mode loads only installation-owned bundles, yet it still went through
the shared $DSH_HOME/profiles/node_modules fallback, whose junctions Windows
can refuse to recreate (EPERM) for minutes — the top startup failure of
v0.9.0. The desktop now sets DSH_DESKTOP_HOST_RESOLVED for the Safe Mode
profile only; the patched Harness then skips healing the fallback and
resolves bare plugin names, including entries created at runtime through
ctx.loader.create, from its own installation. A flag leaked from the parent
environment never reaches a normal profile.

This replaces the hoisted node-linker workaround for Safe Mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(runtime): mirror Harness logs to the console only in development

A packaged app's stdout may be a closed pipe; writing every Harness log line
to it there buys nothing and can fail. harness.log still receives every line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): still blame a migration-pending plugin whose legacy copy is installed

f475ce5 exempted every plugin with a pending deferred migration from
recovery. Only a plugin the migration left manifest-only is not broken; one
whose legacy copy is installed still loads and can be the real culprit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(logging): record Harness runtime warnings and errors in harness.log

Cordis's ctx.logger keeps messages only in an in-memory ring, and its default
level filters out warnings; nothing in the shipped composition exported them.
Session activation failures (a preset that cannot be found or mounted) never
reach the logger at all — they are only pushed to the client. So errors after
startup left no trace for a person or the Repair Agent once the process was
gone.

A new desktop plugin, dsh-desktop-log-bridge, is composed first into both the
normal and the Safe Mode profile. It writes warn and error messages, the
errors already in the ring, and api-session/error events to stderr, which
the desktop records into harness.log. It rate-limits floods and writes one
ready line per launch, so a log without it is known to predate the bridge.

Every bridged line carries a [harness-log] prefix, and latestHarnessAttemptLogs
drops such lines: runtime warnings never become recovery's failure cause or
the plugin it blames.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(safe-mode): disable plugins instead of removing them

Safe Mode now switches a plugin off the way the plugin market's own toggle
does, so it can come back without a reinstall: `disabled: true` rows for the
loader entries the package inserts in the user patch layer, which the loader
re-applies on every boot, plus the market's .dsh-market/state.json list,
which is also the only switch for client-only packages. Nothing is deleted.

Disabled plugins show as disabled with a Re-enable action instead of a
selection. Compatibility issues that call for disabling a plugin use the same
path; the old approach of dropping it from dsh.profile.bundles was silently
undone, because the generation projection rewrites that list on every launch.
Compatibility inspection skips disabled plugins, since they never load.

A disable-carrier (a bundle whose patch disables another plugin) cannot be
switched off on its own without stranding the plugin it replaces, so it
keeps the backed-up removal. Startup recovery keeps removal too: a package
that is itself broken fails before the patch layer's disable applies. When
recovery removes a plugin it also clears its market disable entry, as the
market's own uninstall does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(repair-agent): start a fresh, well-briefed repair session from one card

Recovery and Safe Mode now offer a single "start the repair agent" card. Each
click opens a new session in the Safe Mode Harness and makes it the session
the Harness UI shows: the UI restores its current session from desktop
storage when the page loads, so the Recovery path opens the session before
its page loads and the Safe Mode path reloads the already open page. The
floating repair widget is gone.

The session's workspace is the Harness home (profiles, plugins, patch layer)
instead of the empty launch root, and the first turn of each session carries
the diagnosis. The system prompt is rebuilt around real incidents:

- a directory map that separates the normal profile it repairs from the Safe
  Mode profile it runs in;
- the path to harness.log with how to find the failed launch in it, plus a
  short excerpt, instead of pasting logs; Safe Mode's own logs are no longer
  used when no failed launch was captured;
- one bilingual playbook source, so the Chinese and English prompts can no
  longer drift: plugin load failures, broken packages (which disabling cannot
  fix), the Windows module-fallback EPERM (no Developer Mode advice), a
  corrupt patch layer, startup timeouts, the code -> ptc preset rename, and
  schema errors in copied user presets;
- actions named after the real UI, and a confirm-and-back-up rule before any
  file change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(market): stop shipping dshmarket so market upgrades take effect

Harness resolves every profile bundle from its own installation before the
profile (`resolveBundleDir` in dsh-app-boot). Since #352 pinned dshmarket
1.45.1 as a runtime dependency, the packaged copy won that lookup: upgrading
the market wrote 1.48.0 into the profile, the market and the baseline check
both read 1.48.0 back, and Harness kept loading 1.45.1 after every restart.

Nothing in the app imports dshmarket; the profile copy is installed from npm
by the market installer and `ensureMarketBaseline`. It stays a
devDependency for the tests that exercise its real profile reader, which
keeps it out of the packaged app.

`ensureMarketBaseline` now also notes when the copy Harness would load is not
the profile's, so a shadowed market shows up in harness.log instead of as a
version the UI reports but never runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(recovery): let startup recovery repair a plugin market that breaks startup

The market is a core bundle, so startup-failure attribution never named it
and the Recovery page, finding no culprit, could only offer Safe Mode — which
does not load the market and has nothing to act on. A market release that
stopped Harness from booting therefore failed every launch the same way.
Until now the packaged dshmarket shadowed the profile copy and hid this;
with it gone the profile copy really loads, so the loop is live.

When the failed launch's log (or loader provenance) names dshmarket, the
Recovery page shows it as its own row, apart from the third-party plugin
list whose removal path refuses core bundles:

- Upgrade to the newest release the market check finds compatible with this
  Harness, for a market too old for it; a release already tried here is not
  offered again.
- Install the verified version (VERIFIED_MARKET_BASELINE). That moves down
  from a broken release, up from a stale one, or reinstalls a damaged copy.
  It is the primary action when the market is the only culprit.
- Remove the market; installed community plugins are kept.

Versions come from the main process's own check, never from the page. Each
change stops Harness first, as the shared-tree installer requires, pins the
exact version so the next baseline check does not reinstall the broken
release, then relaunches and returns to recovery if startup still fails.
The market removal behind the settings page is shared as `removeMarket`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): render the plugin market row like any other plugin row

The market row added its own touches: a version after the name, a green
button for restoring the verified version (which can be a downgrade), and a
confirmation dialog plus its own wording for removal. Third-party rows have
none of these, so the market read as a different kind of item.

Show the name only, keep green for the upgrade alone with every other action
in the neutral style, and remove it the way the page removes any plugin —
"Remove this plugin", no extra confirmation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): offer the plugin market only upgrade and removal

Restoring the verified market version was an action no other plugin row
has. Drop it, so the market row is a plugin row like the rest: upgrade to a
compatible release when the market check finds one, and removal.

When the market alone blocks startup, the page now uses the same buttons as
a lone third-party plugin: "Upgrade plugin and restart" with "Uninstall
plugin" beside it when an upgrade exists, otherwise "Remove this plugin and
continue".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: stop tracking the local pnpm content store

0d083d5 committed the whole `.pnpm-store/` (13,357 files, ~235 MB) along
with its four intended changes, before 7a2cb73 added it to .gitignore. An
ignore rule does not untrack files already committed, so they stayed in the
branch and buried the PR's real diff.

Remove them from the index only; the local store is untouched and stays
ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: drop tests that only grep our own source code

About 40 tests read src/main/index.ts, the preload, build/*.html, the NSIS
script or our own packages' client.js as text and asserted that particular
lines were present — `toContain("ipcMain.handle('safe-mode:action'")`,
`indexOf(a) < indexOf(b)` and the like. They execute nothing, so they cannot
catch a behavior regression, yet they fail on any rename or reformat, and on
Windows, where the checkout has CRLF line endings, any expected string that
spans a newline never matches. Two of them failed CI that way on PR #431.

Removed: every test block whose assertions were such text matches, and the
source-text lines inside otherwise behavioral tests (windows-titlebar
allowlist, runtime Node mode, release dev channel, branding postinstall,
harness-node-entry). The dev-channel check now reads electron-builder.dev.cjs
as a config object instead of matching its text.

Kept: tests that execute code, parse package.json/lockfile/YAML/JSON
config, or check the installed third-party bundles in node_modules (patch
and upstream-contract checks), since those inspect what actually ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(repair-agent): drop tests that only check the prompt's wording

Two repair-prompt tests asserted fixed sentences of the generated prompt:
exact Chinese phrases with hard-coded POSIX paths, and a fixed count of
seven playbooks plus two error strings. Any edit to the copy broke them
without saying anything about how the agent behaves, and the path one
failed on Windows, where path.join correctly yields backslashes.

Keep the test of real prompt logic: only the tail of the log is included,
and none at all when no failed launch was captured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(repair-agent): drop the remaining repair-prompt test

It asserted which log lines and which sentence end up in the generated
prompt text. Like the other prompt tests, that pins the copy rather than
anything the agent does with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): offer the repair agent as one line under the summary

The repair agent sat in its own section below the actions — a heading, a
divider and a card, about 140px. In a 1280x800 window that pushed the plugin
list into a scrollbar, which the native Windows recovery UI check rejects:
every plugin has to be visible at once. Its text was also Chinese-only.

Show it as a single link under the summary instead, in both languages. It
sends the same diagnostic request as the card did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): offer the repair agent as a button beside Safe Mode

The recovery page showed the repair agent in its own section under the
technical details — a heading and a card, in Chinese only. Make it a
secondary "Repair agent" button next to "Enter Safe Mode", in both
languages. As before, it appears only when no plugin or market was
identified; a named culprit has its own repair above.

The native Windows UI check expected Safe Mode to be the only action when
nothing is identified; it now expects the agent beside it, and nowhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(runtime): keep a clean launch off stderr

The packaged Windows smoke fails any launch that writes to stderr once a
workspace and session exist. Two lines did on every launch, and neither
reported a problem:

- The log bridge's "recording from here on" marker. It is a notice, so it
  now goes to stdout; stderr carries only bridged warnings and errors.
- `patch: entry dsh-market not found`. The desktop patch configured the
  market's entry (restart off, wait for desktopProfiles) unconditionally,
  but the market is optional, and a patch row whose entry is absent makes the
  loader warn. It was always there; the bridge only made it visible. The row
  now lives in dsh-desktop-market.patch.yml, passed as a second --patch only
  when the profile boots dshmarket, so a market profile behaves as before.

Checked by booting a fresh web profile (no market overlay, no bridged
warning) and one that boots dshmarket (overlay applied, no warning).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(market): remove dshmarket devDependency to avoid shadowing profile updates (#478)

* fix(market): remove dshmarket devDependency to avoid shadowing profile updates

Harness resolves every profile bundle from its installation anchor before
the user profile (resolveBundleDir in dsh-app-boot).

Previously, dshmarket was retained as a devDependency in package.json to
support test/generation-boundary.test.js. However, electron-builder's
files: ["node_modules/**/*"] glob unconditionally includes all modules in
node_modules unless explicitly excluded, causing dshmarket to be bundled
into the packaged application and to exist in dev checkouts. When the user
upgrades dshmarket in their profile to 1.48.0, Harness continues loading
the packaged copy instead of the profile's copy.

This removes dshmarket from devDependencies and package-lock.json:
- test/generation-boundary.test.js implements a local readInstalledVersion helper
- test/market-baseline.test.ts verifies dshmarket is absent from both dependencies
  and devDependencies, and simulates installation shadowing with a mock directory

* style(safe-mode): style repair agent session link with blue accent color

* fix(safe-mode): make the repair agent's failures visible and its guidance true (#479)

* fix(safe-mode): report a repair agent session that could not open

Clicking "let the repair agent read the log" did nothing at all when the
session failed to start: startRepairAgentPrompt returned a bare boolean,
the caller skipped the reload, openHarness found the window already on
the Harness origin and only raised it, and the reason reached the log
alone. A user whose default agent preset was broken saw three clicks
produce no visible change.

The call now carries the reason. The Safe Mode manager stays open and
shows it instead of closing on the way out, so the click can be retried,
and plugin recovery surfaces the same reason through the manager it hands
the window to. The notice keeps the first two lines of Harness's mount
error; the rest is a stack that would push the plugin list off the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(plugin-disable): refuse a bundle whose package yields no loader row

Disabling a broken plugin reported success and changed nothing that
matters. pluginPatchRows reads the loader rows from the package's own
patch; when that file is missing or unparsable the list comes back empty,
so the patch layer was skipped, only the market state was written, and
the result was ok. The bundle stayed in dsh.profile.bundles and the next
launch composed — and failed on — the same package.

A package listed as a bundle is one the loader prepares on every launch,
so no readable row means the package on disk is broken, not that the
plugin has no rows. That case is now refused as broken-package, and Safe
Mode falls back to the backed-up removal it already uses for carriers.
A client-only plugin is untouched: it is not a listed bundle, and the
market state remains its only switch.

The compatibility fixture listed a bundle whose manifest declared no
dsh.bundle — a profile that cannot start — so it now carries the patch
the loader would read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(repair-agent): tighten the prompt and drop the unused repair module

The prompt repeated absolute Windows paths in every playbook entry and
walked through the Safe Mode profile the agent must not touch. Paths are
given once in the directory section and referred to by name afterwards;
the Safe Mode profile is one line. 5290 to 4403 characters, with more in
it than before.

What changed beyond wording:

- The agent fixes what it can fix itself. Disabling a plugin is spelled
  out (patch layer plus market state, never dsh.profile.bundles), and
  only upgrading and reinstalling still go through the UI.
- A broken package is triaged by layer first: node_modules/<package> is
  usually a link the next launch reprojects, so a merely missing link is
  a restart, not a removal. Only a missing manifest or patch in the real
  package is an interrupted install.
- Windows EPERM tries to self-heal once by deleting the entry the error
  names — Safe Mode does not use that folder — before asking the user to
  hunt for the process holding it.
- A clean log is a finding: say nothing is wrong, name the log sections
  checked, and ask what the user actually saw. The "a quiet log proves
  nothing" line that contradicted this is gone, and an empty offline
  diagnosis is no longer printed as if it were a result.
- Failures outside the list are reasoned from the log, with the source
  repository named for confirming Desktop's own behaviour.
- The preset-not-found entry is dropped: an old session naming a deleted
  preset simply cannot open, and recreating the preset to revive it is
  not a repair worth prescribing.

profile-repair.ts goes with it. Startup stopped performing destructive
package repair, which left the module with no caller in src/ and only
its own test — a documented, tested path to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: point the community link at the current Discord invite

The old invite is replaced everywhere it appears: the six READMEs, the
Discord entry on the plugin recovery and Safe Mode pages, and the
recovery smoke test that asserts the URL the click opens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(workspace): start a new session on the default preset, not a stale blank one (#481)

* fix(workspace): start a new session on the default preset, not a stale blank one

Opening a workspace does not create a session when a blank one is already
there: connectWorkspace returns the first blank session whose cwd matches
and only calls sessions.create when it finds none. A blank session is
never run, so it stays blank forever — every later visit to that project
is handed the same one as its "new session", carrying whatever preset was
default when it was made. Changing the default in settings reaches only
sessions created afterwards, which a session made weeks ago is not.

Switching that session's preset instead is not available: the host
resumes a session before recomposing it, and resuming loads the preset it
already names. A blank session naming a preset that has since been
deleted cannot be resumed at all, so it cannot be moved either — the
select call comes back with "resume failed … preset not found".

So the stale session is left alone and simply not reused. A blank session
now qualifies only when the preset it names matches the current default,
and a workspace whose blank session does not is given a fresh one. The
stale session stays in the list to open or delete.

The default is read through ctx.get rather than an inject: reading an
un-injected Remote namespace throws rather than answering undefined, and
this plugin has to keep working where no preset roster is composed. When
it cannot be read, reuse behaves exactly as before.

Verified in a running Harness: opening the project created
session-67b5595c on `standard` and left session-95d736d0 on the deleted
`repair` untouched, where before the stale one was handed back every
time. The patch applies cleanly to the pristine 0.1.5-rc.2 package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(repair-agent): check model and key availability before starting repair

- Add checkModelAvailability in RepairAgentService to verify model keys and default session model status
- Show informative warning dialog in Safe Mode and Recovery views when models are unavailable or unroutable
- Add unit tests covering missing keys, unroutable providers, provider failures, and valid configurations

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix: add loader await timeout diagnostics and refine model unavailable copy (#485)

* fix(loader): add timeout and stuck plugin diagnostics to loader await

- Add timeout handling to cordis-plugin-loader EntryTree.prototype.await (configurable via DSH_LOADER_TIMEOUT_MS, default 100s) with 5s progress heartbeat
- Accurately identify stuck plugins on timeout and emit structured dshPluginFailure errors
- Add unit test coverage in cordis-plugin-loader-patch.test.ts

* fix(repair-agent): update model unavailable dialog copy text

- Refine warning dialog message and detail copy when default model is unconfigured or unavailable
- Update unit tests in test/repair-agent.test.ts

* update

* test(repair-agent): sync model unavailable detail assertion wording (#486)

* feat: adapt desktop to Harness 0.1.6-alpha.2

---------

Co-authored-by: fzfz <fzfzqp@gmail.com>
Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Fairy-happy <243122010@qq.com>
Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Keep-Silence <keep137950@163.com>
Co-authored-by: Wanming08 <152006802+Wanming08@users.noreply.github.com>
yaojin3616 added a commit that referenced this pull request Sep 24, 2026
* feat: adapt Desktop to Harness 0.1.6-alpha.2 (#499)

* feat(release): configure initial 5% rollout on crash service (#401)

- Add configure-rollout.mjs script to manage platform rollout rules on crash service

- Integrate initial 5% rollout step into release workflow

- Add unit tests covering creation, update, and race condition conflict recovery

* fix(llm): send Harness session headers through pi-ai (#329)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* fix(llm): recover missing terminal content (#400)

Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: import the local web Harness home on first launch (#397)

* feat: import the local web Harness home on first launch

Copy ~/.dsh into an unused desktop home so same-machine web users are not treated as a fresh install.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(market-baseline): treat pnpm isolated-store symlinks as valid installs

In dev mode (dsh-desktop-dev), profiles have no nodeLinker:hoisted config
so pnpm installs dshmarket as a symlink into .pnpm/…. The previous
isSymbolicLink() check treated any symlink as a stray generation link,
deleted it, then called pnpm to reinstall — which recreated the same
symlink. The post-install guard rejected it and the app entered Safe Mode
on every dev launch.

Fix: read the symlink target and only treat paths containing `.generations`
as generation links. pnpm isolated-store symlinks (.pnpm/…) are left alone
in demoteMarketGeneration and ensureMarketBaseline, and are accepted by
upgradeMarketInSharedTree's post-install verification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(test): correct patch indentation in provider-error-patch.test.ts (#433)

* fix(test): correct patch indentation in provider-error-patch.test.ts

* fix(types): add type declarations for configure-rollout.mjs and resolve typecheck errors

* fix(diagnostics): 减少误报故障反馈、优化插件自愈免报与修复 Windows 启动缺陷 (#418)

* fix(diagnostics): reduce false-positive crash reports and fix Windows startup issues

- Suppress false-positive unclean-exit reports when previous session log was healthy
- Mark clean exit synchronously on before-quit and handle Windows session-end
- Defer plugin failure reports during startup and discard them when recovery succeeds
- Fix Windows EPERM on junction removal in ensureSymlink (@deepseek-ai/dsh-app-boot)
- Use pathToFileURL in loadDesktopResource to avoid ERR_FAILED (-2) on non-C drives
- Enrich render-process-gone logs in harness.log with URL and GPU feature status
- Enable V8 compile cache (NODE_COMPILE_CACHE) for faster Node.js restarts

* fix(windows): declare asInvoker, enable long paths, and defer profile inspection

* feat(runtime): prefix harness.log lines with ISO date timestamp

* feat(release): sign inner Windows binaries and repackage NSIS installer with SafeNet UKey

* fix(release): harden Windows inner signing with version sync and fallback

* fix(runtime): use platform-specific path join for NODE_COMPILE_CACHE

* fix(types): add type declarations for configure-rollout.mjs and resolve typecheck errors

* test: serialize suites on Windows CI and set global timeout to prevent hangs

* fix(release): verify PE header for .node files to skip non-Windows prebuilds in Jsign

* perf(windows): exclude build-tool deps and add Defender exclusions on install

- Exclude ~2,000 build-tool-only transitive files from the Windows package:
    caniuse-lite (838 JS files, browserslist/babel only)
    lodash (1,048 JS files, only used by @malept/flatpak-bundler linux tool)
    browserslist / electron-to-chromium / update-browserslist-db (15 files)
    @malept/* (flatpak bundler, Linux-only build tool)
  Excluded packages are verified to have no runtime dependency chain:
    - @mixmark-io/domino kept (dsh-tool-web → turndown → domino)
    - jsdom kept (dsh-ppt runtime dep)
    - dsh-ppt's nested typescript kept (only top-level typescript is absent)

- Add Windows Defender exclusions in NSIS installer (customInstall macro):
  Add-MpPreference for $INSTDIR and $APPDATA\dsh-desktop via PowerShell.
  Uses -ErrorAction SilentlyContinue so non-admin installs fail silently.
  Without this exclusion Defender scans 44k files on first launch
  (observed: 121-123 s until Harness ready on a fresh install). With the
  exclusion, first launch should complete in under 20 s.

* fix(release): align package.json version with package-lock.json (0.1.1)

* fix(test): correct patch indentation in provider-error-patch.test.ts

* fix(windows): optimize titlebar layout, integrate more actions into app menu, and fix sidebar overlap (#409)

* perf(windows): cut 20k non-runtime files, elevate defender exclusion, and relax startup timeout (#435)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite in… (#437)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite install

An in-app update can be force-killed before will-quit, and a leftover session marker from another version is a reinstall, not a crash.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(diagnostics): suppress startup failure report when frontend identifies incompatible plugins

* fix(build): preserve runtime skill markdown files in build output and update ppt artifacts

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* fix(windows): clear session marker on overwrite install (#440)

NSIS force-kills the running app, so will-quit never clears session.json and a same-version overwrite looks like an unclean exit.

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix/suppress upgrade unclean exit (#441)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite install

An in-app update can be force-killed before will-quit, and a leftover session marker from another version is a reinstall, not a crash.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(diagnostics): suppress startup failure report when frontend identifies incompatible plugins

* fix(build): preserve runtime skill markdown files in build output and update ppt artifacts

* update

* update

* update

* update

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: stop tracking plugin-management and keep it local only (#443)

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: 建立仓库级 AGENTS.md 开发约束与治理规范 (#428)

* docs: establish repository-wide AGENTS development standards and governance contracts

- Add root AGENTS.md defining project boundaries, TypeScript code style, dependency/data constraints, and verification runbooks.
- Add directory-scoped AGENTS.md rules for src/main, src/preload, packages, patches, and build.
- Add supporting design specifications:
  - docs/code-standards.md (design rationale and phased tool enforcement)
  - docs/patch-plugin-contract.md (patch vs plugin selection and slot compatibility)
  - docs/source-build-contract.md (single authoritative source and build artifact boundaries)

* docs: 补齐 AGENTS 前端硬约束与企业/编译型包覆盖

把主题 token、locale、preload 与 slot 边界写成可执行规则,并标出 enterprise 存量与 ppt-runtime 的维护入口。

---------

Co-authored-by: fairy <3054158379@qq.com>

* fix(security): allow notifications permission from the harness window (#461)

The allowlist only covered clipboard-sanitized-write, so notification
permission requests from the harness page could not be granted. The
loopback main-frame checks are unchanged, so the trust boundary is the
same.

* fix(release): honor platform target and add signed-only dispatch mode (#468)

A filled prerelease tag previously forced a Windows build even when
macos was selected. Dispatch now separates development, signed, and
prerelease so production-identity packages can be signed without publishing.

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(settings): keep long navigation menus scrollable (#456)

* fix(settings): keep long navigation menus scrollable

* fix(settings): add navigation overflow patch

* feat(safe-mode): add floating repair agent widget in recovery and safe mode (#431)

* feat(safe-mode): add floating repair agent widget in recovery and safe mode

- Add floating repair agent FAB widget on plugin recovery and safe mode overlay
- Provide pre-configured quick action prompts with error diagnostics and logs
- Support switching models and uploading/pasting screenshots for vision models
- Connect with isolated safe mode Harness session and stream responses via IPC

* feat(safe-mode): temporarily disable floating repair agent chat widget

* feat(safe-mode): enable repair agent with 0.9.0 incident knowledge and offline diagnostics

* feat(release): expand release notes generator with performance section and full changelog

* feat(window): persist window bounds, maximized state, and zoom level

* fix(repair-agent): handle unconfigured models with api key drawer and prevent silent stream failures

* fix(dev): strip inherited ELECTRON_RUN_AS_NODE to prevent electron launch crash

* feat(dev): forward runtime logs to console in development and log single-instance conflicts

* fix(repair-agent): inherit working default model route and add dual-track history polling

* refactor(repair-agent): rely purely on websocket stream without redundant history polling

* feat(repair-agent): replace floating widget with seamless native Harness UI diagnostic cards

* fix(windows): stop GPU fallback ladder from degrading on TDR device-loss recoveries, recover the menu view from a lost renderer

gcp3 crash data for v0.9.0 showed 23/30 Windows gpu-crash reports at exitCode=34
— Chromium's own exit code when it detects a lost D3D11 device (typically a
driver TDR reset) and exits the GPU process on purpose so it can restart it.
The fallback ladder was counting this self-recovery as evidence the sandbox is
broken and would degrade hardware acceleration after 3 of these on an
otherwise healthy machine. isGpuLossFatal now takes the exit code and treats
exitCode 34 as non-fatal, only logging a breadcrumb.

Separately, ~6 renderer-crash reports were on windows-menu.html: that view runs
in its own WebContentsView outside installMainWindowRendererRecovery, which
only hooks the main window's webContents, so a lost renderer there left a dead
menu until the whole app restarted. It now reloads itself on render-process-gone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(session): skip unreadable session logs instead of failing the whole listing

One corrupt JSONL/zstd session log made sessionPersistence.list() throw, which
failed dsh-workspace and stopped the Harness entirely (0.9.0 startup-failure
reports: "corrupt Zstandard session log", corrupt header id). The log is now
skipped with a warning naming its path and left on disk untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(startup): stop reporting attributed plugin failures, clear stale writer locks, fix repair diagnosis

- Harness startup failures that plugin recovery attributes to a user plugin
  are handed to the user and their pending crash report is discarded; only
  the frontend recovery path did this before, so they were still uploaded.
- Remove dsh-atomic-write locks in DSH_HOME and DSH_HOME/profiles whose owner
  pid is gone before launching; a killed Harness left .credentials.yaml.lock
  behind and the next launch timed out on it.
- Repair Agent diagnosis uses loader provenance and the recovery-resolved
  plugins, and only reports a startup timeout when the runtime says so, since
  "waiting for Harness" + SIGTERM matched ordinary failure cleanup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): ensure safe mode uses hoisted node-linker and hide diagnostics when plugins are identified

* feat(runtime): add preset YAML validation step before launch

* refactor(runtime): remove redundant preset check in favor of native Cordis loader logging

* feat(repair-agent): unify diagnostic cards into a single entry with verification and reporting instructions

* fix(repair-agent): restore the 3 fine-grained diagnostic cards in safe mode

* fix(safe-mode): unify safe mode diagnostic cards into a single AI agent entry

* fix(web-import): never replace a desktop home that already holds plugins or credentials

A 0.9.0-rc1 home with plugins or .credentials.yaml but no settings.yaml or
sessions was treated as unused, so the web import deleted it wholesale. It now
counts as used. An unused home is also moved aside before deletion so
delete-pending entries on Windows cannot block links created at the same path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): don't remove plugins a deferred migration has not installed yet

When the generation migration failed (e.g. pnpm EPERM right after install),
imported plugins stayed manifest-only, failed to prepare, and plugin recovery
removed them as incompatible. Deferred migrations now report the pending
plugins and recovery excludes them. Transient EPERM/EBUSY/EACCES failures no
longer freeze the same profile for six hours, so the next launch retries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): stop the titlebar drag region from swallowing clicks and churning

The drag region now ignores pointer events, and only semantic dialog markers
hide it: class-name guesses like [class*="modal"] matched permanent elements
and hid it for good. Visibility checks run at most once a frame, since
streaming output mutates the DOM continuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(diagnostics): send crashes without an event id right away

Only a plugin-attributed startup failure should wait for recovery to decide
whether to discard it. A crash with no event id compared equal to an unset
pending id and was held back too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(market): pin the exact version a declared dshmarket range names

The installer pins and verifies an exact version, so a declared range such as
^1.47.0 is reduced to the version it names instead of being passed through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: ignore the local pnpm content store

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): resolve Safe Mode plugins from the installation, not the module fallback

Safe Mode loads only installation-owned bundles, yet it still went through
the shared $DSH_HOME/profiles/node_modules fallback, whose junctions Windows
can refuse to recreate (EPERM) for minutes — the top startup failure of
v0.9.0. The desktop now sets DSH_DESKTOP_HOST_RESOLVED for the Safe Mode
profile only; the patched Harness then skips healing the fallback and
resolves bare plugin names, including entries created at runtime through
ctx.loader.create, from its own installation. A flag leaked from the parent
environment never reaches a normal profile.

This replaces the hoisted node-linker workaround for Safe Mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(runtime): mirror Harness logs to the console only in development

A packaged app's stdout may be a closed pipe; writing every Harness log line
to it there buys nothing and can fail. harness.log still receives every line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): still blame a migration-pending plugin whose legacy copy is installed

f475ce5 exempted every plugin with a pending deferred migration from
recovery. Only a plugin the migration left manifest-only is not broken; one
whose legacy copy is installed still loads and can be the real culprit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(logging): record Harness runtime warnings and errors in harness.log

Cordis's ctx.logger keeps messages only in an in-memory ring, and its default
level filters out warnings; nothing in the shipped composition exported them.
Session activation failures (a preset that cannot be found or mounted) never
reach the logger at all — they are only pushed to the client. So errors after
startup left no trace for a person or the Repair Agent once the process was
gone.

A new desktop plugin, dsh-desktop-log-bridge, is composed first into both the
normal and the Safe Mode profile. It writes warn and error messages, the
errors already in the ring, and api-session/error events to stderr, which
the desktop records into harness.log. It rate-limits floods and writes one
ready line per launch, so a log without it is known to predate the bridge.

Every bridged line carries a [harness-log] prefix, and latestHarnessAttemptLogs
drops such lines: runtime warnings never become recovery's failure cause or
the plugin it blames.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(safe-mode): disable plugins instead of removing them

Safe Mode now switches a plugin off the way the plugin market's own toggle
does, so it can come back without a reinstall: `disabled: true` rows for the
loader entries the package inserts in the user patch layer, which the loader
re-applies on every boot, plus the market's .dsh-market/state.json list,
which is also the only switch for client-only packages. Nothing is deleted.

Disabled plugins show as disabled with a Re-enable action instead of a
selection. Compatibility issues that call for disabling a plugin use the same
path; the old approach of dropping it from dsh.profile.bundles was silently
undone, because the generation projection rewrites that list on every launch.
Compatibility inspection skips disabled plugins, since they never load.

A disable-carrier (a bundle whose patch disables another plugin) cannot be
switched off on its own without stranding the plugin it replaces, so it
keeps the backed-up removal. Startup recovery keeps removal too: a package
that is itself broken fails before the patch layer's disable applies. When
recovery removes a plugin it also clears its market disable entry, as the
market's own uninstall does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(repair-agent): start a fresh, well-briefed repair session from one card

Recovery and Safe Mode now offer a single "start the repair agent" card. Each
click opens a new session in the Safe Mode Harness and makes it the session
the Harness UI shows: the UI restores its current session from desktop
storage when the page loads, so the Recovery path opens the session before
its page loads and the Safe Mode path reloads the already open page. The
floating repair widget is gone.

The session's workspace is the Harness home (profiles, plugins, patch layer)
instead of the empty launch root, and the first turn of each session carries
the diagnosis. The system prompt is rebuilt around real incidents:

- a directory map that separates the normal profile it repairs from the Safe
  Mode profile it runs in;
- the path to harness.log with how to find the failed launch in it, plus a
  short excerpt, instead of pasting logs; Safe Mode's own logs are no longer
  used when no failed launch was captured;
- one bilingual playbook source, so the Chinese and English prompts can no
  longer drift: plugin load failures, broken packages (which disabling cannot
  fix), the Windows module-fallback EPERM (no Developer Mode advice), a
  corrupt patch layer, startup timeouts, the code -> ptc preset rename, and
  schema errors in copied user presets;
- actions named after the real UI, and a confirm-and-back-up rule before any
  file change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(market): stop shipping dshmarket so market upgrades take effect

Harness resolves every profile bundle from its own installation before the
profile (`resolveBundleDir` in dsh-app-boot). Since #352 pinned dshmarket
1.45.1 as a runtime dependency, the packaged copy won that lookup: upgrading
the market wrote 1.48.0 into the profile, the market and the baseline check
both read 1.48.0 back, and Harness kept loading 1.45.1 after every restart.

Nothing in the app imports dshmarket; the profile copy is installed from npm
by the market installer and `ensureMarketBaseline`. It stays a
devDependency for the tests that exercise its real profile reader, which
keeps it out of the packaged app.

`ensureMarketBaseline` now also notes when the copy Harness would load is not
the profile's, so a shadowed market shows up in harness.log instead of as a
version the UI reports but never runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(recovery): let startup recovery repair a plugin market that breaks startup

The market is a core bundle, so startup-failure attribution never named it
and the Recovery page, finding no culprit, could only offer Safe Mode — which
does not load the market and has nothing to act on. A market release that
stopped Harness from booting therefore failed every launch the same way.
Until now the packaged dshmarket shadowed the profile copy and hid this;
with it gone the profile copy really loads, so the loop is live.

When the failed launch's log (or loader provenance) names dshmarket, the
Recovery page shows it as its own row, apart from the third-party plugin
list whose removal path refuses core bundles:

- Upgrade to the newest release the market check finds compatible with this
  Harness, for a market too old for it; a release already tried here is not
  offered again.
- Install the verified version (VERIFIED_MARKET_BASELINE). That moves down
  from a broken release, up from a stale one, or reinstalls a damaged copy.
  It is the primary action when the market is the only culprit.
- Remove the market; installed community plugins are kept.

Versions come from the main process's own check, never from the page. Each
change stops Harness first, as the shared-tree installer requires, pins the
exact version so the next baseline check does not reinstall the broken
release, then relaunches and returns to recovery if startup still fails.
The market removal behind the settings page is shared as `removeMarket`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): render the plugin market row like any other plugin row

The market row added its own touches: a version after the name, a green
button for restoring the verified version (which can be a downgrade), and a
confirmation dialog plus its own wording for removal. Third-party rows have
none of these, so the market read as a different kind of item.

Show the name only, keep green for the upgrade alone with every other action
in the neutral style, and remove it the way the page removes any plugin —
"Remove this plugin", no extra confirmation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): offer the plugin market only upgrade and removal

Restoring the verified market version was an action no other plugin row
has. Drop it, so the market row is a plugin row like the rest: upgrade to a
compatible release when the market check finds one, and removal.

When the market alone blocks startup, the page now uses the same buttons as
a lone third-party plugin: "Upgrade plugin and restart" with "Uninstall
plugin" beside it when an upgrade exists, otherwise "Remove this plugin and
continue".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: stop tracking the local pnpm content store

0d083d5 committed the whole `.pnpm-store/` (13,357 files, ~235 MB) along
with its four intended changes, before 7a2cb73 added it to .gitignore. An
ignore rule does not untrack files already committed, so they stayed in the
branch and buried the PR's real diff.

Remove them from the index only; the local store is untouched and stays
ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: drop tests that only grep our own source code

About 40 tests read src/main/index.ts, the preload, build/*.html, the NSIS
script or our own packages' client.js as text and asserted that particular
lines were present — `toContain("ipcMain.handle('safe-mode:action'")`,
`indexOf(a) < indexOf(b)` and the like. They execute nothing, so they cannot
catch a behavior regression, yet they fail on any rename or reformat, and on
Windows, where the checkout has CRLF line endings, any expected string that
spans a newline never matches. Two of them failed CI that way on PR #431.

Removed: every test block whose assertions were such text matches, and the
source-text lines inside otherwise behavioral tests (windows-titlebar
allowlist, runtime Node mode, release dev channel, branding postinstall,
harness-node-entry). The dev-channel check now reads electron-builder.dev.cjs
as a config object instead of matching its text.

Kept: tests that execute code, parse package.json/lockfile/YAML/JSON
config, or check the installed third-party bundles in node_modules (patch
and upstream-contract checks), since those inspect what actually ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(repair-agent): drop tests that only check the prompt's wording

Two repair-prompt tests asserted fixed sentences of the generated prompt:
exact Chinese phrases with hard-coded POSIX paths, and a fixed count of
seven playbooks plus two error strings. Any edit to the copy broke them
without saying anything about how the agent behaves, and the path one
failed on Windows, where path.join correctly yields backslashes.

Keep the test of real prompt logic: only the tail of the log is included,
and none at all when no failed launch was captured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(repair-agent): drop the remaining repair-prompt test

It asserted which log lines and which sentence end up in the generated
prompt text. Like the other prompt tests, that pins the copy rather than
anything the agent does with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): offer the repair agent as one line under the summary

The repair agent sat in its own section below the actions — a heading, a
divider and a card, about 140px. In a 1280x800 window that pushed the plugin
list into a scrollbar, which the native Windows recovery UI check rejects:
every plugin has to be visible at once. Its text was also Chinese-only.

Show it as a single link under the summary instead, in both languages. It
sends the same diagnostic request as the card did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): offer the repair agent as a button beside Safe Mode

The recovery page showed the repair agent in its own section under the
technical details — a heading and a card, in Chinese only. Make it a
secondary "Repair agent" button next to "Enter Safe Mode", in both
languages. As before, it appears only when no plugin or market was
identified; a named culprit has its own repair above.

The native Windows UI check expected Safe Mode to be the only action when
nothing is identified; it now expects the agent beside it, and nowhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(runtime): keep a clean launch off stderr

The packaged Windows smoke fails any launch that writes to stderr once a
workspace and session exist. Two lines did on every launch, and neither
reported a problem:

- The log bridge's "recording from here on" marker. It is a notice, so it
  now goes to stdout; stderr carries only bridged warnings and errors.
- `patch: entry dsh-market not found`. The desktop patch configured the
  market's entry (restart off, wait for desktopProfiles) unconditionally,
  but the market is optional, and a patch row whose entry is absent makes the
  loader warn. It was always there; the bridge only made it visible. The row
  now lives in dsh-desktop-market.patch.yml, passed as a second --patch only
  when the profile boots dshmarket, so a market profile behaves as before.

Checked by booting a fresh web profile (no market overlay, no bridged
warning) and one that boots dshmarket (overlay applied, no warning).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(market): remove dshmarket devDependency to avoid shadowing profile updates (#478)

* fix(market): remove dshmarket devDependency to avoid shadowing profile updates

Harness resolves every profile bundle from its installation anchor before
the user profile (resolveBundleDir in dsh-app-boot).

Previously, dshmarket was retained as a devDependency in package.json to
support test/generation-boundary.test.js. However, electron-builder's
files: ["node_modules/**/*"] glob unconditionally includes all modules in
node_modules unless explicitly excluded, causing dshmarket to be bundled
into the packaged application and to exist in dev checkouts. When the user
upgrades dshmarket in their profile to 1.48.0, Harness continues loading
the packaged copy instead of the profile's copy.

This removes dshmarket from devDependencies and package-lock.json:
- test/generation-boundary.test.js implements a local readInstalledVersion helper
- test/market-baseline.test.ts verifies dshmarket is absent from both dependencies
  and devDependencies, and simulates installation shadowing with a mock directory

* style(safe-mode): style repair agent session link with blue accent color

* fix(safe-mode): make the repair agent's failures visible and its guidance true (#479)

* fix(safe-mode): report a repair agent session that could not open

Clicking "let the repair agent read the log" did nothing at all when the
session failed to start: startRepairAgentPrompt returned a bare boolean,
the caller skipped the reload, openHarness found the window already on
the Harness origin and only raised it, and the reason reached the log
alone. A user whose default agent preset was broken saw three clicks
produce no visible change.

The call now carries the reason. The Safe Mode manager stays open and
shows it instead of closing on the way out, so the click can be retried,
and plugin recovery surfaces the same reason through the manager it hands
the window to. The notice keeps the first two lines of Harness's mount
error; the rest is a stack that would push the plugin list off the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(plugin-disable): refuse a bundle whose package yields no loader row

Disabling a broken plugin reported success and changed nothing that
matters. pluginPatchRows reads the loader rows from the package's own
patch; when that file is missing or unparsable the list comes back empty,
so the patch layer was skipped, only the market state was written, and
the result was ok. The bundle stayed in dsh.profile.bundles and the next
launch composed — and failed on — the same package.

A package listed as a bundle is one the loader prepares on every launch,
so no readable row means the package on disk is broken, not that the
plugin has no rows. That case is now refused as broken-package, and Safe
Mode falls back to the backed-up removal it already uses for carriers.
A client-only plugin is untouched: it is not a listed bundle, and the
market state remains its only switch.

The compatibility fixture listed a bundle whose manifest declared no
dsh.bundle — a profile that cannot start — so it now carries the patch
the loader would read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(repair-agent): tighten the prompt and drop the unused repair module

The prompt repeated absolute Windows paths in every playbook entry and
walked through the Safe Mode profile the agent must not touch. Paths are
given once in the directory section and referred to by name afterwards;
the Safe Mode profile is one line. 5290 to 4403 characters, with more in
it than before.

What changed beyond wording:

- The agent fixes what it can fix itself. Disabling a plugin is spelled
  out (patch layer plus market state, never dsh.profile.bundles), and
  only upgrading and reinstalling still go through the UI.
- A broken package is triaged by layer first: node_modules/<package> is
  usually a link the next launch reprojects, so a merely missing link is
  a restart, not a removal. Only a missing manifest or patch in the real
  package is an interrupted install.
- Windows EPERM tries to self-heal once by deleting the entry the error
  names — Safe Mode does not use that folder — before asking the user to
  hunt for the process holding it.
- A clean log is a finding: say nothing is wrong, name the log sections
  checked, and ask what the user actually saw. The "a quiet log proves
  nothing" line that contradicted this is gone, and an empty offline
  diagnosis is no longer printed as if it were a result.
- Failures outside the list are reasoned from the log, with the source
  repository named for confirming Desktop's own behaviour.
- The preset-not-found entry is dropped: an old session naming a deleted
  preset simply cannot open, and recreating the preset to revive it is
  not a repair worth prescribing.

profile-repair.ts goes with it. Startup stopped performing destructive
package repair, which left the module with no caller in src/ and only
its own test — a documented, tested path to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: point the community link at the current Discord invite

The old invite is replaced everywhere it appears: the six READMEs, the
Discord entry on the plugin recovery and Safe Mode pages, and the
recovery smoke test that asserts the URL the click opens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(workspace): start a new session on the default preset, not a stale blank one (#481)

* fix(workspace): start a new session on the default preset, not a stale blank one

Opening a workspace does not create a session when a blank one is already
there: connectWorkspace returns the first blank session whose cwd matches
and only calls sessions.create when it finds none. A blank session is
never run, so it stays blank forever — every later visit to that project
is handed the same one as its "new session", carrying whatever preset was
default when it was made. Changing the default in settings reaches only
sessions created afterwards, which a session made weeks ago is not.

Switching that session's preset instead is not available: the host
resumes a session before recomposing it, and resuming loads the preset it
already names. A blank session naming a preset that has since been
deleted cannot be resumed at all, so it cannot be moved either — the
select call comes back with "resume failed … preset not found".

So the stale session is left alone and simply not reused. A blank session
now qualifies only when the preset it names matches the current default,
and a workspace whose blank session does not is given a fresh one. The
stale session stays in the list to open or delete.

The default is read through ctx.get rather than an inject: reading an
un-injected Remote namespace throws rather than answering undefined, and
this plugin has to keep working where no preset roster is composed. When
it cannot be read, reuse behaves exactly as before.

Verified in a running Harness: opening the project created
session-67b5595c on `standard` and left session-95d736d0 on the deleted
`repair` untouched, where before the stale one was handed back every
time. The patch applies cleanly to the pristine 0.1.5-rc.2 package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(repair-agent): check model and key availability before starting repair

- Add checkModelAvailability in RepairAgentService to verify model keys and default session model status
- Show informative warning dialog in Safe Mode and Recovery views when models are unavailable or unroutable
- Add unit tests covering missing keys, unroutable providers, provider failures, and valid configurations

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix: add loader await timeout diagnostics and refine model unavailable copy (#485)

* fix(loader): add timeout and stuck plugin diagnostics to loader await

- Add timeout handling to cordis-plugin-loader EntryTree.prototype.await (configurable via DSH_LOADER_TIMEOUT_MS, default 100s) with 5s progress heartbeat
- Accurately identify stuck plugins on timeout and emit structured dshPluginFailure errors
- Add unit test coverage in cordis-plugin-loader-patch.test.ts

* fix(repair-agent): update model unavailable dialog copy text

- Refine warning dialog message and detail copy when default model is unconfigured or unavailable
- Update unit tests in test/repair-agent.test.ts

* update

* test(repair-agent): sync model unavailable detail assertion wording (#486)

* feat: adapt desktop to Harness 0.1.6-alpha.2

---------

Co-authored-by: fzfz <fzfzqp@gmail.com>
Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Fairy-happy <243122010@qq.com>
Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Keep-Silence <keep137950@163.com>
Co-authored-by: Wanming08 <152006802+Wanming08@users.noreply.github.com>

* fix: route Plugin Manager through generations (#503)

* fix: route Plugin Manager through generations

* fix: close P1 Harness integration gaps

* feat(workbench): connect Desktop to the Awesome market (#496)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): add the make-my-workbench flow and GitHub listing guide (#497)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): submit through Awesome PRs and query status (#508)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): publish development and acceptance guides (#509)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): install update and uninstall market entries (#510)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(onboarding): add the desktop first-run notice (#505)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* refactor: build PPT runtime from source

* fix(workbench): reconcile market installs with declared runtime IDs (#514)

* fix(workbench): reconcile market installs after restart

* feat(workbench): declare runtime IDs in market catalog

* fix(workbench): preserve v2 market compatibility

---------

Co-authored-by: yaojin <yaojin@58.com>

* feat(workbench): migrate namespaced runtime IDs (#521)

Co-authored-by: yaojin <yaojin@58.com>

* fix(workbench): read the current session from the uiWorkspace main view (#517)

Harness 0.1.6 removed ClientSessions.open/clear and list.current. #514
moved opening onto uiWorkspace.openSession, but the controller still read
list.current in five places, which is always undefined now: native
navigation to a bound session never switched the workbench, and the
frame never saw a current session.

- Workbench: add public currentSession(), which reads the session holding
  the `mainView` retention, and showSession(id); replace every
  list.current read. Workbench providers read the same fact.
- ui-workspace patch: add deleteSession(), so deleting the current session
  releases the main view, and restore the 0.1.5 guard that stops a
  superseded initial navigation from opening a native session over a
  workbench.
- session-controller patch: drop the dead selection code in delete().
- Tests mock the real 0.1.6 selection model instead of the removed API.

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(workbench): give each workbench its own icon (#516)

WorkbenchIcon had collapsed to the shared market glyph. Use the
workbench's own short icon when it declares one, else a themed glyph
picked from its category and title, else its first character.

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(workbench): single display-mode toggle and no fill behind the open market (#518)

The sidebar showed both list and icon mode buttons; show one button with
the mode a click switches to. Drop the faint fill behind the market entry
while the market is open (hover still highlights it).

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* feat(workbench): polish market cards and the detail view (#520)

* feat(workbench): polish market cards and the detail view

- Favorite with a bookmark instead of a star; the star now means GitHub
  stars, replacing the thumbs-up.
- Show the author as a GitHub avatar and login, without the "作者" label.
- Show downloads next to stars (npm 30-day or GitHub release downloads),
  with a dash when the catalog has no value.
- The workbench name links to its GitHub repository, in the card and the
  detail view; the separate GitHub row is gone.
- Added workbenches show a quiet "已安装" state instead of a primary
  "打开工作台" button.
- The detail view shows screenshots as a large swipeable carousel that
  starts on the first image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(workbench): keep card actions on one row when an update is available

Show a short 检测到更新 action in place of 已安装 instead of adding a
third "更新到 vX.Y.Z" button that wrapped the row; the target version
moves to the tooltip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): let authors choose the market category when submitting (#522)

The submission prompt only said "category 必填", so the Agent picked a
category on the author's behalf. Step 3 now offers a category picker fed
by the market catalog's own category list, and the chosen id is written
into the prompt with an instruction not to change it. Choosing "other"
adds an optional new-category idea, relayed as one sanitized line for the
PR description so the market maintainers decide. With no choice, or no
catalog, the prompt keeps asking the Agent to pick from categories.json.

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* feat(workbench): use repository as the canonical identity (#524)

* feat(workbench): derive identity from market repository

* feat(workbench): derive local identity from repository

---------

Co-authored-by: yaojin <yaojin@58.com>

* fix(workbench): display the catalog version (#525)

Co-authored-by: yaojin <yaojin@58.com>

* fix: recover session deletion and legacy workbench state (#526)

* fix: restore session deletion confirmation

* fix: migrate legacy workbench state

* update

* fix(patches): remove stray blank line breaking session-controller patch (#528)

commit 3d03717 introduced an unprefixed blank line inside the last
hunk of the @deepseek-ai/dsh-api-session-controller 0.1.6-alpha.2
patch, which patch-package 8.0.1 cannot parse. This failed `npm ci`
postinstall on every platform of the v0.10.0 release build.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workbench): enforce runtime session boundaries (#537)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* docs(workbench): require user-selected project locations (#538)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): guide sidebar focus and session ownership (#540)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): move navigation to a compact Dock (#541)

* feat(workbench): promote workbench navigation entry

* feat(workbench): add compact switcher dock

---------

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix(workbench): open added workbenches from market (#542)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix(workbench): keep open action beside updates (#543)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): show Dock in unbound sessions (#544)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): move switcher into sidebar (#545)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* Upgrade Harness to 0.1.7-rc.1 with preset migration

* Audit Harness 0.1.7 patches and remove picker override

* fix(onboarding): show first-run notice only on new installs (#550)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix: skip disabled profile bundles before startup checks

* fix: retain Desktop workflows after PPT baseline merge

---------

Co-authored-by: fzfz <fzfzqp@gmail.com>
Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Fairy-happy <243122010@qq.com>
Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Keep-Silence <keep137950@163.com>
Co-authored-by: Wanming08 <152006802+Wanming08@users.noreply.github.com>
Co-authored-by: Yutong <cinderttt@gmail.com>
Co-authored-by: yaojin <yaojin@58.com>
Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
yaojin3616 added a commit that referenced this pull request Sep 26, 2026
* chore: establish main-based DSH 0.1.7 baseline

* fix(windows): harden installer signing and signed package gate

* refactor(windows): launch Harness with Electron Node mode

* fix(release): place Windows publisher in signtool options

* feat(windows): stage installer directories before atomic promotion

* fix(plugins): keep concurrent generation changes during rollback

* fix(market): validate generation peers before publication

* test(windows): exit packaged native probe after successful call

* fix(plugins): preserve manager registry for generation fetch

* fix(plugins): commit manager install only after enablement

* test(release): assert native probe exit diagnostics

* test(windows): verify native call using child completion marker

* feat(packaging): archive app shell with physical runtime dependencies

* refactor(plugins): route market installs through generation backend

* fix(windows): retain Node until native loader supports Electron runtime

* docs(release): record packaged runtime compatibility gate

* fix(client): define conversation accessory CSS during module load

* fix(windows): embed verified Windows extractor in installer

* fix(release): compare Windows signer common name structurally

* ci(windows): allow rechecking an existing signed installer

* revert(ci): defer standalone smoke workflow until default branch

* ci(signing): skip npm cache restore on self-hosted signer

* ci(windows): recheck signed installers without rebuilding

* fix(windows): sign NSIS uninstaller during repackaging

* fix(signing): retain vendor signatures and configure SHA-256 array

* test(windows): locate installed Harness log across app data paths

* fix: use latest Harness URL in signed installer smoke

* test: bound signed installer smoke and show upgrade stages

* fix: keep prior install when choosing another Windows directory

* fix: remove unused NSIS old-uninstaller function

* fix: remove obsolete NSIS uninstaller helpers

* ci: distinguish startup progress logs from runtime errors

* ci: reuse unsigned artifact and bypass slow signer proxy

* ci: use unique proxy environment names

* ci: clear lowercase proxy inherited by signing runner

* refactor: build PPT runtime from source (#512)

* feat: adapt Desktop to Harness 0.1.6-alpha.2 (#499)

* feat(release): configure initial 5% rollout on crash service (#401)

- Add configure-rollout.mjs script to manage platform rollout rules on crash service

- Integrate initial 5% rollout step into release workflow

- Add unit tests covering creation, update, and race condition conflict recovery

* fix(llm): send Harness session headers through pi-ai (#329)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* fix(llm): recover missing terminal content (#400)

Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: import the local web Harness home on first launch (#397)

* feat: import the local web Harness home on first launch

Copy ~/.dsh into an unused desktop home so same-machine web users are not treated as a fresh install.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(market-baseline): treat pnpm isolated-store symlinks as valid installs

In dev mode (dsh-desktop-dev), profiles have no nodeLinker:hoisted config
so pnpm installs dshmarket as a symlink into .pnpm/…. The previous
isSymbolicLink() check treated any symlink as a stray generation link,
deleted it, then called pnpm to reinstall — which recreated the same
symlink. The post-install guard rejected it and the app entered Safe Mode
on every dev launch.

Fix: read the symlink target and only treat paths containing `.generations`
as generation links. pnpm isolated-store symlinks (.pnpm/…) are left alone
in demoteMarketGeneration and ensureMarketBaseline, and are accepted by
upgradeMarketInSharedTree's post-install verification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(test): correct patch indentation in provider-error-patch.test.ts (#433)

* fix(test): correct patch indentation in provider-error-patch.test.ts

* fix(types): add type declarations for configure-rollout.mjs and resolve typecheck errors

* fix(diagnostics): 减少误报故障反馈、优化插件自愈免报与修复 Windows 启动缺陷 (#418)

* fix(diagnostics): reduce false-positive crash reports and fix Windows startup issues

- Suppress false-positive unclean-exit reports when previous session log was healthy
- Mark clean exit synchronously on before-quit and handle Windows session-end
- Defer plugin failure reports during startup and discard them when recovery succeeds
- Fix Windows EPERM on junction removal in ensureSymlink (@deepseek-ai/dsh-app-boot)
- Use pathToFileURL in loadDesktopResource to avoid ERR_FAILED (-2) on non-C drives
- Enrich render-process-gone logs in harness.log with URL and GPU feature status
- Enable V8 compile cache (NODE_COMPILE_CACHE) for faster Node.js restarts

* fix(windows): declare asInvoker, enable long paths, and defer profile inspection

* feat(runtime): prefix harness.log lines with ISO date timestamp

* feat(release): sign inner Windows binaries and repackage NSIS installer with SafeNet UKey

* fix(release): harden Windows inner signing with version sync and fallback

* fix(runtime): use platform-specific path join for NODE_COMPILE_CACHE

* fix(types): add type declarations for configure-rollout.mjs and resolve typecheck errors

* test: serialize suites on Windows CI and set global timeout to prevent hangs

* fix(release): verify PE header for .node files to skip non-Windows prebuilds in Jsign

* perf(windows): exclude build-tool deps and add Defender exclusions on install

- Exclude ~2,000 build-tool-only transitive files from the Windows package:
    caniuse-lite (838 JS files, browserslist/babel only)
    lodash (1,048 JS files, only used by @malept/flatpak-bundler linux tool)
    browserslist / electron-to-chromium / update-browserslist-db (15 files)
    @malept/* (flatpak bundler, Linux-only build tool)
  Excluded packages are verified to have no runtime dependency chain:
    - @mixmark-io/domino kept (dsh-tool-web → turndown → domino)
    - jsdom kept (dsh-ppt runtime dep)
    - dsh-ppt's nested typescript kept (only top-level typescript is absent)

- Add Windows Defender exclusions in NSIS installer (customInstall macro):
  Add-MpPreference for $INSTDIR and $APPDATA\dsh-desktop via PowerShell.
  Uses -ErrorAction SilentlyContinue so non-admin installs fail silently.
  Without this exclusion Defender scans 44k files on first launch
  (observed: 121-123 s until Harness ready on a fresh install). With the
  exclusion, first launch should complete in under 20 s.

* fix(release): align package.json version with package-lock.json (0.1.1)

* fix(test): correct patch indentation in provider-error-patch.test.ts

* fix(windows): optimize titlebar layout, integrate more actions into app menu, and fix sidebar overlap (#409)

* perf(windows): cut 20k non-runtime files, elevate defender exclusion, and relax startup timeout (#435)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite in… (#437)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite install

An in-app update can be force-killed before will-quit, and a leftover session marker from another version is a reinstall, not a crash.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(diagnostics): suppress startup failure report when frontend identifies incompatible plugins

* fix(build): preserve runtime skill markdown files in build output and update ppt artifacts

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* fix(windows): clear session marker on overwrite install (#440)

NSIS force-kills the running app, so will-quit never clears session.json and a same-version overwrite looks like an unclean exit.

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix/suppress upgrade unclean exit (#441)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite install

An in-app update can be force-killed before will-quit, and a leftover session marker from another version is a reinstall, not a crash.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(diagnostics): suppress startup failure report when frontend identifies incompatible plugins

* fix(build): preserve runtime skill markdown files in build output and update ppt artifacts

* update

* update

* update

* update

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: stop tracking plugin-management and keep it local only (#443)

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: 建立仓库级 AGENTS.md 开发约束与治理规范 (#428)

* docs: establish repository-wide AGENTS development standards and governance contracts

- Add root AGENTS.md defining project boundaries, TypeScript code style, dependency/data constraints, and verification runbooks.
- Add directory-scoped AGENTS.md rules for src/main, src/preload, packages, patches, and build.
- Add supporting design specifications:
  - docs/code-standards.md (design rationale and phased tool enforcement)
  - docs/patch-plugin-contract.md (patch vs plugin selection and slot compatibility)
  - docs/source-build-contract.md (single authoritative source and build artifact boundaries)

* docs: 补齐 AGENTS 前端硬约束与企业/编译型包覆盖

把主题 token、locale、preload 与 slot 边界写成可执行规则,并标出 enterprise 存量与 ppt-runtime 的维护入口。

---------

Co-authored-by: fairy <3054158379@qq.com>

* fix(security): allow notifications permission from the harness window (#461)

The allowlist only covered clipboard-sanitized-write, so notification
permission requests from the harness page could not be granted. The
loopback main-frame checks are unchanged, so the trust boundary is the
same.

* fix(release): honor platform target and add signed-only dispatch mode (#468)

A filled prerelease tag previously forced a Windows build even when
macos was selected. Dispatch now separates development, signed, and
prerelease so production-identity packages can be signed without publishing.

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(settings): keep long navigation menus scrollable (#456)

* fix(settings): keep long navigation menus scrollable

* fix(settings): add navigation overflow patch

* feat(safe-mode): add floating repair agent widget in recovery and safe mode (#431)

* feat(safe-mode): add floating repair agent widget in recovery and safe mode

- Add floating repair agent FAB widget on plugin recovery and safe mode overlay
- Provide pre-configured quick action prompts with error diagnostics and logs
- Support switching models and uploading/pasting screenshots for vision models
- Connect with isolated safe mode Harness session and stream responses via IPC

* feat(safe-mode): temporarily disable floating repair agent chat widget

* feat(safe-mode): enable repair agent with 0.9.0 incident knowledge and offline diagnostics

* feat(release): expand release notes generator with performance section and full changelog

* feat(window): persist window bounds, maximized state, and zoom level

* fix(repair-agent): handle unconfigured models with api key drawer and prevent silent stream failures

* fix(dev): strip inherited ELECTRON_RUN_AS_NODE to prevent electron launch crash

* feat(dev): forward runtime logs to console in development and log single-instance conflicts

* fix(repair-agent): inherit working default model route and add dual-track history polling

* refactor(repair-agent): rely purely on websocket stream without redundant history polling

* feat(repair-agent): replace floating widget with seamless native Harness UI diagnostic cards

* fix(windows): stop GPU fallback ladder from degrading on TDR device-loss recoveries, recover the menu view from a lost renderer

gcp3 crash data for v0.9.0 showed 23/30 Windows gpu-crash reports at exitCode=34
— Chromium's own exit code when it detects a lost D3D11 device (typically a
driver TDR reset) and exits the GPU process on purpose so it can restart it.
The fallback ladder was counting this self-recovery as evidence the sandbox is
broken and would degrade hardware acceleration after 3 of these on an
otherwise healthy machine. isGpuLossFatal now takes the exit code and treats
exitCode 34 as non-fatal, only logging a breadcrumb.

Separately, ~6 renderer-crash reports were on windows-menu.html: that view runs
in its own WebContentsView outside installMainWindowRendererRecovery, which
only hooks the main window's webContents, so a lost renderer there left a dead
menu until the whole app restarted. It now reloads itself on render-process-gone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(session): skip unreadable session logs instead of failing the whole listing

One corrupt JSONL/zstd session log made sessionPersistence.list() throw, which
failed dsh-workspace and stopped the Harness entirely (0.9.0 startup-failure
reports: "corrupt Zstandard session log", corrupt header id). The log is now
skipped with a warning naming its path and left on disk untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(startup): stop reporting attributed plugin failures, clear stale writer locks, fix repair diagnosis

- Harness startup failures that plugin recovery attributes to a user plugin
  are handed to the user and their pending crash report is discarded; only
  the frontend recovery path did this before, so they were still uploaded.
- Remove dsh-atomic-write locks in DSH_HOME and DSH_HOME/profiles whose owner
  pid is gone before launching; a killed Harness left .credentials.yaml.lock
  behind and the next launch timed out on it.
- Repair Agent diagnosis uses loader provenance and the recovery-resolved
  plugins, and only reports a startup timeout when the runtime says so, since
  "waiting for Harness" + SIGTERM matched ordinary failure cleanup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): ensure safe mode uses hoisted node-linker and hide diagnostics when plugins are identified

* feat(runtime): add preset YAML validation step before launch

* refactor(runtime): remove redundant preset check in favor of native Cordis loader logging

* feat(repair-agent): unify diagnostic cards into a single entry with verification and reporting instructions

* fix(repair-agent): restore the 3 fine-grained diagnostic cards in safe mode

* fix(safe-mode): unify safe mode diagnostic cards into a single AI agent entry

* fix(web-import): never replace a desktop home that already holds plugins or credentials

A 0.9.0-rc1 home with plugins or .credentials.yaml but no settings.yaml or
sessions was treated as unused, so the web import deleted it wholesale. It now
counts as used. An unused home is also moved aside before deletion so
delete-pending entries on Windows cannot block links created at the same path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): don't remove plugins a deferred migration has not installed yet

When the generation migration failed (e.g. pnpm EPERM right after install),
imported plugins stayed manifest-only, failed to prepare, and plugin recovery
removed them as incompatible. Deferred migrations now report the pending
plugins and recovery excludes them. Transient EPERM/EBUSY/EACCES failures no
longer freeze the same profile for six hours, so the next launch retries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): stop the titlebar drag region from swallowing clicks and churning

The drag region now ignores pointer events, and only semantic dialog markers
hide it: class-name guesses like [class*="modal"] matched permanent elements
and hid it for good. Visibility checks run at most once a frame, since
streaming output mutates the DOM continuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(diagnostics): send crashes without an event id right away

Only a plugin-attributed startup failure should wait for recovery to decide
whether to discard it. A crash with no event id compared equal to an unset
pending id and was held back too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(market): pin the exact version a declared dshmarket range names

The installer pins and verifies an exact version, so a declared range such as
^1.47.0 is reduced to the version it names instead of being passed through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: ignore the local pnpm content store

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): resolve Safe Mode plugins from the installation, not the module fallback

Safe Mode loads only installation-owned bundles, yet it still went through
the shared $DSH_HOME/profiles/node_modules fallback, whose junctions Windows
can refuse to recreate (EPERM) for minutes — the top startup failure of
v0.9.0. The desktop now sets DSH_DESKTOP_HOST_RESOLVED for the Safe Mode
profile only; the patched Harness then skips healing the fallback and
resolves bare plugin names, including entries created at runtime through
ctx.loader.create, from its own installation. A flag leaked from the parent
environment never reaches a normal profile.

This replaces the hoisted node-linker workaround for Safe Mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(runtime): mirror Harness logs to the console only in development

A packaged app's stdout may be a closed pipe; writing every Harness log line
to it there buys nothing and can fail. harness.log still receives every line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): still blame a migration-pending plugin whose legacy copy is installed

f475ce5 exempted every plugin with a pending deferred migration from
recovery. Only a plugin the migration left manifest-only is not broken; one
whose legacy copy is installed still loads and can be the real culprit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(logging): record Harness runtime warnings and errors in harness.log

Cordis's ctx.logger keeps messages only in an in-memory ring, and its default
level filters out warnings; nothing in the shipped composition exported them.
Session activation failures (a preset that cannot be found or mounted) never
reach the logger at all — they are only pushed to the client. So errors after
startup left no trace for a person or the Repair Agent once the process was
gone.

A new desktop plugin, dsh-desktop-log-bridge, is composed first into both the
normal and the Safe Mode profile. It writes warn and error messages, the
errors already in the ring, and api-session/error events to stderr, which
the desktop records into harness.log. It rate-limits floods and writes one
ready line per launch, so a log without it is known to predate the bridge.

Every bridged line carries a [harness-log] prefix, and latestHarnessAttemptLogs
drops such lines: runtime warnings never become recovery's failure cause or
the plugin it blames.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(safe-mode): disable plugins instead of removing them

Safe Mode now switches a plugin off the way the plugin market's own toggle
does, so it can come back without a reinstall: `disabled: true` rows for the
loader entries the package inserts in the user patch layer, which the loader
re-applies on every boot, plus the market's .dsh-market/state.json list,
which is also the only switch for client-only packages. Nothing is deleted.

Disabled plugins show as disabled with a Re-enable action instead of a
selection. Compatibility issues that call for disabling a plugin use the same
path; the old approach of dropping it from dsh.profile.bundles was silently
undone, because the generation projection rewrites that list on every launch.
Compatibility inspection skips disabled plugins, since they never load.

A disable-carrier (a bundle whose patch disables another plugin) cannot be
switched off on its own without stranding the plugin it replaces, so it
keeps the backed-up removal. Startup recovery keeps removal too: a package
that is itself broken fails before the patch layer's disable applies. When
recovery removes a plugin it also clears its market disable entry, as the
market's own uninstall does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(repair-agent): start a fresh, well-briefed repair session from one card

Recovery and Safe Mode now offer a single "start the repair agent" card. Each
click opens a new session in the Safe Mode Harness and makes it the session
the Harness UI shows: the UI restores its current session from desktop
storage when the page loads, so the Recovery path opens the session before
its page loads and the Safe Mode path reloads the already open page. The
floating repair widget is gone.

The session's workspace is the Harness home (profiles, plugins, patch layer)
instead of the empty launch root, and the first turn of each session carries
the diagnosis. The system prompt is rebuilt around real incidents:

- a directory map that separates the normal profile it repairs from the Safe
  Mode profile it runs in;
- the path to harness.log with how to find the failed launch in it, plus a
  short excerpt, instead of pasting logs; Safe Mode's own logs are no longer
  used when no failed launch was captured;
- one bilingual playbook source, so the Chinese and English prompts can no
  longer drift: plugin load failures, broken packages (which disabling cannot
  fix), the Windows module-fallback EPERM (no Developer Mode advice), a
  corrupt patch layer, startup timeouts, the code -> ptc preset rename, and
  schema errors in copied user presets;
- actions named after the real UI, and a confirm-and-back-up rule before any
  file change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(market): stop shipping dshmarket so market upgrades take effect

Harness resolves every profile bundle from its own installation before the
profile (`resolveBundleDir` in dsh-app-boot). Since #352 pinned dshmarket
1.45.1 as a runtime dependency, the packaged copy won that lookup: upgrading
the market wrote 1.48.0 into the profile, the market and the baseline check
both read 1.48.0 back, and Harness kept loading 1.45.1 after every restart.

Nothing in the app imports dshmarket; the profile copy is installed from npm
by the market installer and `ensureMarketBaseline`. It stays a
devDependency for the tests that exercise its real profile reader, which
keeps it out of the packaged app.

`ensureMarketBaseline` now also notes when the copy Harness would load is not
the profile's, so a shadowed market shows up in harness.log instead of as a
version the UI reports but never runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(recovery): let startup recovery repair a plugin market that breaks startup

The market is a core bundle, so startup-failure attribution never named it
and the Recovery page, finding no culprit, could only offer Safe Mode — which
does not load the market and has nothing to act on. A market release that
stopped Harness from booting therefore failed every launch the same way.
Until now the packaged dshmarket shadowed the profile copy and hid this;
with it gone the profile copy really loads, so the loop is live.

When the failed launch's log (or loader provenance) names dshmarket, the
Recovery page shows it as its own row, apart from the third-party plugin
list whose removal path refuses core bundles:

- Upgrade to the newest release the market check finds compatible with this
  Harness, for a market too old for it; a release already tried here is not
  offered again.
- Install the verified version (VERIFIED_MARKET_BASELINE). That moves down
  from a broken release, up from a stale one, or reinstalls a damaged copy.
  It is the primary action when the market is the only culprit.
- Remove the market; installed community plugins are kept.

Versions come from the main process's own check, never from the page. Each
change stops Harness first, as the shared-tree installer requires, pins the
exact version so the next baseline check does not reinstall the broken
release, then relaunches and returns to recovery if startup still fails.
The market removal behind the settings page is shared as `removeMarket`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): render the plugin market row like any other plugin row

The market row added its own touches: a version after the name, a green
button for restoring the verified version (which can be a downgrade), and a
confirmation dialog plus its own wording for removal. Third-party rows have
none of these, so the market read as a different kind of item.

Show the name only, keep green for the upgrade alone with every other action
in the neutral style, and remove it the way the page removes any plugin —
"Remove this plugin", no extra confirmation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): offer the plugin market only upgrade and removal

Restoring the verified market version was an action no other plugin row
has. Drop it, so the market row is a plugin row like the rest: upgrade to a
compatible release when the market check finds one, and removal.

When the market alone blocks startup, the page now uses the same buttons as
a lone third-party plugin: "Upgrade plugin and restart" with "Uninstall
plugin" beside it when an upgrade exists, otherwise "Remove this plugin and
continue".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: stop tracking the local pnpm content store

0d083d5 committed the whole `.pnpm-store/` (13,357 files, ~235 MB) along
with its four intended changes, before 7a2cb73 added it to .gitignore. An
ignore rule does not untrack files already committed, so they stayed in the
branch and buried the PR's real diff.

Remove them from the index only; the local store is untouched and stays
ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: drop tests that only grep our own source code

About 40 tests read src/main/index.ts, the preload, build/*.html, the NSIS
script or our own packages' client.js as text and asserted that particular
lines were present — `toContain("ipcMain.handle('safe-mode:action'")`,
`indexOf(a) < indexOf(b)` and the like. They execute nothing, so they cannot
catch a behavior regression, yet they fail on any rename or reformat, and on
Windows, where the checkout has CRLF line endings, any expected string that
spans a newline never matches. Two of them failed CI that way on PR #431.

Removed: every test block whose assertions were such text matches, and the
source-text lines inside otherwise behavioral tests (windows-titlebar
allowlist, runtime Node mode, release dev channel, branding postinstall,
harness-node-entry). The dev-channel check now reads electron-builder.dev.cjs
as a config object instead of matching its text.

Kept: tests that execute code, parse package.json/lockfile/YAML/JSON
config, or check the installed third-party bundles in node_modules (patch
and upstream-contract checks), since those inspect what actually ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(repair-agent): drop tests that only check the prompt's wording

Two repair-prompt tests asserted fixed sentences of the generated prompt:
exact Chinese phrases with hard-coded POSIX paths, and a fixed count of
seven playbooks plus two error strings. Any edit to the copy broke them
without saying anything about how the agent behaves, and the path one
failed on Windows, where path.join correctly yields backslashes.

Keep the test of real prompt logic: only the tail of the log is included,
and none at all when no failed launch was captured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(repair-agent): drop the remaining repair-prompt test

It asserted which log lines and which sentence end up in the generated
prompt text. Like the other prompt tests, that pins the copy rather than
anything the agent does with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): offer the repair agent as one line under the summary

The repair agent sat in its own section below the actions — a heading, a
divider and a card, about 140px. In a 1280x800 window that pushed the plugin
list into a scrollbar, which the native Windows recovery UI check rejects:
every plugin has to be visible at once. Its text was also Chinese-only.

Show it as a single link under the summary instead, in both languages. It
sends the same diagnostic request as the card did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): offer the repair agent as a button beside Safe Mode

The recovery page showed the repair agent in its own section under the
technical details — a heading and a card, in Chinese only. Make it a
secondary "Repair agent" button next to "Enter Safe Mode", in both
languages. As before, it appears only when no plugin or market was
identified; a named culprit has its own repair above.

The native Windows UI check expected Safe Mode to be the only action when
nothing is identified; it now expects the agent beside it, and nowhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(runtime): keep a clean launch off stderr

The packaged Windows smoke fails any launch that writes to stderr once a
workspace and session exist. Two lines did on every launch, and neither
reported a problem:

- The log bridge's "recording from here on" marker. It is a notice, so it
  now goes to stdout; stderr carries only bridged warnings and errors.
- `patch: entry dsh-market not found`. The desktop patch configured the
  market's entry (restart off, wait for desktopProfiles) unconditionally,
  but the market is optional, and a patch row whose entry is absent makes the
  loader warn. It was always there; the bridge only made it visible. The row
  now lives in dsh-desktop-market.patch.yml, passed as a second --patch only
  when the profile boots dshmarket, so a market profile behaves as before.

Checked by booting a fresh web profile (no market overlay, no bridged
warning) and one that boots dshmarket (overlay applied, no warning).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(market): remove dshmarket devDependency to avoid shadowing profile updates (#478)

* fix(market): remove dshmarket devDependency to avoid shadowing profile updates

Harness resolves every profile bundle from its installation anchor before
the user profile (resolveBundleDir in dsh-app-boot).

Previously, dshmarket was retained as a devDependency in package.json to
support test/generation-boundary.test.js. However, electron-builder's
files: ["node_modules/**/*"] glob unconditionally includes all modules in
node_modules unless explicitly excluded, causing dshmarket to be bundled
into the packaged application and to exist in dev checkouts. When the user
upgrades dshmarket in their profile to 1.48.0, Harness continues loading
the packaged copy instead of the profile's copy.

This removes dshmarket from devDependencies and package-lock.json:
- test/generation-boundary.test.js implements a local readInstalledVersion helper
- test/market-baseline.test.ts verifies dshmarket is absent from both dependencies
  and devDependencies, and simulates installation shadowing with a mock directory

* style(safe-mode): style repair agent session link with blue accent color

* fix(safe-mode): make the repair agent's failures visible and its guidance true (#479)

* fix(safe-mode): report a repair agent session that could not open

Clicking "let the repair agent read the log" did nothing at all when the
session failed to start: startRepairAgentPrompt returned a bare boolean,
the caller skipped the reload, openHarness found the window already on
the Harness origin and only raised it, and the reason reached the log
alone. A user whose default agent preset was broken saw three clicks
produce no visible change.

The call now carries the reason. The Safe Mode manager stays open and
shows it instead of closing on the way out, so the click can be retried,
and plugin recovery surfaces the same reason through the manager it hands
the window to. The notice keeps the first two lines of Harness's mount
error; the rest is a stack that would push the plugin list off the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(plugin-disable): refuse a bundle whose package yields no loader row

Disabling a broken plugin reported success and changed nothing that
matters. pluginPatchRows reads the loader rows from the package's own
patch; when that file is missing or unparsable the list comes back empty,
so the patch layer was skipped, only the market state was written, and
the result was ok. The bundle stayed in dsh.profile.bundles and the next
launch composed — and failed on — the same package.

A package listed as a bundle is one the loader prepares on every launch,
so no readable row means the package on disk is broken, not that the
plugin has no rows. That case is now refused as broken-package, and Safe
Mode falls back to the backed-up removal it already uses for carriers.
A client-only plugin is untouched: it is not a listed bundle, and the
market state remains its only switch.

The compatibility fixture listed a bundle whose manifest declared no
dsh.bundle — a profile that cannot start — so it now carries the patch
the loader would read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(repair-agent): tighten the prompt and drop the unused repair module

The prompt repeated absolute Windows paths in every playbook entry and
walked through the Safe Mode profile the agent must not touch. Paths are
given once in the directory section and referred to by name afterwards;
the Safe Mode profile is one line. 5290 to 4403 characters, with more in
it than before.

What changed beyond wording:

- The agent fixes what it can fix itself. Disabling a plugin is spelled
  out (patch layer plus market state, never dsh.profile.bundles), and
  only upgrading and reinstalling still go through the UI.
- A broken package is triaged by layer first: node_modules/<package> is
  usually a link the next launch reprojects, so a merely missing link is
  a restart, not a removal. Only a missing manifest or patch in the real
  package is an interrupted install.
- Windows EPERM tries to self-heal once by deleting the entry the error
  names — Safe Mode does not use that folder — before asking the user to
  hunt for the process holding it.
- A clean log is a finding: say nothing is wrong, name the log sections
  checked, and ask what the user actually saw. The "a quiet log proves
  nothing" line that contradicted this is gone, and an empty offline
  diagnosis is no longer printed as if it were a result.
- Failures outside the list are reasoned from the log, with the source
  repository named for confirming Desktop's own behaviour.
- The preset-not-found entry is dropped: an old session naming a deleted
  preset simply cannot open, and recreating the preset to revive it is
  not a repair worth prescribing.

profile-repair.ts goes with it. Startup stopped performing destructive
package repair, which left the module with no caller in src/ and only
its own test — a documented, tested path to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: point the community link at the current Discord invite

The old invite is replaced everywhere it appears: the six READMEs, the
Discord entry on the plugin recovery and Safe Mode pages, and the
recovery smoke test that asserts the URL the click opens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(workspace): start a new session on the default preset, not a stale blank one (#481)

* fix(workspace): start a new session on the default preset, not a stale blank one

Opening a workspace does not create a session when a blank one is already
there: connectWorkspace returns the first blank session whose cwd matches
and only calls sessions.create when it finds none. A blank session is
never run, so it stays blank forever — every later visit to that project
is handed the same one as its "new session", carrying whatever preset was
default when it was made. Changing the default in settings reaches only
sessions created afterwards, which a session made weeks ago is not.

Switching that session's preset instead is not available: the host
resumes a session before recomposing it, and resuming loads the preset it
already names. A blank session naming a preset that has since been
deleted cannot be resumed at all, so it cannot be moved either — the
select call comes back with "resume failed … preset not found".

So the stale session is left alone and simply not reused. A blank session
now qualifies only when the preset it names matches the current default,
and a workspace whose blank session does not is given a fresh one. The
stale session stays in the list to open or delete.

The default is read through ctx.get rather than an inject: reading an
un-injected Remote namespace throws rather than answering undefined, and
this plugin has to keep working where no preset roster is composed. When
it cannot be read, reuse behaves exactly as before.

Verified in a running Harness: opening the project created
session-67b5595c on `standard` and left session-95d736d0 on the deleted
`repair` untouched, where before the stale one was handed back every
time. The patch applies cleanly to the pristine 0.1.5-rc.2 package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(repair-agent): check model and key availability before starting repair

- Add checkModelAvailability in RepairAgentService to verify model keys and default session model status
- Show informative warning dialog in Safe Mode and Recovery views when models are unavailable or unroutable
- Add unit tests covering missing keys, unroutable providers, provider failures, and valid configurations

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix: add loader await timeout diagnostics and refine model unavailable copy (#485)

* fix(loader): add timeout and stuck plugin diagnostics to loader await

- Add timeout handling to cordis-plugin-loader EntryTree.prototype.await (configurable via DSH_LOADER_TIMEOUT_MS, default 100s) with 5s progress heartbeat
- Accurately identify stuck plugins on timeout and emit structured dshPluginFailure errors
- Add unit test coverage in cordis-plugin-loader-patch.test.ts

* fix(repair-agent): update model unavailable dialog copy text

- Refine warning dialog message and detail copy when default model is unconfigured or unavailable
- Update unit tests in test/repair-agent.test.ts

* update

* test(repair-agent): sync model unavailable detail assertion wording (#486)

* feat: adapt desktop to Harness 0.1.6-alpha.2

---------

Co-authored-by: fzfz <fzfzqp@gmail.com>
Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Fairy-happy <243122010@qq.com>
Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Keep-Silence <keep137950@163.com>
Co-authored-by: Wanming08 <152006802+Wanming08@users.noreply.github.com>

* fix: route Plugin Manager through generations (#503)

* fix: route Plugin Manager through generations

* fix: close P1 Harness integration gaps

* feat(workbench): connect Desktop to the Awesome market (#496)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): add the make-my-workbench flow and GitHub listing guide (#497)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): submit through Awesome PRs and query status (#508)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): publish development and acceptance guides (#509)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): install update and uninstall market entries (#510)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(onboarding): add the desktop first-run notice (#505)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* refactor: build PPT runtime from source

* fix(workbench): reconcile market installs with declared runtime IDs (#514)

* fix(workbench): reconcile market installs after restart

* feat(workbench): declare runtime IDs in market catalog

* fix(workbench): preserve v2 market compatibility

---------

Co-authored-by: yaojin <yaojin@58.com>

* feat(workbench): migrate namespaced runtime IDs (#521)

Co-authored-by: yaojin <yaojin@58.com>

* fix(workbench): read the current session from the uiWorkspace main view (#517)

Harness 0.1.6 removed ClientSessions.open/clear and list.current. #514
moved opening onto uiWorkspace.openSession, but the controller still read
list.current in five places, which is always undefined now: native
navigation to a bound session never switched the workbench, and the
frame never saw a current session.

- Workbench: add public currentSession(), which reads the session holding
  the `mainView` retention, and showSession(id); replace every
  list.current read. Workbench providers read the same fact.
- ui-workspace patch: add deleteSession(), so deleting the current session
  releases the main view, and restore the 0.1.5 guard that stops a
  superseded initial navigation from opening a native session over a
  workbench.
- session-controller patch: drop the dead selection code in delete().
- Tests mock the real 0.1.6 selection model instead of the removed API.

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(workbench): give each workbench its own icon (#516)

WorkbenchIcon had collapsed to the shared market glyph. Use the
workbench's own short icon when it declares one, else a themed glyph
picked from its category and title, else its first character.

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(workbench): single display-mode toggle and no fill behind the open market (#518)

The sidebar showed both list and icon mode buttons; show one button with
the mode a click switches to. Drop the faint fill behind the market entry
while the market is open (hover still highlights it).

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* feat(workbench): polish market cards and the detail view (#520)

* feat(workbench): polish market cards and the detail view

- Favorite with a bookmark instead of a star; the star now means GitHub
  stars, replacing the thumbs-up.
- Show the author as a GitHub avatar and login, without the "作者" label.
- Show downloads next to stars (npm 30-day or GitHub release downloads),
  with a dash when the catalog has no value.
- The workbench name links to its GitHub repository, in the card and the
  detail view; the separate GitHub row is gone.
- Added workbenches show a quiet "已安装" state instead of a primary
  "打开工作台" button.
- The detail view shows screenshots as a large swipeable carousel that
  starts on the first image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(workbench): keep card actions on one row when an update is available

Show a short 检测到更新 action in place of 已安装 instead of adding a
third "更新到 vX.Y.Z" button that wrapped the row; the target version
moves to the tooltip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): let authors choose the market category when submitting (#522)

The submission prompt only said "category 必填", so the Agent picked a
category on the author's behalf. Step 3 now offers a category picker fed
by the market catalog's own category list, and the chosen id is written
into the prompt with an instruction not to change it. Choosing "other"
adds an optional new-category idea, relayed as one sanitized line for the
PR description so the market maintainers decide. With no choice, or no
catalog, the prompt keeps asking the Agent to pick from categories.json.

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* feat(workbench): use repository as the canonical identity (#524)

* feat(workbench): derive identity from market repository

* feat(workbench): derive local identity from repository

---------

Co-authored-by: yaojin <yaojin@58.com>

* fix(workbench): display the catalog version (#525)

Co-authored-by: yaojin <yaojin@58.com>

* fix: recover session deletion and legacy workbench state (#526)

* fix: restore session deletion confirmation

* fix: migrate legacy workbench state

* update

* fix(patches): remove stray blank line breaking session-controller patch (#528)

commit 3d03717 introduced an unprefixed blank line inside the last
hunk of the @deepseek-ai/dsh-api-session-controller 0.1.6-alpha.2
patch, which patch-package 8.0.1 cannot parse. This failed `npm ci`
postinstall on every platform of the v0.10.0 release build.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workbench): enforce runtime session boundaries (#537)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* docs(workbench): require user-selected project locations (#538)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): guide sidebar focus and session ownership (#540)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): move navigation to a compact Dock (#541)

* feat(workbench): promote workbench navigation entry

* feat(workbench): add compact switcher dock

---------

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix(workbench): open added workbenches from market (#542)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix(workbench): keep open action beside updates (#543)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): show Dock in unbound sessions (#544)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): move switcher into sidebar (#545)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* Upgrade Harness to 0.1.7-rc.1 with preset migration

* Audit Harness 0.1.7 patches and remove picker override

* fix(onboarding): show first-run notice only on new installs (#550)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix: skip disabled profile bundles before startup checks

* fix: retain Desktop workflows after PPT baseline merge

---------

Co-authored-by: fzfz <fzfzqp@gmail.com>
Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Fairy-happy <243122010@qq.com>
Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Keep-Silence <keep137950@163.com>
Co-authored-by: Wanming08 <152006802+Wanming08@users.noreply.github.com>
Co-authored-by: Yutong <cinderttt@gmail.com>
Co-authored-by: yaojin <yaojin@58.com>
Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* test: make cross-platform packaging checks deterministic

* ci: capture packaged Windows startup diagnostics

* ci: bound and show Windows test progress

* fix: persist early Desktop startup errors

* test: isolate legacy preset runtime port

* ci: consolidate native builds and reuse prepared runtime

* fix: resolve host patch plugins from bundled Harness anchor

* test: compare physical host paths on Windows

* ci: allow platform retries alongside independent builds

* fix: unpack staged PPT plugins and verify physical runtime

* ci: download signing artifacts in verified parallel ranges

* ci: fetch pinned Jsign through GitHub release API

* Fix legacy preset migration from empty Profile patch

* Revert "refactor: build PPT runtime from source (#512)"

This reverts commit 914f1a8.

* refactor(ppt): build bundled runtime from source on 0.1.7 baseline

Port the PPT-only source build, package projection, Electron overlay and afterPack runtime gate from PR #512. Keep workbench and onboarding changes out of the baseline. Pin Electron to 43.0.0 because the Harness native runtime rejects 43.4.0.

* chore: upgrade baseline to Harness 0.1.7-rc.2

* chore: normalize Undici patch context whitespace

* UI
plugin/ sidemenu update

---------

Co-authored-by: fzfz <fzfzqp@gmail.com>
Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Fairy-happy <243122010@qq.com>
Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Keep-Silence <keep137950@163.com>
Co-authored-by: Wanming08 <152006802+Wanming08@users.noreply.github.com>
Co-authored-by: Yutong <cinderttt@gmail.com>
Co-authored-by: yaojin <yaojin@58.com>
Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Fairy-happy added a commit that referenced this pull request Sep 29, 2026
* feat: adapt Desktop to Harness 0.1.6-alpha.2 (#499)

* feat(release): configure initial 5% rollout on crash service (#401)

- Add configure-rollout.mjs script to manage platform rollout rules on crash service

- Integrate initial 5% rollout step into release workflow

- Add unit tests covering creation, update, and race condition conflict recovery

* fix(llm): send Harness session headers through pi-ai (#329)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* fix(llm): recover missing terminal content (#400)

Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: import the local web Harness home on first launch (#397)

* feat: import the local web Harness home on first launch

Copy ~/.dsh into an unused desktop home so same-machine web users are not treated as a fresh install.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(market-baseline): treat pnpm isolated-store symlinks as valid installs

In dev mode (dsh-desktop-dev), profiles have no nodeLinker:hoisted config
so pnpm installs dshmarket as a symlink into .pnpm/…. The previous
isSymbolicLink() check treated any symlink as a stray generation link,
deleted it, then called pnpm to reinstall — which recreated the same
symlink. The post-install guard rejected it and the app entered Safe Mode
on every dev launch.

Fix: read the symlink target and only treat paths containing `.generations`
as generation links. pnpm isolated-store symlinks (.pnpm/…) are left alone
in demoteMarketGeneration and ensureMarketBaseline, and are accepted by
upgradeMarketInSharedTree's post-install verification.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(test): correct patch indentation in provider-error-patch.test.ts (#433)

* fix(test): correct patch indentation in provider-error-patch.test.ts

* fix(types): add type declarations for configure-rollout.mjs and resolve typecheck errors

* fix(diagnostics): 减少误报故障反馈、优化插件自愈免报与修复 Windows 启动缺陷 (#418)

* fix(diagnostics): reduce false-positive crash reports and fix Windows startup issues

- Suppress false-positive unclean-exit reports when previous session log was healthy
- Mark clean exit synchronously on before-quit and handle Windows session-end
- Defer plugin failure reports during startup and discard them when recovery succeeds
- Fix Windows EPERM on junction removal in ensureSymlink (@deepseek-ai/dsh-app-boot)
- Use pathToFileURL in loadDesktopResource to avoid ERR_FAILED (-2) on non-C drives
- Enrich render-process-gone logs in harness.log with URL and GPU feature status
- Enable V8 compile cache (NODE_COMPILE_CACHE) for faster Node.js restarts

* fix(windows): declare asInvoker, enable long paths, and defer profile inspection

* feat(runtime): prefix harness.log lines with ISO date timestamp

* feat(release): sign inner Windows binaries and repackage NSIS installer with SafeNet UKey

* fix(release): harden Windows inner signing with version sync and fallback

* fix(runtime): use platform-specific path join for NODE_COMPILE_CACHE

* fix(types): add type declarations for configure-rollout.mjs and resolve typecheck errors

* test: serialize suites on Windows CI and set global timeout to prevent hangs

* fix(release): verify PE header for .node files to skip non-Windows prebuilds in Jsign

* perf(windows): exclude build-tool deps and add Defender exclusions on install

- Exclude ~2,000 build-tool-only transitive files from the Windows package:
    caniuse-lite (838 JS files, browserslist/babel only)
    lodash (1,048 JS files, only used by @malept/flatpak-bundler linux tool)
    browserslist / electron-to-chromium / update-browserslist-db (15 files)
    @malept/* (flatpak bundler, Linux-only build tool)
  Excluded packages are verified to have no runtime dependency chain:
    - @mixmark-io/domino kept (dsh-tool-web → turndown → domino)
    - jsdom kept (dsh-ppt runtime dep)
    - dsh-ppt's nested typescript kept (only top-level typescript is absent)

- Add Windows Defender exclusions in NSIS installer (customInstall macro):
  Add-MpPreference for $INSTDIR and $APPDATA\dsh-desktop via PowerShell.
  Uses -ErrorAction SilentlyContinue so non-admin installs fail silently.
  Without this exclusion Defender scans 44k files on first launch
  (observed: 121-123 s until Harness ready on a fresh install). With the
  exclusion, first launch should complete in under 20 s.

* fix(release): align package.json version with package-lock.json (0.1.1)

* fix(test): correct patch indentation in provider-error-patch.test.ts

* fix(windows): optimize titlebar layout, integrate more actions into app menu, and fix sidebar overlap (#409)

* perf(windows): cut 20k non-runtime files, elevate defender exclusion, and relax startup timeout (#435)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite in… (#437)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite install

An in-app update can be force-killed before will-quit, and a leftover session marker from another version is a reinstall, not a crash.


* fix(diagnostics): suppress startup failure report when frontend identifies incompatible plugins

* fix(build): preserve runtime skill markdown files in build output and update ppt artifacts

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* fix(windows): clear session marker on overwrite install (#440)

NSIS force-kills the running app, so will-quit never clears session.json and a same-version overwrite looks like an unclean exit.

Co-authored-by: fairy <3054158379@qq.com>

* Fix/suppress upgrade unclean exit (#441)

* fix(diagnostics): suppress unclean-exit after upgrade or overwrite install

An in-app update can be force-killed before will-quit, and a leftover session marker from another version is a reinstall, not a crash.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(diagnostics): suppress startup failure report when frontend identifies incompatible plugins

* fix(build): preserve runtime skill markdown files in build output and update ppt artifacts

* update

* update

* update

* update

---------

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: stop tracking plugin-management and keep it local only (#443)

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: 建立仓库级 AGENTS.md 开发约束与治理规范 (#428)

* docs: establish repository-wide AGENTS development standards and governance contracts

- Add root AGENTS.md defining project boundaries, TypeScript code style, dependency/data constraints, and verification runbooks.
- Add directory-scoped AGENTS.md rules for src/main, src/preload, packages, patches, and build.
- Add supporting design specifications:
  - docs/code-standards.md (design rationale and phased tool enforcement)
  - docs/patch-plugin-contract.md (patch vs plugin selection and slot compatibility)
  - docs/source-build-contract.md (single authoritative source and build artifact boundaries)

* docs: 补齐 AGENTS 前端硬约束与企业/编译型包覆盖

把主题 token、locale、preload 与 slot 边界写成可执行规则,并标出 enterprise 存量与 ppt-runtime 的维护入口。

---------

Co-authored-by: fairy <3054158379@qq.com>

* fix(security): allow notifications permission from the harness window (#461)

The allowlist only covered clipboard-sanitized-write, so notification
permission requests from the harness page could not be granted. The
loopback main-frame checks are unchanged, so the trust boundary is the
same.

* fix(release): honor platform target and add signed-only dispatch mode (#468)

A filled prerelease tag previously forced a Windows build even when
macos was selected. Dispatch now separates development, signed, and
prerelease so production-identity packages can be signed without publishing.

Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(settings): keep long navigation menus scrollable (#456)

* fix(settings): keep long navigation menus scrollable

* fix(settings): add navigation overflow patch

* feat(safe-mode): add floating repair agent widget in recovery and safe mode (#431)

* feat(safe-mode): add floating repair agent widget in recovery and safe mode

- Add floating repair agent FAB widget on plugin recovery and safe mode overlay
- Provide pre-configured quick action prompts with error diagnostics and logs
- Support switching models and uploading/pasting screenshots for vision models
- Connect with isolated safe mode Harness session and stream responses via IPC

* feat(safe-mode): temporarily disable floating repair agent chat widget

* feat(safe-mode): enable repair agent with 0.9.0 incident knowledge and offline diagnostics

* feat(release): expand release notes generator with performance section and full changelog

* feat(window): persist window bounds, maximized state, and zoom level

* fix(repair-agent): handle unconfigured models with api key drawer and prevent silent stream failures

* fix(dev): strip inherited ELECTRON_RUN_AS_NODE to prevent electron launch crash

* feat(dev): forward runtime logs to console in development and log single-instance conflicts

* fix(repair-agent): inherit working default model route and add dual-track history polling

* refactor(repair-agent): rely purely on websocket stream without redundant history polling

* feat(repair-agent): replace floating widget with seamless native Harness UI diagnostic cards

* fix(windows): stop GPU fallback ladder from degrading on TDR device-loss recoveries, recover the menu view from a lost renderer

gcp3 crash data for v0.9.0 showed 23/30 Windows gpu-crash reports at exitCode=34
— Chromium's own exit code when it detects a lost D3D11 device (typically a
driver TDR reset) and exits the GPU process on purpose so it can restart it.
The fallback ladder was counting this self-recovery as evidence the sandbox is
broken and would degrade hardware acceleration after 3 of these on an
otherwise healthy machine. isGpuLossFatal now takes the exit code and treats
exitCode 34 as non-fatal, only logging a breadcrumb.

Separately, ~6 renderer-crash reports were on windows-menu.html: that view runs
in its own WebContentsView outside installMainWindowRendererRecovery, which
only hooks the main window's webContents, so a lost renderer there left a dead
menu until the whole app restarted. It now reloads itself on render-process-gone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(session): skip unreadable session logs instead of failing the whole listing

One corrupt JSONL/zstd session log made sessionPersistence.list() throw, which
failed dsh-workspace and stopped the Harness entirely (0.9.0 startup-failure
reports: "corrupt Zstandard session log", corrupt header id). The log is now
skipped with a warning naming its path and left on disk untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(startup): stop reporting attributed plugin failures, clear stale writer locks, fix repair diagnosis

- Harness startup failures that plugin recovery attributes to a user plugin
  are handed to the user and their pending crash report is discarded; only
  the frontend recovery path did this before, so they were still uploaded.
- Remove dsh-atomic-write locks in DSH_HOME and DSH_HOME/profiles whose owner
  pid is gone before launching; a killed Harness left .credentials.yaml.lock
  behind and the next launch timed out on it.
- Repair Agent diagnosis uses loader provenance and the recovery-resolved
  plugins, and only reports a startup timeout when the runtime says so, since
  "waiting for Harness" + SIGTERM matched ordinary failure cleanup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): ensure safe mode uses hoisted node-linker and hide diagnostics when plugins are identified

* feat(runtime): add preset YAML validation step before launch

* refactor(runtime): remove redundant preset check in favor of native Cordis loader logging

* feat(repair-agent): unify diagnostic cards into a single entry with verification and reporting instructions

* fix(repair-agent): restore the 3 fine-grained diagnostic cards in safe mode

* fix(safe-mode): unify safe mode diagnostic cards into a single AI agent entry

* fix(web-import): never replace a desktop home that already holds plugins or credentials

A 0.9.0-rc1 home with plugins or .credentials.yaml but no settings.yaml or
sessions was treated as unused, so the web import deleted it wholesale. It now
counts as used. An unused home is also moved aside before deletion so
delete-pending entries on Windows cannot block links created at the same path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): don't remove plugins a deferred migration has not installed yet

When the generation migration failed (e.g. pnpm EPERM right after install),
imported plugins stayed manifest-only, failed to prepare, and plugin recovery
removed them as incompatible. Deferred migrations now report the pending
plugins and recovery excludes them. Transient EPERM/EBUSY/EACCES failures no
longer freeze the same profile for six hours, so the next launch retries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(windows): stop the titlebar drag region from swallowing clicks and churning

The drag region now ignores pointer events, and only semantic dialog markers
hide it: class-name guesses like [class*="modal"] matched permanent elements
and hid it for good. Visibility checks run at most once a frame, since
streaming output mutates the DOM continuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(diagnostics): send crashes without an event id right away

Only a plugin-attributed startup failure should wait for recovery to decide
whether to discard it. A crash with no event id compared equal to an unset
pending id and was held back too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(market): pin the exact version a declared dshmarket range names

The installer pins and verifies an exact version, so a declared range such as
^1.47.0 is reduced to the version it names instead of being passed through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: ignore the local pnpm content store

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): resolve Safe Mode plugins from the installation, not the module fallback

Safe Mode loads only installation-owned bundles, yet it still went through
the shared $DSH_HOME/profiles/node_modules fallback, whose junctions Windows
can refuse to recreate (EPERM) for minutes — the top startup failure of
v0.9.0. The desktop now sets DSH_DESKTOP_HOST_RESOLVED for the Safe Mode
profile only; the patched Harness then skips healing the fallback and
resolves bare plugin names, including entries created at runtime through
ctx.loader.create, from its own installation. A flag leaked from the parent
environment never reaches a normal profile.

This replaces the hoisted node-linker workaround for Safe Mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(runtime): mirror Harness logs to the console only in development

A packaged app's stdout may be a closed pipe; writing every Harness log line
to it there buys nothing and can fail. harness.log still receives every line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): still blame a migration-pending plugin whose legacy copy is installed

f475ce5 exempted every plugin with a pending deferred migration from
recovery. Only a plugin the migration left manifest-only is not broken; one
whose legacy copy is installed still loads and can be the real culprit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(logging): record Harness runtime warnings and errors in harness.log

Cordis's ctx.logger keeps messages only in an in-memory ring, and its default
level filters out warnings; nothing in the shipped composition exported them.
Session activation failures (a preset that cannot be found or mounted) never
reach the logger at all — they are only pushed to the client. So errors after
startup left no trace for a person or the Repair Agent once the process was
gone.

A new desktop plugin, dsh-desktop-log-bridge, is composed first into both the
normal and the Safe Mode profile. It writes warn and error messages, the
errors already in the ring, and api-session/error events to stderr, which
the desktop records into harness.log. It rate-limits floods and writes one
ready line per launch, so a log without it is known to predate the bridge.

Every bridged line carries a [harness-log] prefix, and latestHarnessAttemptLogs
drops such lines: runtime warnings never become recovery's failure cause or
the plugin it blames.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(safe-mode): disable plugins instead of removing them

Safe Mode now switches a plugin off the way the plugin market's own toggle
does, so it can come back without a reinstall: `disabled: true` rows for the
loader entries the package inserts in the user patch layer, which the loader
re-applies on every boot, plus the market's .dsh-market/state.json list,
which is also the only switch for client-only packages. Nothing is deleted.

Disabled plugins show as disabled with a Re-enable action instead of a
selection. Compatibility issues that call for disabling a plugin use the same
path; the old approach of dropping it from dsh.profile.bundles was silently
undone, because the generation projection rewrites that list on every launch.
Compatibility inspection skips disabled plugins, since they never load.

A disable-carrier (a bundle whose patch disables another plugin) cannot be
switched off on its own without stranding the plugin it replaces, so it
keeps the backed-up removal. Startup recovery keeps removal too: a package
that is itself broken fails before the patch layer's disable applies. When
recovery removes a plugin it also clears its market disable entry, as the
market's own uninstall does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(repair-agent): start a fresh, well-briefed repair session from one card

Recovery and Safe Mode now offer a single "start the repair agent" card. Each
click opens a new session in the Safe Mode Harness and makes it the session
the Harness UI shows: the UI restores its current session from desktop
storage when the page loads, so the Recovery path opens the session before
its page loads and the Safe Mode path reloads the already open page. The
floating repair widget is gone.

The session's workspace is the Harness home (profiles, plugins, patch layer)
instead of the empty launch root, and the first turn of each session carries
the diagnosis. The system prompt is rebuilt around real incidents:

- a directory map that separates the normal profile it repairs from the Safe
  Mode profile it runs in;
- the path to harness.log with how to find the failed launch in it, plus a
  short excerpt, instead of pasting logs; Safe Mode's own logs are no longer
  used when no failed launch was captured;
- one bilingual playbook source, so the Chinese and English prompts can no
  longer drift: plugin load failures, broken packages (which disabling cannot
  fix), the Windows module-fallback EPERM (no Developer Mode advice), a
  corrupt patch layer, startup timeouts, the code -> ptc preset rename, and
  schema errors in copied user presets;
- actions named after the real UI, and a confirm-and-back-up rule before any
  file change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(market): stop shipping dshmarket so market upgrades take effect

Harness resolves every profile bundle from its own installation before the
profile (`resolveBundleDir` in dsh-app-boot). Since #352 pinned dshmarket
1.45.1 as a runtime dependency, the packaged copy won that lookup: upgrading
the market wrote 1.48.0 into the profile, the market and the baseline check
both read 1.48.0 back, and Harness kept loading 1.45.1 after every restart.

Nothing in the app imports dshmarket; the profile copy is installed from npm
by the market installer and `ensureMarketBaseline`. It stays a
devDependency for the tests that exercise its real profile reader, which
keeps it out of the packaged app.

`ensureMarketBaseline` now also notes when the copy Harness would load is not
the profile's, so a shadowed market shows up in harness.log instead of as a
version the UI reports but never runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(recovery): let startup recovery repair a plugin market that breaks startup

The market is a core bundle, so startup-failure attribution never named it
and the Recovery page, finding no culprit, could only offer Safe Mode — which
does not load the market and has nothing to act on. A market release that
stopped Harness from booting therefore failed every launch the same way.
Until now the packaged dshmarket shadowed the profile copy and hid this;
with it gone the profile copy really loads, so the loop is live.

When the failed launch's log (or loader provenance) names dshmarket, the
Recovery page shows it as its own row, apart from the third-party plugin
list whose removal path refuses core bundles:

- Upgrade to the newest release the market check finds compatible with this
  Harness, for a market too old for it; a release already tried here is not
  offered again.
- Install the verified version (VERIFIED_MARKET_BASELINE). That moves down
  from a broken release, up from a stale one, or reinstalls a damaged copy.
  It is the primary action when the market is the only culprit.
- Remove the market; installed community plugins are kept.

Versions come from the main process's own check, never from the page. Each
change stops Harness first, as the shared-tree installer requires, pins the
exact version so the next baseline check does not reinstall the broken
release, then relaunches and returns to recovery if startup still fails.
The market removal behind the settings page is shared as `removeMarket`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): render the plugin market row like any other plugin row

The market row added its own touches: a version after the name, a green
button for restoring the verified version (which can be a downgrade), and a
confirmation dialog plus its own wording for removal. Third-party rows have
none of these, so the market read as a different kind of item.

Show the name only, keep green for the upgrade alone with every other action
in the neutral style, and remove it the way the page removes any plugin —
"Remove this plugin", no extra confirmation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): offer the plugin market only upgrade and removal

Restoring the verified market version was an action no other plugin row
has. Drop it, so the market row is a plugin row like the rest: upgrade to a
compatible release when the market check finds one, and removal.

When the market alone blocks startup, the page now uses the same buttons as
a lone third-party plugin: "Upgrade plugin and restart" with "Uninstall
plugin" beside it when an upgrade exists, otherwise "Remove this plugin and
continue".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: stop tracking the local pnpm content store

0d083d5 committed the whole `.pnpm-store/` (13,357 files, ~235 MB) along
with its four intended changes, before 7a2cb73 added it to .gitignore. An
ignore rule does not untrack files already committed, so they stayed in the
branch and buried the PR's real diff.

Remove them from the index only; the local store is untouched and stays
ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: drop tests that only grep our own source code

About 40 tests read src/main/index.ts, the preload, build/*.html, the NSIS
script or our own packages' client.js as text and asserted that particular
lines were present — `toContain("ipcMain.handle('safe-mode:action'")`,
`indexOf(a) < indexOf(b)` and the like. They execute nothing, so they cannot
catch a behavior regression, yet they fail on any rename or reformat, and on
Windows, where the checkout has CRLF line endings, any expected string that
spans a newline never matches. Two of them failed CI that way on PR #431.

Removed: every test block whose assertions were such text matches, and the
source-text lines inside otherwise behavioral tests (windows-titlebar
allowlist, runtime Node mode, release dev channel, branding postinstall,
harness-node-entry). The dev-channel check now reads electron-builder.dev.cjs
as a config object instead of matching its text.

Kept: tests that execute code, parse package.json/lockfile/YAML/JSON
config, or check the installed third-party bundles in node_modules (patch
and upstream-contract checks), since those inspect what actually ships.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(repair-agent): drop tests that only check the prompt's wording

Two repair-prompt tests asserted fixed sentences of the generated prompt:
exact Chinese phrases with hard-coded POSIX paths, and a fixed count of
seven playbooks plus two error strings. Any edit to the copy broke them
without saying anything about how the agent behaves, and the path one
failed on Windows, where path.join correctly yields backslashes.

Keep the test of real prompt logic: only the tail of the log is included,
and none at all when no failed launch was captured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(repair-agent): drop the remaining repair-prompt test

It asserted which log lines and which sentence end up in the generated
prompt text. Like the other prompt tests, that pins the copy rather than
anything the agent does with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(safe-mode): offer the repair agent as one line under the summary

The repair agent sat in its own section below the actions — a heading, a
divider and a card, about 140px. In a 1280x800 window that pushed the plugin
list into a scrollbar, which the native Windows recovery UI check rejects:
every plugin has to be visible at once. Its text was also Chinese-only.

Show it as a single link under the summary instead, in both languages. It
sends the same diagnostic request as the card did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(recovery): offer the repair agent as a button beside Safe Mode

The recovery page showed the repair agent in its own section under the
technical details — a heading and a card, in Chinese only. Make it a
secondary "Repair agent" button next to "Enter Safe Mode", in both
languages. As before, it appears only when no plugin or market was
identified; a named culprit has its own repair above.

The native Windows UI check expected Safe Mode to be the only action when
nothing is identified; it now expects the agent beside it, and nowhere else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(runtime): keep a clean launch off stderr

The packaged Windows smoke fails any launch that writes to stderr once a
workspace and session exist. Two lines did on every launch, and neither
reported a problem:

- The log bridge's "recording from here on" marker. It is a notice, so it
  now goes to stdout; stderr carries only bridged warnings and errors.
- `patch: entry dsh-market not found`. The desktop patch configured the
  market's entry (restart off, wait for desktopProfiles) unconditionally,
  but the market is optional, and a patch row whose entry is absent makes the
  loader warn. It was always there; the bridge only made it visible. The row
  now lives in dsh-desktop-market.patch.yml, passed as a second --patch only
  when the profile boots dshmarket, so a market profile behaves as before.

Checked by booting a fresh web profile (no market overlay, no bridged
warning) and one that boots dshmarket (overlay applied, no warning).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(market): remove dshmarket devDependency to avoid shadowing profile updates (#478)

* fix(market): remove dshmarket devDependency to avoid shadowing profile updates

Harness resolves every profile bundle from its installation anchor before
the user profile (resolveBundleDir in dsh-app-boot).

Previously, dshmarket was retained as a devDependency in package.json to
support test/generation-boundary.test.js. However, electron-builder's
files: ["node_modules/**/*"] glob unconditionally includes all modules in
node_modules unless explicitly excluded, causing dshmarket to be bundled
into the packaged application and to exist in dev checkouts. When the user
upgrades dshmarket in their profile to 1.48.0, Harness continues loading
the packaged copy instead of the profile's copy.

This removes dshmarket from devDependencies and package-lock.json:
- test/generation-boundary.test.js implements a local readInstalledVersion helper
- test/market-baseline.test.ts verifies dshmarket is absent from both dependencies
  and devDependencies, and simulates installation shadowing with a mock directory

* style(safe-mode): style repair agent session link with blue accent color

* fix(safe-mode): make the repair agent's failures visible and its guidance true (#479)

* fix(safe-mode): report a repair agent session that could not open

Clicking "let the repair agent read the log" did nothing at all when the
session failed to start: startRepairAgentPrompt returned a bare boolean,
the caller skipped the reload, openHarness found the window already on
the Harness origin and only raised it, and the reason reached the log
alone. A user whose default agent preset was broken saw three clicks
produce no visible change.

The call now carries the reason. The Safe Mode manager stays open and
shows it instead of closing on the way out, so the click can be retried,
and plugin recovery surfaces the same reason through the manager it hands
the window to. The notice keeps the first two lines of Harness's mount
error; the rest is a stack that would push the plugin list off the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(plugin-disable): refuse a bundle whose package yields no loader row

Disabling a broken plugin reported success and changed nothing that
matters. pluginPatchRows reads the loader rows from the package's own
patch; when that file is missing or unparsable the list comes back empty,
so the patch layer was skipped, only the market state was written, and
the result was ok. The bundle stayed in dsh.profile.bundles and the next
launch composed — and failed on — the same package.

A package listed as a bundle is one the loader prepares on every launch,
so no readable row means the package on disk is broken, not that the
plugin has no rows. That case is now refused as broken-package, and Safe
Mode falls back to the backed-up removal it already uses for carriers.
A client-only plugin is untouched: it is not a listed bundle, and the
market state remains its only switch.

The compatibility fixture listed a bundle whose manifest declared no
dsh.bundle — a profile that cannot start — so it now carries the patch
the loader would read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(repair-agent): tighten the prompt and drop the unused repair module

The prompt repeated absolute Windows paths in every playbook entry and
walked through the Safe Mode profile the agent must not touch. Paths are
given once in the directory section and referred to by name afterwards;
the Safe Mode profile is one line. 5290 to 4403 characters, with more in
it than before.

What changed beyond wording:

- The agent fixes what it can fix itself. Disabling a plugin is spelled
  out (patch layer plus market state, never dsh.profile.bundles), and
  only upgrading and reinstalling still go through the UI.
- A broken package is triaged by layer first: node_modules/<package> is
  usually a link the next launch reprojects, so a merely missing link is
  a restart, not a removal. Only a missing manifest or patch in the real
  package is an interrupted install.
- Windows EPERM tries to self-heal once by deleting the entry the error
  names — Safe Mode does not use that folder — before asking the user to
  hunt for the process holding it.
- A clean log is a finding: say nothing is wrong, name the log sections
  checked, and ask what the user actually saw. The "a quiet log proves
  nothing" line that contradicted this is gone, and an empty offline
  diagnosis is no longer printed as if it were a result.
- Failures outside the list are reasoned from the log, with the source
  repository named for confirming Desktop's own behaviour.
- The preset-not-found entry is dropped: an old session naming a deleted
  preset simply cannot open, and recreating the preset to revive it is
  not a repair worth prescribing.

profile-repair.ts goes with it. Startup stopped performing destructive
package repair, which left the module with no caller in src/ and only
its own test — a documented, tested path to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: point the community link at the current Discord invite

The old invite is replaced everywhere it appears: the six READMEs, the
Discord entry on the plugin recovery and Safe Mode pages, and the
recovery smoke test that asserts the URL the click opens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(workspace): start a new session on the default preset, not a stale blank one (#481)

* fix(workspace): start a new session on the default preset, not a stale blank one

Opening a workspace does not create a session when a blank one is already
there: connectWorkspace returns the first blank session whose cwd matches
and only calls sessions.create when it finds none. A blank session is
never run, so it stays blank forever — every later visit to that project
is handed the same one as its "new session", carrying whatever preset was
default when it was made. Changing the default in settings reaches only
sessions created afterwards, which a session made weeks ago is not.

Switching that session's preset instead is not available: the host
resumes a session before recomposing it, and resuming loads the preset it
already names. A blank session naming a preset that has since been
deleted cannot be resumed at all, so it cannot be moved either — the
select call comes back with "resume failed … preset not found".

So the stale session is left alone and simply not reused. A blank session
now qualifies only when the preset it names matches the current default,
and a workspace whose blank session does not is given a fresh one. The
stale session stays in the list to open or delete.

The default is read through ctx.get rather than an inject: reading an
un-injected Remote namespace throws rather than answering undefined, and
this plugin has to keep working where no preset roster is composed. When
it cannot be read, reuse behaves exactly as before.

Verified in a running Harness: opening the project created
session-67b5595c on `standard` and left session-95d736d0 on the deleted
`repair` untouched, where before the stale one was handed back every
time. The patch applies cleanly to the pristine 0.1.5-rc.2 package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(repair-agent): check model and key availability before starting repair

- Add checkModelAvailability in RepairAgentService to verify model keys and default session model status
- Show informative warning dialog in Safe Mode and Recovery views when models are unavailable or unroutable
- Add unit tests covering missing keys, unroutable providers, provider failures, and valid configurations

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix: add loader await timeout diagnostics and refine model unavailable copy (#485)

* fix(loader): add timeout and stuck plugin diagnostics to loader await

- Add timeout handling to cordis-plugin-loader EntryTree.prototype.await (configurable via DSH_LOADER_TIMEOUT_MS, default 100s) with 5s progress heartbeat
- Accurately identify stuck plugins on timeout and emit structured dshPluginFailure errors
- Add unit test coverage in cordis-plugin-loader-patch.test.ts

* fix(repair-agent): update model unavailable dialog copy text

- Refine warning dialog message and detail copy when default model is unconfigured or unavailable
- Update unit tests in test/repair-agent.test.ts

* update

* test(repair-agent): sync model unavailable detail assertion wording (#486)

* feat: adapt desktop to Harness 0.1.6-alpha.2

---------

Co-authored-by: fzfz <fzfzqp@gmail.com>
Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Fairy-happy <243122010@qq.com>
Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Keep-Silence <keep137950@163.com>
Co-authored-by: Wanming08 <152006802+Wanming08@users.noreply.github.com>

* fix: route Plugin Manager through generations (#503)

* fix: route Plugin Manager through generations

* fix: close P1 Harness integration gaps

* feat(workbench): connect Desktop to the Awesome market (#496)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): add the make-my-workbench flow and GitHub listing guide (#497)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): submit through Awesome PRs and query status (#508)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): publish development and acceptance guides (#509)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): install update and uninstall market entries (#510)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(onboarding): add the desktop first-run notice (#505)

Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* fix(workbench): reconcile market installs with declared runtime IDs (#514)

* fix(workbench): reconcile market installs after restart

* feat(workbench): declare runtime IDs in market catalog

* fix(workbench): preserve v2 market compatibility

---------

Co-authored-by: yaojin <yaojin@58.com>

* feat(workbench): migrate namespaced runtime IDs (#521)

Co-authored-by: yaojin <yaojin@58.com>

* fix(workbench): read the current session from the uiWorkspace main view (#517)

Harness 0.1.6 removed ClientSessions.open/clear and list.current. #514
moved opening onto uiWorkspace.openSession, but the controller still read
list.current in five places, which is always undefined now: native
navigation to a bound session never switched the workbench, and the
frame never saw a current session.

- Workbench: add public currentSession(), which reads the session holding
  the `mainView` retention, and showSession(id); replace every
  list.current read. Workbench providers read the same fact.
- ui-workspace patch: add deleteSession(), so deleting the current session
  releases the main view, and restore the 0.1.5 guard that stops a
  superseded initial navigation from opening a native session over a
  workbench.
- session-controller patch: drop the dead selection code in delete().
- Tests mock the real 0.1.6 selection model instead of the removed API.

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(workbench): give each workbench its own icon (#516)

WorkbenchIcon had collapsed to the shared market glyph. Use the
workbench's own short icon when it declares one, else a themed glyph
picked from its category and title, else its first character.

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* fix(workbench): single display-mode toggle and no fill behind the open market (#518)

The sidebar showed both list and icon mode buttons; show one button with
the mode a click switches to. Drop the faint fill behind the market entry
while the market is open (hover still highlights it).

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* feat(workbench): polish market cards and the detail view (#520)

* feat(workbench): polish market cards and the detail view

- Favorite with a bookmark instead of a star; the star now means GitHub
  stars, replacing the thumbs-up.
- Show the author as a GitHub avatar and login, without the "作者" label.
- Show downloads next to stars (npm 30-day or GitHub release downloads),
  with a dash when the catalog has no value.
- The workbench name links to its GitHub repository, in the card and the
  detail view; the separate GitHub row is gone.
- Added workbenches show a quiet "已安装" state instead of a primary
  "打开工作台" button.
- The detail view shows screenshots as a large swipeable carousel that
  starts on the first image.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(workbench): keep card actions on one row when an update is available

Show a short 检测到更新 action in place of 已安装 instead of adding a
third "更新到 vX.Y.Z" button that wrapped the row; the target version
moves to the tooltip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: yaojin3616 <yaoj.alex@gmail.com>

* feat(workbench): let authors choose the market category when submitting (#522)

The submission prompt only said "category 必填", so the Agent picked a
category on the author's behalf. Step 3 now offers a category picker fed
by the market catalog's own category list, and the chosen id is written
into the prompt with an instruction not to change it. Choosing "other"
adds an optional new-category idea, relayed as one sanitized line for the
PR description so the market maintainers decide. With no choice, or no
catalog, the prompt keeps asking the Agent to pick from categories.json.

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* feat(workbench): use repository as the canonical identity (#524)

* feat(workbench): derive identity from market repository

* feat(workbench): derive local identity from repository

---------

Co-authored-by: yaojin <yaojin@58.com>

* fix(workbench): display the catalog version (#525)

Co-authored-by: yaojin <yaojin@58.com>

* fix: recover session deletion and legacy workbench state (#526)

* fix: restore session deletion confirmation

* fix: migrate legacy workbench state

* update

* fix(patches): remove stray blank line breaking session-controller patch (#528)

commit 3d03717 introduced an unprefixed blank line inside the last
hunk of the @deepseek-ai/dsh-api-session-controller 0.1.6-alpha.2
patch, which patch-package 8.0.1 cannot parse. This failed `npm ci`
postinstall on every platform of the v0.10.0 release build.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* fix(workbench): enforce runtime session boundaries (#537)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* docs(workbench): require user-selected project locations (#538)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): guide sidebar focus and session ownership (#540)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): move navigation to a compact Dock (#541)

* feat(workbench): promote workbench navigation entry

* feat(workbench): add compact switcher dock

---------

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix(workbench): open added workbenches from market (#542)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix(workbench): keep open action beside updates (#543)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): show Dock in unbound sessions (#544)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbench): move switcher into sidebar (#545)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix(registry): recover locks from exited processes

* Upgrade Harness to 0.1.7-rc.1 with preset migration

* Audit Harness 0.1.7 patches and remove picker override

* fix(onboarding): show first-run notice only on new installs (#550)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix(workbenches): refresh and restore market catalog

* fix(workbench): bind workspace-switch sessions

* feat(workbench): align mode navigation and isolate failures

* docs(workbench): refresh specification version

* docs(workbench): make specifications direct and concise

* test(workbench): assert direct specification wording

* docs(workbench): reference rules from the self-test

* feat(workbenches): add bid workspace icon

* fix(workbench): refine switcher controls

* fix(workbench): compact ordering controls

* feat(workbench): add drag reorder handles

* fix(workbench): migrate navigation patches to Harness 0.1.7

* fix(models): keep provider model settings visible

* fix(test): 规范化 workbench 客户端源码换行以兼容 Windows CI

Windows checkout 会把 client.js 转成 CRLF,跨行 toContain 断言因此失败并阻断打包。

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(onboarding): migrate settings to Harness 0.1.7

* fix(client): restore Harness plugin activation

* fix(conversation): guard session accessory on home

* Restore workbench UI slots in Harness rc.2 patches

* Avoid duplicate web import preload bridge

* fix: reserve macOS caption space for workbench panels

* fix(workbench): reserve macOS caption controls in top header

* refactor(windows): run Harness through Electron Node mode

* fix(windows): verify PPT with packaged Electron Node

* test(windows): preserve explicit Koffi probe exit

* test(windows): wait for Electron Node probe exit

* fix(workbenches): restore market workbench registration and install states

* style(workbenches): match general settings switch

* fix(workbenches): retain switcher styles across client rebuilds

* fix(workbenches): restore native workspace on mode switch

* fix(workbenches): open only from installed tab

* docs(workbench): clarify requirements, identity and validation flow (#592)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix(workbench): preserve native composer button styles (#593)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* fix(workbench): require intent before agent development (#605)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* feat(workbenches): align market cards and native plugin lifecycle (#609)

* fix(workbenches): confirm market removal in recovery states

* feat(workbenches): separate sidebar home and mode switch

* style(workbenches): align sidebar row with native panels

* feat(workbenches): align market cards and native plugin lifecycle

---------

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

* Restore installed workbench update checks

* Refine workbench update actions and failure hints

* Restore workbench styles after head changes

* fix: hide foreign sessions in custom workbenches (#617)

Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>

---------

Co-authored-by: 姚劲 <yaoj.alex@gmail.com>
Co-authored-by: fzfz <fzfzqp@gmail.com>
Co-authored-by: Debug Agent <debug-agent@noreply.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: fairy <3054158379@qq.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Keep-Silence <keep137950@163.com>
Co-authored-by: Wanming08 <152006802+Wanming08@users.noreply.github.com>
Co-authored-by: Yutong <cinderttt@gmail.com>
Co-authored-by: yaojin <yaojin@58.com>
Co-authored-by: cinderzhan <cinderzhan@users.noreply.github.com>
Co-authored-by: 姚劲 <yaojin@dataelem.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 插件安装边界丢失 npm registry:使用镜像源的用户插件更新必然失败(ERR_PNPM_NO_MATCHING_VERSION)

1 participant