Skip to content
dsh.fish
Bundle

@dsh-external/dsh-side

DSH plugin: Codex-style side conversations (/side, /btw) in an ephemeral right-workspace panel with Side/Subagents/Goal sections and a ChatGPT-style pinned-notes board (置顶摘要小黑板), plus a best-effort sidebar collapse hotzone.

Source
invalidnaaaame
stars
2 stars
License
MIT
Updated
Updated 16 hours ago

Readme

# dsh-side-workspace

**English** | [中文](README.zh.md)

> A DeepSeek Harness (DSH) plugin built in the spirit of the ChatGPT / Codex trio —
> **side conversations · integrated workspace · pinned notes (置顶摘要小黑板)**

- Repository: `dsh-side-workspace`
- Plugin ID / npm package: `@dsh-external/dsh-side` (unchanged)

![dsh-side-workspace UI](assets/screenshot-workspace.png)

> **Disclaimer**: an independent open-source project. Not affiliated with,
> endorsed by, or sponsored by OpenAI or DeepSeek. "ChatGPT" and "Codex" are
> trademarks of their respective owners, referenced only to describe feature
> parity.

Official naming reference (the basis for the repo name):

| This plugin's feature | Official name | Source |
| --- | --- | --- |
| `/side` conversations | Codex **side conversations** | [openai/codex#18190](https://github.com/openai/codex/pull/18190) |
| Integrated right workspace | ChatGPT **Workspace** | OpenAI workspace agent |
| Pinned summary board | ChatGPT **Notes** (pinned message notes) | [Pin a Note to Any Message](https://www.getaiworkspace.com/chatgpt-message-notes) |

---

## Features

### 1. Side conversations `/side` — Codex side conversations

`/side <question>` forks the current conversation's full history into a
**background side conversation** via a boundary context, while the main
conversation stays focused — the two never block each other; `/btw <question>`
is a one-shot side question (read-only). The child is an ordinary top-level
session (`ctx.agents.create`) that inherits the parent's agent preset, model,
and cwd, and goes through the deployment's own approval/sandbox policy — side
output **never flows back** into the main conversation's log.

- Default `ephemeral`: archived on creation, never enters the ordinary session
  list; idle children past the TTL (default 60 min) are swept by the host;
  running/approval-waiting children never expire; a new `/side` replaces idle
  older `/side` children of the same parent.
- Cleanup is a small state machine (active → expiring → removed /
  cleanup-failed with bounded backoff retries) — a failed dispose never loses
  the management record.
- **Bare `/side` opens an EMPTY side conversation**: no question is injected,
  the child waits in a `ready` state, and you type the question directly in
  the panel composer; afterwards it returns to the normal run/complete flow.
- **Per-side model and reasoning-effort picker**: expand the "Model" row at
  the top of the side detail page — models are grouped by provider; picking a
  model reveals that model's reasoning-effort options. It goes through the
  host's `session.selectModel`, persists on that child, and applies to its
  next step.

### 2. Integrated right workspace — ChatGPT Workspace

The session-header toggle opens the shell's right column (the details column;
the main conversation is squeezed), hosting a grouped list + detail pages:

- **Side**: this session's side conversations (run status, activity line,
  failed/cleanup-failed markers); clicking opens the detail page (full
  transcript + follow-up composer; `/btw` is read-only).
- **Subagents**: the session's subagent catalog (reference-counted live feed;
  the header badge shares the same source).
- **Goal**: the session goal (pause/resume/complete/clear via the goal RPC).
- Any entry opens its detail page **inside the same right column** — the left
  sidebar and the main conversation never switch; Escape unwinds step by step
  (detail → list → close panel).
- **New-status notifications**: when a side conversation settles (completed /
  failed / cleanup-failed), a warning dot appears on the small panel button
  above the main conversation (unseen count in the tooltip); opening the
  panel marks them read. History already present when you open a session is a
  silent baseline — never replayed as notifications.

### 3. Pinned summary board — ChatGPT Notes

A per-session board pinned to the top of the right workspace with its own pin
toggle: a slim chip when collapsed; when expanded it edits a small structured
document — title / objective / one-line status / next steps (checkable) /
decisions and any other non-empty section.

- **User edits win**: in this phase AI never writes to the board; every edit
  goes through the **same** CAS domain function the server uses (optimistic
  local apply + a serialized PATCH behind it), so the UI and the server can
  never disagree about what a patch means.
- **Conflict handling**: a stale-revision response resyncs the board and parks
  the edit with a banner + retry; patch ids make retries idempotent, and a
  lost-but-applied response is detected by content comparison so nothing is
  double-applied.
- Persisted at `$DSH_HOME/dsh-side-boards.json` (atomic write, tolerant of
  corrupt files); route `/plugins/dsh-side/board`
  (GET/PATCH/DELETE, same-origin, follows the deployment's ordinary access
  rules).
- **A record only — never touches /goal automatically**: only the manual
  button on the objective section syncs — `goals.create` when no goal exists,
  or `goals.edit` on the projection's CAS revision when one does; non-empty
  next steps ride along as a compact "下一步" list appended to the objective
  text.

### 4. Sidebar collapse hotzone (best-effort)

A thin drag strip just inside the left sidebar's right edge: drag left past
the threshold to fully collapse the sidebar to its 56px rail, drag right from
the rail to expand. Everything delegates to the host's own `toggleSidebar()`;
geometry discovery is structural (never shadows the `sidebar` slot, the host
handle, or internal stores).

---

## Install (DSH web profile)

This is a **bundle-type plugin** (`dsh.bundle.patch` declared): mount it ONLY
through the profile's bundle list — never additionally insert it into the
profile's `cordis.patch.yml` (double-mounting fails loudly). **You must
restart `dsh web` yourself after changes** (this plugin ships no HMR):

```powershell
# 1. add the link dependency to the PROFILE package.json
"@dsh-external/dsh-side": "link:<absolute path to this repo>"
#    ...and register "@dsh-external/dsh-side" in the same manifest's
#    dsh.profile.bundles list

# 2. install in the profile directory
pnpm install --config.confirmModulesPurge=false

# 3. restart dsh web (stale plugin trees survive browser refresh)
```

## Config

| Key | Default | Description |
| --- | --- | --- |
| `retention` | `ephemeral` | `ephemeral` (archive + TTL expiry) / `persistent` (ordinary durable top-level sessions) |
| `idleTtlMinutes` | `60` | Idle minutes before an ephemeral side is swept (1–1440) |

## Usage

```
/side <question>   Start a continuable side conversation (runs in the
                   background; the right panel reveals it when it settles)
/side              Open an EMPTY side conversation and ask directly in the panel
/btw <question>    One-shot side question (read-only, no follow-ups)
/side list         List this session's side conversations
```

## Diagrams

**UI layout (three-column grid + right workspace + pinned board + notification dot)**

![workspace-layout](assets/workspace-layout.svg)

**Architecture & data flow (browser/host halves + shell services + lifecycle)**

![architecture](assets/architecture.svg)

> The hero screenshot lives at `assets/screenshot-workspace.png`; to replace
> it, capture a new full-window shot with one running `/side` and the board
> expanded, then overwrite that file.

## Architecture

```
src/
  index.ts              node half: create/archive/cleanup state machine + TTL
                        sweep + web routes (/plugins/dsh-side/list, /last,
                        /board) + command wiring
  side.ts               fork cut (host fork RPC contract), model inheritance,
                        message shapes
  prompts.ts            boundary prompt / persona / mode line (self-written
                        equivalent of the /side contract)
  registry.ts           parent → child registry + cleanup lifecycle + pure
                        TTL/retry decisions
  board.ts              pure board domain: sections, items, CAS patch engine,
                        locked-item protection, size caps
  board-persistence.ts  BoardStore: one atomic JSON file under DSH_HOME,
                        per-board save chains, patch idempotency
  client/               browser half: right workspace (Side/Subagents/Goal +
                        board), transcript layer (paged history readers with
                        seed cut, streaming merge, FIFO cache), catalog
                        refcount, action gates, session-fenced pollers,
                        sidebar collapse hotzone
tests/                  165 unit tests (domain / persistence / client stores /
                        registry / transcript / …)
```

## Development

```powershell
pnpm install --config.confirmModulesPurge=false   # see the link notes below
pnpm check    # typecheck (node + client) && vitest && build
```

`pnpm build` emits `lib/index.js` (ESM node half) + `lib/index.d.ts` +
`lib/client.js` (CJS browser half wrapped in `window.__ModuleLoader__.load`;
react/cordis/dsh-client-* externalized, lucide imported per-icon, bundle
≈195kB / gzip 43kB).

**Dependency notes**: devDependencies are pinned to the official
`@deepseek-ai/*` packages **as published on npm** (`0.1.1-rc.2` line — the same
build the installed DSH ships; cordis follows its own 4.0.1 line and
schemastery 3.18.1). Never substitute the unrelated bare `cordis`/`schemastery`
npm packages. `pnpm-lock.yaml` is not committed (regenerate locally).

> Note: the npm registry may be slow from some networks; if tarball downloads
> fail, simply re-run `pnpm install` (each attempt makes progress).

## Known limitations (honest notes)

- **Single details track**: the shell has one right column — the board and the
  workspace share it vertically (no "double squeeze"); a second column would
  require host support.
- **No public width setter**: the shell's internal layout store now ships
  `setSidebar`/`setDetails` (clamped 264–420 / 300–520), but `ctx.layout`
  still exposes only `toggleSidebar/openDetails/closeDetails` — the collapse
  hotzone stays toggle-based until those setters reach the public face.
- The side registry is **process-local**: after a `dsh web` restart sides no
  longer appear in the panel (that is the default semantics — see the
  retention contract below).

## Roadmap

- Image input for side conversations (`sessions.prompt` accepts inline image
  parts; the composer UI is pending);
- Optionally inject board content into the side boundary context (let a side
  work against the objective/next steps);
- AI proposals for the board (the `assistant-proposal` data model and locked
  items are ready; the proposal entry point is pending);
- True free-drag sidebar (the host's internal layout store already has
  `setSidebar`; waiting for exposure on `ctx.layout`);
- Notification extension: subagent/goal phase changes into the same unseen
  queue;
- Persistent mode: attach children to the parent's workspace (gap found in
  the fork spike, see `docs/fork-spike.md`).

## Retention contract (read before changing)

**Default is `ephemeral` (ChatGPT-style temporary side threads):** archived on
creation (never in the left session list), registry is process-local (forgotten
on restart), idle past TTL is swept by the host (running children are never
swept), and cleanup failures stay visible with bounded retries. Set
`retention: persistent` for the pre-0.2 durable top-level-session behavior.

## License

MIT. Third-party notices in
[licenses/THIRD-PARTY-NOTICES.md](licenses/THIRD-PARTY-NOTICES.md).

Install

dsh plugin --profile web add github:invalidnaaaame/dsh-side-workspace

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source