refactor!: align with the DSH plugin conventions and drop the npm publish workflow

Follow the cordis-plugin-development references instead of the ad-hoc choices
the first version made.

Client half:
- register the row button under this package's own id instead of shadowing the
  shipped `archive` action at a lower priority, so the official hover buttons
  keep their cells and archiving stays where the harness put it
- drop the synthetic `pointerout` dispatched at a `[data-row-key]` ancestor:
  a plugin does not read or drive another package's DOM, so the tooltip is now
  positioned from its own button alone
- keep the self-rendered primitives, the `--dsw-*` token-only styling, and the
  modal focus/Escape behavior, and document why

Host half:
- resolve a conversation's descendants with `sessionQuery.traceSession()`
  instead of listing every stored header and re-deriving `parentSession` edges
- decide liveness from the traced `SessionRecord.live` flag, which removes the
  `agents` and `sessions` dependencies from `inject`
- build the orphan-sweep corpus from `sessionQuery.listSessions()`, which
  already merges live and persisted sessions
- document that `sessionPersistence.locate()` is a JSONL-backend diagnostic
  hook, not part of the seam, and keep probing it explicitly
- record only tree roots in the deferred-deletion ledger: the activation sweep
  removes a root with everything under it, so a separately recorded child was
  deleted twice or needed a pass that never runs
- return the activation's sweep promise from `apply()` so a test can await it

Manifest and docs:
- version 2.0.0, `private`, `dsh.manifestVersion`, and `engines`
- delete the Gitea npm publish workflow and every npm-publishing task: the
  package is distributed only through the Git repository and tags
- rewrite the README around the current install paths and the plugin's limits
This commit is contained in:
pyh
2026-09-30 15:30:39 +08:00
parent acaca33c49
commit da8e5e43a4
8 changed files with 253 additions and 343 deletions
+48 -62
View File
@@ -1,14 +1,27 @@
/**
* Browser half of the session-delete bundle.
*
* One row in a conversation's "..." menu opens an irreversible-deletion dialog.
* Confirming posts to the Host half's authenticated route; the Host owns every
* policy decision and the Client only renders its answer.
* One row in a conversation's "..." menu and one hover button on the
* conversation's sidebar row open an irreversible-deletion dialog. Confirming
* posts to the Host half's authenticated route; the Host owns every policy
* decision and the Client only renders its answer.
*
* Harness Client packages may change without notice, so this bundle imports no
* Harness Client module: its menu row and dialog copy the shipped primitives'
* markup, stylesheet declarations, and focus/Escape behavior, keeping only
* `--dsw-*` theme-token references. React comes from the page's module table.
* Two client-authoring rules shape this file:
*
* - No Harness Client module is imported. Those packages change without notice,
* a plain-JavaScript bundle has no type check, and a throwing component blanks
* the whole slot entry, so the menu row, the row button, the dialog, and the
* General-settings row re-implement the shipped markup, stylesheet
* declarations, and focus/Escape behavior themselves. Class names are renamed
* under `dsd-` and every color, radius, elevation, and motion value stays a
* `--dsw-*` theme-token reference.
* - Nothing reads another package's DOM. An earlier version nudged the row's
* hover preview out of the way by dispatching a synthetic pointer event at a
* `[data-row-key]` ancestor; that is exactly the kind of dependency on a
* self-owned rendering surface a plugin must not take. The tooltip below is
* positioned from its own button alone.
*
* @module @dsh-plugin/session-delete/client
*/
window.__ModuleLoader__.load({
id: '@dsh-plugin/session-delete',
@@ -18,14 +31,12 @@ window.__ModuleLoader__.load({
const NS = 'session-delete'
const ROUTE = 'api/plugin/session-delete/delete'
const ORPHAN_ROUTE = 'api/plugin/session-delete/orphans'
/** The shipped menu rows occupy 100–400; this one follows them. */
const MENU_ORDER = 500
/** The shipped hover button's id; reusing its id takes that cell over. */
const ROW_ACTION_ID = 'archive'
const ROW_ACTION_ORDER = 100
/** Below the shipped entry's implicit 0: the lowest priority renders. */
const ROW_ACTION_PRIORITY = -1
/** Between the shipped `archive` (100) and `pin` (200) hover buttons. */
const ROW_ACTION_ORDER = 150
/** General-settings position: after the log-upload row, before the version. */
const SETTINGS_ORDER = 95
const SETTINGS_ORDER = 96
const TOOLTIP_DELAY_MS = 500
const TOOLTIP_GAP = 8
/** Document base captured at bundle registration, before any routing. */
@@ -83,8 +94,9 @@ window.__ModuleLoader__.load({
* Menu-row declarations mirror `dsh-client-ui-primitives`' Menu.module.css
* `.item/.itemIcon/.itemLabel`; dialog declarations mirror its
* Modal.module.css and Button.module.css plus the workspace dialog's
* destructive accent and secondary status lines. Class names are renamed
* under `dsd-`, and every color, radius, elevation, and transition stays a
* destructive accent and secondary status lines; the settings row copies
* the General-section row pattern. Class names are renamed under `dsd-`,
* and every color, radius, elevation, and transition stays a
* `--dsw-*`/`--ds-*` token reference.
*/
const CSS = `
@@ -172,8 +184,7 @@ window.__ModuleLoader__.load({
/**
* Icon paths are the shipped `IconTrashOutline` and `IconCloseOutline`
* artwork verbatim: a 16-unit viewBox the glyph fills, stroked at the
* regular weight of 1. Redrawing a smaller glyph in a 24-unit box is what
* made an earlier version of this row look undersized.
* regular weight of 1.
*/
function TrashIcon({ size = 16 }) {
return h('svg', {
@@ -244,42 +255,15 @@ window.__ModuleLoader__.load({
}
/**
* Withdraw the session row's hover preview while this button's own tooltip
* is showing.
* The row's hover button. It carries its own id in the row-action list, so
* the shipped `archive` and `pin` buttons keep their cells instead of being
* shadowed.
*
* The shipped row buttons get this for free: `Tooltip` reports itself
* through the primitives' private `TooltipSuppression` context, and the
* enclosing `HoverCard` — whose anchor is the whole row, so a button inside
* it counts as hovering the row — hides its card while that bubble is up.
* That context object belongs to another module instance and cannot be
* reached from here, so this reproduces the same outcome through the DOM:
* a `pointerout` on the card's anchor wrapper makes React deliver
* `onPointerLeave` to that wrapper alone, which is exactly the cancel the
* hover card performs on a real leave. Nothing else in the tree is affected
* because the related target is the wrapper's own parent.
*
* @param button - the hovered row button.
*/
function withdrawRowPreview(button) {
try {
const wrapper = button.closest('[data-row-key]')?.parentElement
if (wrapper === null || wrapper === undefined) return
wrapper.dispatchEvent(new PointerEvent('pointerout', {
bubbles: true,
cancelable: true,
composed: true,
pointerType: 'mouse',
relatedTarget: wrapper.parentElement,
}))
} catch {
/* an unchanged host simply keeps showing its card */
}
}
/**
* The row's hover button, in the cell the shipped archive action occupied.
* Its tooltip copies the primitive's bubble: the same delay, the same
* bottom/end placement, and the same fade.
* Its tooltip copies the primitive's bubble — the same delay, the same
* bottom/end placement, the same fade — and is placed from this button's own
* rect alone. While the pointer rests on the row, the row's own hover
* preview may appear alongside it; that is accepted rather than suppressed
* by reaching into the row's DOM.
*/
function DeleteSessionRowButton(props) {
const t = translate(props.t)
@@ -289,7 +273,6 @@ window.__ModuleLoader__.load({
const [anchor, setAnchor] = React.useState(null)
const show = () => {
if (buttonRef.current !== null) withdrawRowPreview(buttonRef.current)
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => {
const rect = buttonRef.current?.getBoundingClientRect()
@@ -549,8 +532,11 @@ window.__ModuleLoader__.load({
}
/**
* Post to one of this package's Host routes. Routes sit behind the
* connection's trust fence, so the page's own credentials apply.
* Post to one of this package's Host routes. The registration path on the
* Host is absolute while this one is document-relative, so the request is
* built against the base captured at bundle registration and works under a
* mounted prefix. Routes sit behind the connection's trust fence, so the
* page's own credentials apply.
* @param route - document-relative route path.
* @param body - JSON body to send.
* @returns the Host's result value.
@@ -569,7 +555,9 @@ window.__ModuleLoader__.load({
payload = null
}
if (!response.ok || payload?.ok === false) {
throw new Error(payload?.error?.message ?? `HTTP ${response.status}`)
const code = payload?.error?.code
const detail = payload?.error?.message ?? `HTTP ${response.status}`
throw new Error(code === undefined ? detail : `${detail} (${code})`)
}
return payload
}
@@ -611,15 +599,13 @@ window.__ModuleLoader__.load({
inject: () => ({ requestSessionDelete }),
}, DeleteSessionMenuItem))
// This entry reuses the shipped archive action's id, so the row keeps
// one hover button and archiving stays available from the "..." menu.
// Same id at the same priority is refused as a duplicate, and the
// lowest priority renders, so this row must sit below the shipped 0.
// This entry carries its own id, so the shipped `archive` and `pin`
// hover buttons keep their cells and are not shadowed by a lower
// priority. Archiving therefore stays exactly where the harness put it.
ctx.slots.inject('sidebar.workspaces.session.row.action', () => ctx.slots.register({
name: 'sidebar.workspaces.session.row.action',
id: ROW_ACTION_ID,
id: NS,
order: ROW_ACTION_ORDER,
priority: ROW_ACTION_PRIORITY,
locale: NS,
inject: () => ({ requestSessionDelete }),
}, DeleteSessionRowButton))