Repository navigation
Releases: YichuAI/deepseek-harness-vscode
Release list
0.4.1 — on-screen model picker + E2E Web UI proof
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'ssession/selectModeland lands as a durablemodel/selectionevent — 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 ownBrowserSessionAuth, then hits the live Host —401with no cookie (the Host gates/),200with 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/headercan still override it. tsc --noEmitclean; e2e 96; live Host0.2.1-rc.242/42; version gate 52.npm run verify-webuipasses against127.0.0.1:3081:401baseline,200with the minted cookie.
Unofficial community connector for DeepSeek Harness. Not affiliated with or endorsed by DeepSeek.
v0.4.0
[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 UIopens when the Host was started somewhere else. The command refused because
the launch token is a per-processrandomByteskept 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-onlySet-Cookie, then302to 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
automode with a live Host, the command fell
through tostart-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.
ApprovalStorerefused to grant any
request whose reason mentioneddanger-full-accessorescalation, 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. autoApproveApprovalsstill 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-runningcarried the same install
guidance as a genuinely missing command, so a running desktop app — which starts its Host on an
OS-assigned port — produced aGet Harnessbutton. 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
ProgramW6432are 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 insettings.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 bumpsrenderVersionper delta. The
provider now tracksuid → revper 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 --noEmitclean; e2e 96; live Host0.2.1-rc.241/41; version gate 52.- Measured, not assumed:
GET /with a minted cookie answers200 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
[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/dshinstalls 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 0launch URL it
printed (41/41). - Every surface that named
0.2.0-rc.2now names0.2.1-rc.2— the README badge, ten
architecture references, the protocol module's commentary, and the doc comments in
client/events/launcher/wirethat 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, the401fingerprint and the--port 0launch-URL shape are
identical between0.2.0-rc.2and0.2.1-rc.2.scripts/protocol-probe.tswas run
against both a0.2.1-alpha.2Host and a0.2.1-rc.2Host; 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 --noEmitclean. - Live Host
0.2.1-rc.2: 41/41 via local mint and 41/41 via launch URL.
v0.2.9
[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 undermedia/walkthrough/. Step one
completes ononView: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
viewsWelcomecontribution. 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
toe:\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 nowTEST_CWDwhen 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 ofsession/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
envWithAugmentedPathwas 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. Theplatformargument 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, everycommand:link contributed, everycompletionEvents
value resolvable, step and walkthrough ids unique, and the focused view container really one of
ours. tsc --noEmitclean; e2e 96; live Host 41/41 in a workspace created for the run;
check-versionsgreen.
v0.2.8
[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.sessionModenow defaults toauto, notcredentials. The two are answers
to different questions:credentialsdecides who you are and says nothing about whether
anything is listening, so on a machine where nobody had rundsh webthe first connect could
only fail — and the status row's remedy was a paragraph ending in "or setsessionModeto …".
automints from$DSH_HOME/.credentials.yamlwhen that file is readable and falls through to
starting a Host when nothing answers.autois 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
credentialson 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.
parseSessionModestill accepts all four values, and only an unknown or absent one falls back
to the new default.
Fixed
ConnectionHint.repairwas a field nothing drew. Every hint filled it in —diagnostics
for most failures,installHoston a remote with nodsh— andprotocol-test.tsasserted
that each one named a command, so the code and the tests agreed with each other while the panel
rendered onlyhint.installand one line ofhint.fix. The status row now draws a second
button from it, andAppController.runHintRepairruns 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-runningused to answer every
mode withconnect— a Retry offering the action guaranteed not to help, sincecredentials
cannot start a Host by design. It now namesuseAutoSessionforcredentials, the paste prompt
forpaste, and keepsconnectonly where the launcher was already tried. alternatives()no longer offers the mode the reader is already in. Anautowindow was
told to setsessionModetoauto, 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,
autobeing both halves, an
explicitcredentialsnot being rewritten, every failure naming a repair that can help it, and
every repair carrying a label. tsc --noEmitclean; e2e 96 (3 of 5 chains); live Host 41/41;check-versionsgreen.
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
[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 UIno longer answers apastewindow withcredentialsadvice. The v0.2.6 offer
was gated onauth.localMintEnabled(), which isfalseformanagedand forpaste. Both
were therefore answered with the sentence written for the third mode: a window set topaste
was told to switch tomanaged, and a window already inmanagedwas 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
credentialswindows 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 thefixline from the same classifier the status row uses
(controller.diagnostics()), rather than a second guess that would eventually disagree with it.
Changed
-
planOpenWebUitakes the mode, not a bit about minting.OpenWebUiInput.mintedLocallyis
gone,mode: SessionModereplaces it, and the branch readstraitsOf(mode)— which is the
single place that already knew what each mode permits. A boolean that isfalsefor 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-urlforpaste— 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 — andstart-managedfor
managed, which starts the Host and opens the URL it printed.explain-remotesurvives for the
one case a button cannot fix, and now says which machine to rundsh webon, per mode.
explain-no-urlandexplain-minted-remoteare removed with the reasoning that produced them. -
OPEN_WEB_UI_TEXTmoved 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
ARCHITECTUREfiles andRELEASE.md.check-versionscompares 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:
pasteis asked
rather than told to start something;managedstarts rather than told to change modes;
credentialswith no live cookie is offered a Host rather than turned away; and a remote
pastewindow 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.mdsays to fold the@vscode/vscebump into
"the next version that edits a shipped file anyway", and this is one — but the pin that matters
for publishing is already inrelease.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/vscodeto the engine floor: it needs its own verification run against 1.85, which is
recorded in the dependency appendix. - Gates on this tree:
tsc --noEmitclean, protocol-test 512, e2e 96 (3 of 5 chains),
live integration 41/41,check-versionsgreen.
v0.2.6
[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 UIno 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,/answers401without the tokendsh webprinted at startup, and that
token israndomByteskept in aWeakMapinside the Host process (browser-auth.ts), so the
credentialsroute 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
manageddoes 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 tomanaged; explain why that offer does
not hold over Remote-SSH; explain that this route has no URL at all. It sits outside
extension.tsbecause 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
credentialsthe command asks
whether to switchsessionModetomanaged, persists that atConfigurationTarget.Global,
reconnects withforce, 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 readingcredentialswould 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 rundsh webon 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
ARCHITECTUREfiles andRELEASE.md.check-versionscompares all of them, which is why the
earlier version's number is left standing inside its own entry. - Gates on this tree:
tsc --noEmitclean, protocol-test 504, e2e 96 (3 of 5 chains),
live integration 41/41,check-versionsgreen (49 with--online).
v0.2.5
[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 itsSTORY.zh-CN.mdtwin — 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.vscodeignorelist 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.webpis 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.jpgis 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.comserves.mp4asapplication/octet-streamwith
X-Content-Type-Options: nosniff, and a bare.mp4link 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.pngand
03-editor-sidebar.pngwere 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 becauseREADME.mdand
CHANGELOG.mdare 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 --noEmitclean, protocol-test 493, e2e 96 (3 of 5 chains),
live integration 41/41,check-versionsgreen.
v0.2.4
[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 installsdshon 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 intoprocess's
own context, which VS Code has already put on the correct side of the connection. It never
elevates: a system-ownednpmprefix fails withEACCESand 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,launchPlanand 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
dshhas 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 anot-foundfailure —
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'sPATH. Two deliberate limits: a non-not-foundfailure (timeout, early exit,
refused URL) says something about the Host rather than about which command started it, so it
gets no second attempt; and alaunchCommandthe user set is never substituted, because
silently running a different build is how a real setup problem gets hidden. Thenpxstep
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.
installGuidanceFortook
(platform, arch)and now takes a third input,vscode.env.remoteName.platformstill
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/answers401— 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 undermanaged, and
auth.browserUrl()— the URL a pasted session was adopted from — underpaste. 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 oncookieHeader(), 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
credentialsthere 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 callsopenWebUi.
Fixed
-
A question raised in another session was rendered as a card in the conversation on
screen.$eventsis host-wide — every session's approvals and questions arrive down the
one socket — andemitQuestionRequestedreadrequest.sessionIdto find out whose they
were. An approval'srequestdoes 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
agentIdthe 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 ofagentIdisSessionId, validated before the frame is
forwarded. Both waterfalls now take their session from it, withrequest.sessionIdas 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/emitQuestiontake the frame's scope as an argument, withnullmeaning
"omitagentId" — 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
[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 webitself. 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 0by 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 ondeactivate(), which is whydeactivateis 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. Thepasteroute, as a command: paste the
dsh web: http://…line and the extension trades the token for a session. This is what
autoSession: falseshould 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 oneinstallroute. 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
errorstate, 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 realdsh weband 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 afterstop(). Exit code is 0
only when all three hold.
Changed
-
deepseekHarness.autoSession(boolean) becomesdeepseekHarness.sessionMode(enum).
The old switch could only turn authentication off —falsemeant "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), andpaste(you supply the URL). Upgrading changes
nothing for anyone who does not change it;credentialsis deliberately the default so that
a silent change of behaviour is not what this release delivers.traitsOf()inapp/session-mode.tsis the one place that answers "what does this mode
permit", somanagedcannot accidentally fall back to reading the credential file — which
would make choosing it buy nothing. -
deepseekHarness.launchCommand— new setting, defaultdsh 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 andnpxresolve the way they would in a
terminal. -
DeepSeek Harness: Open Web UIcan 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 undermanagedthe 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 inauthRequired()also branches on whether local
minting is even enabled — inmanaged/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 draws404 badge not found— on GitHub, and on the Marketplace listing too, which renders
this README verbatim. Both badges now readbadgen.net, which still serves real Marketplace
data. (The version half is worth watching for exactly that reason: it readsv0.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 survivingimg.shields.io/vs-marketplaceURL, and under--onlinefetches each
badge and fails when the SVG announcesnot foundorretired. -
A successful stop could be reported as
signalled.taskkillloses 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. -
envWithAugmentedPathused the hostPATHdelimiter. It takes aplatformargument
and then ignored it for the separator, so a Windows host building a macOSPATHproduced a
single unusable entry. Found by an assertion that passed on Linux and could not pass here —
which is the argument for the assertion.pathAugmentationsanddesktopInstallCandidates
now join with the matchingwin32/posiximplementation for the same reason. -
adoptLaunchUrlno 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
authorizeIndexcompares the query token against the process secret withtimingSafeEqual
on every request to/, so the same URL can be redeemed repeatedly — confirmed both in
dsh-client-connection/lib/index.jsand by redeeming it twice inlaunch-probe.ts. -
A factual claim the new feature made false is gone: "Never starts a process for you. The
extension does not spawndsh web" was true and is now only true by default.
Notes
-
The measured behaviour behind managed launch, all from
launch-probe.tson Windows
x64 against@deepseek-ai/dsh@0.2.0-rc.2: Host up in ~13 s from a coldnpx(including
the download), launch URL on stdout with a 43-character token, exchange303, bareGET /
401, second exchange303, child alive and holding the port across 3 s of sampling, and
the port refusing connections for 4 s afterstop(). -
dshhas a background-boot concept —--dsh-boot-bgappears 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
dshwas
0.1.0-rc.6, which predates the{version, refs, records}credential-file schema the
desktop app writes — it fails withthe 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 newexitedbranch 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...