Watch them work.
Take the keyboard.
dsh can already delegate to Claude Code and Codex — one task in, one sentence out. What it cannot do is let you see it happen. dsh-crew gives each crew member a real PTY in the session's workspace, streamed to a pane in the Web UI. The agent seats them and hands them work; you watch every keystroke of it, and you can type into any pane at any moment.
dsh plugin --profile web add @dshworks/dsh-crew
> where does this repo refresh auth tokens? ⏺ Read src/auth/session.ts (214 lines) ⏺ Grep "refresh" — 6 files ⏺ One place: session.ts:88, in ensureFresh(). It refreshes inside 60s of expiry, and the interceptor already calls it. The retry path at :141 does not go through it. A 401 there refreshes a second time. >
› where does this repo refresh auth tokens? • src/auth/session.ts owns it. ensureFresh() swaps on a 60s skew; the interceptor calls it. Worth noting: refreshToken() is unguarded, so two requests racing an expiry both hit /oauth/token. The server rotates them, so the loser gets 400 invalid_grant and the session dies. ›
The two halves
The human half
A seat bar and one pane per agent. The pane is a real terminal — colour, cursor addressing, the product's own TUI — because the plugin carries the CLI's bytes instead of reimplementing its surface.
Type into it whenever you want. Interrupt with Ctrl-C. Or use the broadcast line and watch two agents answer the same question.
The agent half
Five tools, so the model in the centre column can run the team — and the delegation and the observation are the same terminal, not a task API beside a log.
Turn them off with tools: false and the split view still
works, human-driven.
| tool | what it does |
|---|---|
crew_list | who can be seated, who already is |
crew_seat | starts a pane, returns its id and first screen |
crew_send | types it, presses Enter, waits for quiet — or returns a job id |
crew_peek | the screen right now, as the human sees it |
crew_dismiss | ends the process |
Background delegation. crew_send(…, run_in_background: true)
puts the wait on the harness's job seam instead of the calling turn: the call returns a
job id, the model keeps working, and the completion notice wakes it with the answer.
job_kill stops the watch and leaves the pane seated — a
cancelled delegation is not a reason to close a terminal someone is watching.
What the shell could not tell us
The test suite seats bash, because a shell has a deterministic
screen and no account. That is also its limit. Three corrections came only from running
the real products, and each one had shipped green tests first.
Enter has to be its own keystroke
Claude Code and Codex both read one burst ending in a carriage return as a
paste, and keep the return as a newline. So message + "\r"
in a single write typed the task and submitted nothing — and the pane then went
quiet, so a settle-on-quiet rule handed the model back its own unsent question as
the answer.
Fixed by writing the message, pausing 250ms, then "\r" alone.
bash submits either way and never saw it.
An unpainted screen is not a settled one
Claude Code draws its first frame after about 4.5 seconds and is perfectly quiet until then. Seating now waits for content and calm before it reports a crew member ready.
The first screen may be a dialog — and answering it is not ours to do
In a directory it has not been trusted in, each product opens on its own trust prompt rather than a composer. A task sent into a dialog is typed into the dialog, and its digits can pick an option: a "count from 1 to 12" prompt once chose 2. No, quit.
So the tools tell the caller to answer the dialog first — an empty message presses Enter — and send the task once the composer is up. Auto-answering a product's trust prompt is not a plugin's decision to make.
The opt-in suite runs behind CREW_REAL_CLI=1 and seats each
product in a throwaway workspace, so answering a trust prompt in a test never marks a
real project trusted. Last verified against claude 2.1.232 and
codex 0.147.0 on 2026-08-17.
Install
dsh plugin --profile web add @dshworks/dsh-crew
dsh web
dsh plugin forwards to pnpm, so pnpm must be on PATH. A Crew
tab appears beside Chat and Trajectory in the next session. Nothing else to configure:
the seat bar shows every crew member it can find and disables the rest, with the reason
on the button — a missing codex reads as
codex is not on the host PATH, not a launch that fails a second later.
Adding a crew member is one row, never a package
# ~/.dsh/profiles/web/cordis.patch.yml
- id: dsh-crew
config:
agents:
- id: aider
label: Aider
command: aider
accent: '#7c3aed'
A row whose id matches a built-in overrides that built-in
field by field, so pointing claude at a wrapper script is one line.
enabled: false removes one.
What it is not
A pane starts a real process on your machine, in the workspace of the dsh session
that opened it. That is the feature, and it is also the risk: the two routes this
plugin mounts are fenced on the Host authority and
Sec-Fetch-Site, the control route requires application/json,
and the WebSocket needs a single-use 30-second token. That fence answers "did this come
from the dsh UI on this machine" — it is not authentication, and
network reachability is still your webserver's bind policy.
Geometry is fixed at spawn, because the harness's subprocess seam allocates a PTY
and exposes no resize; a pane shrinks its rendered grid to fit rather than reflowing
under the agent. And if you only want the final sentence with no terminal at all, the
harness's own subagent-claude-code provider is the smaller tool — this one
exists for when you want to watch.
Not affiliated with DeepSeek, Anthropic, or OpenAI. Claude Code and Codex are their authors' products; dsh-crew only carries their bytes.