DeepSeek Harness plugin

omdsh-codemode

Code mode for the DeepSeek Harness web GUI: a third segment beside Chat and Work whose column is the harness's own terminal, running in the conversation's workspace

Jump to install

Source facts

Repository
omdsh-plugins/omdsh-codemode
Latest update
Aug 20, 2026
Category
Development & Runtime
GitHub stars
0
Format
plugin
Catalog evidence
Upstream dsh.bundle evidence
Evidence path
package.json#dsh.bundle
Checked against
0.1.0-rc.8
Upstream check date
2026-08-20

This evidence comes from the upstream catalog. This site has not installed, run, or security-reviewed the plugin.

Install

Start with a prompt that asks an agent to review the GitHub repository and source. Switch to the command if you want to install it yourself.

Copy this prompt into DSH, Codex, or another agent and ask it to review the GitHub repository and source first.

Do not install or run any commands yet. Read this plugin's GitHub repository, README, and relevant source code. Then answer the questions below clearly and directly so I can decide whether it fits my needs:

1. What is this plugin, and what problem does it solve?
2. Who is it for, and what are its typical use cases?
3. How is it used after installation? Include one minimal example.
4. What known limitations or privacy, security, compatibility, or maintenance risks does it have?
5. Give a clear recommendation: recommend, conditionally recommend, or do not recommend, with reasons.

Distinguish statements documented by the repository, inferences from source code, and unknowns. If evidence is insufficient, say so explicitly. Do not guess or simply repeat the README.

GitHub: https://github.com/omdsh-plugins/omdsh-codemode
Plugin: omdsh-codemode
Author: omdsh-plugins

Check the source files

Read the README and other files from this plugin directory before installing.

File explorer4 files
README.mdSource · read only
README language

omdsh-codemode

English | 中文

Code mode for the DeepSeek Harness web GUI: a third segment beside Chat and Work whose column is the harness's own terminal, running in the conversation's workspace.

The web GUI and the terminal are two front doors onto the same harness. This plugin is the seam that lets one be shown inside the other — pressing Code replaces the conversation column with a real dsh --profile omdsh-tui, banner and all, in the directory the conversation is already accounted under.

What it adds

SurfaceWhere it comes from
The Code segment in the mode switchA registration in sessionModes, the segment registry omdsh-basemode publishes
The terminal columnAn entry in conversation, ui-layout's single seat for the whole centre, registered at a lower priority than the shipped conversation and disposed when the mode is left
The socket behind itGET /omdsh-codemode/terminal (WebSocket upgrade), fenced exactly like /api
Code conversations in the sidebar, under the workspace they ran inWorkspace.attachSession on the harness's own registry, once the terminal has written something
New Session starting another terminal instead of leaving the modeThe newSession answer this plugin's segment registers, which the switch offers to whoever holds the column
Sidebar rows following a /rename made inside the terminalThe window title the terminal announces, read off the bytes this plugin already relays
The red dot on those rowsThe tone and the owns classifier this plugin's segment carries; the sidebar's dots are painted by omdsh-basemode for whatever modes are registered

Nothing in the harness is modified, and nothing of the terminal is reimplemented. What is on screen is @omdsh-plugins/omdsh-tui's own front door — its transcript, tool cards, /resume, every key it binds — because this plugin starts that program and relays bytes.

The column is taken, not owned

conversation is a single-occupancy slot: entries are priority-sorted and the lowest renders, so registering below the shipped conversation shadows it outright. That is the whole implementation of "the main view shows only the terminal" — and it is also why the registration is added when the segment is pressed and disposed when the segment loses the column, rather than held from mount. A plugin that registered there permanently would not be a mode; it would be a replacement.

Taking the seat means wearing what the seat publishes. The mode switch rides shell.overlay — a layer spanning the sidebar and the details panel too — and centres itself on the box carrying data-conversation-scroll, the attribute the conversation skeleton puts on its scrollport. This column carries it as well, because without it the switch loses its anchor the instant Code mode takes the column and snaps to the centre of the whole frame, which reads as the control jumping sideways on every mode change.

Which segment is on is the switch's business, not this plugin's. The registry allows exactly one active segment, so pressing Chat or Work clears this one, and the column follows that flag down. There is no second source of truth to disagree with the pressed state.

A Code conversation is a conversation

Code mode names the session its terminal drives (code-session-<uuid>, handed to the terminal as --session-id) instead of letting the process mint one for itself. Everything else here follows from that one decision:

  • The row in the sidebar. A terminal conversation reaches the sidebar on its own — the web GUI lists every persisted session — but as a stray under Ungrouped, because only the process that created a session ever accounts for it and that process is not this host. Knowing the id up front lets the host attach it to the workspace it ran in, so it lands in the group where the user started it.
  • Coming back to it. Clicking that row opens the conversation, this plugin sees a session it started, and the column follows: the terminal boots on the same id, which the terminal app treats as create-or-continue. Same conversation, same agent memory, further turns appended to the same log and the same row.
  • The red dot. "Did Code mode start this?" is a question about the id, so it needs no bookkeeping, survives a restart, and answers the same in every tab.

The id is the whole record — there is no table beside the session store to disagree with it.

Accounted once it has begun, and not before. The terminal app writes a session header the moment it starts, so a terminal opened and never typed into leaves a real, if tiny, log behind. The host reads such a conversation as blank — and blank is not merely "hidden from the sidebar". It is the frame's word for a conversation New Session may reuse: workspaces.connectWorkspace scans the workspace's account for one and opens it. A turn-less Code conversation left in that account is therefore handed straight back the next time the user presses New Session in Chat or Work — the column turns into a terminal and the mode changes under them — and the same reuse drives the frame's initial workspace selection, so a page can open into a terminal nobody asked for. There is nothing to weigh against that, because the sidebar draws no blank rows in the first place. So the account follows the conversation in both directions: attached once a turn has run in it, taken back out while none has.

Terminals are keyed by that session, not by the directory. A directory key made "one agent per tree" structural but left a Code conversation unaddressable: a workspace could only ever have its most recent terminal, and one the user left could never be reopened. Two terminals in one tree are now possible, and are the user's own explicit act — the same thing two dsh windows in one directory have always been.

A terminal outlives its socket. Leaving Code mode, switching conversations, and reloading the page all drop the socket, and none of them mean "kill my agent mid-turn" — so the process stays for a grace period, its output accumulates into a bounded transcript, and the next connection replays that transcript before going live. What ends one is the user exiting it, the grace expiring, this plugin unloading, or the host process going away — a terminal belongs to the host that started it, and an abandoned one would go on holding its conversation against whoever opens it next.

A conversation renamed in the terminal is renamed in the sidebar

/rename in a Code terminal — and the name the agent generates after the first turn — is a durable change made by another process. The web host holds no agent for that conversation, so nothing pushes the new name to the page; worse, the host reads the projection table (where cold conversations' names live) once, when it starts, and serves every later listing from memory. Reloading the browser does not help: the staleness is behind the RPC, not in front of it.

The terminal announces the change itself, through the oldest channel there is — the window title (OSC 0), which every terminal program writes and every emulator parses. Both halves of this plugin read it off the same bytes they were already relaying:

  • The host re-reads that one conversation from its log (the projection cache's own cold read, which stores what it folds), so the next listing is current. It repeats on a short widening schedule, because the terminal makes a rename durable on its own timing — a read fired the instant the title changes can rightly still see the old name.
  • The browser re-pulls the session list a beat later, and stops as soon as the row moves.

The announcement is the trigger, never the answer: its text is the terminal's own label, and the session log stays the authority on what the conversation is called.

Where the terminal runs

A terminal needs a directory, and a conversation is only one of the ways to name one. Three answers, in the order of how much the page already knows:

1. The conversation on screen — the project it is grouped under, falling back to its own recorded directory. 2. The project the runtime itself would land in, when nothing is open. recentWorkspaceId is the harness's own answer to "where were you", and the one it restores a conversation from; the top of the workspace list stands in when it names none. 3. Wherever you say, when no project is registered at all. Pressing Code on a fresh install opens the Host's directory picker, registers what you choose, and starts a terminal there — the same gesture the frame's own empty state offers under Choose workspace.

The second and third answers are why this segment is live on a page that has nothing open. It used to report the first one only, which meant it was permanently greyed out on a fresh install — a state invisible for as long as omdsh-chatmode was composed beside it, because its managed Chat workspace means a conversation is always open. Both happen on the press and never while deriving, so a page nobody pressed Code on starts no terminal and mints no conversation.

The third answer needs the Host's native picker, which is what dsh mounts on macOS and Windows when it is serving the machine it runs on. A remote or headless Host drives an in-app directory browser instead — ui-workspace's own component, which a contributed mode has no way to open. There is no way to ask which is mounted, so the first press that finds out is what makes the segment stop offering the cold start and say what is missing instead; registering any project brings it straight back.

What pressing Code shows you

Four answers, in the order of how much is actually known:

1. The conversation the browser named — the Code row that was clicked, or the terminal this page is already showing. 2. This host's live terminal in that directory, which is what makes a page reload land back on the same agent mid-turn: the socket drops, the process does not. 3. The project's most recent Code conversation, which is what pressing Code means on a host that has just started: come back to the work, not to an empty prompt. The most recent by the same clock the sidebar orders by, and never one nothing was said in — a terminal somebody opened and walked away from leaves one of those behind, and there is nothing in it to come back to. 4. A new conversation, when the project has none.

Which project all four are asked about is the conversation on screen — with one exception, and it is the reason this section has a fifth paragraph. A conversation that lives in no project names no directory a terminal could run in: a chat is filed in a store the plugin that owns it keeps, and nobody works there. Pressing Code beside one used to open a terminal inside that folder. It now comes back to where Code last was instead — this page's own live terminal in that project, else that project's most recent Code conversation, offered the same way answer 3 is — which is the rule Work follows from a chat as well. A page that has never had Code anywhere falls through to the cold start below, and that skips a group whose conversations are all in no project too. Which conversations those are is omdsh-basemode's inProject, declared by the mode that owns them, so nothing here knows which of them is Chat.

The order is the whole safety argument, and the third answer sits where it does deliberately. The browser can see the session list; it cannot see a running process. A conversation started a moment ago has nothing on disk to be "most recent", so a surface that outranked the live table would revive an older conversation over a running agent — and two live copies of one conversation interleave their sequence numbers until the log stops loading at all. So the browser offers (resume= on the socket) and the host decides, taking the offer only when it has nothing running in that directory. A log that some earlier accident already damaged that way is not beyond saving: pnpm run repair:sessions is the recovery tool for exactly that shape, and [Commands](#commands) says how to run it.

New Session starts another terminal

Pressing New Session while Code mode holds the column means another Code conversation — in the project the button named, or the directory the terminal on screen is already in — not a web conversation and not a change of mode. The switch registry offers the request to whichever segment holds the column before the frame handles it, and this one answers.

The conversation is named here rather than asked for, and that is what makes the request idempotent: the socket reconnects, the column remounts, the window resizes, and every one of those asks for the same conversation instead of starting another terminal. The socket says fresh, which tells the host this id is newly minted rather than resumed — nobody can be holding it, so its terminal becomes this directory's terminal from the moment it starts.

Until its first turn is persisted the new conversation has no row at all — nothing in the harness has heard of it — so there is nothing in the sidebar to highlight. The row appears on its own once that turn lands, and the highlight lands with it.

The selection still does not move, and never will: a Code conversation is shown, never selected, because making one the runtime's current session is what makes this host resume it, on a log its terminal is still appending to. What moves instead is the sidebar's cursor. This plugin publishes the conversation its terminal is driving as the mode system's column, and omdsh-basemode paints the highlight onto that row — so opening a Code conversation moves the cursor onto it, and the web conversation left selected behind the terminal gives the highlight up until you go back to it.

Two details are worth knowing before they surprise you:

  • A terminal nobody typed into leaves no row. The harness persists a session lazily, on its first turn, so opening Code mode and walking away costs nothing and adds nothing. The row appears within seconds of the first turn — the terminal renames its window when the conversation is named, which is the earliest this host can know one began, so the account is settled on that announcement rather than on the next attempt of its retry schedule — and never sooner. Until then it is a blank conversation and is treated as one: opening a blank Code conversation does not take the column, which is what stops the frame's own New Session — it reuses a workspace's blank conversation, whoever started it — from dropping a person into a terminal they did not ask for.
  • A restored terminal starts at the banner. It is dsh --resume's own behavior — the terminal app does not redraw a resumed transcript — so the conversation continues while the screen starts clean. Pressing Work on a Code conversation reads the same log in the web view, which is where its transcript is legible.

What it runs

The launcher is this runtime's own entry, re-executed: the process serving the page is already a dsh launch, so its entry is the one launcher known to exist and known to be the same installation the user is talking to. No PATH lookup, no second install to keep in step.

# The profile's own cordis.patch.yml, to change any of it:
- id: codemode
  config:
    profile: omdsh-tui       # the profile the column boots
    reconnectGraceMs: 300000  # how long a dropped terminal survives

A deployment whose runtime was not started by dsh — a packaged shell, a test — sets command (and args) instead; without either, the socket refuses with a message saying so rather than guessing at a binary.

Those four knobs — profile, command, args, reconnectGraceMs — are a plain TypeScript interface on the host half, not a settings namespace. So they are edited where the sample above puts them, in the profile's own cordis.patch.yml, and this plugin's card in the plugin hub carries no form. They are composition facts rather than a person's preferences — which launcher a deployment re-executes is decided once, by whoever assembled the profile.

Whichever launcher is resolved, it is invoked with --session-id <id> appended, because Code mode names the conversation it starts. The profile it boots must understand that flag: omdsh-tui takes it as create-or-continue for exactly that session, which is what makes a Code conversation something the sidebar can hold and a click can reopen.

Security

The socket hands out a live agent process, so it is fenced exactly like /api: a Host header naming us (loopback, or an authority the deployment was told to serve) plus same-origin browser markers. This is a DNS-rebinding and cross-site defence, not authentication — a deployment that publishes /api to a network publishes this with it.

Install

Requires a dsh on your PATH, and the web profile that carries the mode switch.

npx @omdsh-plugins/omdsh-plughub add omdsh-codemode

That is the plugin hub's installer with argv where the button was. It resolves this plugin from the collection's registry, installs it from its GitHub repository, and writes the pnpm build-allowlist entry a bare dsh plugin add github:… would leave to you — the entry carries the commit pnpm resolved, so it can be copied out of a failure and never written down in advance.

dsh plugin --profile web add @omdsh-plugins/omdsh-codemode is not that command yet: this package is not on npm, and pnpm answers ERR_PNPM_FETCH_404. The same install is also a button, on this plugin's card in Settings → Plugins → Plugin hub, once the hub itself is in the profile.

omdsh-basemode — the switch it registers into — is published, so that one installs by name:

dsh plugin --profile web add @omdsh-plugins/omdsh-basemode

Or from a checkout, which is what an unpublished build wants. lib/ must exist before dsh web runs — the loader imports lib/index.js directly, and a path-installed package never has its prepare run, so nothing builds it for you:

pnpm install
pnpm run build

dsh plugin --profile web add "$PWD"           # this plugin
dsh plugin --profile web add ../omdsh-basemode    # the switch it registers into

The terminal it starts lives in its own profile (omdsh-tui by default), and that one is checkout-only: unlike every other companion named here it is not in the collection's registry, so there is no @omdsh-plugins/omdsh-tui to add. Install it the way that repository describes, which is one script:

cd ../omdsh-tui && pnpm install && pnpm run install:profile
dsh --profile web

Never add the terminal to the web profile. @omdsh-plugins/omdsh-tui-app is a surface bundle, exactly as @deepseek-ai/dsh-web-app is, and a profile composes exactly one surface over dsh-base. Stacking them collides on seven loader ids — code-runtime, storage, storage-json, storage-domain, session-projection-cache, session-stats, agent-presets — and the whole page dies at mount on the first one:

Error: dsh: plugin tree failed to load: failed to apply loader entry include
(cordis:include): duplicate loader entry id: code-runtime

Code mode boots that profile as a child process; it does not compose it. The two never share