dsh-side-tasks
Codex-style side tasks for the DSH Web GUI. Each side task forks the current conversation into a branch — a new session id that inherits the full project context (cwd, model, history up to the last completed turn) — and shows it as a chat tab in the dsh-better-sidebar right sidebar. You keep talking to the branch while it runs in the background; the main conversation is never blocked. Branches are temporary: closing one confirms and permanently deletes it from the DSH store.
Features
- Branch of the current conversation —
sessions.fork: new session id +
full inherited project context (as of the moment you open it)
- Chat tab — message history (including the inherited context), input +
send (Enter to send, Shift+Enter for a newline), live polling updates
- Sidebar tab — registered via
ctx.betterSidebar.registerTab
(requires dsh-better-sidebar); opened from the + menu or the tab bar; badge shows the running branch count; the tab's ✕ closes with a confirm
- Single side task — a new branch replaces the previous one (the ledger
keeps exactly one)
- Live status — idle / running / done / failed / cancelled via Host
polling + SSE push
- Cancel a running branch turn (idempotent)
- Close with confirm — ✕ asks 「此侧边任务将不保留,是否关闭?」 then
cancels, drops the branch, and permanently deletes its session from the DSH SQLite store (events cascade)
- Clean up history — one-click removal of leftover side-task sessions
from previous runs
Requirements
- DSH 0.1.0-rc.6+, Node >= 22.5 (
node:sqlitepowers permanent deletion
on close; on Node 22.0–22.4 the plugin still runs, but deletion degrades to a no-op)
- dsh-better-sidebar >= 0.12.0 (required) — the side task is delivered as
a right-sidebar tab; without it the plugin does not load and logs an install hint (no DOM fallback)
Files
dsh-side-tasks/
├── package.json # dual-face bundle declaration (Host + ./client)
├── cordis.patch.yml # one-line insert row into the web profile roster
├── src/
│ ├── index.js # Host half: BranchService (fork/prompt/cancel/close
│ │ # + history proxy + poll + SSE + SQLite deletion)
│ └── client.js # Browser half: better-sidebar tab + chat panel
├── README.md
└── README.zh.mdInstall
# from the plugin directory (this repo root)
dsh plugin --profile web add link:/path/to/dsh-side-tasks
# verify the insert row
dsh --profile web --dump-config | grep -i side-tasks
# restart the web GUI (Host code changes need it; the page interrupts once)
# then refresh http://127.0.0.1:3080Uninstall:
dsh plugin --profile web remove dsh-side-tasksVerify
- [ ] The 「侧边任务」 tab appears in the better-sidebar
+menu - [ ] Opening it forks the current conversation (history mirrors the context)
- [ ] Sending a message runs in the background; replies appear via polling
- [ ] The main conversation keeps working while a branch runs (parallelism)
- [ ] The tab badge shows the running state
- [ ] ✕ asks 「此侧边任务将不保留,是否关闭?」; confirming removes the branch
and its session from the DSH store
Design notes
- Host injects
apiProxy/webServer/systemPrompt; a branch is created
with sessions.fork({ sessionId }) (inherits cwd/model/lineage/seed history), then sessions.prompt(mode: 'queue') queues chat messages.
- Status reconciliation polls
sessions.listand confirms aturn/endat or
after the last prompt (mirrors the task-board settlement logic).
- Routes:
GET /api/side-tasks/state,GET /api/side-tasks/history,
POST /api/side-tasks/action (fork / prompt / cancel / close / purge), GET /api/side-tasks/events (SSE + heartbeat), guarded by a loopback / same-origin fence; the action union contains no command/path/shell fields.
- The client is plain DOM (no React) with a thin React shell for the
better-sidebar tab; styled with the DSH --dsw-* theme tokens. Chat messages poll the history route every 2 s (merged by seq).
- better-sidebar integration follows its external plugin guide:
ctx.get('betterSidebar') is probed (never injected), with a short retry ladder for the 454 KB bundle; registration is wrapped in ctx.effect for HMR-safe disposal; tab close is intercepted (DOM capture) so a declined confirm keeps the tab open.
- Permanent deletion: the platform exposes no session-delete API, so on
close the plugin removes the session row from the DSH SQLite store (~/.dsh-cc/sessions.sqlite, configurable via dbPath) — events cascade via ON DELETE CASCADE and the search index reconciles. Only sessions this plugin forked are ever touched. A running DSH process keeps in-memory caches, so deleted branches vanish from the session list on the next DSH restart.
node:sqliteis loaded lazily so the plugin boots on older Node 22 (the
delete feature degrades instead of crashing).
License
MIT