English | 简体中文
dsh-agent-hub
A DeepSeek Harness plugin that brings several different agents (Claude, Codex, MiniMax…) together in one place, with the long-term goal of coordinating multiple tasks across them.
Current modules: Connectors (Linear, Notion and other external services), Board (Sentry alerts, Linear tickets and local tasks), Reader (RSS and GitHub stars), Plan usage, Claude Agent, Cursor, Qoder & glasses, Hermes, Relay (view and take over Qoder and Hermes conversations held elsewhere), Phone access, Source Control (Git, with worktrees), Code editor, Bots and group chats (multi-agent collaboration), Settings sync (share settings across machines through a Git remote), Review (PRs waiting for my review, and answering reviews on my own PRs), Script tasks (restricted helpers driven by native scheduled tasks).
Of these, Reader, Relay, Clipboard window, Cardputer and Codex are optional modules, each switched on or off in the Modules card at the top of Settings → Agent Hub: only Codex is on by default; the other four are off. A module that is off stops its background work, and its sidebar panel or page and its settings group are hidden; switches apply immediately, with no restart. See Configuration: Settings → Agent Hub.
The settings page folds by module: each collapsed group takes a single line showing its name and a summary of its current state (e.g. "Linear connected · Notion sign-in expired"); click the line to expand it. Expanded state is remembered per browser, and the top of the page offers "Expand all / Collapse all". A group with a problem that needs attention (expired sign-in, invalid value) expands itself and is flagged.
[!IMPORTANT]
Codex requires the separate dsh-codex-subscription plugin. Codex models, Codex plan usage and account switching are all provided by dsh-codex-subscription; this plugin only reads its account store and calls its interfaces. Install and enable dsh-codex-subscription from DSH's plugin manager, then sign in under Settings → Codex subscription. While it is missing or off, the Codex card in Plan usage and the Codex models setting under Settings → Agent Hub → Codex show a hint with a one-click Install (or Turn on, when it is installed but off) through DSH's plugin manager; restart DSH when asked, then sign in under Settings → Codex subscription.
Configuration: Settings → Agent Hub
The plugin's own options live on the Agent Hub page of DSH's settings (below "Plan usage"), with the Modules card on top and the groups under four headings: Agents & models (Claude Agent, Qoder & glasses, Model selector, Bots), Workflows (Connectors, Review, Script tasks, Board, Git commits), Conversations & interface (Conversations, Fonts and sizes), Access & devices (Phone access, plus Cardputer and Clipboard & translation while their modules are on). The page's toolbar stays pinned at the top: while any group is expanded the button reads "Collapse all" (clicking it further down the page also scrolls back to the top); once everything is collapsed it becomes "Expand all". On narrow screens only the icons show. Profile-wide settings sync and plugin upgrades are not on this page; see Sync & updates.
Modules: below the toolbar and above the four headings, a Modules card is pinned with one switch per optional module:
- Reader, Relay, Codex: synced between machines with Settings sync.
- Clipboard window, Cardputer: this machine only, not synced.
Turning a module off stops its background work (scheduled refreshes, daemons, the native helper, the device port and so on), and hides its sidebar panel or page and its settings group; turning it back on restores them. Switches apply immediately, with no restart. The configuration lives in the modules section of cordis.patch.yml; the old palette.enabled key is replaced by modules.palette.
The table below lists the options by module (configuration is stored where it was in earlier versions; the regrouping needs no migration):
| Module | Option | Default | Description |
|---|
| Modules | Reader | Off | Reader in the left sidebar: feeds, scheduled refresh and the reading page; synced |
| Relay | Off | The Relay panel in the left sidebar and its completion notifications; synced |
| Clipboard window | Off | The clipboard & translation floating window, its global hotkey and clipboard recording (modules.palette, replacing the old palette.enabled); desktop only, this machine only, not synced |
| Cardputer | Off | Cardputer pairing, port and dictation (modules.device); this machine only, not synced |
| Codex | On | The Codex models setting and the Codex card in Plan usage; needs the separate dsh-codex-subscription plugin; synced |
| Conversations | Instructions (Markdown) | Blank | Injected into every conversation with any model: DSH models get it at the end of the system prompt, Claude Agent appends it to Claude Code's system prompt, and bots get it too, after their persona. Blank injects nothing. Claude Agent sessions resume into a fresh process on their next turn to pick up the change |
| Canvases | On | Have every model deliver standalone charts, tables and dashboards as interactive, self-contained HTML files in ~/.dsh/canvases, linked in its reply and browsed together in the Canvases panel on the left |
| Archify diagrams | Off | With canvases on, turning it on downloads a pinned Archify release (checked by its SHA-256, installed in ~/.dsh/dsh-agent-hub/archify/<version>) and every model draws architecture, workflow, sequence, data-flow, state, ERD and similar diagrams with it, still saved as canvases; the settings show the install state with a retry, and until it is installed canvases stay hand-written. It needs Node and a local Chrome or Edge, and is slower and costlier than a hand-written canvas |
| Selection discussions | On | Selecting chat text or a code block shows "Ask about this"; the temporary discussion runs no tools and does not read the whole main session |
| Discussion model | Automatic | Reuses the background helper model route, or a chosen plain text model; Claude Agent, Cursor and Bots are not supported. Changes affect new discussions only |
Changes save automatically: switches, model choices and resets save at once; typed text and numbers save when you pause typing or leave the field. Every option can be "Reset to default" on its own. Leaving the page saves valid changes and discards drafts from that visit that were invalid or failed to save, without affecting new edits after you come back. Saving goes through DSH's settings service into the current profile's cordis.patch.yml (for the desktop profile: $DSH_HOME/profiles/desktop/cordis.patch.yml) and applies immediately, with no restart. You can also edit that file directly (restart DSH afterwards):
- id: agent-hub
config:
modules: # Module switches (the defaults shown)
reader: false # Reader; synced
relay: false # Relay; synced
palette: false # Clipboard window; this machine only (replaces the old palette.enabled)
device: false # Cardputer; this machine only
codex: true # Codex; needs the dsh-codex-subscription plugin; synced
prompt:
instructions: | # Global instructions (Markdown), injected into every conversation
## Communication
- Answer in English; keep code and identifiers as they are
canvas: true # Canvases: charts, tables and dashboards as HTML in ~/.dsh/canvases
archify: false # Install Archify and draw diagrams with it (needs canvases on)
connectors:
servers: # Only the id and URL; sign-ins live in DSH's credential store
- id: linear
url: https://mcp.linear.app/mcp
repos: [C:\pro\app] # Optional: which local repositories (main trees) this Linear account is for; omitted = all repositories
claude:
remoteControl: true
fullAccess: false # true = full access for new processes; once off, existing full-access processes restore approvals at the next safe turn
keepOnline: 3
idleHours: 2
models: [opus, sonnet] # empty = all
helperProvider: deepseek-official # Model for background requests (compaction, titles); both blank = automatic
helperModel: deepseek-v4-flash
codex:
models: [gpt-6.1-sol] # empty = all; ["(none)"] = hide all
models:
hiddenProviders: [deepseek-official, deepseek-account]
effort: high # Default thinking level when picking a model; omitted = each model's own default
efforts: # Per-model overrides, over effort
- provider: claude-agent
model: opus
effort: max
appearance:
uiFont: default # default / roboto-mono / jetbrains-mono / source-code-pro / fira-code / ibm-plex-mono
codeFont: jetbrains-mono
uiScale: 100 # 80–130 (%); scales all interface text, keeping relative sizes; conversation text size is still set by DSH General → Font size
regions: # Per-region adjustment, in percentage points on top of the whole (−20–20); 0 = follow the whole
sidebar: 10 # sidebar / main / panel / settings / popup
remote:
httpsPort: 443
git:
provider: deepseek-official
model: deepseek-v4-flash
instructions: Use Conventional Commits, write the subject in Chinese
team:
agents:
- id: reviewer
description: Read-only diff review
provider: claude-agent
model: opus
persona: You are a rigorous code reviewer…
- id: coder
provider: openai-codex
model: gpt-5.6-terra
tools: [read, edit, bash]
sync:
remote: git@github.com:me/dsh-settings.git
branch: main
fetchOnStart: true
localSettings: [terminal-controller, ui-theme.config.preference] # Whole entries, or settings within an entry
review:
repos: [/Users/vi/pro/app] # Repository main folders on this machine; not exported
at: "09:00" # First migration only; afterwards edit the time in the host automation task
bot: reviewer # Model + persona (analysis prompt) both come from this bot
commentBot: en # Bot used by default when ✦ Process is clicked on a hand-written comment under the board's Changes; blank = not processed
helperProvider: minimax-cn # Automatic sync coordination, Dig in, Rewrite; both blank = Claude Agent's background request model
helperModel: MiniMax-M3
style: |
Comments in English, praise the implementation first, short and casual, no dashes
rules: |
- Ignore files under generated/
explain: | # Answer requirements for ✦ Explain under the board's Changes; blank = purpose / effect / how it takes effect
Answer in three parts: **Purpose**, **Scope of impact**, **Risks**, at most two sentences each
explainBot: reviewer # ✦ Explain and follow-ups use this bot's persona and model; blank = the helper model
maxPerRun: 10
alerts:
url: https://acme.sentry.io/issues/?environment=prod&project=1234567&statsPeriod=14d
repo: C:\\pro\\app # Local repository main working tree; not exported
bot: reviewer
Under the hood, every section of Config is declared .volatile() (src/config.ts): DSH lets the settings interface edit only volatile fields, the Loader hands new values straight to the running plugin without remounting it, and each module reads the latest values every time it uses them; phone access rebuilds its forwarding when the port changes.
Export settings / Import settings… at the top of the page carry the settings to another machine (between Windows, macOS and Linux in any direction):
- Export downloads
agent-hub-config-<date>.json, containing only the settings the current profile has changed; options left at their defaults are not written, so after import they follow the target machine's defaults. Sign-ins and tokens live in DSH's credential store and are never written to the file; imported connectors must be connected again on the new machine.
- A connector's linked repositories are exported in portable form:
{ "name": "app", "path": "C:/pro/app" } (paths always use /). On import, a registered repository with the same path is looked for first, then a unique repository with the same name; if neither is found, the original path is kept and you are told, so you can link it again under Connectors later. The Sentry alerts "Analysis repository path" is valid only on its machine and, like the review repository list, is not exported. Multi-line text (global instructions, personas, etc.) is normalized to LF line endings.
- Before importing, you see how many settings will be applied, how many will go back to their defaults, and which settings this version does not know or would refuse were skipped; on confirmation the current profile's settings are replaced in one go, and unsaved drafts are discarded.
Module: Script tasks
Settings → Agent Hub → Script tasks: binds trusted local scripts to DSH native scheduled tasks, where a dedicated session on a chosen model or compatible Bot calls the one restricted tool. Program, arguments and working directory are saved explicitly; by default no task is created and nothing runs. Time and time zone are edited in the native task manager. A A Qoder CN daily credits template is included: it appears only on Windows with the Qoder CN desktop app installed, only checks by default, allows claiming only when a claim argument is added explicitly, and never switches to the international edition. Runs use the current user's permissions; this is not a script sandbox.
See usage, result protocol and recovery boundaries.
Settings → Sync & updates
Profile-wide settings sync and plugin upgrades live on a separate Sync & updates page in DSH's settings (below Agent Hub). Both affect every plugin, and both may need a restart to take effect: when such a change is pending, a banner at the top of the page explains why, lists the upgrades queued for the restart, and offers Restart now (DSH Desktop only; under dsh web it asks you to restart by hand).
| Group | Option | Default | Description |
|---|
| Settings sync | Remote repository | Blank | Any URL git understands (GitHub, Gitee, a self-hosted server, a local or network path), using this machine's git sign-in; blank = no sync. See Module: Settings sync |
| Branch | main | The branch used on the remote |
| Pull at startup | On | Pulls the remote when DSH starts: applies the remote's changes directly when nothing conflicts (this machine's changes are not pushed); a conflict is only announced |
| Not synced | terminal-controller, review.auto, remote.phoneSettings | Whole entries, or settings within an entry, that each machine keeps for itself; picked from this machine's and the remote's configuration, no typing needed |
These options keep the same storage keys as before (still under config.sync of the agent-hub entry), so moving them off the Agent Hub page needs no migration. On a phone only a notice is shown; settings sync can only be set up on a computer.
Plugin updates:
- Click Check updates to read the latest versions of installed plugins from the current profile's registry; nothing is installed automatically.
- Tick the plugins to upgrade (select all is supported), review the current and target versions, then confirm.
- Normal mode: installs through DSH's native installer one at a time, showing progress, install output and failure reasons live; failed items can be retried. Settings and existing enablement are kept, and build scripts are not approved automatically. Agent Hub itself goes last, so the queue is never replaced midway. Restart DSH afterwards to apply.
- HMR hot-reload mode: DSH refuses to replace installed plugins in place, so the button becomes Upgrade at restart. The exact chosen versions go into the pending-restart list (
$DSH_HOME/dsh-agent-hub/pending-upgrades.json); you can keep queuing, or cancel items one by one. At restart, once DSH has fully quit, the restart helper runs pnpm add <name>@<version> --save-exact once in the profile folder; if it fails, package.json and the lockfile are restored. When DSH reopens, a toast in the bottom-right corner reports the result; this page keeps the pnpm output until you click "Dismiss".
Built-in, read-only, locally linked and Git-sourced plugins are not upgraded; the list explains why each is skipped. Upgrades requested by settings sync go through the same pending-restart list.
Module: Connectors
Connect external services (Linear, Notion, Sentry, GitHub…) as remote MCP servers once, here, and every model in DSH can use their tools: DeepSeek, Codex, MiniMax and other DSH models use them directly, and Claude Agent through a bridge. The entry point is Settings → Agent Hub → Connectors.
- Add: click a preset (Linear, Notion, Sentry, GitHub), or "Other MCP server" and enter a Streamable HTTP URL. Connecting starts as soon as it is added, and each card shows only the next step:
- Services with browser sign-in (OAuth per the MCP spec: RFC 9728 resource metadata → RFC 8414 authorization server → RFC 7591 dynamic registration → PKCE) open the authorization page; once done, the card turns into "Connected · N tools" by itself. The callback lands on a
127.0.0.1 port opened just for that sign-in (RFC 8252), so it works in both the desktop app and a browser. If the popup is blocked, click "Open the sign-in page again".
- Services without browser sign-in (such as GitHub) ask for an access token straight away; services that do support it can also use an API key via "Use a token" (a Linear personal API key works). The token is tried with a real connection first and is not saved if refused.
- When authorization expires the card shows "Sign-in expired", with one-click reconnect. OAuth tokens are refreshed automatically before they expire.
- Disconnect / Remove: deletes the credentials and withdraws all of that service's tools.
- Credentials: OAuth tokens and API keys live in DSH's credential store (
dsh-agent-hub/connector-<id> in $DSH_HOME/.credentials.yaml), never in cordis.patch.yml, and are never sent to the browser. The configuration file holds only the id and URL. Credentials are bound to the exact MCP URL, so changing the URL means connecting again; legacy API-key / no-auth records are bound to the current URL the first time they are read, and legacy OAuth records are migrated after checking the resource identifier. Legacy records carry no URL history, so if you changed the URL before upgrading, disconnect and connect again.
- Tool names: connector
<id>'s tools are named mcp__<id>__<tool> in every model (e.g. mcp__linear__list_issues). Every call goes through DSH's tool policy and approvals.
- Claude Agent: each connector is an in-process MCP server of the same name inside Claude Code, with the same tool names as on the DSH side, deferred behind Claude Code's tool search and loaded on demand. Claude Agent sessions use only the connectors here: the plugin starts Claude Code with
--strict-mcp-config and turns off claude.ai connectors, so none of the MCP servers configured in Claude Code itself (~/.claude, plugins, claude.ai) load, and every model reaches external services through the same set of connectors. When connectors are added or removed, the session resumes into a fresh process on its next turn.
- Linear and worktrees: with Linear connected, the branch-name field of the Git pane's "New worktree" dialog also searches issues: on opening it lists the unfinished issues assigned to me, and typing an identifier () or keywords filters instantly. Picking one fills in Linear's suggested branch name (e.g. ), and the worktree is created as usual; the new session's composer is prefilled with the issue identifier, title and link, ready to send, and the agent can read the latest content with . The link between worktree and ticket (Linear's own convention) and is not stored anywhere else: when a branch name contains an identifier with one of the workspace's team prefixes, the worktree list shows the issue identifier and status on that row; click it to open it in Linear.
Module: Board
Board, below "Automation tasks" in the left sidebar, opens in the main area. It is a lightweight take on the board in @linxin666/dsh-client-ui-task-board: just lanes, cards, details and local tasks, without scheduled runs, subtasks or permission prompts. The board puts task cards in one set of lanes (Backlog / Todo / In progress / Done / Canceled) and can show alerts and reviews on the far left:
- Sentry alerts: with a Sentry Issues URL and a local repository configured, the leftmost lane shows unresolved
prod issues; switch between 7d / 14d, and sort ascending or descending by Last Seen / Age / Events. Cards show the title, an error preview, a likely path:line, time and event count; a read failure (Sentry not connected, permissions, etc.) shows its reason right in the alerts lane. Details unfold into the analysis conclusion, source location, exception chain and event evidence; the source location opens the code in the issue's worktree.
- Starting an analysis means choosing a bot (this device remembers the last choice; Seer is never offered). On click the card immediately enters "Preparing" (at most 2 analyses at once, the rest queue); if it cannot start, the reason shows on the card with a retry. It then fetches and creates or reuses a branch and worktree, named with the normalized issue ID, from the latest
origin/<default branch>. Sentry evidence is capped at about 60 KB, and the record is written to <repo>/.reviews/sentry-<ID>.md; during analysis only the record may be written.
- "Fix" continues in the same worktree, with the same bot and the same session context, allows code changes and asks for one commit containing
Fixes <ID>, then goes back to read-only. When Last Seen is later than the time recorded at analysis/fix (after allowing for the rounding in the list's "3 hours ago"), the card is marked "New events" and can be fixed again.
- "Resolve" is an explicit Sentry call made by the interface, with 10 seconds to undo; the model may never write to Sentry. After the undo window, only worktrees and branches the plugin created itself, that are still clean and have no new commits relative to their base, are cleaned up; everything else is kept.
- Linear tickets: from every connected Linear account, "Mine" by default, switchable to "Everyone"; done and canceled ones are kept for the last 14 days only. Status is stored in Linear. Dragging a card to another lane moves it to the team's ordinary working status of that kind, e.g. dragging to "In progress" picks In Progress, not In Review; the details let you pick a specific status.
- Local tasks: for work outside Linear, kept only in
$DSH_HOME/dsh-agent-hub/board.json on this machine. Click + in a lane header or "New task" to type straight into the lane; tasks can be dragged, retitled, described, moved to another repository and deleted.
- To review: when repositories are ticked under Settings → Agent Hub → Review, this lane appears after the alerts lane (leftmost when alerts are not configured), and its header switches between To review | Mine | All: PRs waiting for my review (opening one shows its review sheet), my own PRs (opening one shows its response sheet), and every open PR. Cards in this lane cannot be dragged. See .
Under the hood, each connected service is an instance of DSH's own @deepseek-ai/dsh-mcp-client plugin, mounted by this plugin as a child plugin (with an Authorization: Bearer header), so tool naming, result projection, MCP resources and server instructions, and reconnects all follow DSH's implementation; the instance is remounted when its token refreshes.
Module: Plan usage
See the remaining allowance and reset times of Claude, Codex (multiple accounts), Cursor, Qoder and MiniMax plans in one place.
- Settings → Plan usage: one card per account. Each window (5 hours / weekly …) shows the remaining percentage and a reset countdown; the tick on the progress bar marks "how much should be left right now at an even pace". MiniMax also shows the count used and the plan's expiry date. Cursor shows what is left of the billing cycle's included allowance (the Auto and API pools fold under "More limits") and on-demand spend ("On-demand $219.48 · no cap", or "On-demand $12.30 / $50.00" with a cap, changing color near or over it). Qoder shows one row per credits pool (plan, add-on, team, dedicated packs) with its remaining percentage and "Used 84 / 1,800 credits".
- Sidebar glance card (above "Settings"): one cell per account, showing its name, the remaining percentage of its tightest window and a thin progress bar (Qoder's pools are drawn down in turn, so its cell shows the remaining share of all credits; in the popover each pool has a row, with remaining credits in the last column). The detail popover opens to the right of the sidebar, with each account's windows, remaining amounts and reset countdowns aligned in columns: hovering opens it and moving away hides it; clicking pins it open until you click ×, press Esc or click the glance card again. "Open details" at the bottom of the popover opens the details page.
- Quick account switching in the popover: below the Claude accounts the popover also lists Codex accounts (shown when dsh-codex-subscription is installed with two or more accounts signed in); "Switch" changes the active account through the Codex plugin's own interface, effective from the next Codex request, and usage refreshes right away. Switching never touches Codex's credentials.
- When the Codex plugin is missing: if
dsh-codex-subscription is not installed or not turned on, the Codex card shows a hint with a one-click Install or Turn on (restart DSH when asked, then sign in under Settings → Codex subscription). With Codex switched off in the Modules card, the Codex card is not shown.
Local token statistics are attributed to the provider and model a Bot actually used; Claude's running totals keep separate baselines per account and session. Upgrading rescans the session logs that still exist and keeps the historical totals of deleted sessions; deleted logs cannot have their old Bot attribution restored.
Data sources
| Plan | Method | Notes |
|---|
| Claude | claude -p /usage --output-format json --no-session-persistence | A local command built into the CLI, costing no tokens; OAuth refresh is left to the CLI itself. Plan and email come from claude auth status |
| Codex | Reads the account store of dsh-codex-subscription read-only and calls wham/usage for each account | Never refreshes or writes credentials (refresh tokens rotate, and refreshing on its behalf would sign the Codex plugin out). Once a non-active account's token expires, its last snapshot is shown with the hint "switch to this account and use it once to update" |
| Cursor | Cursor's DashboardService: GetCurrentPeriodUsage, GetPlanInfo, GetHardLimit (the same calls and arithmetic as the CLI's /usage) | Called read-only with the Cursor CLI's own sign-in token (cursor-access-token in the Keychain on macOS, auth.json elsewhere), never refreshed or written; when the token is refused or you are not signed in, it suggests running agent login. With neither the CLI installed nor a sign-in, no card is shown. These interfaces are not public and may break when Cursor changes them |
| Qoder | Readings Qoder CLI CN records itself: every start of interactive qodercn (and /usage) requests /api/v2/quota/usage and writes the full response to ~/.qoder-cn/logs/runs/*/qodercli.log | Qoder's sign-in credentials and request signing are encrypted inside the CLI, so the plugin does not request on its behalf; it only reads the logs of interactive runs incrementally (--acp, -p, the remote-control daemon and subcommands do not read the quota and are skipped), takes the latest entry, and labels the data time from the log time (over 10 minutes shows "data from x hours ago"). With the CLI installed but no reading yet, it asks you to open qodercn once in a terminal |
| MiniMax | GET /v1/api/openplatform/coding_plan/remains | The key is taken from pi-ai's credential records (minimax-cn / minimax), then MINIMAX_CN_API_KEY / MINIMAX_API_KEY; it is used only on the host and never reaches the browser |
The host runs no background timer: the browser polls every 60 seconds (only while the page is visible), and each source has a TTL cache (Claude 5 minutes, Cursor 2 minutes, Qoder 30 seconds, the rest 60 seconds). Every Claude read cold-starts the CLI (seconds of CPU, hundreds of MB of memory), so it reads rarely when idle; after each Claude Agent turn, the next poll rereads it in the background (if over 60 seconds since the last read). Plan and email (claude auth status) are cached for 30 minutes; /usage runs with --strict-mcp-config, so it does not start your configured MCP servers. When the cache is stale the old value is returned first and refreshed in the background; only clicking "Refresh" waits for the latest result. When a read fails, the last successful result persisted in $DSH_HOME/dsh-agent-hub/usage.json is used as a fallback, labeled with its data time.
Module: Claude Agent
A Claude Agent group appears in DSH's existing model selectors (the composer's model slot and the /model dialog). Pick one of its models and every turn of that session is handed to a long-lived Claude Code session, using your Claude Code subscription allowance.
- How it runs: the official Claude Agent SDK drives the
claude CLI installed and signed in on this machine. Claude Code completes the turn with its own tools, skills, ~/.claude settings and permission rules; the reply streams back into DSH as text and thinking, and each internal tool call shows as a reasoning note line (→ Bash: ls, or ✗ … on failure).
- Multiple accounts: Settings → Agent Hub → Claude accounts lets you add accounts and copy a dedicated login command; run it in a terminal, then complete the official browser sign-in with
/login inside Claude. Pick the account directly in the sidebar usage popover; it applies from the next turn without interrupting a running one. Added accounts use the official CLAUDE_CONFIG_DIR to isolate sign-in and settings, while the default account keeps the original CLI folder. Switching accounts never resumes a Claude session across accounts; instead the DSH conversation becomes context for the new account. Removing only unregisters the account and does not delete Claude's own files. Each account's usage is cached separately, so switching does not cold-start again; when refreshing, at most two CLIs cold-start at once.
- Approvals: tool calls Claude Code needs to ask about open DSH's native approval dialog; when the DSH session's approval policy is
never, they are refused outright.
- Session continuity: each reply records Claude Code's session id in
replayState, and the next turn sends only what it has not seen yet (if you switched to another model midway, the conversation in between goes along as context); after a host restart it resumes from Claude Code's own records. A change in reasoning effort or in the bridged tools (connectors, team tools) resumes the session into a fresh process, while a model change switches in place.
- Calling DSH skills with
/: a DSH skill invoked with /name in the composer (including commands from .cursor) has its instructions sent to Claude Code with that turn's prompt, ahead of your own words, so /name is not taken as one of Claude Code's own commands; the reply starts with a note line ⚡ Skill /name. When a name clashes with a built-in Claude Code command the DSH skill wins, and a /command with no matching DSH skill is passed to Claude Code as usual.
- Background requests: Claude Agent only answers the conversation itself. DSH's other requests for a Claude Agent session, such as compacting the context when the conversation gets too long or generating the session title, are answered by , so the turn no longer fails with . "Automatic" uses the model new agents get; when that is Claude Agent or a Bot (which may itself run on Claude Agent), the first other available model. Claude Agent's own context is still managed by Claude Code; these requests only affect the conversation record and title DSH keeps.
Sign-in failure hints: if a Claude Agent turn fails because Claude Code on this machine is not signed in or its sign-in expired, the chat states the reason and the fix (instead of a vague "invalid API key"), and a re-sign-in dialog lists the steps: open a terminal from the sidebar, run claude, type /login and finish in the browser, then click "Check again". Checking again really runs one local CLI command to confirm the sign-in (no tokens spent); once it passes, send again. These failures use their own error code, CLAUDE_AUTH, and are never retried automatically.
Configuration (Remote Control, sessions kept online, idle close time): see Settings → Agent Hub.
Compliance boundaries
- The plugin never reads, stores or forwards any Claude credentials, and implements no sign-in; authentication and token refresh are left entirely to the official CLI.
- When starting the CLI,
ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL and CLAUDE_CODE_OAUTH_TOKEN are removed, so requests always go through the CLI's own claude.ai sign-in (also a prerequisite for Remote Control).
- Claude Code's system prompt is not replaced, request headers are not changed, and it is not used as a bare API proxy: only turns the user starts in a session reach Claude Code; helper requests such as session titles and compaction summaries are always refused and handled by DSH itself.
- For personal use on your own machine only. Anthropic does not allow third-party products to offer claude.ai sign-in or subscription allowance to others without approval; do not distribute this feature for others to use.
- The display name is "Claude Agent" (brand guidelines rule out "Claude Code").
Module: Cursor
A Cursor group appears in DSH's model selectors (the composer's model slot and the /model dialog). Pick one of its models and every turn of that session is handed to the agent of this machine's Cursor CLI, on your Cursor account's own allowance. Install the Cursor CLI and run agent login first.
- How it runs: the plugin starts a long-lived
agent acp process (the Cursor CLI's Agent Client Protocol server), and each DSH session maps to one ACP session in it. Cursor completes the turn with its own tools, rules, MCP servers and sign-in; the reply streams back into DSH as text and thinking, each internal tool call shows as a reasoning note line (→ Shell: npm test, or ✗ Shell: npm test: exit 1, … on failure), and the plan checklist shows as a note too. The process closes after 30 minutes without conversation, and the next turn loads the session again. On Windows the node bundled in the CLI's latest version folder is run directly, bypassing agent.cmd's PowerShell wrapper.
- Models and reasoning effort: the model list and each model's reasoning options come from the CLI (parameterized model selection). Cursor's models name their reasoning options differently (
effort, reasoning, reasoning_effort, some with a thinking switch); they are folded into one column of DSH's reasoning effort picker: Off (thinking off, or none) plus the model's own levels; models with only a switch show Off / On. So the default thinking level under "Settings → Agent Hub → Model selector" applies to Cursor too. Speed (Fast), context length (1M) and Auto's optimize_for keep the CLI defaults and are not adjusted in DSH. A change of model or level switches in place in the current session, without restarting the process.
- Your Cursor settings stay untouched: the CLI saves the model parameters chosen over ACP as its own defaults (
~/.cursor/cli-config.json). The plugin writes that file back exactly as it was after each change, so the session's choice still applies while the default model and level you get with agent in a terminal are unaffected by DSH.
- Model catalog: the first read opens a probe session in a temporary folder (no prompt sent, no cost); the model list is usable as soon as it is listed, and each model's reasoning options are read one by one in the background (about 1.5 seconds each), the selected model first. The catalog persists in
$DSH_HOME/dsh-agent-hub/cursor-models.json and refreshes in the background after 12 hours; the probe session is deleted from Cursor's session records once used.
- Approvals: tool calls Cursor needs to ask about (commands not on its allowlist, file writes, etc.) open DSH's native approval dialog, which only ever offers "allow this once" and never clicks "always allow" for you. When the DSH session's approval policy is
never, or there is no approval channel (such as headless), they are refused outright. The global blocked commands apply too: a blocked command is refused without a dialog.
Known issue (Cursor CLI 2026.10.01, Windows): the Cursor CLI loads Claude Code's hooks (~/.claude/settings.json). It wraps hook commands in PowerShell syntax but hands them to Git Bash, causing a syntax error, so every shell tool call is blocked (the reply says "Hook blocked … syntax error near unexpected token &'"). This has nothing to do with DSH; using agent directly in a terminal does the same. The CLI has an internal switch to stop loading third-party configuration (thirdPartyExtensibility`), but it is not yet confirmed which user-facing setting exposes it; until Cursor fixes this, Cursor cannot run shell commands on Windows machines with Claude Code hooks configured, while reading and writing files is unaffected.
Module: Cardputer
First turn on Cardputer in the Modules card at the top of Settings → Agent Hub (off by default, this machine only, not synced; turning it off stops the port and the dictation process). Settings → Agent Hub → Cardputer adds devices, checks the pairing digits, renames and removes them. A paired device uses one TLS-PSK WebSocket to view sessions, send messages, stop/continue tasks, handle approvals, and upload PCM audio for speech recognition. With no device and no pairing in progress, no port is opened; with no device online, no heartbeat timer runs. Keys are stored only on this machine and are not synced.
- State updates are coalesced to 250 ms, approvals are pushed immediately, and duplicate snapshots are not resent. Connections, messages, audio, commands and the file name cache are all capped; a disconnect cancels speech recognition, and unloading the plugin cleans up approvals and dictation processes.
- Dictation uses a separate Rust
dsh-type, started on demand and exiting after five idle minutes. Both Automatic and Type use Unicode events and never touch the clipboard; Paste is not supported for now, so old settings need changing to Automatic. Failed input is not retried automatically, to avoid typing twice.
- Run
node scripts/build-native.mjs on the target system to build the helper for the current platform; the output is packaged with the plugin. The macOS Intel/ARM64 sources pass Rust type checking, but native macOS linking, signing, permissions and input method behavior have not been verified on real hardware; Windows tests have not injected text into real applications either.
- This repository has only the host implementation and a Node simulator, no flashable firmware. ESP32 PSK/WebSocket, screen interaction, microphone and the hardware long-press guard still need firmware work and integration. High-risk detection is a conservative heuristic, not a general command safety parser; the device must not replace the security policy on the computer.
Protocol, build and verification boundaries · Interactive simulator
Module: Clipboard & translation
Turn on Clipboard window in the Modules card at the top of Settings → Agent Hub (off by default, desktop only, this machine only and not synced; not shown or changeable on a phone); the hotkey and other options are under Settings → Agent Hub → Clipboard & translation. Once on, a global hotkey (Ctrl+Alt+V by default; press a new combination in the settings to record it) summons a floating window with two modes in one window, switched with Tab:
- Clipboard: keeps a history of copied text in a local SQLite database (
~/.dsh/dsh-agent-hub/clipboard.db; the history is not synced with the settings, only pinned items travel to other devices with Settings sync). The left column, "Pinned", is ordered by when items were pinned, with fixed numbers, and Ctrl/⌘ 1–9 types the corresponding pinned item directly; the right column, "Recent", is ordered by time; a fixed-height preview area below shows the selected item's full text and source. The search at the top filters both columns: when a pinned item matches, the selection lands on the first "Pinned" item, otherwise in "Recent", and clearing the search returns to "Recent". ↑↓ select, ← → switch columns (with text in the search box, only when the caret is at either end), Enter types into the app that was in front before the window opened, Ctrl/⌘ C copies and closes, Ctrl/⌘ P pins / unpins, Del deletes (undoable for 6 seconds, no confirmation dialog). While the window is open, new copies appear live at the top of "Recent" with a flash; if you have not moved the selection yet, the new item becomes the selection. In narrow windows (< 600 px) the two columns stack vertically.
- Editing: the preview box is itself the editor: click the text in the preview (the caret lands where you click), or press
Ctrl/⌘ E, or click the pencil button on the row or the preview bar, and the preview expands upward over the list and becomes editable, folding back after saving. Ctrl/⌘ Enter (or Ctrl/⌘ S) saves and Esc cancels (discarded edits can be undone from the toast). Saving updates the item in place, keeping its position, source, pinned state and number; when a pinned item's content changes, other devices follow at their next settings sync. Editing text to match another existing entry merges the two (keeping the pin). Long text over 8000 characters, whose preview is truncated, is read in full before editing is allowed; if the window loses focus and closes midway, unsaved content is kept and can be resumed with "Continue editing" next time. Images cannot be edited.
- Images: copied images (screenshots, "Copy image" in a browser, etc.) also go into "Recent", with a thumbnail, dimensions, source and time left. Images are temporary: cleared automatically if not copied or used again within 5 hours, at most 100 kept, not pinnable and not synced, so they take no space in the sync repository and never bloat the database.
Enter puts the image back on the clipboard and sends one ⌘V / Ctrl+V to the target app (an image cannot be "typed", so this is the only place the window simulates a paste; dictation still never touches the clipboard); only copies; or clicking the preview shows it large, / closes it; searching "image" or a size filters images. A copy carrying both text and an image (such as a spreadsheet cell) is recorded as text; the icon attached when copying files in Finder is not recorded. Everything is stored as PNG (converted from DIB on Windows when there is no PNG format), and on Windows both PNG and DIB are offered when put back on the clipboard; images over 16 MB are not recorded.
Implementation and resource use:
- The window, hotkey, clipboard listening and history all live in the native helper
dsh-type --palette (the same binary as Cardputer dictation, in two run modes and two processes, with input serialized by a cross-process lock that refuses when busy). The host only starts it, restarts it (exponential backoff from 1 s; after 5 crashes within 10 minutes it stops and shows the reason in the settings), sends it settings and handles translation; opening the window, searching, pinning and typing never go through the host.
- Clipboard listening: Windows uses
AddClipboardFormatListener, waking only on system notifications, so idle CPU is 0, and multiple notifications from one copy are merged within 80 ms. Sync tools such as phone link and remote desktop often use delayed rendering (announcing formats first and fetching the data from the network only when read), so when the clipboard cannot be read yet or is locked, it retries after 120 ms, 300 ms, 0.7 s, 1.5 s and 3 s, and only counts a copy as handled once it is sure nothing needs recording (not text, sensitive, ignored); each time the window is summoned it also checks the clipboard sequence number again, so a missed copy is caught up when the window opens. macOS has no change notification, so it reads only changeCount (an integer): every 250 ms right after a copy, relaxing to 1 s and then 2 s. Content the system marks as sensitive (Windows' ExcludeClipboardContentFromMonitorProcessing, CanIncludeInClipboardHistory=0; macOS's ConcealedType, TransientType) is skipped before its content is read; programs can also be ignored by name. A single text over 64 KB is not recorded; duplicates (images compared byte by byte) just move to the top.
- The window size is taken in the screen's logical pixels: 50% of the screen width (640–860) and 56% of its height (420–600), always the same on a given screen; when moving to a screen with a different scale factor it moves there first and then sizes itself, and it handles the system's DPI-change callback, so the window no longer jumps between sizes. Row height, the preview area, header and footer are all fixed heights, so selection and data changes never shift the layout.
- The helper sends the page only increments: the database remembers which rows the last 128 changes touched, so a page a few steps behind receives just those rows (and deleted ids); only after old records are cleaned up, or when far behind, is the whole list sent. The list is virtualized and reuses row nodes, updating only rows whose content changed.
- The interface is a single-file page of about 25 KB (no framework), created only on first use, hidden after use with WebView2 suspended (Windows), and released after 30 idle minutes, leaving only the small native process. Measured on a Windows machine: with just the hotkey and listening, the helper uses about 2 MB of private memory; with the page hidden and suspended, the combined working set is about 60 MB (the WebView2 processes still commit around 130 MB, paged out by the system). The first summon after a cold start is slower (creating the WebView); 5 seconds after the feature is turned on, the host tells the helper to warm up.
Module: Qoder & glasses
Install Qoder CLI CN first (irm https://qoder.com.cn/install.ps1 | iex; on macOS / Linux curl -fsSL https://qoder.com.cn/install | bash), then run qodercn and /login.
- Model routing: a Qoder group appears in the model selector (Qwen, GLM, Kimi, DeepSeek, MiniMax and more, each name followed by its credit multiplier per call, e.g.
GLM-5.3 · 0.5×). Picking one hands the turn to a long-lived qoderclicn --acp process, through the same generic ACP layer as Cursor (src/acp/): tool approvals open DSH's dialog, replies stream, sessions continue with session/load, and the process closes after 30 minutes without conversation. The model list comes from a probe session that sends no prompt, cached in $DSH_HOME/dsh-agent-hub/qoder-models.json and refreshed every 12 hours. When not signed in it reports QODER_AUTH and does not retry automatically.
- Tasks from the glasses: each repository ticked in the settings keeps a
qoderclicn remote-control --spawn same-dir daemon running while DSH runs. Say "Hey Quark, open Qoder" on the glasses, then "start a task on my computer…", and the task runs directly in that repository on this machine, without creating a worktree automatically; only when the task explicitly asks to "open a worktree" does the agent create one with Qoder's EnterWorktree. Each new task is a fresh session created by Qoder's server, and the daemon cannot fold it into an old one; to continue a previous task, open it in the panel, the app or the web and ask a follow-up there.
- Worktrees in one place: Qoder puts worktrees under
<repo>/.qoder/worktrees/ by default, and the CLI has no option to change that. Before each daemon start, DSH writes two hooks, WorktreeCreate / WorktreeRemove, into the repository's .qoder/settings.local.json (exec form, running $DSH_HOME/dsh-agent-hub/qoder-worktree.cjs with the host's bundled runtime, no shell, the same on Windows and macOS; your other settings and hooks in the file stay untouched). So a worktree a task asks for is created at <repo>/.worktrees/qoder/<name> on branch qoder/<name>, alongside DSH's other worktrees (Source Control, the board, reviews and alert fixes all create theirs at <repo>/.worktrees/<branch>), where Source Control's worktree list shows, opens and removes it. Using qodercn --worktree directly in this repository goes to the same place. When Qoder removes a worktree it calls the remove hook: a worktree with no changes is deleted along with its branch; one with uncommitted changes is kept so you can look at the diff; a branch with commits of its own is kept too. /.worktrees/ and /.qoder/settings.local.json are written into the repository's own .git/info/exclude (the tracked .gitignore is not changed), so they never show up in git status or diffs.
Module: Hermes
Install Hermes first (iex (irm https://hermes-agent.nousresearch.com/install.ps1); on macOS / Linux curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash).
- One entry per persona: in the model selector's Hermes group, each entry is a Hermes persona (
default is the root folder itself, the rest are the profiles under profiles/), followed by its configured model, e.g. qweno · gpt-5.6-terra. The list is read straight from the persona folders (config.yaml and profile.yaml) with no probe session, so a newly created persona appears at once. The root is %LOCALAPPDATA%\hermes on Windows and ~/.hermes elsewhere; HERMES_HOME overrides it.
- Running: once a persona is picked, the turn goes to
hermes-acp with the process's HERMES_HOME pinned to that persona's folder (set explicitly for default too, so the usual persona set by hermes profile use cannot pull it away), one long-lived process per persona. Answers use that persona's own model, memory, SOUL and skills; to change the model, run hermes -p <persona> model. Dangerous commands and file changes open DSH's approval dialog. It goes through the same generic ACP layer as Qoder: sessions continue with session/load, and the process closes after 30 minutes without conversation.
- Cold start: starting Hermes plus creating a session takes about 20 seconds altogether. For a turn when the process is not running yet, the reply first shows a line
⏳ Starting Hermes… (Qoder does too).
- Memory is shared: conversations in DSH are written into that persona's memory and
state.db, so the same persona's Telegram bot will know about them later. To keep them apart, tick only personas dedicated to writing code.
Module: Relay
The Relay panel in the left sidebar: what Qoder and Hermes do outside DSH, listed in one place, where you can read the full record or carry on the conversation in DSH. Off by default; turn on Relay in the Modules card at the top of Settings → Agent Hub (synced).
- Where it comes from: for Qoder, the CLI's own session files (
~/.qoder-cn/projects/*/<session>.jsonl), so tasks from the glasses, the Qoder app and qoder.cn, and qodercn sessions in a terminal, are all there, and still there after DSH restarts; glasses tasks additionally carry the daemon's live state (the tool being called, the worker's memory and CPU, failures reported by the server). For Hermes, each persona's state.db is opened read-only (with Node's built-in SQLite), listing conversations from Telegram, the terminal, cron jobs and the API. Sessions DSH itself started over ACP, Hermes' one-off probes and subagents are not listed (they are already in DSH, or are not conversations). While the panel is open, these files are checked for changes every 3 seconds and reread only when they changed.
- List: grouped into Today, Yesterday, Last 7 days and Older, filterable by "All / Qoder / Hermes" and searchable. Each row has the source icon (glasses / cloud / terminal / Telegram / cron job…), the title, which agent (Qoder with the folder name, Hermes with the persona), and the start of the answer or the tool being called; running, failed, ended and cloud each have their own marker, and those taken over are tagged
DSH. Pinned at the top is "Glasses workspaces & cloud", the content of the former Qoder glasses panel.
- Reading: the full record is on the right: questions, answers (rendered as Markdown, raw HTML shown as text), and consecutive tool calls folded into one line, "N tool calls". The header shows status, source, folder, branch, model and number of turns; "More actions" has Open on qoder.cn, Continue in a terminal here, Copy resume command and Copy session ID.
- Taking over: type in the composer at the bottom and send, or click "Take over in DSH" in the header. This creates a new DSH session (with that agent as the model: Qoder uses the original session's model, or
auto when DSH does not offer it; Hermes uses that persona), keeps the original conversation's title, and jumps there. Clicking the same conversation again later opens that same session (the index is in $DSH_HOME/dsh-agent-hub/relay.json; once the DSH session is archived, taking over again creates a new one).
- Qoder: continues in place. The new session's first turn has
qoderclicn --acp session/load the original session directly, without repeating any of the context; after that it is an ordinary Qoder session (approval dialogs, phone access and Cardputer all work). A task still running cannot be taken over (two processes would write one session at once), so take it over once it has answered; after a takeover, follow-ups from the glasses or the app and the DSH side go on separately. If the original session fails to load (e.g. its folder was deleted), a new session starts with the record instead.
Module: Phone access
Serves DSH over Tailscale to the Tailscale account this computer belongs to. What the phone opens is DSH itself: you can see every session's live progress, keep any session going (cold sessions resume on demand) and approve on the phone, across all models (Claude Agent, Codex subscription, DeepSeek, MiniMax…).
- Entry: "Phone access" at the bottom of the sidebar (with the sidebar collapsed only a phone icon shows, with a green dot when on). The panel gives exactly one next step for each state: not installed → Download Tailscale; not connected → open Tailscale; HTTPS not enabled on the tailnet → open the DNS page of the Tailscale admin console and turn on HTTPS Certificates (once); ready → Turn on. Coming back from another window re-checks automatically.
- Once on: the panel shows a QR code and the URL (
https://<machine>.<tailnet>.ts.net); scan it with the phone to open it, no pairing needed. The URL contains no credentials, so it is safe to show anywhere.
- The phone interface: on narrow touch devices, the plugin adjusts DSH's official interface with a pure CSS layer: 16px input fields to avoid iOS zoom, 44px touch targets, safe-area padding, the right sidebar (Source Control, terminal, files) filling the whole screen when expanded (DSH's own narrow-screen layout only knows the old right column and squeezes it to zero height), hover tooltips and the desktop-only "Open in file explorer" hidden, composer popup menus turned into bottom sheets, and settings in a single column. The desktop is unaffected.
- Settings: DSH only opens settings reads and writes to pages opened on this machine, so "Settings → Agent Hub" on a phone only shows a notice; change settings on the computer.
- Lifecycle: when DSH exits the serve rule is withdrawn, and it is restored automatically at the next start. Clicking "Stop" withdraws the rule and remembers that it is off.
How it works:
tailscale serve serves <machine>.ts.net over HTTPS on the tailnet and forwards requests to a forwarding port the plugin opens on 127.0.0.1. The plugin only rewrites the rule for its own port; if that port is already taken by another serve rule, it reports an error rather than overwriting it.
- The forwarding port hands requests to DSH's own web service as loopback. Because the Host is rewritten, DSH's existing DNS-rebinding and cross-site checks no longer apply, so the request is checked again against the public domain before forwarding: the Host must be this machine's ts.net name, the Origin may only be same-origin, and requests with
sec-fetch-site: cross-site are refused (except top-level navigations that open the page from a link in another app or web page, i.e. GET with sec-fetch-mode: navigate: these are not forwarded to DSH but answered with a tiny page that reloads itself, turning the navigation into a same-origin one so DSH's SameSite=Strict session cookie is sent). In addition, only requests whose Tailscale-User-Login equals the account this machine belongs to are let through; that header is written by tailscaled.
- Account identity only decides "whether you may come in"; signing in still uses DSH's native mechanism: when the phone opens it for the first time without a DSH session cookie, the plugin redirects through DSH's own launch token URL (
connection.authenticatedUrl), and DSH sets its standard session cookie (30 days). The plugin stores and checks no credentials.
The port can be changed in Settings → Agent Hub (443 by default, giving a URL without a port).
How this differs from the Claude app's Remote Control: phone access covers every session of every model; Remote Control covers only online Claude Agent sessions, but through the official client (see above).
Codex: Codex models in DSH call the subscription interface directly through dsh-codex-subscription and create no Codex threads of their own, so the Codex app's Remote Control cannot see them; use phone access to follow them. This plugin only reads from Codex and never writes, and does not affect Codex's own Remote Control.
Module: Source Control (Git)
A tab in the right sidebar (alongside "Files" and "Terminal") that works on the current session's workspace folder. It opens three ways: the Git button at the top right of the session title bar (its badge shows the number of uncommitted changes; clicking again while the Git tab is in front collapses the sidebar; it is also on the page of a new session that has not sent its first turn, left of the sidebar toggle), Git on the empty sidebar's guide card, and Ctrl+Shift+G (⌘⇧G on macOS).
- Changes: the commit box is at the top (
Ctrl+Enter commits; its dropdown has "Commit & push" and "Amend last commit"). A line above the commit button shows whose identity this commit will use, and where that identity comes from (this repository, a matching rule, the global default, environment variables). The identity is written in the repository's own .git/config, shared by all worktrees, so terminals, agents and other editors commit with it too. When the repository has no identity of its own yet, the first commit from here asks you to choose one first (a preset identity, the former global identity or a new one) and binds it before committing. Before committing, the host checks with git var: if the identity actually in effect is no longer the one shown in the interface (e.g. it was changed in a terminal), the commit is refused. When amending, if the original author is not the current identity, you are told the original author will be kept, with one click to switch it to the current identity (--reset-author). When nothing is staged, the button reads "Stage all & commit". Below come merge conflicts, staged, changes and stashes. Row buttons open the file, stage/unstage and discard changes; right-click gives the full menu. Keyboard: ↑↓ move, Enter shows the diff, Space stages or unstages, Delete discards changes.
- List / tree: the toggle in the top bar shows changes as a folder tree: paths containing a single subfolder merge into one row (e.g.
src/client/git), and folders come before files. A folder row can be staged, unstaged or discarded as a whole, and ←→ fold or unfold it. The file list in commit details can be toggled too, and the choice is remembered globally; previous/next file in the diff follows the order currently shown.
- Diffs: opening any file (from the changes list, an expanded commit, uncommitted changes) shows the diff over the agent window in the middle, covering the conversation area; the Source Control panel keeps its size, position and scroll, and only highlights the file currently open. Closing it (
Esc or ✕) brings the conversation back as it was. Diffs are side by side by default (inline when narrower than 640px, toggleable), with word-level highlighting, "Show full file", and Alt+↑↓ to move to the previous/next file. On a phone, or when the middle area is too narrow, the diff opens full screen. Diffs of workspace files refresh automatically as the agent changes them. Conflicted files are colored as "current / incoming", and you can take either side as a whole or mark them resolved.
- Graph: all branches laid out as swimlanes in time order, with a dashed node at the top for uncommitted changes.
Performance: the change list, graph and diffs render only visible rows, and rows whose position did not change are not re-rendered while scrolling, so thousands of files and tens of thousands of diff lines stay smooth. The graph loads 400 commits per page, loading the next page when you scroll to the bottom. Status is polled quickly only while the Git tab is in front and the page is visible; with the tab in the background, only the title bar badge checks every 10 seconds. On Windows and macOS the host answers polls with file watching: when nothing changed, git does not run; when only workspace files changed, only git status reruns, reusing refs, stashes and in-progress operations; only changes in .git (commits, fetches, branch switches) reread everything, with a full recalibration at least every 30 seconds, and the watcher closes after 3 minutes unused. So with focus it polls every 1.5 seconds (2.5 seconds on Linux, which has no watching), otherwise every 10 seconds. The plugin's own operations, commands run on the command line and the "Refresh" button always reread everything. Unchanged results trigger no re-render, the graph is reread only when refs change, and an open diff is not re-parsed when its content has not changed. Reads use GIT_OPTIONAL_LOCKS=0, so they never fight the agent's own git commands for index.lock.
Security: git runs on the host, through DSH's authenticated /api channel (agent-hub/git). The folder must be inside a workspace registered in DSH; branch, tag, commit and other arguments are validated on the host, names starting with - are refused, and paths always go after --. Network operations never show interactive prompts (GIT_TERMINAL_PROMPT=0).
Module: Code editor
View and change code in the workspace beside the session. The editor is a kind of tab in the right sidebar, so multiple tabs, two-pane split, floating window, full screen, side by side with the session and per-session layout memory all come from DSH's own sidebar; when the window is narrower than 768px (phones) the sidebar goes full screen automatically.
- Opening: the file tree, file links in the session, line references in tool calls and search results all open text files in a code tab, at the matching line. Markdown and HTML still render first in DSH's preview; Edit on the right of the preview title bar switches to the editor, and Preview in the editor switches back. Images, PDFs, Office files and spreadsheets keep their existing previews.
- Editing and saving: built on CodeMirror 6, with syntax highlighting for 40-odd common languages; multiple cursors (Alt+click, Ctrl+D), folding, bracket matching, and auto-indent that detects spaces or tabs from the file's existing style. Saving is manual only, with Ctrl/⌘+S, keeping the file's original line endings (LF/CRLF) and BOM. Tab titles and the path bar mark unsaved changes with a dot; closing a tab with unsaved changes asks first: save and close / don't save / cancel. Unsaved content is also backed up in the browser and restored, with a notice, after a refresh or crash.
- When the agent edits the file at the same time: with no unsaved changes, the editor follows the disk content live, keeping the cursor and scroll position; with unsaved changes it does not overwrite them, but shows a conflict at the top, where you can Compare (show the diff against the disk version in the editor, keeping or reverting each change), Use the disk version or Overwrite with mine. Saving sends the content hash from when editing started, and the host refuses to write if the file was changed by someone else, so the agent's changes are never silently overwritten.
- Change markers: right of the line numbers, lines added (green), modified (blue) and deleted (red triangle) relative to HEAD are marked; they refresh after commits and when the window regains focus.
- Formatting: Shift+Alt+F. The project's own formatter comes first, with the project's configuration applied: the project's Biome (when there is a
biome.json) or Prettier, then ruff/black, gofmt, rustfmt, clang-format, shfmt, stylua, taplo and others on PATH, and finally, for web languages, the Prettier bundled with the plugin. The result is applied as a minimal diff, the cursor stays put, and one undo reverts it.
- Add to session: Ctrl/⌘+L inserts the selected range into the current session's composer as
@path L12-30 (multiple selections separated by commas; with no selection just @path), and the model reads it with its file tools. When the file has unsaved changes you are told that the model reads the version on disk.
- Definitions and references: F12 or Ctrl/⌘+click (an underline appears when hovering with Ctrl/⌘ held) goes to the definition, and Shift+F12 lists references in the search tab. Definitions are found by search, without a language server: the identifier at the cursor is matched against each language's declaration forms (, , , , and so on), the same for every language, with no index. Results are sorted by closeness to the current file; a single hit jumps directly, several show candidates; pressing F12 on a declaration lists references instead.
Performance: CodeMirror and the language packs are a separate chunk (lib/client.editor.js), loaded only when a code tab is first opened, so DSH startup is unaffected. The editor renders only visible lines and parses syntax incrementally; files over 2 MB or with very long lines turn syntax highlighting off, and files over 10 MB go to DSH's paged preview. The same file open in several tabs shares one copy of its content, and switching tabs keeps undo history and scroll position. Workspace search uses ripgrep (@vscode/ripgrep, the same as VS Code; git grep stands in when it cannot be installed), honors .gitignore, returns at most 2000 results and times out after 20 seconds; a new search cancels the one in progress, and the result list renders only visible rows.
Security: reading, writing, searching and formatting all run on the host, through DSH's authenticated /api channel (agent-hub/files). Both the session folder and the files read or written (after resolving symlinks) must be inside workspaces registered in DSH; writes are serialized per file and preconditioned on the content hash.
Module: Settings sync
Syncs the current profile's entire plugin configuration (cordis.patch.yml, including each plugin entry's disabled) and its plugin enablement list (dsh.profile.bundles in package.json, ordered) across machines through one Git remote. Each machine keeps a complete git repository in $DSH_HOME/dsh-agent-hub/sync/<profile>/; the remote is only a relay, and every machine holds the full history.
- What is synced: the repository holds only
cordis.patch.yml, plugins.json (bundle order + install sources), pins.json (the clipboard window's pinned items), reader-feeds.json and reader-favorites.json (Reader feeds and favorites), and .gitattributes. Paths under the home folder are written as ~/… (/Users/vi/pro/app and C:\Users\vi\pro\app are the same value), and text is normalized to LF; comments and !!js expressions are kept as they are. Install sources meaningful only on one machine, such as link: and file:, are not shared, and the repository keeps other machines' sources. Not synced: sign-ins, build-script approvals, version exemptions, lockfiles, and this plugin's board tasks and Claude account list.
- Pinned clipboard: synced by default. A pinned item's identity is its text, so the same text pinned on two machines counts as one item, and when applied locally it is pinned in place in the local history without adding another row. Unpinning on one machine unpins it on the others at their next sync (the item goes back to "Recent" and is not deleted); when the two sides pinned at different times the earlier one wins, so
Ctrl/⌘ 1–9 numbers match everywhere. Pins have no conflicts and need no choices. A machine whose clipboard window is not on (or whose helper is not running) takes no part: it neither reads nor changes local pins, remote pins are kept as they are, and turning it on later never mistakes "not received yet" for "deleted". Pinned content goes into your Git remote, so do not keep passwords there; to keep one machine's pins local, add agent-hub.config.palette.pins to config.sync.localSettings.
- Reader feeds and favorites: merged the same way as pins (
src/sync/collection.ts): a feed's identity is "type + address", a favorite's is "link + quoted text", so the same one added on two machines counts once; deleting on one machine deletes it on the others after they sync. Entries, read state and article text stay on each machine. To keep them local: agent-hub.config.reader.feeds, agent-hub.config.reader.favorites.
- Not synced: some settings only make sense on one machine, such as the terminal's default shell (
C:\Program Files\PowerShell\7\pwsh.exe in terminal-controller) or this machine's light/dark choice. What is listed under "Not synced" can be a whole entry (terminal-controller) or a setting within an entry (ui-theme.config.preference); these are not uploaded or compared, and applying the remote configuration keeps this machine's existing values, with whole entries left where they were. The defaults are , (automatic review sync) and (allowing a phone to change settings); the Clipboard window and Cardputer module switches (, ) are on the list by default too. The list itself is synced with the settings, so every machine excludes the same set.
Module: Review
Every day, reads the PRs waiting for my review on GitHub, analyses each in a read-only session of its own, and writes the result into a review sheet; I tick, edit and preview it on the board, and once I confirm, the plugin posts the review. The model never posts anything itself. Needs gh installed and signed in on this machine (gh auth login). The entry points are Settings → Agent Hub → Review (ticking repositories turns it on) and the To review lane at the left of the board.
- The review sheet is the only state:
<repo>/.reviews/pr-<number>.md, whose folder ships its own .gitignore containing *, so the whole folder stays out of git. The front matter holds the PR, the head at analysis time, the status, the decision (approve / request-changes / comment), the summary, the session id, and the review id after posting; the body has the change description, must fix, nice to have, this PR's rules, and notes. Each finding is one line, - [x] F1 \path:88` description, with the English comment to post below it, starting with > EN:`. The agent writes it with its own editing tools, while the interface's edits are applied by the host on the file's latest content (serialized per file, atomically replaced), so neither overwrites the other; hand edits work too, and reading is lenient. The card status (not analysed / analysing / ready / new commits / submitted / unreadable) is derived from GitHub's list and the review sheet, with nothing stored separately.
- Sync: one GraphQL search (
review-requested:@me) gets the PRs of every repository, matched to the ticked repositories by their GitHub remote. The daily automatic task starts analyses for new PRs and those with new commits (drafts excluded, at most "Analyses per sync" per run); the sync button in the lane header and cache refreshes while the board is visible only read the list and start no analyses.
- Automation task: the daily sync uses DSH's official Schedule, with no second timer; the first migration takes the daily time from the old setting, after which time and time zone are edited only in the host's "Automation tasks". Settings → Agent Hub → Review shows the status, next run and a way to manage it; turning it on creates the native task, and after turning it off or deleting the task in the host it is not recreated automatically. The official Schedule service must be enabled. The task is linked to a dedicated session, where the settings page's "Helper model" calls one restricted sync tool; PR analysis is still done by the review bot.
- Each run's result: the sync tool writes a summary from the actual state, kept in the linked session; the task title is updated to a one-line result such as "Board review · last run N analysed". Failed, cancelled and still-analysing PRs are counted separately; it waits at most 20 minutes, and past that limit only records the snapshot at that moment, never counting queued as done. The host's "Records" tab still only means delivery receipts, and the full result is in the linked session; a model that did not run or a failed delivery should not be taken as a successful sync. Failures set up no extra retry timer; retry by hand on the board.
- Analysis: the PR is fetched to
refs/reviews/pr-<number> (no worktree, no branch switch, the working tree untouched), and a new session opens in the workspace of the repository's main folder, using the review bot by default; right-click, long-press or on a board card and "Choose bot…" assigns another Bot to this one task (an explicit choice opens a new session for that Bot). The session is titled ; the review sheet's front matter is written first, then the first turn is sent: the diff range, changed files, unresolved review threads (resolved bugbot threads are removed before reaching the model) and the three layers of rules (global, , the sheet's own rules). At most 2 run at once, the rest queue. New commits start round 2: the previous round's sheet is kept, renamed to , and the new round looks only at the increment and says which old findings were resolved. After changing a sheet's rules, "Analyse again with these rules" has the original session look again directly when the head is the same.