Skip to content
dsh.fish
Bundle

dsh-client-ui-selection-add

Web selection-add feature: float an 'Add to conversation' button over a text selection in the conversation body and prefill the composer draft

Source
pangzi-club
License
MIT
Updated
Updated 6 days ago

Readme

# dsh-client-ui-selection-add

English | [中文](README.zh.md)

> Source: [pangzi-club/dsh-client-ui-selection-add](https://github.com/pangzi-club/dsh-client-ui-selection-add)

Web selection-add feature plugin: its browser half contributes a floating
**Add to conversation** button into the conversation-owned
`conversation.input.dock` slot. While a session's composer is mounted the
plugin listens for text selection; a non-empty, non-editable selection floats a
small button just above its bounding box, and clicking the button fills the
composer **draft** (`inputActions.setDraft`) without sending — the user reviews
and hits Enter (or the send button) themselves. Its host half is empty on
purpose: capturing a selection and prefilling the draft is pure browser-surface
presentation, so there is nothing for the Host to own.

The component is mounted in a **session-scope** list slot, so the framework
delivers the session standard kit (`useSession`, `sessionId`, `useProjection`,
`useInput`, `inputActions`) plus the `session`/`input` owner share and the
`selection` locale seat; the component only reaches for `inputActions` and `t`.
Because the button is a body-level portal, the dock entry renders nothing itself
— the composer layout is untouched.

Two behaviors worth calling out:

- **Prefill, not auto-send.** The click is `setDraft(text)`; `submit()` is
  deliberately not called, so the user can edit the quoted text before it goes
  to the model. (Sending on click is a one-line change in `SelectionAddButton`.)
- **Editable-field guard.** Selections inside an `input`, `textarea`, or
  `contenteditable` never show the button — selecting inside the composer is
  editing the draft, not quoting the conversation, and the button would fight
  the caret.

The button copy is bilingual under the `selection` namespace of `dsh-client-locale`.
The anchor capture lives in a framework-free `selection.ts` module
(`selectionAnchorOf` / `isEditableField`), unit-tested on its own.

## Model Experience

None. Everything here is a browser interaction: the model never sees a tool,
a session event, or a schema. The plugin is a pure presentation surface over
the input machine's public `inputActions` face.

#### KV Cache effect

No invalidation. The plugin reads no session data and publishes no session
events, so the conversation log, projections, and KV caches are untouched.

## Known Limitations and Deferred Work

- **Not scoped to the transcript body.** The selection listener is `document`-wide;
  the only guard today is the editable-field check. Selecting in the sidebar or
  settings shows the button because the plugin cannot identify the transcript
  scroll container without changing `ui-conversation` (the no-core-change choice
  above). Scoping it requires either a proper `conversation.selection.*` slot
  (see below) or a `data-*` hook the conversation skeleton already owns.
- **Prefill only.** The user must confirm before send; there is no "send now"
  affordance on the floating button.
- **Multi-line selections** anchor above the bounding box's top (the first
  line); a selection that starts near the viewport top may clip the button.
- **View-chat only, by construction.** The button only mounts while a session
  composer is mounted, so it does not appear in the no-session hero. It does not
  distinguish which view tab is active.

## Extension point (the "proper slot") upgrade

The current path rides the existing `conversation.input.dock`, so it requires
zero changes to `ui-conversation`. To make the capture a first-class extension
point instead, add a session-scope slot to `ui-conversation`
(`packages/client/ui-conversation/src/client/apply.ts` `children` plus the
`contract/slots.ts` `SlotMap` merge), e.g. `conversation.selection.action`
`{ kind: 'list', scope: 'session' }`, and register this plugin's entry there.

## Build and publish independently (community plugin)

The package is a **buildable, independently publishable DSH client plugin**. It
does **not** need the deepseek-harness monorepo to compile: `package.json`,
`tsconfig.json`, and `tsdown.config.ts` are all standalone, and the
`@deepseek-ai/*` imports are `import type` only, so they resolve from
`node_modules` at their published versions.

### Build

```bash
pnpm install
pnpm bundle        # tsc (emits lib/types/**/*.d.ts) + tsdown (emits lib/*.js)
pnpm watch         # tsdown --watch
```

Output:
- `lib/index.js` — host half (empty `apply`; the browser half does all the work)
- `lib/invariant.js` — invariant companion
- `lib/client.js` — the browser module-table bundle
  (`window.__ModuleLoader__.load({ id, factory })`)
- `lib/types/**/*.d.ts` — TypeScript declarations

### Publish

`pnpm bundle` first (or rely on `prepack`, which runs it automatically), then:

```bash
npm publish            # or pnpm publish
```

The published surface is the `files` list: `lib/*.js` (+ maps) and
`lib/types/**/*.d.ts`, plus `cordis.patch.yml`.

### Install into DSH (community plugin)

This package declares the `dsh.bundle` manifest, so it installs via the same
channel as any community plugin (installing it from npm):

```bash
npm install dsh-client-ui-selection-add
dsh plugin --profile web add dsh-client-ui-selection-add
```

or, for a local build:

1. Symlink/junction this directory into
   `$DSH_HOME/profiles/node_modules/dsh-client-ui-selection-add`.
2. Append the host row to `$DSH_HOME/profiles/web/cordis.patch.yml`:
   ```yaml
   - insert:
       - id: ui-selection-add
         name: dsh-client-ui-selection-add
   ```
   (or just `dsh plugin add` writes it for you via `dsh.bundle.patch` →
   `cordis.patch.yml`).

The client half registers into the framework-owned `conversation.input.dock`
slot and pulls the `locale` service. Both are provided by the stock `dsh web`
build (`@deepseek-ai/dsh-client-ui-slots`, `@deepseek-ai/dsh-client-locale`,
`@deepseek-ai/dsh-client-ui-conversation`), so a stock `dsh web` build resolves
the runtime. Because these framework packages are **not** all baseline
module-table rows (they're separate `dsh.client` graph rows), a bare
community install only works against a DSH Web build that already activates
them — which is the case for upstream `dsh web`.

### Cross-reference: deepseek-harness monorepo

This repository is the standalone home of the package. The same sources are also
wired into the deepseek-harness monorepo as
`@deepseek-ai/dsh-client-ui-selection-add` at `packages/client/ui-selection-add`;
there the tsdown preset and the three registration surfaces
(`tsconfig.client.json` reference, `packages/bundle/web-app/cordis.patch.yml`
`dsh.client` roster row, `packages/bundle/web-app/package.json` dependency) come
from the monorepo itself. This checkout builds and publishes from the
self-contained configs above instead.

Install

dsh plugin --profile web add github:pangzi-club/dsh-client-ui-selection-add#9d225e41a69a951fabbbde4e6a339cca948df2c2

Profile: web

Source