Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ jobs:
# exist during check/test; distributable assets remain frontend-build's owner.
- name: Create Tauri resource directories
shell: bash
run: mkdir -p dist src/mobile-web/dist
run: mkdir -p dist src/mobile-web/dist packages/dsh-acp/dist-profile

- name: Install Linux system dependencies (Tauri)
if: runner.os == 'Linux'
Expand Down
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ The check detects:
bitfun-desktop Tauri build script references this directory as a resource,
so `cargo check -p bitfun-desktop` and `cargo check --workspace` fail
without it)
- Missing `packages/dsh-acp/dist-profile` (fix: `pnpm run prepare:dsh-profile`
— the DeepSeek Harness bridge, referenced as a Tauri resource the same way;
`BITFUN_SKIP_DSH_PROFILE=1` writes a placeholder instead if you do not need
the bridge)
- Missing sherpa-onnx prebuilt libs (the sherpa-onnx-sys build script
downloads from GitHub at build time; if the download fails on poor
connectivity, set `SHERPA_ONNX_LIB_DIR` to the prebuilt lib directory
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTING_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ pnpm run check:build-prereqs -- --fix # 尝试自动修复缺失的前置依赖
- 缺少 `src/mobile-web/dist`(修复:`pnpm run prepare:mobile-web` —
bitfun-desktop 的 Tauri 构建脚本将该目录作为资源引用,缺失时
`cargo check -p bitfun-desktop` 和 `cargo check --workspace` 会失败)
- 缺少 `packages/dsh-acp/dist-profile`(修复:`pnpm run prepare:dsh-profile` —
DeepSeek Harness bridge,同样以 Tauri 资源方式引用;不需要该 bridge 时可用
`BITFUN_SKIP_DSH_PROFILE=1` 写一个占位目录)
- 缺少 sherpa-onnx 预编译库(sherpa-onnx-sys 构建脚本会在构建时从
GitHub 下载;若网络连通性差导致下载失败,设置
`SHERPA_ONNX_LIB_DIR` 指向 `target/sherpa-onnx-prebuilt/` 下的预编译
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"type-check:skin-market": "pnpm --dir src/skin-market-web type-check",
"test:skin-market": "pnpm --dir src/skin-market-web test",
"prepare:mobile-web": "node scripts/mobile-web-build.cjs",
"prepare:dsh-profile": "node scripts/prepare-dsh-profile.mjs",
"frontend:build-all": "node scripts/frontend-build-all.mjs",
"preview": "pnpm --dir src/web-ui preview",
"desktop:dev": "node scripts/dev.cjs desktop",
Expand Down
5 changes: 5 additions & 0 deletions packages/dsh-acp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
lib/
dist-profile/
.sessions/
.env
30 changes: 30 additions & 0 deletions packages/dsh-acp/NOTICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Third-party notices

This package is a fork of parts of [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness),
which is distributed under the MIT License. Copyright (c) DeepSeek.

| File here | Upstream source | Relationship |
| --- | --- | --- |
| `src/codec.ts` | `packages/acp/acp/src/codec.ts` (`@deepseek-ai/dsh-acp`) | verbatim copy |
| `src/bridge.ts` | `packages/acp/acp/src/index.ts` (`@deepseek-ai/dsh-acp`) | session, turn, approval, and teardown machinery kept; the `session/event` dispatch replaced |
| `src/app.ts` | `packages/examples/acp-demo/src/index.ts` (`@deepseek-ai/dsh-acp-demo`) | composition kept; the transport swapped for `src/bridge.ts` |
| `src/bin.ts` | `packages/examples/acp-demo/src/bin.ts` (`@deepseek-ai/dsh-acp-demo`) | copy with a different bin name |
| `cordis.yml` | `examples/acp-agent/cordis.yml` | trimmed for an IDE launch |

The MIT License text ships with the upstream project; this fork carries the same
terms.

## Why fork rather than depend

`@deepseek-ai/dsh-acp` is automation-only by design. Its `session/event` handler
publishes committed assistant text and nothing else:

> Emit only committed assistant text. Raw chunks, reasoning, tools, plans,
> titles, and retry markers are presentation or trace data and stay off the
> automation wire.

There is no configuration flag that changes this, so an IDE client sees no tool
cards, no reasoning, and no plan — and receives permission requests for tool
calls it was never told about. This fork exists to publish exactly that withheld
data. Upstreaming it as a verbosity level in `@deepseek-ai/dsh-acp` remains the
better long-term outcome.
132 changes: 132 additions & 0 deletions packages/dsh-acp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# @bitfun/dsh-acp

An Agent Client Protocol server for [DeepSeek Harness](https://github.com/deepseek-ai)
(`dsh`), written for an IDE rather than for automation.

The harness ships no ACP entry point of its own. The published
`@deepseek-ai/dsh-acp` is an automation surface: it deliberately withholds tool
calls, reasoning, and mode selection, because a script does not need to watch an
agent think. An IDE needs exactly those, so BitFun ships this bridge and runs it
inside the harness the user already installed.

## For users

Three steps, and BitFun stores none of your DeepSeek configuration:

1. Install the harness — `npm install -g @deepseek-ai/dsh` (BitFun's agent
settings page has a one-click button that runs the same thing). It is a Node
program and needs **Node 20.12 or newer** — its own toolchain targets Node 22
LTS. dsh declares no `engines`, so npm installs it onto an older Node without
complaint and it then fails to boot with a `SyntaxError` about
`util.parseEnv`; BitFun checks the version up front and says so instead.
2. Configure a model and an API key inside dsh, e.g. through `dsh web`'s Models
page. That writes `~/.dsh/settings.yaml` and `~/.dsh/.credentials.yaml`.
3. Pick **DeepSeek Harness** in BitFun and start a session.

The model and the key stay where you put them. This bridge reads them through
the harness's own `dsh-settings-file`, `dsh-credentials-local`, and
`dsh-agent-default-model` services, so switching models in dsh switches them in
BitFun too. BitFun writes no DeepSeek credentials of its own.

## How BitFun launches it

BitFun runs `dsh --profile bitfun-acp`. A dsh profile is just a directory under
`$DSH_HOME/profiles/`, and BitFun materializes this one on first use — see
`src/crates/interfaces/acp/src/client/dsh_profile.rs`.

```
$DSH_HOME/profiles/bitfun-acp/
package.json dsh.profile.bundles: [] — an empty root, so the
harness's default tool rows cannot leak into a
preset that asked to be minimal
cordis.patch.yml generated from cordis.yml; the whole file as one
`insert:` layer over that empty root
lib/** the compiled bridge
presets/** the modes the session offers
node_modules/ only what the harness closure does NOT already carry
.bitfun-bridge.json build stamp; BitFun re-syncs when it changes
```

Everything else resolves out of the user's own dsh: on every launch the harness
symlinks its entire dependency closure into `$DSH_HOME/profiles/node_modules`,
which ordinary Node parent-directory lookup finds from inside the profile. That
is why this package vendors almost nothing and never installs a second copy of
the harness.

A remote workspace works the same way: BitFun packs the profile above into a tar
stream and extracts it into `$DSH_HOME/profiles/bitfun-acp/` on that host over
the session's own transport — no SFTP, so container connections are covered too
— and skips the upload when the stamp there already matches. `dsh` itself, the
models, and the key are the remote host's, exactly as they are locally. Steps 1
and 2 above therefore have to have been done on that host — including the Node
version, which the same probe checks — and BitFun says so if they were not.

## Reopening a conversation

The bridge advertises ACP's `loadSession` capability, so closing a session and
clicking it again reopens the one that was there instead of quietly starting a
blank one. Without it a client has only `session/new` to fall back on, and the
reopened conversation loses its history, its context, and the mode it ran under
— it comes back under the roster default and switchable again.

`session/load` resumes the stored session out of the harness's own persistence
(`$DSH_HOME/acp-sessions/<project>/<session-id>/`), replays its events to the
client as `session/update` notifications, and answers with the session's mode.
Three consequences worth knowing:

- **The stored mode wins over the roster default.** Which preset a session ran
under is read back from its own log, so a conversation started in `minimal`
reopens in `minimal` however the default has moved since.
- **A conversation that has started comes back locked.** The mode picker shrinks
to the one mode in force, because the composition is already baked into the
transcript — the same rule a live session follows after its first turn.
- **A session belongs to the directory it was created in.** Loading it against
another `cwd` is refused rather than answered with a session whose sandbox
boundary points somewhere else.

`node scripts/smoke.mjs --load <session-id>` drives exactly this path against a
real installation; the id is a directory name under the path above, and `--cwd`
has to name the workspace the session was created in.

## Development

```sh
npm ci # the pinned harness toolchain
npm run build # tsc -> lib/
node scripts/build-profile.mjs # lib/ + presets -> dist-profile/
node scripts/smoke.mjs # drive the profile over real ACP
npm test # vitest
```

`pnpm run prepare:dsh-profile` from the repository root does the install and
both build steps, and Tauri ships `dist-profile/` as a resource. A failure
there fails the desktop build: an app that silently ships no bridge is
indistinguishable from a working one until a user starts a DeepSeek session.
`BITFUN_SKIP_DSH_PROFILE=1` opts out on purpose.

Every `@deepseek-ai/*` dependency is pinned to the **`0.1.0-rc.6`** train — the
set npm's `next` dist-tag points at. Half of these packages still carry a
`latest` of `0.0.1-rc.1`, so an unpinned install mixes two trains and disagrees
with itself; that is worth knowing before loosening a range. They are
`devDependencies` because they are compile-time only: at runtime the profile
resolves them from the user's own dsh.

This package is **not** a pnpm workspace member and carries its own
`package-lock.json`, so installing the harness is the desktop bundle build's
cost rather than every contributor's `pnpm install`.

To work against a local [deepseek-harness](https://github.com/deepseek-ai)
checkout instead of the pinned versions, run `node scripts/link-local-dsh.mjs`
— it symlinks the checkout into `node_modules`, and the build then leaves that
tree alone.

## Layout

| Path | What it is |
| --- | --- |
| `src/app.ts` | the cordis plugin: config schema and ACP stdio transport |
| `src/bridge.ts` | session lifecycle, prompts, tool calls, permissions |
| `src/codec.ts` | harness events ⇄ ACP notifications |
| `src/tool-view.ts` | how a tool call is titled and rendered |
| `cordis.yml` | the composition; the profile patch is generated from it |
| `presets/` | the modes a session can switch between |
Loading