dsh-recent-tasks
English | 中文

A collapsible 「最近任务」/ Recent tasks block at the section level of the DeepSeek Harness sidebar. Chats that have no workspace land there instead of piling up in 未分组 — and because the block is a real workspace record rather than a look-alike drawn by a plugin, a chat started from it genuinely belongs to it.
DSH groups every chat under the workspace its session cwd resolves to. A chat started without a workspace gets no group at all, so it falls into the 未分组 bucket together with archived folders and deleted projects. This plugin registers one real workspace record for a dedicated folder: "no workspace" becomes an ordinary group with ordinary behaviour — rename, drag order, pin, archive, search — and that group is promoted into a collapsible block of its own, level with 工作区 and collapsing independently of it.
| Group | A genuine workspace record for <recentTasksDir>, registered through ctx.workspaceRegistry and created as the last group; the plugin never rewrites the durable order afterwards, so where you drag it is where it stays |
| Entry | + 最近任务对话 beside Settings at the sidebar foot, plus the Mod+Alt+N / Mod+Shift+N shortcut — both start a session in that workspace |
| Settings | 最近任务 rows inside Settings → General — folder, group name, auto-repair — backed by this plugin's official live config |
| Host API | POST /dsh-recent-tasks/api/state and /ensure, read-only and idempotent; the only write path is the official settings form |
Screenshots
The block behaves like any other group: GPT-6 特点介绍 is the chat that is running right now, older ones show how long ago they were used, and + 最近任务对话 at the sidebar foot starts a new chat in that folder. (This capture predates the promotion to a section-level block; the section below describes how the list reads now.)
Why a real workspace record
The alternative — a plugin-drawn group — cannot work: the sidebar's groups are the workspace storage domain, and no slot lets a third-party plugin insert a parallel group. Registering a record instead of shadowing the sidebar means:
- sessions started there are normal sessions whose
cwd is the recent-tasks folder, so the group's sessionIds fills itself through the official path (Workspace.sessionIds is filtered by realpath(cwd) === path, which also makes it impossible for a chat to be dragged into another group);
- the file-access boundary is unchanged: the session's sandbox root is that folder, and escalation inside the chat works exactly as in a workspace chat (design §8.7);
- nothing official is patched, no
workspace locale string is touched, and 未分组 keeps working untouched; the plugin's own group name is kept in the active language by renaming its own record, never the built-in namespace and never another group.
最近任务 reads as a block-level section beside the official workspaces heading, and the two collapse independently: the arrow in front of "Workspaces" hides the other workspace groups (Recent tasks stays), while the arrow in front of "Recent tasks" collapses its own chats, which sit one indent step in from its title.
- This is presentation only: the group is still one real workspace record, and the plugin writes no workspace data — it neither reorders the list nor touches any group but its own.
- It keys on official stable attributes (
data-row-key, role, the inline --dsh-workspace-indent) and never on official class names, which are build hashes that change between releases. If a DSH update moves those anchors the promotion disables itself: the sidebar falls back to the stock look with nothing left behind, one line in the console.
- It also stands down for the flat list, while searching, in the 56px rail, and in tree mode when the folder is nested under another workspace (then it is not a peer). Switching back re-applies it immediately.
- Known trade-offs: the block scrolls with the workspace list; while "Workspaces" is collapsed the highlight of a chat inside a hidden group is not visible until you expand it again; and on the single frame in which the official UI re-renders the container (before our replay runs) a collapsed group can flash back into view.
- The collapse flag lives in your own browser storage (
dsh.recent-tasks.workspaces-collapsed.v1); it is never reported anywhere.
Requirements
- DSH with a
web or desktop profile, 0.2.0-rc.1 or newer (developed and verified on 0.2.0-rc.2, cordis ~4.0.4)
- Node.js
>= 22.19.0 — only to build from source
- pnpm is recommended when installing into a profile
- No network access, no native modules, no install scripts, no runtime dependencies
Install
The bundle entry (id: recent-tasks) is self-declared by this package's cordis.patch.yml — no manual patch file is needed.
From npm (recommended)
dsh plugin --profile desktop add dsh-recent-tasks
Published on npm as dsh-recent-tasks. The package declares its repository, so the plugin market matches it back to this repo and can offer version and download information; installs are prebuilt and skip the build-approval step entirely. This is the path the plugin market prefers.
From GitHub
Install by repository identity to get the tip of main — useful for trying an unreleased change, and it is also how the plugin market resolves this package to winditer/dsh-recent-tasks when npm is not used:
dsh plugin --profile desktop add github:winditer/dsh-recent-tasks
For a web profile use --profile web. Restart DSH (quit fully and reopen); the 最近任务 group appears at the foot of the sidebar.
The built browser half (dist/client.js) is committed on purpose. A github: install never runs this package's build — pnpm blocks lifecycle scripts by default — so the committed bundle is what makes this install path work out of the box. Nothing is compiled on the consuming side.
From the app
Settings → Plugins → install bundle, pointing at this directory (or a file: spec). The script scripts/install-into-profile.sh is the manual equivalent: pnpm add file:<repo> plus a dsh.profile.bundles entry.
The dependencies entry is not optional — a bundle listed only in dsh.profile.bundles is erased by the desktop crash-recovery path, which re-derives the list from dependencies.
From the prebuilt tarball
Every release attaches the packed package, so the whole repository is not downloaded and nothing is compiled on the consuming side:
dsh plugin --profile desktop add https://github.com/winditer/dsh-recent-tasks/releases/download/v0.3.0/dsh-recent-tasks-0.3.0.tgz
The URL is pinned to the tag rather than written as releases/latest/download/… on purpose: latest resolves per request but the filename is literal, so a versioned asset name breaks that URL the moment the next release renames it.
From source (development)
git clone https://github.com/winditer/dsh-recent-tasks.git && cd dsh-recent-tasks
npm install
npm run build
dsh plugin --profile desktop add .
dist/client.js is regenerated by node scripts/build.mjs before every launch; if it is ever missing, the host half still loads and the app logs client bundle not found; run \pnpm run build` before launch`.
Settings
All three live rows are ordinary official settings: they appear both in Settings → General (rendered by this plugin's own row) and in Settings → Plugins (auto-derived from the plugin's Config), and both surfaces write the same document.
recentTasksDir | ~/Documents/DSH 最近任务 | Folder the group owns. Created on first use; must stay an absolute path. Changing it registers/uses the record for the new folder immediately. |
title | 最近任务 | Group name used when a record is created. While the stored title is still one of this plugin's own default names (最近任务 / Recent tasks) it follows the UI language; any other title is yours and is never rewritten. |
autoRepair | true | Recreate the record if it was deleted from the sidebar. Switching it off turns deletion into a visible error instead of a silent rebuild. |
enabled | true | Master switch (patch only — no form). |
apiPrefix | /dsh-recent-tasks/api | Host API path (patch only). |
Edits are persisted by dsh-config-editor into the active profile's cordis.patch.yml (atomic write, validation, rollback) and hot-apply to the running host: the loader commits new values into the plugin's live Volatile refs and emits loader/volatile-update, which the host half uses to re-ensure immediately. This plugin never writes a config file of its own.
Precedence: the profile patch layer wins over this package's bundled defaults, so a user edit survives upgrades.
Behaviour details
- The API is small and local. Two idempotent POSTs (
state, ensure) requiring content-type: application/json; the JSON-only rule is also the CSRF defence, since a cross-site "simple request" cannot set that type without a preflight this route never answers. Nothing secret is exposed and the plugin never accepts a path or a command from the browser.
- Failure is never silent. Every refusal is a code (
bad-config, dir-not-writable, dir-not-directory, missing-record, disabled, unavailable, …) which the client renders from this plugin's own locale namespace; the host never ships display text.
- State is read-only.
state never mkdirs, never creates, never reorders — a vanished record is reported (workspaceId: null + missing-record) and only ensure repairs it.
- Reuse over creation. If the configured folder is already a workspace, that record is reused and its title is left alone (DSH's
create is idempotent per realpath and ignores the title for an existing record).
- The group name follows the UI language. The sidebar row and the workspace switcher both render the record's durable
title verbatim — DSH localizes only its own default-workspace sentinel, and a plugin cannot add to the built-in workspace locale namespace. So while the stored title is still one of this plugin's own default names (最近任务 / Recent tasks), the client half rewrites it to the active language through the official workspaces.rename — the same call the sidebar's own rename dialog makes — which updates both surfaces immediately and durably. Rename the group yourself in the sidebar, or configure a different title, and the plugin never touches it again. The folder on disk is never renamed: the path is the session-grouping key, so moving it would strand existing chats in 未分组.
- Nested folders are flagged. A recent-tasks folder inside another workspace renders as a child of it; the settings row warns instead of pretending otherwise.
- Deletion is honest. If the group is deleted while
autoRepair is off, both the entry point and the shortcut report missing-record — a chat is never quietly started somewhere else.
- Shortcut fallback.
session.new already owns Mod+N (desktop) and Mod+Alt+N (web), and shortcuts.register rejects an overlapping default. Desktop gets Mod+Alt+N; web/macOS and web/Windows get Mod+Shift+N; web/Linux stays unbound. A residual collision is caught and the sidebar entry stays the guaranteed way in.
Troubleshooting
| No 最近任务 group appears | Check Settings → General → 最近任务 for a coded reason; missing-record + auto-repair off, or dir-not-writable. |
| The group is nested under another workspace | recentTasksDir is inside a registered workspace — pick a folder outside every workspace. |
The entry point says unavailable | The host half is not mounted (row disabled, or the row mounted before workspaceRegistry). Reload the app once after installing. |
| Settings row shows the read-only hint | No configForms in this composition (no official settings surface); edit the recent-tasks row in the profile's cordis.patch.yml. |
| Chats still land in 未分组 | They were created without this workspace's cwd. Existing chat history is never migrated (design §6.3/N3): the group owns new chats started through the entry or the shortcut. |
Development
node scripts/build.mjs
node --test test/*.test.js
node scripts/build.mjs && node --test test/*.test.js
npm pack --dry-run
node_modules/@deepseek-ai/* are symlinks into the extracted 0.2.0-rc.1 tree under .asar/, which only exists so node --test can resolve @deepseek-ai/schemastery; production resolves those specifiers through DSH's own profile interception, so the package declares them as optional peers and ships no runtime dependency.
Layout
src/host.js → lib/index.js | Host half: Config, the registry service, the JSON API, the volatile-config listener |
src/client.js → dist/client.js | Browser half: sidebar entry, settings row, refresh loop, shortcut |
src/client-core.js | React-free logic: state machine, planCreate, snapshot store, shortcut resolution, collapse flag, promotion predicate |
src/client-adapter.js | Sidebar section adapter: locates the official nodes at runtime, stamps data-dsh-rt-*, injects the collapse toggle, replays on re-render (presentation only, writes no data) |
src/protocol.js | Shared vocabulary: codes, methods, path handling, response parsing |
src/locales.js | recentTasks zh/en dictionaries (flat keys, 1:1 key sets) |
assets/ | Screenshots used by this README (and picked up by the plugin market) |
screenshots.json | The screenshot list the plugin market reads (its convention: declared beside package.json, paths relative to the repo) |
test/ | protocol · host · client · adapter (hand-rolled DOM) · bundle |
docs/superpowers/ | This repo's own specs and implementation plans (specs/, plans/) |
.agents/notes/ | Implementation notes, including verified official facts and deviations from the draft spec |
License
MIT © 2026 dsh-recent-tasks contributors