Skip to content

Releases: YichuAI/deepseek-harness-vscode

0.4.1 — on-screen model picker + E2E Web UI proof

Choose a tag to compare

@YichuAI YichuAI released this 22 Oct 15:08

0.4.1 — on-screen model picker + end-to-end Web UI proof

模型和思考深度进了编辑器,「打开网页」也终于有了一键端到端验证。

Added

  • Model & thinking depth selection, in VS Code. A model ▾ / thinking ▾ bar under the composer applies from the next turn; the ⚙ Settings panel has a Model group too, with "reset to Host default" and "fall back when unavailable". The choice goes through the Host's session/selectModel and lands as a durable model/selection event — reload the session and it is still there.
  • npm run verify-webui. End-to-end proof that "Open Web UI" works: it mints a cookie through the extension's own BrowserSessionAuth, then hits the live Host — 401 with no cookie (the Host gates /), 200 with the minted one. Run it before a release instead of trusting the unit test.

Verification

  • Protocol assertions 576 → 602: model-selection persistence, and that request/header can still override it.
  • tsc --noEmit clean; e2e 96; live Host 0.2.1-rc.2 42/42; version gate 52.
  • npm run verify-webui passes against 127.0.0.1:3081: 401 baseline, 200 with the minted cookie.

Unofficial community connector for DeepSeek Harness. Not affiliated with or endorsed by DeepSeek.

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 20 Oct 15:52

[0.4.0] — 2026-10-20

Tagline: the extension knew enough to act in three places and refused — and it re-sent the
whole conversation for every streamed token.

Tagline (ZH): 有三处,扩展知道的足够多却选择了拒绝——而且每个流式 token 都会把整段对话重发一遍。

Fixed

  • Open Web UI opens when the Host was started somewhere else. The command refused because
    the launch token is a per-process randomBytes kept in a WeakMap — true, and not the whole
    question: the cookie this extension mints is accepted at / (measured: 200 text/html). What
    no API can do is put a cookie into another process's browser, so the extension now runs a
    one-shot loopback redirect: host-only Set-Cookie, then 302 to the Host. Cookies are scoped
    by host and not by port (RFC 6265 §8.5), which is what makes a listener on another loopback
    port able to install one for the Host. Bound to loopback, one nonce path, one use, 20 s, and
    the value is never logged.
  • The same change removed a worse bug. In auto mode with a live Host, the command fell
    through to start-managed, which started a second Host and moved the window onto it. The
    feature worked; the session was the wrong one. A connected window now takes the cookie route
    before the launcher is ever considered.
  • A privileged approval is answerable from VS Code. ApprovalStore refused to grant any
    request whose reason mentioned danger-full-access or escalation, and the card told the user
    to use the Web UI — which serves the same waterfall with one "Allow once" button. Two clients of
    one Host, unequal, and a dead end whenever that browser could not open. The reason now marks the
    card (red, verbatim) instead of vetoing the click.
  • autoApproveApprovals still refuses those. The distinction is deliberate: a person may
    grant unrestricted access in one click; a setting may not grant it while nobody is looking. The
    veto moved from "can this be allowed" to "can this be allowed without a human", which is where
    it belonged.
  • Installing is a button, not a prompt. runtime-not-running carried the same install
    guidance as a genuinely missing command, so a running desktop app — which starts its Host on an
    OS-assigned port — produced a Get Harness button. Nothing at the configured port is not
    evidence of anything missing. The inline offer now survives only when a launch actually failed
    to find the command (managedAttempted: 'not-found').
  • Desktop detection looks in more places. Program Files, Program Files (x86) and
    ProgramW6432 are checked after the per-user install, because one missed path turns "open the
    app you already have" into "install this".

Added

  • ⚙ Settings & help, in the panel. Every knob that used to live only in the command palette
    and in settings.json, one row each with the sentence that explains it: host, port, session
    mode, discovery, auto-approve, system messages, launch command. Plus the actions — Find running
    Host, Install Host, Diagnostics, Show logs, Open Web UI — and a shortcut line. Values are read
    from the configuration on every push rather than mirrored, and written through one extension-side
    writer (Global target), so the existing config-change handler stays the only thing that reacts.
  • A starter prompt. The first-run card offers "Try: explain this workspace", which fills the
    composer and does not send — answering "what do I even type" without deciding it for the user.
  • view/snapshot-delta.ts — the pure half of the incremental transcript, so the arithmetic
    can be asserted without a VS Code host.

Changed

  • The transcript is sent once, then only what moved. A 40-turn conversation pushed 82.5 KB on
    every render, 93% of it the transcript, and streaming bumps renderVersion per delta. The
    provider now tracks uid → rev per webview and sends the changed items plus a removal list; the
    webview merges them into the copy it holds. Same push, same 40 turns: 3.6 KB, 4.4%. A first
    paint and a session change still send everything, because a delta against a transcript the other
    side does not have renders a one-item conversation.

Verification

  • Protocol assertions 533 → 576: the cookie bridge over a real socket (302, the cookie, the
    redirect target), approvals offered rather than refused and still refused by auto-approve, delta
    computation and client merge, the settings panel's rows and wiring, and the install offer's
    presence only where the evidence supports it.
  • tsc --noEmit clean; e2e 96; live Host 0.2.1-rc.2 41/41; version gate 52.
  • Measured, not assumed: GET / with a minted cookie answers 200 text/html; the bridge's
    redirect was followed end to end and landed on that 200; a 40-turn transcript's delta payload is
    3.6 KB against a full push of 82.5 KB.

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 14:17

[0.3.0] — 2026-10-19

Tagline: the support claim named a release two versions behind the runtime it was measured
on — so the claim is now the measurement.

Tagline (ZH): 支持声明指着的那个版本,比真正测过的运行时落后两个版本——于是声明改成了实测。

Changed

  • Supported upstream is @deepseek-ai/dsh@0.2.1-rc.2. That is what
    npm i -g @deepseek-ai/dsh installs today. The suite was run end to end against a Host
    started by that exact release, twice: once minting the cookie from
    $DSH_HOME/.credentials.yaml (41/41), once redeeming the --port 0 launch URL it
    printed (41/41).
  • Every surface that named 0.2.0-rc.2 now names 0.2.1-rc.2 — the README badge, ten
    architecture references, the protocol module's commentary, and the doc comments in
    client/events/launcher/wire that say which runtime a behaviour was observed on.
    Historical CHANGELOG entries keep their original numbers; they are history, not claims.

Verified, not assumed

  • The wire did not move. Slash endpoints, /api/remote.mux, the cookie gate, the
    13-method control surface, the 401 fingerprint and the --port 0 launch-URL shape are
    identical between 0.2.0-rc.2 and 0.2.1-rc.2. scripts/protocol-probe.ts was run
    against both a 0.2.1-alpha.2 Host and a 0.2.1-rc.2 Host; the reports are identical
    after normalising numbers, down to the 16/29 candidate routes that exist.
  • What upstream added, this extension does not use: session/title, session/conflict,
    settings/conflict, workspace/pinSession / unpinSession / rename / delete, and a
    session-format migration chain (v3 → v4). None is needed to read history, prompt, answer
    approvals or review changes, so none was adopted speculatively.

Verification

  • Protocol assertions 533, unchanged — a compatibility claim is not a protocol change, and
    inventing fakes for it would have made the number grow without the coverage growing.
  • End-to-end chains 96; version gate 52; tsc --noEmit clean.
  • Live Host 0.2.1-rc.2: 41/41 via local mint and 41/41 via launch URL.

v0.2.9

Choose a tag to compare

@github-actions github-actions released this 19 Oct 13:34

[0.2.9] — 2026-10-19

Tagline: the first thirty seconds were an icon and a grey line — and the live test only
passed on the machine that had already been running it.

Tagline (ZH): 头三十秒只有一个图标和一行灰字——而实机测试只在那台一直在跑它的机器上通过。

Added

  • A walkthrough, contributed so VS Code opens it on install. Four steps — where the panel
    is, what the status row is telling you, what the composer can do, and the difference between
    an approval and a review — each with a markdown page under media/walkthrough/. Step one
    completes on onView:deepseekHarness.sessionView, step two on
    onCommand:deepseekHarness.connect, so the ticks track something the user actually did
    rather than something we hoped they read.
  • A first-run card in place of the one grey line. Until a connection has succeeded once, the
    empty panel says what happens next and carries the button that starts it. After that it falls
    back to the single line it always was: the status row is the actionable part by then.
  • Deliberately no viewsWelcome contribution. Welcome content applies to empty tree views
    only (VS Code docs; microsoft/vscode#105832 settled it), and this panel is a "type": "webview"
    view. Contributing it would have added a second dead contribution point to a project that just
    spent a release making the first one work. The webview draws its own welcome instead.

Fixed

  • The live integration test no longer assumes it is running on Windows. Its workspace defaulted
    to e:\deepseek\workspace_test, and step 5 asserted that the Host already knew that directory.
    On Linux the path matched no workspace, so the suite failed at 41-assertion step 5 with nothing
    wrong with the extension. The workspace is now TEST_CWD when the caller sets one, otherwise a
    directory created under the system temp dir and removed on success. Step 5 passes against a Host
    that has never seen the path — which it also would not have done on a fresh Windows box, since
    the old default only worked because earlier runs had seeded it there.
  • The derived-registry assertion runs in the order the RPCs allow. There is no workspace/list
    — listWorkspaces() folds the registry out of session/list — so "this workspace is known" is
    now checked after a session exists, instead of demanding that some previous run had created one.
  • One definition of "same path". The comparison moved to harness/path-identity.ts, where
    envWithAugmentedPath was already spelling the same rule inline twice. Windows ignores case and
    treats both separators as separators; POSIX ignores neither and keeps \\ as a filename
    character. The platform argument means both halves are asserted on one machine, and those
    assertions run on Linux in CI — the platform where the old rule could not be tested.

Verification

  • Protocol assertions 523 → 533: five on the first-run card (it explains, its button posts
    connect, it is built once rather than per render, and a connected panel does not show it) and
    five on path identity across both platforms.
  • Version gate 43 → 52: nine checks that a walkthrough only references things that exist —
    media files present and non-empty, every command: link contributed, every completionEvents
    value resolvable, step and walkthrough ids unique, and the focused view container really one of
    ours.
  • tsc --noEmit clean; e2e 96; live Host 41/41 in a workspace created for the run;
    check-versions green.

v0.2.8

Choose a tag to compare

@github-actions github-actions released this 18 Oct 15:41

[0.2.8] — 2026-10-18

Tagline: the default mode was a valid credential for a Host that did not exist — and the
repair the diagnosis had been naming since it was written was never drawn.

Tagline (ZH): 默认模式是一份「给一个不存在的 Host 用的」有效凭据——而诊断从写出来那天就在点名的修复,从来没有被画出来。

Changed

  • deepseekHarness.sessionMode now defaults to auto, not credentials. The two are answers
    to different questions: credentials decides who you are and says nothing about whether
    anything is listening, so on a machine where nobody had run dsh web the first connect could
    only fail — and the status row's remedy was a paragraph ending in "or set sessionMode to …".
    auto mints from $DSH_HOME/.credentials.yaml when that file is readable and falls through to
    starting a Host when nothing answers.
  • auto is safe as a default because of the order it runs in. The mint happens first, inside
    client.connect, so an already-running Host is adopted with exactly the cookie earlier versions
    sent — byte for byte, same code path. Only a port with nothing on it reaches
    launcherForMode(). A working setup sees no change at all.
  • An explicit credentials on disk is still honoured and never rewritten. There are machines
    that always have a Host running and whose owners chose that mode on purpose; a release that
    silently migrated them would be overruling a decision it does not have the facts to make.
    parseSessionMode still accepts all four values, and only an unknown or absent one falls back
    to the new default.

Fixed

  • ConnectionHint.repair was a field nothing drew. Every hint filled it in — diagnostics
    for most failures, installHost on a remote with no dsh — and protocol-test.ts asserted
    that each one named a command, so the code and the tests agreed with each other while the panel
    rendered only hint.install and one line of hint.fix. The status row now draws a second
    button from it, and AppController.runHintRepair runs whichever command the diagnosis named.
    The webview still supplies no command id, so the button is not an injection surface.
  • The repair is now the one that can actually help. runtime-not-running used to answer every
    mode with connect — a Retry offering the action guaranteed not to help, since credentials
    cannot start a Host by design. It now names useAutoSession for credentials, the paste prompt
    for paste, and keeps connect only where the launcher was already tried.
  • alternatives() no longer offers the mode the reader is already in. An auto window was
    told to set sessionMode to auto, which is the same class of mistake v0.2.7 fixed in
    Open Web UI: advice that names the reader's current state reads as if nothing was read at all.

Added

  • DeepSeek Harness: Switch to Auto and Connect (deepseekHarness.useAutoSession) — the
    command the repair button runs. Persists the mode, then forces a reconnect, because the previous
    attempt leaves the client convinced it has already tried.

Verification

  • Protocol assertions 512 → 523: the default and its fallback, auto being both halves, an
    explicit credentials not being rewritten, every failure naming a repair that can help it, and
    every repair carrying a label.
  • tsc --noEmit clean; e2e 96 (3 of 5 chains); live Host 41/41; check-versions green.

Spotlight: the button was in the data structure the whole time. The failure classifier was
written to answer "what should the user do", and it has been producing a {command, label} pair
for every failure since — a shape that only makes sense if something is going to press it. Nothing
did. The remedy lived in prose, and the one release note that could have caught it described the
field as if it worked.

v0.2.7

Choose a tag to compare

@github-actions github-actions released this 15 Oct 14:13

[0.2.7] — 2026-10-15

Tagline: the offer from v0.2.6 was gated on a boolean that cannot tell paste from managed,
so the two routes that start nothing got each other's advice.

Tagline (ZH): v0.2.6 那个提议被一个分不开 paste 和 managed 的布尔值卡着,于是两个什么都不启动的模式,各拿到了对方的建议。

Fixed

  • Open Web UI no longer answers a paste window with credentials advice. The v0.2.6 offer
    was gated on auth.localMintEnabled(), which is false for managed and for paste. Both
    were therefore answered with the sentence written for the third mode: a window set to paste
    was told to switch to managed, and a window already in managed was told to switch to the
    mode it was already in. The reported symptom was exactly this — a wall of text instead of a
    browser.

  • …and it no longer turns away credentials windows that have no live cookie. That state fell
    into the same sentence, because the plan read "no cookie" as "nothing to be done" and returned
    explain-no-url. It is the opposite: the cookie is not what the web UI needs, and the Host the
    offer starts issues its own session. This is the case the offer was built for, and it now
    reaches it whether or not a cookie happens to be present.

  • A failed launch quotes the failure instead of blurring it. An empty launchUrl() covers two
    different things — a Host already answering on the port (adopted, so nothing here ever printed a
    URL for it) and a Host that could not be started at all — and they shared one message. The
    second now carries the fix line from the same classifier the status row uses
    (controller.diagnostics()), rather than a second guess that would eventually disagree with it.

Changed

  • planOpenWebUi takes the mode, not a bit about minting. OpenWebUiInput.mintedLocally is
    gone, mode: SessionMode replaces it, and the branch reads traitsOf(mode) — which is the
    single place that already knew what each mode permits. A boolean that is false for two modes
    with opposite remedies is how a command comes to look broken while every string in it is true.

  • The outcomes are now five, and none of them is a dead end. open; offer-managed (with
    reason: 'minted' | 'no-session', which changes the explanation and not the offer); and, new,
    ask-launch-url for paste — the mode whose contract is "hand me the line", so the command now
    runs the same prompt as Use Launch URL instead of describing it — and start-managed for
    managed, which starts the Host and opens the URL it printed. explain-remote survives for the
    one case a button cannot fix, and now says which machine to run dsh web on, per mode.
    explain-no-url and explain-minted-remote are removed with the reasoning that produced them.

  • OPEN_WEB_UI_TEXT moved to functions where the target matters. "Which Host" is the first
    thing a reader needs and the last thing a fixed sentence can say, so the three texts that name
    it take it as an argument.

Notes

  • The advertised assertion count moved with it — 504 → 512 across both READMEs, both
    ARCHITECTURE files and RELEASE.md. check-versions compares all of them, which is why the
    earlier versions' numbers are left standing inside their own entries.
  • The eight new assertions are mostly about the distinction that was missing: paste is asked
    rather than told to start something; managed starts rather than told to change modes;
    credentials with no live cookie is offered a Host rather than turned away; and a remote
    paste window is still asked, because the remote check disqualifies the remedies that start a
    process and not the one that asks.
  • The dependency chores stay deferred. RELEASE.md says to fold the @vscode/vsce bump into
    "the next version that edits a shipped file anyway", and this is one — but the pin that matters
    for publishing is already in release.yml (npx vsce@4.0.1-3), and a release that changes
    behaviour and one that changes the packager should not be the same release. Same for pinning
    @types/vscode to the engine floor: it needs its own verification run against 1.85, which is
    recorded in the dependency appendix.
  • Gates on this tree: tsc --noEmit clean, protocol-test 512, e2e 96 (3 of 5 chains),
    live integration 41/41, check-versions green.

v0.2.6

Choose a tag to compare

@github-actions github-actions released this 14 Oct 15:45

[0.2.6] — 2026-10-14

Tagline: a command that could only refuse now does the thing, because the Host that prints a
launch URL is one this extension is allowed to start.

Tagline (ZH): 一条只会拒绝的命令现在真去办事了——因为会打印启动 URL 的那个 Host,正是这个扩展有权启动的。

Fixed

  • DeepSeek Harness: Open Web UI no longer dead-ends. The command had exactly one ending: a
    sentence saying it could not open the web UI. The sentence was accurate — the Harness gates its
    web UI per process, / answers 401 without the token dsh web printed at startup, and that
    token is randomBytes kept in a WeakMap inside the Host process (browser-auth.ts), so the
    credentials route cannot compose one however much it holds the signing secret. What was wrong
    was treating that as the answer for all three routes. Managed launch and paste both do hold a
    URL — the launch URL is a reusable per-process secret, not a single-use grant — and the command
    now opens it. The third route is handled below.

  • An already-running Host is reported as adopted, not as a failed launch. Switching to
    managed does not guarantee a Host was started: if something is already answering on the
    configured port it is adopted, and nothing on this machine ever printed a URL for it. That used
    to be indistinguishable from the command doing nothing; it now says so, and says how to get a
    URL of your own.

Added

  • planOpenWebUi — the decision, as a pure function over plain values
    (src/app/session-mode.ts). Four outcomes: open a URL we already hold (the launched Host first,
    the adopted launch URL as fallback); offer to switch to managed; explain why that offer does
    not hold over Remote-SSH; explain that this route has no URL at all. It sits outside
    extension.ts because the failure that motivated it was a branching problem rather than a
    plumbing one, and because as an interface of plain values it can be asserted without a VS Code
    host — which is what the ten new assertions do.

  • There is now something to offer on the third route. From credentials the command asks
    whether to switch sessionMode to managed, persists that at ConfigurationTarget.Global,
    reconnects with force, and opens the URL the Host it started printed. It is persisted rather
    than remembered for one call, because that Host has to outlive the next connect: a reconnect
    still reading credentials would abandon it. Over Remote-SSH the offer is deliberately withheld
    — a Host started in a remote window listens on the remote's loopback while the browser this
    command opens is on the machine in front of you, so the button could not work — and the message
    names the machine to run dsh web on instead.

  • OPEN_WEB_UI_TEXT, kept beside the decision so "what does this command say" is answerable
    from one place. The previous wording is exactly what made the command look broken: it described
    the constraint perfectly and never mentioned that a Host started from here would lift it.

Notes

  • The advertised assertion count moved with it — 493 → 504 across both READMEs, both
    ARCHITECTURE files and RELEASE.md. check-versions compares all of them, which is why the
    earlier version's number is left standing inside its own entry.
  • Gates on this tree: tsc --noEmit clean, protocol-test 504, e2e 96 (3 of 5 chains),
    live integration 41/41, check-versions green (49 with --online).

v0.2.5

Choose a tag to compare

@github-actions github-actions released this 13 Oct 16:30

[0.2.5] — 2026-10-14

Tagline: a documentation release that had to be a version, because the README ships inside the
package.

Tagline (ZH): 一次必须算作版本的文档发布,因为 README 就在安装包里。

Added

  • STORY.md, with its STORY.zh-CN.md twin — how the extension got built, told straight:
    58 days and 24 tagged versions, of which eleven days actually contain code; the change that
    mattered most was deleting the Accept button; the won't-do list that is longer than the
    feature list; and the three bugs a model could not have caught (a connection that said
    "connected" and then went silent, a green test that was green by accident, and 331,709
    against 331,706). Both READMEs link to it. It sits on the .vscodeignore list next to
    RELEASE*.md: the story is repository content, not extension content, and keeping it out is
    what holds the package at fourteen files.

  • A 30-second trailer, at the top of both READMEs. docs/media/promo-preview.webp is an
    8-second silent loop cut from the film — 80 frames at 10 fps, under 900 KB — and it links to
    docs/media/promo-1080p.mp4 (7.8 MB); docs/media/promo-cover.jpg is the poster the
    development story opens with. Why an animated image rather than a <video> tag is worth
    writing down: GitHub's renderer strips <video> out of a README entirely (checked by
    rendering the tag through the Markdown API — it comes back as an empty paragraph),
    raw.githubusercontent.com serves .mp4 as application/octet-stream with
    X-Content-Type-Options: nosniff, and a bare .mp4 link renders as nothing but a link. An
    animated WebP is the only form that moves in a README, and it survives the Marketplace's
    image pipeline too.

  • The screenshots are redacted. docs/screenshots/01-shared-session.png and
    03-editor-sidebar.png were replaced with captures that do not carry the author's home
    directory or workspace path in the panel's status row.

Changed

  • Both READMEs were reworked for someone skimming. An "At a glance" table and a jump list at
    the top; the long paragraphs in Design and Verified against broken up or turned into
    tables; and What's new rebuilt around a table of what someone installing from the
    Marketplace is actually getting. No technical claim changed, and the mirror stays exact — the
    two READMEs are still one document in two languages.

  • The README now says what this release is. Its current-release section opens by naming it:
    the first build to reach the Marketplace since 0.2.0.

Notes

  • No runtime change. 0.2.5 is 0.2.4's behaviour byte for byte, in a package whose shipped
    documentation is different. It carries a version number because README.md and
    CHANGELOG.md are two of the fourteen files inside the VSIX: editing them edits the
    artifact, and a version number names the artifact.
  • Gates on this tree: tsc --noEmit clean, protocol-test 493, e2e 96 (3 of 5 chains),
    live integration 41/41, check-versions green.

v0.2.4

Choose a tag to compare

@github-actions github-actions released this 13 Oct 15:43

[0.2.4] — 2026-10-13

Tagline: three things a day of testing turned up: a card from someone else's conversation, a remote
with no dsh on it, and a launch URL that had lost its token.

Tagline (ZH): 测试一天翻出三件事:别人的对话的卡片、没装 dsh 的远程机,以及丢了 token 的启动 URL。

Added

  • DeepSeek Harness: Install Host — and it installs in the extension host, which on a
    remote connection is the remote. The advice-only version of this feature fails in a way
    nobody notices: the user installs dsh on the laptop in front of them, reconnects to a
    remote that still has none, and the panel says the same thing again. src/harness/install.ts
    has no notion of "remote" in it at all, and that is the design — it spawns into process's
    own context, which VS Code has already put on the correct side of the connection. It never
    elevates: a system-owned npm prefix fails with EACCES and the refusal plus npm's own
    output is what names the unwritable prefix.

  • src/harness/waterfall.ts — the rule that a forwarded waterfall belongs to the session
    its frame names, and to nothing else. One exported function, pure, asserted exhaustively
    including the field's actual empty shapes ('' from a template literal, a non-string, an
    absent key). See the attribution fix below for why it needs to exist at all.

  • waterfallSession, launchPlan and the remote install route are all asserted directly.
    Each is one small decision where the wrong answer still looks plausible, which is exactly the
    kind of code a test should not reach through three layers of UI.

Changed

  • A missing dsh has an automatic answer now, and it runs on the right machine.
    launchPlan(command) returns the ordered commands managed launch will try: the configured
    one, plus — only for the default command, and only after a not-found failure —
    npx -y @deepseek-ai/dsh@latest web --port 0. Under Remote-SSH that fetches the CLI on the
    remote and starts the Host there, with nothing installed globally and nothing needed on the
    remote's PATH. Two deliberate limits: a non-not-found failure (timeout, early exit,
    refused URL) says something about the Host rather than about which command started it, so it
    gets no second attempt; and a launchCommand the user set is never substituted, because
    silently running a different build is how a real setup problem gets hidden. The npx step
    also gets a longer timeout (300s vs 120s), since fetching the package is the slowest part and
    a cold cache would otherwise read as a broken install.

  • The install advice follows the connection, not the platform. installGuidanceFor took
    (platform, arch) and now takes a third input, vscode.env.remoteName. platform still
    describes the target correctly, which is precisely the bug: a Windows remote host was
    told to download a desktop .exe — something that machine cannot run, on a screen the user's
    mouse cannot reach. A remote window now always gets the npm route, with no download link.
    The not-found fix text and the "Harness is not running" advice are remote-aware for the same
    reason: "open the desktop app" is advice for a machine you are sitting at.

  • "Open Web UI" adds the token it has. The harness gates its web UI per process — a bare
    http://host:port/ answers 401 — so the command used to print a message telling the user
    to go and find the launch URL in a terminal. Two of the three session routes leave the
    extension holding one, so it opens it: the launcher's URL under managed, and
    auth.browserUrl() — the URL a pasted session was adopted from — under paste. It is a
    reusable per-process secret rather than a single-use grant, so it is still good when the
    command is used later. browserUrl() is gated on cookieHeader(), so it can never offer a
    URL for a Host the plugin is not authenticated against; a stale token opens a page saying
    "unauthorized", which reads like an extension bug rather than a restarted Host. Under
    credentials there was never a token (the cookie is minted from the signing secret without
    any exchange), and the message now says exactly that instead of pointing at a file the mode
    exists to avoid.

  • The panel's "open web UI" link is the same function as the command. It was a second,
    shorter message with its own copy of the reasoning; it now calls openWebUi.

Fixed

  • A question raised in another session was rendered as a card in the conversation on
    screen.
    $events is host-wide — every session's approvals and questions arrive down the
    one socket — and emitQuestionRequested read request.sessionId to find out whose they
    were. An approval's request does carry one. A question's does not: the event is
    delivered on an Agent-scoped waterfall and the wire request contains no session of any kind.
    So the read found nothing, and the code fell back to whatever session the panel happened to
    be showing
    . A browser tab's question therefore appeared in VS Code as an answerable card
    about a different run.

    The field that says which session it is had been in agentId the whole time, unread. That
    is not an inference from naming: the shipped Host registers the wire codec as
    lookups.register('agent', { wire: 'agentId', wireTypeSymbol: '@deepseek-ai/dsh-session/types#SessionId', resolve: (sessionId) => this.get(sessionId) })
    — the declared wire type of agentId is SessionId, validated before the frame is
    forwarded. Both waterfalls now take their session from it, with request.sessionId as the
    fallback for an approval on a host that omits it, and the fallback to the active session is
    gone
    . A frame that names no session produces no card and is logged; respondQuestion
    already throws for a frame the client never adopted, so there is no second path in either.
    Guessing is how this bug happened, so guessing is not the fallback.

  • The end-to-end fake Host can now put a waterfall from another session on the wire.
    emitApproval / emitQuestion take the frame's scope as an argument, with null meaning
    "omit agentId" — which is the only way to exercise the decline-rather-than-guess rule
    without hand-writing a frame. Chain 4 gained five assertions from it, and the live
    integration test gained one that reads the attributed session off a real
    user-questions/request, which is the only place that fact can be measured rather than
    reasoned about.

v0.2.2

Choose a tag to compare

@github-actions github-actions released this 12 Oct 14:51

[0.2.2] — 2026-10-12

Tagline: it no longer has to read a file full of your API keys, and autoSession: false finally means something.
Tagline (ZH): 它不必再读那个装满你 API key 的文件了,而 autoSession: false 也终于有了实际含义。

Added

  • Managed launch — opt-in, and the extension starts dsh web itself. Until now the only
    way to authenticate was to read $DSH_HOME/.credentials.yaml, which also holds the user's
    model API keys. That was the single largest reason not to install this plugin, and it was a
    trade made for a real reason: minting the cookie locally costs no paste, and the alternative
    — the sanctioned per-process launch-token exchange — needs a line that only exists on the
    console of a process nobody controls.

    deepseekHarness.sessionMode: "managed" removes the trade entirely. The extension runs
    deepseekHarness.launchCommand (dsh web --port 0 by default), captures the launch URL it
    prints, and redeems that token. Owning the process is what makes this possible: its stdout
    is ours, so there is nothing to paste and no credential file to read. The Host it started is
    stopped on deactivate(), which is why deactivate is now async.

    Discovery always runs first, so an already-running Host — the desktop app's, most likely —
    is adopted rather than duplicated.

  • DeepSeek Harness: Use Launch URL. The paste route, as a command: paste the
    dsh web: http://… line and the extension trades the token for a session. This is what
    autoSession: false should always have been.

  • Install guidance, and it appears only on a failed connection. classifyConnectFailure
    gained five inputs (platform, arch, credentialsUnavailable, desktopInstalled,
    managedAttempted) and now returns at most one install route. Windows and macOS get a
    link to the download page; Linux gets the build commands to copy, because upstream ships no
    desktop build for it. The panel renders one button, only while the connection is in the
    error state, and the button carries no payload — which route to take is decided
    extension-side from the same hint that produced it, so the webview cannot hand it a URL.

    The button is withheld when the desktop app is already on disk (the fix then says to open
    it), when a managed launch ran and exited (the fix says the port is taken), and when no
    platform is known. Telling somebody to install something they already have is worse than
    saying nothing.

  • Screenshots in the README. It had four badges and zero images, which is a strange way to
    describe a visual product.

  • scripts/launch-probe.ts. Runs the real launcher against a real dsh web and reports
    what a unit test cannot: whether a launch token can be redeemed twice, whether the Host
    outlives the child we spawned, and whether the port is quiet after stop(). Exit code is 0
    only when all three hold.

Changed

  • deepseekHarness.autoSession (boolean) becomes deepseekHarness.sessionMode (enum).
    The old switch could only turn authentication off — false meant "do not authenticate",
    because the command that would have offered the other route had been unregistered. A switch
    that can only disable things is not a setting, it is a trap, and it made the extension look
    like it could not work without reading the credential file.

    The three modes are credentials (mint locally — the previous behaviour, and still the
    default), managed (start the Host), and paste (you supply the URL). Upgrading changes
    nothing for anyone who does not change it; credentials is deliberately the default so that
    a silent change of behaviour is not what this release delivers.

    traitsOf() in app/session-mode.ts is the one place that answers "what does this mode
    permit", so managed cannot accidentally fall back to reading the credential file — which
    would make choosing it buy nothing.

  • deepseekHarness.launchCommand — new setting, default dsh web --port 0. --port 0
    because managed launch reads the real port off the launch URL, so pinning one only adds a
    way to fail. Run through a shell, so wrappers and npx resolve the way they would in a
    terminal.

  • DeepSeek Harness: Open Web UI can now do what it says. It used to be a message
    explaining why it could not: the harness authenticates its web UI per process, and a Host
    somebody else started never shows us its URL. A Host we started is the case where the
    extension has the URL, so under managed the command opens it.

  • A failed connect now names the route you are not on. alternatives() switches on the
    active mode, because advice that tells you to do the thing you are already doing is worse
    than no advice. The refusing message in authRequired() also branches on whether local
    minting is even enabled — in managed/paste, the absence of a cookie is the design, not a
    misconfiguration, and pointing at a file the user asked us not to open would be wrong.

  • The README leads with the product and trails with the essay. The design philosophy was
    the fourth section, above the fold, before a reader had been told what the thing does. The
    first screen is now a value proposition, a screenshot of the same session in two windows,
    and seven bullets; the "DSH owns the write" essay sits after Development. The credential
    discussion moved into "How the session is obtained" and is now about three modes rather than
    one unavoidable privilege.

  • Marketplace category Other → AI, and the keyword list expanded to the terms people
    actually search (deepseek-harness, dsh, ai-agent, coding-agent, …).

Fixed

  • The Marketplace badges in the README pointed at a namespace shields.io never served.
    img.shields.io/vs-marketplace/v/… had been sitting there since the version gate landed, and
    it draws 404 badge not found — on GitHub, and on the Marketplace listing too, which renders
    this README verbatim. Both badges now read badgen.net, which still serves real Marketplace
    data. (The version half is worth watching for exactly that reason: it reads v0.2.0, which is
    the truth the Marketplace has been sitting on.)

    There was no "correct shields URL" to switch to. shields' namespace is
    visual-studio-marketplace, and that one now answers with an SVG labelled "retired badge"
    — the integration was withdrawn rather than renamed. The gate that was meant to catch this
    compared substrings, and a dead badge string-matches perfectly. It now asserts the host,
    rejects any surviving img.shields.io/vs-marketplace URL, and under --online fetches each
    badge and fails when the SVG announces not found or retired.

  • A successful stop could be reported as signalled. taskkill loses a race with a child
    that exits on its own, and then reports "no process found" — which reads like a failure while
    the Host is already gone, i.e. exactly the desired outcome. stop() now checks whether the
    child is still alive after a failed attempt, and only reports an unconfirmed stop when it
    truly is.

  • envWithAugmentedPath used the host PATH delimiter. It takes a platform argument
    and then ignored it for the separator, so a Windows host building a macOS PATH produced a
    single unusable entry. Found by an assertion that passed on Linux and could not pass here —
    which is the argument for the assertion. pathAugmentations and desktopInstallCandidates
    now join with the matching win32/posix implementation for the same reason.

  • adoptLaunchUrl no longer re-redeems a launch URL it has already redeemed. The
    comparison is against the URL, not the authority: a new Host can bind a port a dead one
    just released, and a persisted cookie for that authority belongs to the old Host's secret —
    so matching on authority alone would turn a port reuse into an unrecoverable
    "authentication failed".

  • The README called the launch token a "one-shot token". It is not. Upstream's
    authorizeIndex compares the query token against the process secret with timingSafeEqual
    on every request to /, so the same URL can be redeemed repeatedly — confirmed both in
    dsh-client-connection/lib/index.js and by redeeming it twice in launch-probe.ts.

  • A factual claim the new feature made false is gone: "Never starts a process for you. The
    extension does not spawn dsh web" was true and is now only true by default.

Notes

  • The measured behaviour behind managed launch, all from launch-probe.ts on Windows
    x64 against @deepseek-ai/dsh@0.2.0-rc.2: Host up in ~13 s from a cold npx (including
    the download), launch URL on stdout with a 43-character token, exchange 303, bare GET /
    401, second exchange 303, child alive and holding the port across 3 s of sampling, and
    the port refusing connections for 4 s after stop().

  • dsh has a background-boot concept — --dsh-boot-bg appears in the HTML it serves — so
    "does the child we spawned actually own the port?" was worth answering rather than assuming.
    It does. If it did not, killing the child would leave a Host running that the user could not
    account for, which is the specific mess this extension exists to avoid creating.

  • Prototype note for anyone reading the diff: this machine's globally-installed dsh was
    0.1.0-rc.6, which predates the {version, refs, records} credential-file schema the
    desktop app writes — it fails with the value for "version" in … must be a string, because
    it parses that YAML as a flat map of credential-ref → string. That is an upstream version
    skew, not a defect here, and it is exactly the failure the new exited branch is designed to
    make legible: the panel sends you to the log, and the log carries the child's own stack
    trace.

  • Also published to Open VSX, starting with 0.2.1. Cursor, VSCodium and Windsurf read
    Open VSX rather than the Microsoft Marketplace, and an extension present in only...

Read more