dsh-interaction-kit
English | 中文
Interaction and convenience upgrades for the DSH Web UI. One plugin, three surfaces, all built from the shipped UI primitives so nothing looks bolted on.
- Right-click a Session — the same menu the
⋯button opens (fork, rename, archive, pin, permanent delete, …), instead of having to hunt for the three dots. - Branch chip under the composer — the workspace's current Git branch, click to switch. The menu is the native searchable menu: pinned search field, sticky section heading, branch list.
- Worktree checkbox — opt in per Session. On the first file write, a worktree is created and the Session moves into it, so parallel work stops colliding in one checkout.
Features
Session right-click menu
Right-clicking anywhere on a Session row opens exactly the menu the ⋯ trigger opens — it drives
the real trigger, not a copy of it, so every item (including ones other plugins add) is present.
The trigger is identified by its accessible name (会话"{title}"的操作 / Session "{title}" actions), never by position. That matters: the hover strip next to it also holds archive and
pin, so a positional lookup would archive the Session instead of opening the menu. If the
trigger cannot be identified, the plugin installs no listener and leaves the native behaviour alone.
Permanent delete
A row appended to that menu, styled as a danger action. Where the built-in menu only archives, this removes the Session for good:
- archive with
stopActivity(so a running turn is stopped first), - unarchive, then unpin (clears the flags that pin the record),
- detach the Session from every workspace that lists it,
- delete the Session log directory under
$DSH_HOME/sessions/, - delete the single projection-cache file for that Session,
- clear the plugin's own per-Session state,
- emit
api-session/removedso open views drop it.
Confirmation is a destructive-action dialog, not a plain OK box.
Known residue:
storages/session_projcache.jsonkeeps one stale derived entry for the deleted Session. It is a cache index, it is never shown, and it is deliberately left alone rather than hand-editing a shared app file.
Branch chip and switching
The chip sits in conversation.input.left — the composer's tool row, immediately beside the mode
selector — and shows the branch of the Session's workspace, with a warning tone when the checkout is
detached, plus an uncommitted-file count.
That seat is deliberate. conversation.composer.dock is below the card, but the shipped InputBar
renders it only for variant: "composer", and a Session with no messages yet is rendered as the
hero variant — so that seat is empty for exactly as long as the repository is still being chosen.
conversation.input.left is mounted in both variants, which is why the branch and the worktree
option are available before the first message rather than after it.
Clicking it opens a list of local branches. A repository with a thousand branches is normal, so the list renders at most 60 rows and the pinned search field narrows the rest; the heading states the matched/total count while a filter is active. Creating a new branch (and checking it out) is the last row.
Switching never discards work: a checkout that Git refuses is reported in place, and the menu stays open with the error rather than closing on a half-applied change.
Worktree mode
A checkbox on the same row as the branch chip, deliberately not inside the branch menu. When it is on and the Session has no worktree yet, the first write tool call creates one:
<repository>/.worktrees/<branch> branch: <session>-<slug>
and relocates the Session's working directory into it. Subsequent writes stay there. Turning the checkbox off returns the Session to the repository root.
Creation prefers the host's own worktrees service, so it honours whatever sandbox and policy the
profile applies; it falls back to git worktree add only when no live Agent exists to create one.
The label names the checkout this Session is in — worktree(feature/topic-b) — rather than counting
worktrees. A count says nothing about where the next write lands; the branch is the name an operator
recognises, and a checkout created without one falls back to its directory name.
Once the conversation has started, the option is locked on. Turning it off then would not undo
anything: the writes that already ran in the worktree would stay there while later ones reopened the
main checkout, splitting one Session across two directories. The Host refuses that transition with
worktree-locked and reports worktreeLocked in status; the checkbox disables itself and its
tooltip says why. A Session that has not exchanged a message is still free to change its mind.
Install
dsh plugin --profile web add github:yixin2021/dsh-interaction-kit
Reload the page. Removing it:
dsh plugin --profile web rm dsh-interaction-kit
From a local checkout, install the directory instead:
dsh plugin --profile web add /absolute/path/to/dsh-interaction-kit
Updating
package.json in the profile records the spec, but the lockfile pins the exact commit the spec
resolved to:
specifier: github:yixin2021/dsh-interaction-kit#main
version: https://codeload.github.com/yixin2021/dsh-interaction-kit/tar.gz/3eeee4519883...
Which means the spec you typed is not what you are running — the recorded commit is. Two consequences, and they differ:
Following a branch (#main) needs a remove. Re-adding an identical spec is refused as
already-installed, because from the manager's view nothing changed:
dsh plugin --profile web rm dsh-interaction-kit
dsh plugin --profile web add github:yixin2021/dsh-interaction-kit#main
In the desktop app that is the plugin's Remove followed by installing from the source again.
Switching to a tag or commit needs no remove — the spec differs, so it installs over the top and resolves fresh:
dsh plugin --profile web add github:yixin2021/dsh-interaction-kit#v1.0.0
dsh plugin --profile web add github:yixin2021/dsh-interaction-kit#e4aa663
After either one, restart DSH: the Host half is imported once at profile start, so reloading the page alone leaves the old Host code running.
Two install modes, one tradeoff
| Spec | Editing the source… | Use it when |
|---|---|---|
/local/path or link:/local/path |
takes effect immediately (plus the reload rules below) | you are developing the plugin |
github:owner/repo |
has no effect until you push and reinstall | you want the installed copy to be exactly what is published |
A link: install is a symlink into your checkout, so the running plugin sees your working tree.
A github: install is a real directory unpacked from the tarball — read-only in practice, and
identical to what everyone else gets. Switching between them is just rm + add.
What needs a reload and what needs a restart
| Change | Takes effect |
|---|---|
client.js |
Client bytes are snapshotted when the plugin activates — disable/enable the plugin (or restart), then reload the page |
index.js |
Imported once at profile start — restart DSH |
How it works
| Half | File | Responsibility |
|---|---|---|
| Host | index.js |
One authenticated route, POST /api/interaction-kit, plus a tools/pre-execute hook for auto-worktree. All git and filesystem work happens here. |
| Client | client.js |
Five slot registrations: composer tool row, branch menu overlay, Session menu row, Session row right-click, delete dialog. |
The page never runs git itself. The Client half only sends {action, sessionId, …} and renders
the answer, so a repository path or a branch name from the page is validated on the Host
(NAME_PATTERN) before it reaches a command line.
State lives in $DSH_HOME/storages/interaction-kit-toolkit.json:
{ "version": 1, "worktreeMode": {}, "worktrees": {} }
Writes are queued and atomic, so two Sessions toggling at once cannot corrupt it.
Slots
| Slot | Purpose |
|---|---|
conversation.input.left |
Branch chip + worktree checkbox, beside the mode selector |
shell.overlay |
Branch menu, create-branch dialog, delete confirmation |
sidebar.workspaces.session.menu.item |
Permanent delete row |
sidebar.workspaces.session.row.action |
Right-click → menu |
Development
npm run check # node --check on both halves
npm test # client + layout harnesses
node verify-host.mjs <sessionId> # drives the Host route against a sandboxed $DSH_HOME
verify-layout.mjs renders the slot components against a stub React and asserts the resulting tree
— which primitive was used, which classes, which locale strings, and the safety properties (no
archive click, no viewport-wide listener). The harnesses are development-only: files in
package.json keeps them out of the published tarball, so a github: install never receives them.
Releasing
# 1. bump "version" in package.json, then
git commit -am "chore: release v1.0.1"
git tag -a v1.0.1 -m "v1.0.1"
git push origin main --follow-tags
Consumers pin #v1.0.1; anyone on #main re-resolves on their next remove + add.
License
MIT
No comments yet. Be the first to write one.