Host + client · unpublished
AI Workspace
Managed dev processes with captured logs, a live panel, and agent instructions to read the logs instead of re-running commands.
@malipetek/dsh-ai-workspaceInstall
dsh plugin add @malipetek/dsh-ai-workspace
Not on npm yet. The name is reserved for the first release, so this command will work once it is.
Package
Browse the source →- Package
- @malipetek/dsh-ai-workspace
- Version
- 1.0.0 (unpublished)
- Published
- —
- Installs / mo
- —
- Unpacked size
- —
- Surface
- Host + client (web)
- License
- MIT
- Keywords
- dsh, dsh-plugin, deepseek-harness, cordis, dev-server, logs
Documentation
from the package READMEA DeepSeek Harness plugin that gives a project's long-running dev processes a
managed home: it discovers them, runs them in the background, captures their
output to log files, shows them in a live panel, and tells the agent to read
those logs instead of re-running pnpm dev / pnpm build / tsc --watch.
It is the DeepSeek Harness port of the ai-workspace VS Code extension. The
premise is the one worth preserving: if the dev processes are already running and
their output is already captured, an agent never needs to start a second copy —
which would fight over ports and duplicate work.
Why there are two halves, and why there is a panel
The extension opened one terminal per process. A DeepSeek Harness plugin cannot
do that: terminal creation is Agent-gated (ctx.terminals refuses any owner
that is not a live registered Agent with OWNER_NOT_LIVE), and the one UI-facing
controller only allocates a discovered shell, never an arbitrary command, with
a sidebar tab kind that is hardwired.
So this bundle pairs:
index.js(Host) — discovery, process supervision, log capture, the agent instruction, and the/ai-workspacecommand.client.js(Client) — a sidebar row plus a page whose grid of process cards is the closest available thing to those small terminal windows. Each card carries a live tail of that process's log.
The halves talk through a file, not a bespoke RPC, because an out-of-tree plugin
cannot declare its own Remote. The Host writes a compact JSON snapshot to
~/.dsh/ai-workspace/status.json; the Client reads it through the existing
session-scoped remote.workspaceFiles Remote, exactly as the Projects panel
does.
What it does
- Discovery — scans
projectsRootone level deep for projects carrying.ai-workspace.json(or the legacy.claude-workspace.json), and re-scans every 60 s so a project added while the profile runs shows up. Extra roots can be listed explicitly. - Onboarding — every other project that has a package manifest or a compose file shows up as a not-yet-configured candidate, with an offline proposal for its dev processes. See Onboarding.
- Supervision — spawns each process detached in its own process group, so the
whole group can be terminated; counts error-looking output lines with the
extension's own pattern set; and detects the first bound-port announcement
(
Local: http://localhost:5173,listening on port 3000) for the card badge. - Port management — before starting a process, works out which port it wants, and if that port is taken, moves it and injects the new one through the flag, argument or environment variable that runtime understands. See Port management.
- Capture — mirrors stdout and stderr into
<project>/.ai-workspace/logs/<name>.log. That file is the durable artifact the agent reads, and it survives restarts (/ai-workspace restart, or the Restart button, appends a separator rather than truncating). - Panel — one section per project, one card per process, each with status, command, port, error count, restart count, a 132 px live log tail, and Start / Restart / Stop buttons, plus Refresh, Live/Paused, Rescan, Start all and Stop all. It follows the Host's push feed, so a card updates when the Host writes. A filter box narrows projects, processes and candidates at once — necessary at 144 projects — and a collapsible Not onboarded backlog sits below the managed projects.
- Agent instruction — a compact system-prompt section that states the rule ("do not run build/dev/watch/test commands"), names the running projects, and points at the tool. It renders as an empty string while nothing runs, so an idle plugin costs no prompt tokens, and it is bounded so a large workspace cannot bloat a request.
- Model-facing tool —
ai_workspacelets the agent read a process's captured output, list error-like lines, see the onboarding backlog, and start / stop / restart a process, without a shell command. See Model-facing tool. - Agent dispatch —
/ai-workspace init <project>hands the agent a turn that inspects the project's manifests and writes.ai-workspace.json;/ai-workspace fix <project>does the same for a config that is missing, malformed, or out of date. Both work on a project that has no config yet, and the turn is seeded with what the offline detector already found.
Onboarding other projects
A project is only managed once it has a config file, so a folder of 143 projects with 4 configs would leave 139 invisible. Onboarding closes that gap.
Nothing is ever written without you asking. Detection is read-only: the
candidate list and every proposal are computed in memory and shown in the panel.
A file appears in your repo only when you press Onboard, run
/ai-workspace onboard <project>, or run an explicit --all.
What the detector proposes
It reads package.json scripts, the lockfile that names the package manager
(pnpm / yarn / bun / npm), and shallow-parses a compose file for its
service names. The rules come from what real projects look like:
| Situation | Proposal |
|---|---|
A conventional dev / develop / serve / start script |
That one script, named after the project. |
Both dev and start |
dev only — start is its production twin, and running both fights over a port. |
check:watch, test:*, lint:*, build:watch, docs:dev |
Ignored. These are helpers, not app servers. |
dev:api, dev:web, … with no plain dev |
One process per sibling server. |
Both dev and dev:* siblings |
dev only, with the siblings named in a note — a root dev usually orchestrates them. |
A bare watch and nothing else |
watch, at low confidence: a compile watcher is a real long-running process, but it could be a test watcher in disguise. |
| A compose file and no scripts | One docker compose up, however many services it declares, with the services named in a note. |
A compose file beside a dev script |
dev only, with a note — assume the script does not start the infra, and add it only if it does not. |
No conventional script (docs:dev only, or just test) |
Nothing. The project is marked as needing an agent. |
Proposals are high confidence (one unambiguous long-running process) or
needs review (sibling servers, a compose stack, or a bare watch).
The panel
The Not onboarded (N) section lists every candidate with its manager, a confidence badge, the proposed commands, and the detector's notes. It carries view chips — All, High, Review, Agent — so the agent-territory projects can be isolated from the batch-onboardable ones. Each card has:
- Onboard — write the proposal to
.ai-workspace.json. Disabled when the detector found nothing to propose. - Ask agent — dispatch an agent turn for the projects a heuristic should not guess at.
plus Onboard all high-confidence (N), which writes only the unambiguous ones.
Once a project is onboarded, its card header offers Offboard, which removes
.ai-workspace.json and stops that project's processes, returning it to the
backlog. It asks for a second click first, and it only ever removes the file
this plugin writes — a project configured by a hand-written
.claude-workspace.json shows no such control, and asking for it by command is
refused with an explanation. Offboarding is deliberately not exposed to the
ai_workspace tool: removing a config is the user's call.
Commands
/ai-workspace candidates list the backlog with each proposal
/ai-workspace onboard <project> write the detected config for one project
/ai-workspace onboard --all write every high-confidence proposal
/ai-workspace onboard --all --low write every proposal, including "needs review"
/ai-workspace offboard <project> remove the plugin's config and stop its processes
/ai-workspace init <project> dispatch an agent to write the config
/ai-workspace fix <project> dispatch an agent to repair a config
Choosing between the detector and an agent
The detector is instant and free, and it is right about the ordinary case — a
single app with a dev script. Reach for the agent when the detector says
needs review or finds nothing: monorepos where several packages each need
their own process, compose stacks whose infra must be started separately, and
script names that do not follow the convention. init hands the agent the
detector's reading as a hypothesis, so it confirms or corrects rather than
starting from nothing.
Port management
The prompt section tells the agent not to start a second copy of a dev server because "a second copy causes port conflicts". This is the half that makes that safe for your processes too: before a process starts, the plugin works out which port it wants, and moves it if that port is already taken.
Working out the port, in priority order:
"port": Non the process — explicit intent wins.- A port named on the command line (
--port 5173,-p 3000,PORT=4000,localhost:8080). - The project's own config:
PORT=in.env/.env.local/.env.development, orserver: { port: … }in a Vite/Astro/Rsbuild config. - The runtime's default (Vite 5173, Astro 4321, Next/Nuxt/Remix 3000, Angular 4200, Storybook 6006, Jupyter-free Node servers 3000, uvicorn/gunicorn 8000, Flask 5000, Phoenix 4000, …).
Working out the runtime means unwrapping the launcher first: pnpm dev runs
the dev script body, which is where vite actually appears. Each package
manager forwards extra arguments differently, so injection respects that — npm
needs --, deno task forwards nothing and falls back to $PORT.
Injecting the port uses whatever the runtime understands: a --port / -p
flag (replacing one already present), a bare positional argument
(manage.py runserver), or PORT in the environment. docker compose is left
alone entirely — its ports come from the compose file, and injecting a flag would
break it.
Two details worth knowing:
- The claim set spans every project. Live probing alone cannot stop two of our own processes starting at the same instant from both seeing a port free, so a claim set guards that race. Two Vite apps in different repositories cannot both land on 5173.
- A pinned port is honoured. The extension this ports had
free && !forcedin its decision, which meant"port": 5173was never used as-is — it always shifted to 5174. That is fixed here: a pinned port that is free is used.
When a port moves, the reason is written into the log, shown as a port moved
badge in the panel, and returned by the ai_workspace tool's logs action. When
no injection path is known, the plugin still assigns a port through $PORT and
says "verify it binds" rather than silently claiming success.
Project config — .ai-workspace.json
The same shape the VS Code extension uses, so existing repos work unchanged:
{
"processes": [
{ "name": "frontend", "command": "pnpm dev", "cwd": "./frontend" },
{ "name": "backend", "command": "pnpm start:dev", "cwd": "./backend" },
{ "name": "database", "command": "docker compose up db", "autoStart": false }
]
}
| Field | Meaning |
|---|---|
name |
Short unique slug. Becomes the log filename and the card title. |
command |
Exact shell command, run by a login shell in the project. |
cwd |
Path relative to the project root. Defaults to the root. |
env |
Extra environment variables for this process. |
autoStart |
false opts the process out of "Start all" and of autoStart mounting. |
port |
A number pins that exact port; false opts this process out of port management. |
Plugin config
Set in cordis.patch.yml. A matching override in the profile's own
cordis.patch.yml replaces this config wholesale, so restate every field you
need.
| Field | Default | Meaning |
|---|---|---|
projectsRoot |
/Users/malipetek/Documents/Documents/Projects |
Folder scanned one level deep for project configs. |
projects |
[] |
Extra absolute project roots outside projectsRoot. |
autoDiscover |
true |
Scan projectsRoot at all. |
autoStart |
false |
Start the processes when the plugin mounts. |
managePorts |
true |
Move a process to a free port when the one it wants is taken. |
flushMs |
1000 |
How often the Host rewrites the status snapshot. |
maxTailLines |
30 |
Trailing log lines carried into the snapshot. |
writeAgentsMd |
false |
Also maintain a marked section in the project's AGENTS.md / CLAUDE.md. |
autoStart is off by default on purpose: installing the bundle must not
spawn servers on its own. Press Start all in the panel, or turn it on once
you trust the discovered configs.
Enabling auto-start
# ~/.dsh/profiles/desktop/cordis.patch.yml
- id: ai-workspace
name: '@malipetek/dsh-ai-workspace'
config:
projectsRoot: /Users/malipetek/Documents/Documents/Projects
projects: []
autoStart: true
flushMs: 1000
maxTailLines: 30
writeAgentsMd: false
Commands
The panel's buttons execute a Host-registered command, because a root-scoped panel has no other way to call into the Host:
/ai-workspace status
/ai-workspace candidates
/ai-workspace rescan
/ai-workspace onboard <project>
/ai-workspace onboard --all [--low]
/ai-workspace offboard <project>
/ai-workspace start <project> [process]
/ai-workspace stop <project> [process]
/ai-workspace restart <project> [process]
/ai-workspace start-all
/ai-workspace stop-all
/ai-workspace init <project> [extra instruction]
/ai-workspace fix <project> [extra instruction]
<project> matches a project's exact root, its exact folder name, or a unique
fragment of either. Omitting it acts on every project. init and fix accept
either a managed project or a not-yet-onboarded candidate.
Note that each button click appends command/run + command/done to the
session log and shows a command row in the conversation — that is how the
Harness records command lifecycles, not something this plugin can suppress
(recordInput: false only hides the arguments).
Agent instructions
Two channels, in order of importance:
System-prompt section (
ai-workspace.dev-processes, order 1500). Native to DeepSeek Harness, active for every session while the bundle is enabled, and empty when nothing is running, so an idle plugin costs no prompt tokens.It is deliberately compact: the total count, up to 12 project names with their running processes, a capped roll-up of what is showing error-like output, and the rule. Detail lives behind the
ai_workspacetool, because a per-process table is fine for four projects and a liability for forty — it would put a forty-row table into every request and churn the KV prefix on every change./ai-workspace statusand the panel still show everything.AGENTS.md/CLAUDE.mdsection — off by default. DeepSeek Harness already loads those files, but so do other tools (Claude Code, Cursor…), so this exists to carry the same instruction outside the Harness. It only ever edits a file that already exists; it never creates one.
Model-facing tool
The bundle registers one tool, ai_workspace, so the agent reads captured output
directly instead of shelling out:
| Action | Effect |
|---|---|
status |
Every managed process, its state, port and error count |
logs |
The recent output of one process (project, process, lines) |
errors |
Error-like lines across every process, or one project |
candidates |
Projects with no config yet, with the detected proposal for each |
start / stop / restart |
Lifecycle for one project or process |
rescan |
Re-read the projects folder now |
logs and errors read at most the last 256 KB of a log file and return at most
200 lines, so a chatty dev server cannot flood the transcript. start without a
project names one rather than starting everything — bulk control stays in the
panel's Start all.
Registration hands the tool registry a plain JSON Schema and imports no DeepSeek
Harness package, so it cannot fail on module resolution (the same shape
dsh-projects-manager uses). If it ever does fail, the reason is recorded in
status.json under wiring.tool instead of disappearing into the Host log.
This is what makes the plugin's premise hold: an agent that can read a process's output with one tool call has no reason to start a second copy of your dev server to find out what happened.
Install
"/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh" \
plugin --profile desktop add /Users/malipetek/Documents/deepseek-harness/default-workspace/dsh-ai-workspace
Restart the app after changing either half. The profile composes configuration
changes live — the Host half of this plugin activates through HMR, which is why
status.json appears immediately — but it watches no modules (root: []), so the
browser needs a fresh JavaScript generation before the sidebar row and page
appear.
Later edits
The profile links this directory (link:), so editing index.js or client.js
in place affects the installed plugin directly. Each change still needs the app
restarted before the running JavaScript generation is replaced.
Limitations
- No UI terminals. Explained above; the panel is the substitute.
- The panel follows the Host's push feed. The
workspaceFiles.changesstream announces each snapshot rewrite, so a card updates when the Host writes rather than up to one poll later. Polling stays installed as the fallback — at 15 s once the feed is live, at 3 s when it could not open — and the header says which is in use (streaming/polling). A dropped or unsupported feed degrades to polling instead of breaking the page. The file Remote also exposes achangesstream that could make this push-based. - Reading the snapshot needs an open Session to authorize the Remote call.
- macOS process-tree containment is best effort. The Host spawns detached and
signals the group, but macOS has no stronger persistent process-range owner, so
a grandchild that escapes the group (a container daemon, a detached worker) can
outlive a Stop. The same caveat applies to the extension's
killTree. cwdis resolved at discovery time, and a running process keeps its original definition across a re-scan; change a command and restart the process to apply it.- Onboarding detection is a heuristic, by design. It reads script names, not
script bodies, so a project whose
devscript is a one-shot must be corrected by hand; and the compose reader is a shallow line parser that needs the standard two-space service indentation. Anything it gets wrong,/ai-workspace fixhands to an agent. - Onboarding writes into your repos. One
.ai-workspace.jsonper project, only on an explicit action, and never over an existing file — but it does add an untracked file that will show up ingit statusuntil you commit or ignore it. Logs go to.ai-workspace/logs/in the same tree. - UI copy is English-only, but routed. The panel's copy is keyed and rendered
through the locale service: the bundle registers an
ai-workspacenamespace and declares it on both slot registrations, so the renderer injects the reactivetseat and the sidebar label is a locale-aware thunk. Onlyenis registered — the locale fallback chain always resolves it, so another language shows English copy rather than blanks, and addingzhis a translation with no code change. The display metadata read by the Plugin Manager lives separately inlocale/en.json, because the client bundle cannot read a JSON file at runtime. - The prompt section is a summary, not an inventory. It names at most 12
projects and 6 erroring processes.
/ai-workspace statusand theai_workspacetool'sstatusaction report everything.
Differences from the VS Code extension
| VS Code extension | This plugin | |
|---|---|---|
| Observation | One real terminal per process | One card with a log tail per process |
| Restart | rs + Enter in the terminal |
Restart button / /ai-workspace restart |
| Port management | Detects collisions and injects a free port | Detects collisions and injects a free port, across every project at once |
| Delegated AI tasks | Manages them as logged processes | Not ported |
| Project discovery | Detects manifests, optionally validated by opencode |
Offline detector over the whole projects folder, plus init / fix agent dispatch |
| Instruction channel | Writes AGENTS.md |
System-prompt section (+ optional AGENTS.md) |