omdsh-plugins/omdsh-chatmode0

@omdsh-plugins/omdsh-chatmode

Chat mode for the DeepSeek Harness web GUI: a Chat/Work switch above the conversation, a workspace-free session-intent surface, and the managed Chat workspace its conversations are kept in

包名
@omdsh-plugins/omdsh-chatmode
版本
0.1.2
许可证
MIT
最近更新
2026年8月17日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:omdsh-plugins/omdsh-chatmode

omdsh-chatmode

English | 中文

Chat mode for the DeepSeek Harness web GUI: start talking without choosing a project directory, and keep those conversations together under Chat in the sidebar.

The harness is a coding agent, and its New Session screen asks for a workspace before it will take a message — reasonably, because a coding agent works somewhere. But not every question is work. This plugin adds the other posture beside it: a Chat / Work switch above the conversation, a managed workspace that chat conversations live in, and an agent composition with no tools at all behind them.

What it adds

SurfaceWhere it comes from
The Chat and Work segments in the mode switchTwo registrations in sessionModes, the segment registry omdsh-base publishes
A line stating what a chat session will not do, above the composerAn entry in conversation.input.dock
A preset chip on the new-session screen offering what the mode can useAn entry in conversation.hero.agentPreset, shadowing the shipped chip
The Chat workspace group in the sidebarA real directory (/sessions/chat) this plugin registers and keeps titled Chat — the sidebar renders it like any other workspace
That group staying the first one, however many projects you openThe host half puts it first at boot; the browser half holds it there — see Chat stays on top
The Chat Mode agent presetagent-presets/chat/, installed once into /.agent-presets/
The green and blue dots on those conversations in the sidebarThe tone and owns these two segments carry; the dots themselves are painted by omdsh-base for whatever modes are registered

Nothing in the harness is modified. This package is a profile bundle: dsh plugin installs it into a profile and appends it to that profile's layer stack, so its one loader row is composed over the shipped tree. Removing the plugin removes the row, both surfaces, both segments, and every seam it used.

The mode is derived, not stored

A session is a chat exactly when it is accounted under the managed Chat workspace. The switch reads that and reports it, which is why opening a conversation from the sidebar moves the switch: it is describing where the current session lives, not deciding it. A stored flag would eventually disagree with the screen.

Pressing a segment is therefore a navigation rather than a state write:

  • Chat — reopen the chat you left, or start one in the Chat workspace.
  • Work — reopen the working conversation you left, else start one in the first project workspace, else fall back to the shipped "Choose workspace" screen.

Because the mode is derived, every navigation re-asserts it — including one that lands in the same mode. Opening a second working conversation looks like nothing happening from a "work → work" reading, but it is the user saying which conversation they want to see, and a contributed posture may be holding the column over it. Re-deriving on the navigation is what hands that column back.

With one exception: a conversation another mode owns is that mode's to report. Taking the column on its navigation would only have it taken back a moment later, once the owner answers the same event — a flicker of the wrong column, and whatever that mode was showing torn down and rebuilt for nothing. A press is never that: pressing Work on a Code conversation is asking to read it in the web view, which is a change of column with no change of conversation, and the only thing that could say so is the press itself.

New Session belongs to the mode it was pressed in

New Session means "another conversation like this one", and what that is differs by posture. So the request is offered to the segment holding the column before the frame handles it:

modes.register({
  id: 'code',
  // …
  // Return true to say "I started it"; the column stays where it is.
  newSession: (workspaceId) => startAnotherTerminal(workspaceId),
})

A mode that answers keeps the column, which is what makes New Session stay in the mode the user is in rather than dropping them back into Work. A mode that declines — or has no answer, which is Chat and Work — gets the frame's own New Session, and gives the column up with it. The request is never offered during a press: entering a mode with nothing to return to starts a session too, and that runs while the mode being left is still the active one.

Routing that request is omdsh-base's job, not this package's. What is this package's is hearing about the case with nothing to derive from: New Session reuses the workspace's existing blank conversation, so pressing it while already on that conversation opens the id that is already open — no selection moves, no list changes, no store publishes. The registry announces the passthrough (onNewSession) and this package re-derives on it. Without that, a person in a contributed posture presses New Session and the screen does not change.

A blank chat session is put on the chat preset through the agent-preset RPC — once per session, and only while it is still blank, because the host refuses the swap after a turn has run and because a user who deliberately picks another preset for a chat should keep it.

The switch is not this package's

Chat and Work are two segments among however many the profile composed. The control they sit in, the registry they register through, and the coloured dots their conversations get in the sidebar all belong to omdsh-base — including the rule that exactly one segment is active, which is what lets pressing Code clear these two.

This package reaches that switch the same way any other mode plugin does:

// Never in a top-level `inject` — see rule 9 of the conventions. Without the
// mode system there is no switch for a segment to appear in, and everything
// else on this page keeps working.
ctx.inject(['sessionModes'], (mctx) => {
  const modes = mctx.get('sessionModes') as SessionModes | undefined
  if (modes === undefined) return
  mctx.effect(() => modes.register({ id: 'chat', order: 0, tone: CHAT_TONE, /* … */ }))
})

Chat and Work stay derived from where the current session lives, so opening a conversation from the sidebar still moves the switch, and pressing either takes the column back from whatever had it. owns is what each answers for itself: Chat claims the conversations accounted under the managed workspace, and Work is marked fallback — "a conversation in a project" is what one is when nothing more specific is true.

Neither companion plugin is required, and neither appears in a top-level inject. Without omdsh-base there is no switch for a segment to appear in, so the two pills are simply not there — the dock note, the preset chip, and the derived mode itself go on working, because they read where the current session lives rather than the switch. Without omdsh-shortcuts the segments' tooltips name no key: this package binds nothing and registers no command, it only appends the chord that plugin reports for mode.chat and mode.work when one reaches this surface. Both services are reached from restricted fibers started inside apply — which is what keeps a missing companion from leaving a loader entry pending and failing the page's boot sweep, a dead UI rather than a missing segment.

The preset chip belongs to the mode

The deployment's preset roster is one list, the same in every workspace. A picker built straight on it offers Chat Mode while the user is in a project, and offers four coding compositions while the user is in the Chat workspace. Neither is a choice worth making: Chat mode is the tool-free composition, and a project session put on it could not touch the project.

So this plugin takes the chip and filters the roster by mode:

  • Work — every preset except chat, as a menu, which is the shipped chip minus the one row a project session cannot use.
  • Chat — a plain label reading the preset's name. The mode already decided; a menu with one row is a control that does nothing.

It also re-derives what it shows whenever the current session changes, so moving between workspaces reports that session's composition rather than the one you left.

Taking the seat is done with the slot system's own rule rather than around it. conversation.hero.agentPreset is a single cell, and a single cell goes to the lowest priority, so this package registers at priority: -1 over ui-agent-preset's default 0. Nothing is unregistered: withdrawing this plugin's row hands the seat straight back to the shipped chip. (hero-seat-shadow.client.spec.ts drives that against the real registry, including the collision a same-priority registration would throw.)

Preset names still come from the harness. It ships its four presets with Chinese metadata on disk and localizes them in the browser, out of the settings.agentPreset dictionary ui-agent-preset registers — so this chip reads that dictionary at call time instead of copying it. A locally authored preset is never translated: its file is its copy. Where the dictionary is absent (a composition without ui-agent-preset), the lookup echoes its key back and the chip falls back to file metadata rather than showing a locale key.

The chip is the only part of that screen this package owns. The blank-chat hero around it is still the shipped one, and the harness publishes no seam for the rest of it; upstream it would be two small additions — a chain around the hero's intent surface, and a way for whoever draws it to own the composer's call to action.

The chat agent

agent-presets/chat/agent.cordis.yml is defined by what it does not mount: no bash/pwsh, no filesystem read/write/search, no editor, no skills, no subagents, no workflows, no plan mode, no todo. The web surface disables every agent-plane row in its own host composition, so absence here is real absence — a chat session composes no tool catalog and cannot touch the host. It keeps a persona, context compaction, and ask_user.

That is what makes running in a directory the user never chose safe: there is nothing in the session that can reach it.

That persona is the complete system prompt: no tool guidance, no runtime context snapshot, and an explicit instruction to say so rather than pretend when a request needs a repository. Nothing else this package ships reaches a model request — it assembles no provider request of its own, and the preset it installs shortens one, because an agent with no tools sends no tool catalog.

Chat stays on top

The Chat group is the first one in the sidebar, and stays first for as long as the plugin is installed. It is the group with no project behind it — the one a person reaches for when the question is not about a repository — so having it drift down the list as projects accumulate would put the least specific thing behind the most specific ones.

It takes both halves, because the fact is undone in two different ways.

The host half asserts it at boot: workspaceRegistry.create is idempotent per directory, so the pin is a separate insertBefore beside the title re-assertion. That is what the first render is owed, before any browser has attached.

Opening a project is the other way, and it happens after boot: create PREPENDS a new workspace, so each directory added pushes Chat down one place while the app is running. The registry publishes no event to hook, so the browser half reconciles instead — it is already told about every workspace the sidebar draws, because it draws them from that list, and the sidebar's group order is the registry order. When Chat is not the first row it asks the host to move it back, through the same insertBefore the drag-to-reorder gesture calls. The correction is therefore durable and shared: it is written to the registry, not painted over one tab. See pin.ts.

Two things follow from that, and both are deliberate:

  • It outranks a drag. Dragging the Chat group down the sidebar puts it back. That is the same trade the title makes — the host half re-asserts Chat over a rename — and for the same reason: both are facts this plugin manages, not the user's arrangement of their own projects.
  • omdsh-sidechat is pinned by the same act. A standalone side conversation is accounted under this same managed workspace (it finds it by the title, as everything here does), so its conversations sit in the group this keeps on top. That package needs to know nothing about any of it.

Install

dsh plugin --profile web add @omdsh-plugins/omdsh-chatmode
dsh plugin --profile web add @omdsh-plugins/omdsh-base       # the switch its segments appear in

That second line is not optional decoration: without omdsh-base there are no Chat and Work pills at all. Everything else this package does goes on working — "The switch is not this package's" says exactly what is and is not there in that state, and why it is inert rather than fatal.

dsh plugin forwards to pnpm in $DSH_HOME/profiles/web, then reconciles that profile's dsh.profile.bundles against what is installed: this package declares dsh.bundle, so it joins the layer stack automatically. On the next boot the host half creates /sessions/chat, registers it as the Chat workspace, and installs the chat preset.

Either way, lib/ must exist before dsh web runs. The loader imports lib/index.js directly, and a missing one is not a degraded UI — the whole profile tree fails to load:

dsh: plugin tree failed to load: ... failed to import loader entry chatmode
(@omdsh-plugins/omdsh-chatmode): Cannot find module '.../lib/index.js'

From a local path or link:

pnpm does not run a linked or path-installed package's prepare, so nothing builds it for you. Build first:

pnpm install
pnpm run build                                          # required — nothing else runs it

dsh plugin --profile web add 

dsh web

Rebuild after every source change, for the same reason.

From GitHub

A git dependency builds itself through prepare, which this package supports: its committed @deepseek-ai/* devDependencies name the published harness release, so a bare clone can install and compile.

dsh plugin --profile web add github:omdsh-plugins/omdsh-chatmode#

The first attempt fails: pnpm ≥10 refuses to run a git dependency's prepare until you allow the package by name. Add it to the profile's own pnpm-workspace.yaml ($DSH_HOME/profiles/web/pnpm-workspace.yaml, which dsh writes with packages: - ., nodeLinker: hoisted, autoInstallPeers: false):

allowBuilds:
  '@omdsh-plugins/omdsh-chatmode': true

then re-run the add, and dsh web. That entry authorizes this package to run install-time code on your machine — here, tsc and tsdown — so pin a commit rather than tracking a branch, and read the diff before moving the pin.

Remove it the same way:

dsh plugin --profile web remove @omdsh-plugins/omdsh-chatmode

The chat directory, its session logs, and the preset are left on disk — removing a plugin is not a request to delete conversations.

Commands

pnpm install
pnpm run build       # tsc emits lib/types,