# JIRA Open Tasks Panel (DeepSeek Harness Plugin)

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

[中文](README.md) · **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 → Plugins → JIRA** (`JIRA_BASE_URL` / `JIRA_API_TOKEN` act as fallback); the project key and JQL are **configured per workspace** and persisted.

## 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 → Plugins → JIRA** edits the base URL and access token (the token is written to the credential store and never sent back to the browser)
- 🟢 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:

```bash
dsh plugin --profile web add dsh-jira-tasks
```

Or straight from GitHub (the repository root is the package directory; `lib/` is prebuilt):

```bash
dsh plugin --profile web add github:liu3734/jira-tasks-dsh-plugin
```

**Restart DSH** to activate.

<details>
<summary>Manual install (without npm)</summary>

1. Copy this repository (its root is the package directory) to `~/.dsh/profiles/web/packages/dsh-jira-tasks/` (skip `.git/`)
2. Edit `~/.dsh/profiles/web/package.json`:
   - Add to `dependencies`: `"dsh-jira-tasks": "file:./packages/dsh-jira-tasks"`
   - Append to `dsh.profile.bundles`: `"dsh-jira-tasks"`
3. Run `pnpm install` in the profile directory
4. Restart DSH

> Note: `pnpm install` **copies** the package into `node_modules/` (not a symlink) — after editing sources, sync `node_modules/dsh-jira-tasks` or re-run install.
</details>

<details>
<summary>Dynamic plugin (temporary, lost on restart)</summary>

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.
</details>

## Configuration

### 1. JIRA base URL and token

Open **Settings → Plugins → Plugin configuration → JIRA** and fill in:

- **JIRA base URL**: e.g. `http://jira.example.com/` (stored in the user settings document and read back by the form)
- **Access token / PAT**: written to the credential store (`$DSH_HOME/.credentials.yaml`) under the plugin-owned ref `JIRA_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 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 ref `JIRA_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_TOKEN
> ```
>
> Click **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 is empty), hot-reloaded without a restart:

```yaml
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:

```jql
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 ⚙.

## Uninstall

```bash
dsh plugin --profile web remove dsh-jira-tasks
```

## Troubleshooting

<details>
<summary>Panel shows "query failed"</summary>

| 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 |
</details>

<details>
<summary>Can the settings card override the token from the environment?</summary>

**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 environment` is someone exporting `JIRA_TASKS_TOKEN` itself — 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
</details>

<details>
<summary>Panel does not appear</summary>

- Make sure it is installed and DSH was **restarted**; in new sessions the panel sits below the input
- Check the DSH startup log for profile plugin load errors
</details>

## Architecture & Implementation Details

<details>
<summary>Expand</summary>

```
┌─────────── Browser (Client) ───────────┐      ┌──────────── Host ──────────────┐
│ conversation.composer.dock (active)        │      │ webServer route /jira/api/search │
│ conversation.input.dock (new, order:99)    │      │   ↓                            │
│   ↓ on mount/refresh fetch POST            │      │ settings.get("jira-tasks")      │
│ render: list / error / unconfigured        │      │ credentials.resolve(TOKEN_REFS) │
│ localStorage per-workspace key/JQL         │      │ subprocess.spawn(curl …)        │
│ settings.plugin.item (Settings card)       │      │   ↓ stdout JSON                 │
└────────────────────────────────────────────┘      │ parse issues → {ok,issues}      │
                                                    └────────────────────────────────┘
```

- **Host**: registers the `jira-tasks` settings namespace (`baseUrl`, readable) and a `webServer` route `POST /jira/api/search`; the address comes from the settings document, the token 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; registers `conversation.composer.dock` (active sessions) and `conversation.input.dock` (new sessions, flex `order: 99` below the input, aligned width), plus `settings.plugin.item` (`key: "jira-tasks"`) for the Settings card — the address is written through `settingsScope` and the token through `remote.credentials`.
- **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.
- **New-session display**: the DSH shell does not render `composer.dock` during the hero (blank session) phase, so the plugin also registers `input.dock` and de-duplicates by "session has messages".

**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 card + same `.credentials.yaml` fallback |
</details>

## License

MIT
