Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add dsh-jira-tasks
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
README
中文 · English
Shows the current JIRA project's open / reopened issues assigned to the current user below the DSH composer input. The JIRA base URL and token are configured in Settings → JIRA 配置 (the settings dialog's left navigation) (JIRA_BASE_URL / JIRA_API_TOKEN act as fallback); the project key and JQL are configured per workspace and persisted.
DSH versions targeted: 0.1.7 and 0.2.0 (verified on
0.1.7-rc.2and0.2.0-rc.2), on both the browser (web profile) and the Desktop app (the Electron-owned desktop profile). The client slotsconversation.input.dock/plugins.item/settings.section, the settings servicectx.configForms(namespace = profile entry idjira-tasks), and thectx.effect/configForms.whileServeddisposal contracts are unchanged between 0.1.7 and 0.2.0 (verified package by package), so onelib/covers both; the 0.1.6-erasettingsScope/settings.register/settings.plugin.itemAPIs are gone and no longer used. The package declares its DSH range as>=0.1.7-rc.2 <0.3.0-0(engines.dshplus an optional@deepseek-ai/dshpeer): since 0.2.0 DSH checks that range on install and on profile startup, skipping a mismatched bundle and offering the exact-versionallow-versionexemption in the plugin manager. The peer is markedoptionalsopnpmnever pulls the whole@deepseek-ai/dshtree into the profile.
Features
- 📋 Panel shown below the composer in both new and active sessions (aligned with the input width in new sessions)
- 👤 Defaults to the current user (
assignee = currentUser()) with status开启 / 重新开启(Open / Reopened) - ⚙️ Settings → JIRA 配置 (left navigation, below Agent presets) edits the base URL and access token (the token is written to the credential store and never sent back to the browser); the same JIRA card stays available in the Plugins panel
- 🟢 The card auto-probes the connection and shows a status light: green = usable, red = unusable, grey = unconfigured; Test connection verifies unsaved drafts immediately
- ⚙️ Project key and JQL are saved per workspace; unconfigured workspaces show "unconfigured"
- 🔄 Auto-query on every new session, with a one-click refresh (⟳)
- 🔗 Click an issue to open its JIRA detail in a new tab
- 🎨 Uses DSH theme tokens; adapts to light / dark themes
Install
Published to npm. For the browser (web profile):
dsh plugin --profile web add dsh-jira-tasks
Or straight from GitHub (the repository root is the package directory; lib/ is prebuilt):
dsh plugin --profile web add github:liu3734/jira-tasks-dsh-plugin
Restart DSH to activate.
Desktop app (DeepSeek Harness.app)
The Desktop app does not use the web profile; it boots the Electron-owned desktop profile (~/.dsh/profiles/desktop/). That profile name is reserved: an npm-installed dsh refuses both boot and plugin operations for --profile desktop (profile "desktop" is managed exclusively by the Electron application). Use the Desktop-provided command instead:
Open the Desktop app once (it initializes the desktop profile), then quit it completely
In the app menu, open Manage dsh Command → Install (puts the Desktop's own
dshon your PATH)Install the plugin:
dsh plugin --profile desktop add dsh-jira-tasksConfirm
"dsh-jira-tasks"is listed indsh.profile.bundlesof~/.dsh/profiles/desktop/package.json, then start the Desktop app
The renderer's origin is
dsh-app://app. Electron forwards every non-static request to the local Host with its cookie, so the plugin's absolute-pathfetch("/jira/api/search")keeps working;target="_blank"issue links are handed to the system browser. The project key / JQL live in that origin's localStorage, so they are separate from the browser configuration athttp://127.0.0.1:<port>— configure each surface once.
Nothing shows up? Check
dsh.profile.bundlesfirst. DSH mounts this package as a profile layer only when"dsh-jira-tasks"is listed indsh.profile.bundlesof that profile'spackage.json(~/.dsh/profiles/web/package.jsonor~/.dsh/profiles/desktop/package.json); being independenciesalone is not enough — the startup log then printspatch: entry "jira-tasks" not foundand the plugin silently never loads.dsh plugin --profile <name> addnormally appends that row, but it will not re-append when the package is already a dependency; add"dsh-jira-tasks"todsh.profile.bundlesby hand.
Using GitHub Packages instead: configure
@liu3734:registry=https://npm.pkg.github.com/plus a read token in the profile's.npmrc, then rundsh plugin --profile web add @liu3734/dsh-jira-tasks.
- Copy this repository (its root is the package directory) into the profile's
packages/dsh-jira-tasks/(~/.dsh/profiles/web/for the browser,~/.dsh/profiles/desktop/for the Desktop app; skip.git/) - Edit that profile's
package.json:- Add to
dependencies:"dsh-jira-tasks": "file:./packages/dsh-jira-tasks" - Append to
dsh.profile.bundles:"dsh-jira-tasks"
- Add to
- Run
pnpm installin the profile directory - Restart DSH (the Desktop app must be fully quit and reopened)
Note:
pnpm installcopies the package intonode_modules/(not a symlink) — after editing sources, syncnode_modules/dsh-jira-tasksor re-run install.
In a DSH session, use the Cordis tools: cordis_define (kind: new, idPrefix: "jira", sources in plugin/host.js / plugin/client.js) → cordis_run to activate. Dynamic plugins live only in process memory and disappear on restart — for trial use only.
Configuration
1. JIRA base URL and token
Open Settings → JIRA 配置 (the last item in the left navigation, below Agent presets; the JIRA card in the Plugins panel is the same form) and fill in:
- JIRA base URL: e.g.
http://jira.example.com/(written to this plugin entry'sConfig.baseUrl— thejira-tasksrow in the profile'scordis.patch.yml— and read back by the form; a host-refused write now surfaces as an error instead of failing silently) - Access token / PAT: written to the credential store (
$DSH_HOME/.credentials.yaml) under the plugin-owned refJIRA_TASKS_TOKEN; the browser only ever sees "configured", never the token itself
Leaving the token blank on save keeps the existing one; clearing the address on save removes the override and falls back to the credential store / environment. Auth is auto-detected: tokens containing : use Basic, otherwise Bearer (JIRA PAT).
The settings-page token takes precedence over the environment. When the environment that launched DSH already defines
JIRA_API_TOKEN(a Windows user-level variable counts), the card is still editable: it stores the token in its own refJIRA_TASKS_TOKEN, which DSH accepts (it only refuses to write a ref the launching environment shadows), and the Host resolves tokens in this order:JIRA_TASKS_TOKEN (settings card) > JIRA_API_TOKEN > JIRA_TOKENClick Clear settings token in the card to fall back to the environment variable again.
Connection test
The card's footer carries a status light and a Test connection button:
- Opening the card auto-probes once (against JIRA
/rest/api/2/myself), and saving re-probes - Green = address and token work (the current user is shown); red = unusable (JIRA's reason, e.g. 401, is shown); grey = address or token not configured
- Test connection probes what is currently in the fields, saved or not, so you can check before saving
Environment variables / credentials still work as a fallback (used when the settings card leaves baseUrl empty), hot-reloaded without a restart. Address precedence is Config.baseUrl > JIRA_BASE_URL > JIRA_URL, and JIRA_BASE_URL may live in the launching environment or in .credentials.yaml — note that a stale record (e.g. an old domain left in the credential file) becomes effective again as soon as the settings page clears the address:
JIRA_BASE_URL: "http://jira.example.com/"
JIRA_API_TOKEN: "<PAT or user:token>"
- Base URL aliases:
JIRA_BASE_URL/JIRA_URL - Token resolution order:
JIRA_TASKS_TOKEN(written by the settings card) →JIRA_API_TOKEN→JIRA_TOKEN; the first match wins
2. Project key and JQL (per workspace)
- Click ⚙ on the panel header to open settings (the form shows the target workspace)
- Project key: e.g.
HCPFYH1— saved and queried immediately; auto-loaded for new sessions in that workspace - JQL: leave empty for the default, or write a custom JQL where
{projectKey}(or{key}) is replaced by the project key
Default query:
project = "{projectKey}" AND status in ("开启", "重新开启") AND assignee = currentUser() ORDER BY updated DESC
The status names follow the Chinese workflow (
开启/重新开启). For English statuses (Open/Reopened), set a custom JQL in ⚙.
Compatibility & verification
| Dimension | Coverage |
|---|---|
| DSH versions | 0.1.7-rc.2 and 0.2.0-rc.2 (verified); declared range >=0.1.7-rc.2 <0.3.0-0 — the 0.1.7 and 0.2.x lines, excluding 0.3.0 and its prereleases |
| Surfaces | Browser (dsh web / web profile) and the Desktop app (Electron, desktop profile, renderer origin dsh-app://app) |
| Host APIs | Config / webServer.register / subprocess.spawn / credentials.resolve / settings.configure — signatures unchanged from 0.1.7 to 0.2.0 |
| Client APIs | window.__ModuleLoader__.load, slots.inject/register, configForms.get/whileServed, remote.credentials, slots conversation.input.dock / plugins.item / settings.section — contracts unchanged from 0.1.7 to 0.2.0 |
How it was verified (a throwaway DSH_HOME, the user's own profile untouched):
# Boot the target dsh in isolation; the profile's dsh.profile.bundles lists dsh-jira-tasks
DSH_HOME=/tmp/dsh-check node <dsh-0.2.0-rc.2>/lib/bin.js --profile web --no-open --port 19399
curl -sX POST http://127.0.0.1:19399/jira/api/test -d '{}' # connectivity / auth
curl -sX POST http://127.0.0.1:19399/jira/api/search -d '{"projectKey":"<KEY>"}' # issue query
curl -s "http://127.0.0.1:19399/?token=<token from the startup log>" | grep -o dsh-jira-tasks # client bundle in __DSH_BOOT__
For the Desktop app additionally confirm the client bundle appears in the app's Code Cache (the renderer loaded and executed it) and that dsh.jiraTasks.config.v1 exists in its localStorage (the panel works and writes per workspace).
Uninstall
dsh plugin --profile web remove dsh-jira-tasks # browser
dsh plugin --profile desktop remove dsh-jira-tasks # Desktop app (needs the Desktop-provided dsh)
Troubleshooting
| Message | Fix |
|---|---|
| JIRA base URL not configured | Address missing — see "Configuration 1" above |
| JIRA token not configured | Token missing — see "Configuration 1" above |
| 401 … | Invalid token or wrong auth scheme; verify with curl -H "Authorization: Bearer <token>" <base>/rest/api/2/myself |
| Cannot parse JIRA response | Network / proxy issue, curl produced no output |
Yes. The card stores the token under the plugin-owned ref JIRA_TASKS_TOKEN instead of writing the environment's JIRA_API_TOKEN. DSH only refuses to write a ref the launching environment shadows, so its own ref is always writable: even with JIRA_API_TOKEN exported by the shell or the OS, the field accepts input, the save succeeds, and the Host prefers it:
JIRA_TASKS_TOKEN (settings card) > JIRA_API_TOKEN > JIRA_TOKEN
- The card names the effective source: with a saved token it says "overrides environment variable JIRA_API_TOKEN"; without one it says "currently using environment variable JIRA_API_TOKEN — fill in and save to override"
- Saving re-probes the connection; Clear settings token (fall back to environment) removes the override
- The one remaining case that reports
is supplied read-only by the launching environmentis someone exportingJIRA_TASKS_TOKENitself — that ref really is read-only then; remove it (Windows: System Properties → Environment Variables, or PowerShell[Environment]::SetEnvironmentVariable('JIRA_TASKS_TOKEN', $null, 'User')) and restart DSH
- Make sure it is installed and DSH was restarted (the Desktop app must be fully quit and reopened); in new sessions the panel sits below the input
- Check
dsh.profile.bundlesin the right profile:~/.dsh/profiles/web/package.jsonfor the browser,~/.dsh/profiles/desktop/package.jsonfor the Desktop app (the most common cause of a silent no-show — see the Install note; that profile can only be edited with the Desktop-provideddsh) - Check the DSH startup log:
patch: entry "jira-tasks" not foundmeans the profile layer was never composed;Plugin dsh-jira-tasks@<version> is incompatible with dsh <runtime>means the running version falls outside the declared>=0.1.7-rc.2 <0.3.0-0range, so the bundle is skipped (follow the message, usingdsh plugin allow-versionor the plugin manager, to grant the exact-version exemption);webserver: duplicate exact routemeans a route was registered twice (the plugin releases its routes throughctx.effect, so this points at a second copy) - A console error
client-modules: could not load "dsh-jira-tasks"means/plugins/dsh-jira-tasks/client.jswas not served — confirmexports["./client"]resolves to the builtlib/client.js - The Desktop panel says "unconfigured" although you configured it: project key / JQL are stored per origin, so
dsh-app://app(the built-in window) andhttp://127.0.0.1:<port>(a browser) hold two independent configurations
Architecture & Implementation Details
┌────────────── Browser (Client) ──────────────┐ ┌─────────────── Host ───────────────┐
│ conversation.input.dock (both states) │ │ webServer route /jira/api/search │
│ CSS order:99 -> below the input, full width │ │ entry Config.baseUrl (volatile) │
│ on mount/refresh: fetch POST │ │ credentials.resolve(TOKEN_REFS) │
│ render: list / error / unconfigured │ │ subprocess.spawn(curl ...) │
│ localStorage: per-workspace key/JQL │ │ parse stdout JSON │
│ plugins.item (Plugins panel card) │ │ return {ok, issues} │
│ settings.section (Settings nav page) │ │ │
└──────────────────────────────────────────────┘ └────────────────────────────────────┘
- **Host**: declares the entry's own `Config` (`baseUrl`, `volatile` — since DSH 0.1.5 a settings page is built from it; `settings.configure({ auto: false }, ctx.fiber)` turns the auto-generated page off and hands its disposer back to `ctx.effect`) plus `webServer` routes `POST /jira/api/search` and `POST /jira/api/test`, each wrapped in its own `ctx.effect` (a reload otherwise hits "duplicate route" and fails activation; non-POST requests get 405); the token comes from the `credentials` service resolved in the order `JIRA_TASKS_TOKEN` (written by the Settings card) → `JIRA_API_TOKEN` → `JIRA_TOKEN` (`$DSH_HOME/.credentials.yaml` / environment, hot-reloaded), so a saved card token overrides the environment; queries run through `subprocess` spawning `curl` directly, with the auth header passed via stdin (`--config -`) so the token never appears in argv.
- **Client**: a standard `window.__ModuleLoader__.load({ id, factory })` web bundle requiring only `"react"`; registers **only `conversation.input.dock`** (`order: 10`) — that seat renders in both new and active sessions and is a full-width row of `composerStack` (`flex-direction: column`), where the panel's own CSS `order: 99` places it below the input card at the card's width. `conversation.composer.dock` is deliberately NOT used: in 0.1.7 and 0.2.0 alike that seat renders into InputBar's `.dock` **row**, side by side with the context meter, so a full-width panel there gets its right-hand content covered by the meter. The address/token form comes from `ctx.configForms.get("jira-tasks")` and the token is written through `remote.credentials` to `JIRA_TASKS_TOKEN`; `configForms.whileServed` keeps it hidden when the host serves no such namespace. It is registered in **two places**: the Plugins-panel card `plugins.item` (`id: "jira-tasks"`, `order: 41`) and a Settings-navigation section `settings.section` (`id: "jira-tasks"`, `order: 30` — above Agent presets' 20, so it sits below it; `label: "JIRA 配置"`; ids outside the shell's `navIcon` allow-list fall back to the default gear icon). Both share one `JiraSettingsPage`, whose outer container is chosen by the external `variant: "page"` prop (`li.jt-set-card` card vs `div.jt-set-page` page) while the field block is the same array. The card's `summary` view is rendered by a dispatcher that calls **no hooks**, while the form lives in a separate `JiraSettingsPage`, so flipping `view` on one mounted instance never changes its hook count.
- **Why not the `shell` service**: `shell` wraps commands with `sandbox-exec`, which is broken on some macOS versions (`sandbox_apply: Operation not permitted`); `subprocess` is the raw process seam without this issue.
- **Why the Desktop app needs no code branch**: its Host is the same DSH as the web profile (same `@deepseek-ai/dsh-web-app` + `dsh-host-webserver`); only the renderer origin differs (`dsh-app://app`), and Electron's `protocol.handle` forwards every non-static request to the local Host with its cookie, so both `fetch("/jira/api/*")` and `/plugins/...` work. `http(s)` `target="_blank"` links are handed to the system browser by the main window's `setWindowOpenHandler`. The only difference that matters is origin-scoped localStorage, so each surface keeps its own project key / JQL.
- **One registration covers both states**: `conversation.input.dock` renders whenever a session and its input exist, so new and active sessions need no separate registrations (the older `composer.dock` + blank-session de-duplication was both unnecessary under 0.1.7 and the cause of the meter-row collision).
**Differences from the dynamic version**
| Aspect | Dynamic plugin | Persistent install (this package) |
|---|---|---|
| Persistence | Lost on restart | Survives restart |
| Client→Host | `host.call` / `harness.handle` | `webServer` route + `fetch` |
| Client bundle | Injected per session | `/plugins/dsh-jira-tasks/client.js` |
| Config / credentials | Env / `.credentials.yaml` only (no Settings card) | Settings nav page + Plugins-panel card + same `.credentials.yaml` fallback |
</details>
## License
MIT
Comments
Comments live in GitHub Discussions. Sign in with GitHub to post or react.