# AI Session Title

Adds an **AI generate** control inside the session rename dialog of DeepSeek
Harness (DSH). It asks the session's current model for a short title derived
from the real conversation — plus whatever draft you already typed — and fills
it into the name field. You review it and press 重命名, or keep your own text.

English | [中文](README.md)

[![npm](https://img.shields.io/npm/v/dsh-ai-session-title)](https://www.npmjs.com/package/dsh-ai-session-title)
[![license](https://img.shields.io/npm/l/dsh-ai-session-title)](LICENSE)
![dsh](https://img.shields.io/badge/dsh-%5E0.2.1--rc.3-blue)
![node](https://img.shields.io/badge/node-%3E%3D20-blue)

Requires **dsh ≥ 0.2.1-rc.3** and **Node ≥ 20**.

## What it looks like

![AI generate inside the rename dialog](docs/screenshot.png)

- Open the built-in rename dialog of any session (`···` → 重命名会话).
- A sparkle button appears at the trailing edge **inside** the name field, on
  the same border — it is the shipped dialog, not a replacement.
- Clicking it reads that session's transcript (plus the current field text as a
  reference draft), generates a title, and writes it back into the field.
- The native 取消 / 重命名 footer is unchanged; generation never renames on its
  own, you still confirm.

## Install

### From npm (recommended)

```sh
dsh plugin --profile <your profile> add dsh-ai-session-title
```

Then restart `dsh web` and reload the page.

> Desktop builds that bundle their own `dsh` should install from the app's
> **Settings → Plugins**; the CLI never rewrites the app-owned profile while the
> app is running.

### From this checkout

```text
plugin_manager install_bundle  target = <absolute path to this directory>
```

or let an agent do it with the `plugin_manager` tool, then reload the Web UI.

### From the plugin market

The market reads the
[awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
catalog, not this repository. Once an entry exists there, install it from
**Plugins → Discover**.

## How it works

Two halves in one package:

| Half | File | Role |
| --- | --- | --- |
| Host | `index.js` | Registers the global `/ai-title` command. It reads the session's event log, picks the route the next request would use (`session.requestHeader().config`, else the newest assistant message), streams one completion through `ctx.llm` with `purpose: 'session-title'`, and returns a cleaned title. Text after the command name is treated as the user's draft title. |
| Client | `client.js` | Watches for the shipped dialog's name field and decorates it in place, then calls `ctx.remote.commands.execute(sessionId, '/ai-title <draft>', [])` and writes the returned text into the field through a native `input` event. |

Two details worth knowing:

- **Why decorate the DOM instead of registering a slot**: the shipped dialog
  keeps its target session in component state and renders through
  `shell.overlay`, exposing no slot inside itself, so there is no insertion
  point for a plugin. The session id is recovered by matching the field's seeded
  title against the Workspace Service's session list.
- **Why a command**: the client→host RPC surface is generated at build time and
  cannot be extended by third-party bundles, so a command is the only stable
  channel. The cost is a small `command/run` + `command/done` pair per
  generation in the session flow — also the only visible feedback while it runs.

## Configuration (optional)

Once installed, open **More → Settings**: the navigation gains an **"AI session
title"** section. Edit it and save there:

| Field | Meaning |
| --- | --- |
| `style` | Title structure — what the generated title looks like; see the table below |
| `datePrefix` | Put an `MM-DD ` prefix in front of the title, off by default |
| `prompt` | Custom structure instruction, used only when `style` is `custom`; left empty it behaves like `concise` |
| `maxTitleChars` | Hard character cap on the returned title, default `30` |

The four presets are four ways of finding a session again later:

| `style` | Name | Sample | Use it when |
| --- | --- | --- | --- |
| `concise` | Subject | `Login timeout` | Default. A 3~8 character noun phrase that survives the narrowest sidebar row |
| `descriptive` | Action | `Refactor login state` | Starts with a verb, so the row says what you did |
| `outcome` | Subject + outcome | `Paging off-by-one · fixed` | Subject first, then what came out of it — the most useful when reviewing work |
| `custom` | Custom | decided by the prompt | Describe your own shape in `prompt` |

The day is a switch, not a fifth preset: with `datePrefix` on, every structure
above gains a `10-30 ` prefix, so "date + subject" and "date + subject +
outcome" are one click apart instead of two more cards.

That prefix is not written by the model: the plugin formats it from the first
message's timestamp and reserves those six characters out of the cap before
asking for the body, so the total still honours `maxTitleChars`.

Whichever structure is picked, the hard rules stay: one line of plain text, no
quotes, label, or Markdown, in the language of the conversation. A preset only
changes the wording, so a custom prompt cannot corrupt what the command result
carries.

The retired `english` and `dated` values are still accepted: `english` keeps
producing English titles, and `dated` means "concise plus the date prefix", so an
older configuration keeps working after the upgrade.

That page is registered by the plugin itself (`settings.section`) and edits the
plugin's own `ai-session-title` entry, so it is exactly equivalent to writing the
`cordis.patch.yml` block below by hand.

A few advanced keys are kept off the settings page; write them into the
profile's `cordis.patch.yml` when needed:

```yaml
- id: ai-session-title
  name: dsh-ai-session-title
  config:
    provider: deepseek
    model: deepseek-chat
    maxOutputTokens: 1024
```

| Key | Meaning |
| --- | --- |
| `provider`, `model` | Force a route. Both or neither; defaults to the route the next request would use, then the newest assistant message. |
| `timeoutMs` | Generation deadline, default `60000`. |
| `maxOutputTokens` | Output budget for the one call, default `1024`. Raise it when a reasoning model spends the budget thinking and answers with nothing visible (`max-tokens`). |

## Compatibility

`engines.dsh` is `^0.2.1-rc.3`. DSH decides compatibility from `engines.dsh`
alone (falling back to lockstep `@deepseek-ai/dsh-*` peer ranges when it is
absent), and the market shows that requirement on the plugin card.

The plugin imports exactly one `@deepseek-ai/*` package at runtime:
`@deepseek-ai/schemastery`, which the host half uses to declare `Config` (the
harness ships it). Beyond that the host half uses only `ctx.commands` /
`ctx.llm`, and the client half only `ctx.remote` and DOM APIs. That keeps it loadable from any profile without a
dependency install, but it also means a harness that renames those services or
changes the rename dialog's markup needs a matching plugin release.

## Development

```sh
npm test        # host-half smoke test — no harness, no model
```

The client half has no build step: `client.js` is served as written. Edit it and
reload the Web UI (the profile installs this package as a `link:`, so no
reinstall is needed).

## Layout

```text
cordis.patch.yml     inserts row id `ai-session-title` as this package
index.js             host half (command + generation)
client.js            client half (in-place decoration + remote calls)
test/host.smoke.mjs  host-half smoke test (npm test)
locale/*.json        bundle display metadata (title / description)
icon.svg             bundle icon
screenshots.json     market card screenshot declaration
docs/screenshot.png  the screenshot above
```

## Troubleshooting

**The button is missing.** Check that the plugin is enabled, that you reloaded
the page, and that your dsh satisfies `^0.2.1-rc.3`.

**"the model did not return a usable title".** Usually a reasoning model
spending the whole output budget on reasoning — raise `maxOutputTokens`.

**"cannot determine which session to name".** The field's title matched no
session in the list (for example a same-titled session was just renamed). Open
that session in the sidebar first, then retry.

## License

MIT — see [LICENSE](LICENSE).
