# Patch reference

`lib/client.js` is generated by `scripts/build-client.mjs` from
`vendor/dsh-client-ui-workspace-0.2.1-rc.3.js` plus the ordered modules in
`patches/manifest.json`. **Never hand-edit `lib/client.js`**; change a patch (or
the vendored baseline) and rebuild.

## Architecture: replacement, not shadow registration

The first design attempt shadowed the shipped `sidebar.workspaces` occupant with
a `priority: -100` registration. That cannot work, and the two contract facts are
worth recording:

1. `SlotCore.register` throws when a new registration declares a child slot that
   is already declared (`slot "..." is already declared (by ...)`); child-slot
   declarations are exclusive to one registration.
2. The renderer's `boundRenderSlot` only authorises the `renderSlot` props of an
   entry for the child keys in **that entry's own** `children` table
   (`slot '...' is not declared by this entry's children`, `SlotOwnershipError`).

So a second occupant can neither redeclare the official child slots nor render
them, i.e. it would lose the search row, session "..." menus, row hover actions,
and the directory-flow pickers. The supported shape for a full browser fork is a
replacement: `cordis.patch.yml` disables the shipped `ui-workspace` loader row
while this bundle is enabled, and this bundle runs the official client code with
the patches below. Disabling/uninstalling the bundle removes the layer and the
shipped row returns unchanged. Native mode is therefore a rendering mode of the
same patched component tree, not a second registration.

## Patch contract

Each `patches/NN-name.mjs` module:

- is plain ESM, no dependencies;
- exports `apply(source: string): string` (may be async); it must be a pure
  function that returns the patched source;
- may export `name`, `description` and `anchors` (informational);
- asserts every anchor it relies on: the anchor must match **exactly once**, and a
  missing/duplicated anchor must throw with enough detail to re-anchor
  (see docs/upgrade-checklist.md section 5);
- should throw when applied to an already-patched bundle instead of silently
  duplicating its edits (the build always starts from the pristine vendor copy,
  so this only catches operator error);
- is listed in `patches/manifest.json` with a `file` and `description`; the
  manifest order is the application order.

`scripts/build-client.mjs` verifies the baseline bytes/SHA256 before applying
anything, refuses a build (exit 1) when any manifest patch file is missing or any
`apply()` throws, runs `node --check` on the result and only then writes
`lib/client.js`. `BUILD_ALLOW_MISSING=1` allows an early smoke build with the
available patch files; such an artifact must never be installed.

## Current patch set (2026-10-27)

| # | file | purpose | byte delta |
|---|---|---|---|
| 01 | `01-bundle-identity.mjs` | rewrite the `__ModuleLoader__` id to the plugin package name and namespace the four CSS-module style-tag ids | -80 |
| 02 | `02-sections-derive.mjs` | section / pinnedSection / latestActivity derivation, fixed-section workspace identity + legacy-title list, native-mode passthrough | +4476 |
| 03 | `03-sections-render.mjs` | three section headers with state-coloured segment icons (ECG / checklist / zzz), project-row inset variable, flat History body, section collapse, native-mode passthrough, pin action wiring | +9777 |
| 04 | `04-view-options.mjs` | view-options sections block, active-days threshold, pin rows in the workspace row menu, browser/SessionTree prop wiring | +2991 |
| 06 | `06-native-toggle.mjs` | plugin-owned state + actions, History bootstrap, `useSections`/`sectionsActions` injection, footer return button + row-inset CSS | +7934 |

Total: 232860 -> 257958 bytes. Baseline SHA256
`bafd2d6eea2f82674cc0606bec13fc891e44b42fb2ac973f01cb32c735d4062f` (prefix
`BAFD2D6EEA2F8267`), generated SHA256
`8cc6ce00ce4ac0e08957a9e38b01d30039f38fd9a503bd802f1a9aac480c29c0`.

Patch 05 (`05-drag-indicator.mjs`) is **retired** as of 0.2.1: the drag
indicator uses the official style again, so the file and its manifest entry were
removed. (Numbers stay stable so old anchor notes remain readable.)

## Patch 01 - bundle identity

Baseline anchor: lines 1-2, `window.__ModuleLoader__.load({ id:
"@deepseek-ai/dsh-client-ui-workspace" })`. The id becomes
`dsh-sidebar-sections` because our loader row imports our package
name. No slot priority override is added: the shipped row is disabled instead.

The patch also rewrites the **four CSS-module style-tag ids** from
`"@deepseek-ai/dsh-client-ui-workspace/<Module>.module.css"` to
`"dsh-sidebar-sections/<Module>.module.css"` (Rows, AnimatedRows,
WorkspacePicker, WorkspaceBrowser). The shipped bundle dedupes its `<style>` tags
by that id. When the plugin is disabled live, its old tags stay in the document;
if the DSH host was updated in the meantime, the newly loaded official bundle
would see its own tag id already taken and skip injecting the new (re-hashed) CSS,
leaving the official sidebar unstyled until a full reload. Namespacing keeps both
sets independent — identical-version CSS is byte-identical, so old leftovers are
harmless, and a changed host version always injects its own styles. This is the
0.2.8 fix for the 0.2.1-rc.3 update regression.

## Patch 02 - section derivation

Baseline anchors: `groupByWorkspace` (L1027), the archived-only empty-group drop
(L1040), `deriveGroups` (L1091).

- Prepends the plugin helpers (`SECTIONS_HISTORY_PATH`,
  `SECTIONS_HISTORY_TITLE` = `会话任务`, `SECTIONS_HISTORY_LEGACY_TITLES` =
  `["历史任务"]` for the one-time title migration, `SECTIONS_DEFAULT_STATE`, path
  canonicalisation, activity-days surface).
- Adds a `sectionsMode` parameter to `groupByWorkspace` and keeps the fixed
  section workspace in the archived-only filter only in sections mode.
- In `deriveGroups`, when `sections.mode === "sections"`, tags each group with
  `section` (active / history / inactive), `pinnedSection`, `latestActivity` and
  `activityDays`:
  1. `D:\files\history-tasks` (canonicalised path) -> `history`, expanded and flat;
  2. manual pin (`pins[workspaceId]`) wins over every automatic rule;
  3. running / pending-attention member or the current main-view group -> `active`;
  4. otherwise max member `updatedAt` (fallback workspace `createdAt`) vs
     `now - activeDays * 86400000`.
- The section computation is wrapped in try/catch: a derivation failure logs a
  warning and falls back to the official group row for that group.
- `mode === "native"` returns exactly the official derivation (extra fields
  absent, official expansion behaviour).

## Patch 03 - sectioned rendering

Baseline anchors: the `SessionTree` comment/signature (L3023-3024), the
`deriveGroups` call (L3069), the reveal effect (L3080), `childrenByParent`
(L3137), the `renderGroup` head (L3153), the workspace drop marker/props
(L3165-...), the `ProjectRowItem` call (L3223), the workspace actions object
(L3250), the `groupRows` line (L3330), and the zh/en dictionaries.

- Adds `SectionsSectionIcon` (Active: custom ECG trace; Session tasks: shipped
  `IconChecklistOutlineRegular`; Inactive: custom descending "zzz", all in the
  official 16x16/1px stroke style) and `SectionHeaderRow` (segment icon + collapse
  chevron + label + count + Session tasks "+" button) using the official row
  classes/primitives, and `dsws-section-count` for the count.
  The icon slot carries the shipped `folder` class plus `folderActive` while the
  section is expanded (official business-primary blue), so a collapsed section
  keeps the tertiary colour; the chevron slot keeps the shipped hover-only reveal
  rule (icon hidden and chevron shown on hover/selection) with the arrow rotating
  by expanded state.
  The count is the number of workspaces for Active/Inactive (like the legacy
  implementation) and the number of sessions for the Session tasks section.
- `sectionsActive = sections.mode !== "native"` is computed next to the
  `deriveGroups` memo; all additions below are gated on it.
- Sections mode: the fixed-section group is forced to a root position and rendered without
  its workspace project row (flat sessions, still the official session rows,
  watermarks and menus); the three fixed headers wrap the official
  `renderGroup` output; collapsed sections persist in the plugin store.
- Native mode: `groupRows = rootGroups.map(renderGroup)` - the official tree.
- Project rows are inset under their fixed section header: each groupSection
  publishes `--dsh-sections-row-inset: 21px` in sections mode (0px in native
  mode); patch 06's CSS uses it to shift the official project/session row boxes -
  and therefore their content, hover and selected backgrounds - right by the same
  value. 21px is derived from the header layout: label at padding 8 + icon slot
  16 + gap 6 = 30px, first 14px glyph centre at 37px; folder slot centre at
  padding 8 + half slot 8 = 16px -> inset 37 - 16 = 21px, so the project folder
  icon axis sits under the first glyph of the section label. The count/section
  header rows are excluded. The official `--dsh-workspace-indent` values are
  untouched, so native mode is the shipped layout.
- The workspace row gets `pinnedSection` and (sections mode only) a `pin` action.

## Patch 04 - view options, threshold and pins

Baseline anchors: `ProjectRowItem` props and workspace menu (L1950-2002),
`ViewOptionsMenu` props/state/items/select handler (L2877-2958),
`WorkspaceBrowser` props/store hooks/`ViewOptionsMenu` call/SessionTree call
(L3509-4019).

- View-options menu prepends: "分段" label, sections/native toggle
  ("切回官方列表" / "启用分段视图"), "活跃阈值 · N 天", decrease / increase /
  reset rows. The official group-by / order / archive items are untouched.
- Workspace row menu gets pin-to-active / pin-to-inactive / unpin rows when the
  pin action is wired (sections mode only), checked through `Menu.selectedIds`.
- `WorkspaceBrowser` consumes the injected `useSections` snapshot and
  `sectionsActions`, forwards them to `ViewOptionsMenu` and `SessionTree`
  (`setSectionCollapsed`, `setWorkspacePin`, `setSectionsMode`,
  `retryHistoryInit`).
- Every inserted marker is re-counted and every untouched official snippet is
  re-checked after the transform.

## Patch 05 - retired (official drag indicator)

As of 0.2.1 the plugin no longer overrides the drag insertion indicator: the
official marker CSS (blue rule + chevron caps) is used as shipped, and the
"plain blue line" substitute was removed at the user's request. Drag semantics
were never touched. The file `patches/05-drag-indicator.mjs` no longer exists;
the numbering of the remaining patches is unchanged for anchor-note stability.

## Patch 06 - plugin state, History bootstrap, footer toggle

Baseline anchors: the `viewStore` creation, `browserInjected`, and the end of
`apply()` (the last registration is `conversation.hero.workspace`).

- Adds the plugin-owned snapshot store `dsh.workspace.sections.v2` (sanitised:
  `mode`, clamped `activeDays` 1-90, `pins`, `sectionCollapsed`, History state)
  and the actions `setMode`, `setActiveDays`, `resetActiveDays`, `setPin`,
  `setSectionCollapsed`, `retryHistoryInit`.
- Fixed-section workspace bootstrap: subscribe to `workspaces.list`; when the
  snapshot is ready and no workspace has the canonical
  `D:\files\history-tasks` path, call `workspaces.create({ path })` and then
  `workspaces.rename(id, "会话任务")` once. When the workspace already exists but
  still carries a legacy title (`历史任务`), rename it once to `会话任务` through
  the official `workspaces.rename` API; other user-chosen titles are left alone.
  Failures are stored in the plugin state and surfaced by patch 03's in-section
  error + retry row. The host half creates the directory before this can run.
- Adds `sectionsActions` and `hooks.sections` to `browserInjected`.
- Registers `sidebar.footer.action` id `workspace-sections-return` (order 400,
  locale `workspace`): the button renders only while `mode === "native"` and
  calls `setMode("sections")`. Installs the small plugin stylesheet (section
  count, footer button and the sections-mode row inset). The row-inset rule
  targets the official project/session row classes derived from the baseline CSS
  class map (plus a width adjustment for the overflow button); section headers
  are excluded via `:not(.dsws-section-header)`. No rule touches the header
  icon/chevron: they follow the shipped project-row behaviour (icon visible,
  chevron on hover, icon blue while expanded).
- It does **not** remove or re-register any official registration: the official
  menu items, row actions, dialogs, toasts, shortcuts, picker and services all
  keep running from this bundle.

## Editing or adding a patch

1. Edit `patches/NN-name.mjs` (or add a new file and append it to
   `patches/manifest.json` in application order).
2. `node scripts/build-client.mjs --check-only` from the plugin root.
3. Confirm the build prints the expected byte delta and `node --check OK`.
4. Run the full build and the two test scripts:
   `node scripts/build-client.mjs && node scripts/smoke-load.mjs &&
   node scripts/derive-selftest.mjs`.
5. Update this file (anchors/status) and the CHANGELOG.

## Re-anchoring on a baseline upgrade

Follow [docs/upgrade-checklist.md](upgrade-checklist.md) sections 3 and 5: extract
the new baseline, diff it, refresh `vendor/` + `patches/manifest.json`, run the
build and fix anchor failures one patch at a time. Do not weaken assertions to
make a stale anchor pass.

## Verification log (2026-10-22, this workspace)

- Full build from the pristine vendor baseline: the five current patches applied
  (byte deltas above), output 257958 bytes, SHA256
  `8cc6ce00ce4ac0e08957a9e38b01d30039f38fd9a503bd802f1a9aac480c29c0`,
  `node --check OK`; `--check-only` leaves `lib/client.js` untouched.
- `node scripts/smoke-load.mjs`: bundle id `dsh-sidebar-sections`,
  exports `apply, inject`, inject list `slots, sessions, workspaces, locale,
  remote, remote.directoryPicker, layout, shortcuts, remoteAccess`, requires only
  the five frozen platform seed modules, 4 official CSS tags injected at module
  evaluation.
- `node scripts/derive-selftest.mjs`: 22 assertions covering the SPEC 2.1 rules
  (threshold, running/pending/current, pins beating threshold and running,
  history identity + flat expansion, ungrouped, native passthrough,
  archived-only History retention, archived sessions not counting as activity).
- Live install into the `desktop` profile (`plugin_manager install_bundle`):
  `application: applied`; `dsh.profile.bundles` now ends with
  `dsh-sidebar-sections`.
- Live Client slot inspection: `sidebar.workspaces` has one occupant (this
  bundle; the shipped row is disabled) and still declares all official child
  slots; `session.menu.item` occupants pin 100 / rename 200 / fork 300 /
  archive 400 / session-id 450 active; `session.row.action` archive 100 / pin 200
  active; `sidebar.workspaces.directoryFlow` occupant active;
  `conversation.hero.workspace` occupant active; `sidebar.footer.action` entry
  `workspace-sections-return` active.
- Live app screenshot: the sidebar renders the `活跃` section header with a count
  above the official project rows.
- Native path: a temporary forced-native build (reverted again) rendered the
  official list in the live app and showed the `会话任务` workspace created by the
  fixed-section bootstrap. Bundle disable -> enable live-reloaded the client
  bundle, which is also the documented iteration loop for client-half changes.
- Rename pass (0.2.6): the section label and the fixed workspace title became
  `会话任务`; the pre-existing workspace (formerly `历史任务`) was renamed once
  through the official `workspaces.rename` API and the new title is visible in
  `~/.dsh/storages/workspace.json` (`updatedAt` moved to the rename time).
- Not verified in-app: pointer interaction (clicking the view-options menu, the
  pin rows, session menus, drag) - see docs/official-parity.md for the remaining
  human checklist.
