# Vendored official source — provenance and patch record This directory is a **verbatim copy** of the official DSH sidebar workspace browser, vendored so this plugin can own the sidebar region while keeping the official UI 1:1. ## Provenance | | | |---|---| | Upstream | `deepseek-ai/deepseek-harness` | | Path | `packages/client/ui-workspace/src/` | | Version | **0.2.0-rc.2** | | Commit | `639ed01539` (`dsh-v0.2.0-rc.2`) | | Re-synced | 2026-09-30 (from `477b4f420` / `dsh-v0.1.7-rc.2`, copied 2026-09-26) | ### How the 0.1.7 → 0.2.0 re-sync was done Upstream changed only **145 lines** in this package (`19 files changed, 145 insertions(+), 46 deletions(-)`), of which **7** are files this tree patches (`client/shortcuts.ts` among them). Rather than re-applying the patch table by hand, each file was three-way merged with `git merge-file`: ``` base = upstream 0.1.7-rc.2 (git archive of the tag we vendored) ours = this tree (0.1.7 + our patches) theirs = upstream 0.2.0-rc.2 (git archive of the target tag) ``` **All 10 patched files merged with zero conflicts**, and the result was verified in both directions: upstream's 8 changes present, our 17 patch anchors intact. Two files needed attention beyond the merge — `index.ts` (`forkSession` gained an `onCreated` observer and a `productAnalytics` call) and `navigation.ts` (the same signature change) — and all three test fixtures gained a `title` field, because `sessionTitle` reads the durable title rather than `displayTitle` as of 0.2.0. Upstream made the same fixture change in its own `tree.client.spec.ts`, which is what confirmed the fix. `devDependencies` moved to `0.2.0-rc.2` together with a new `@deepseek-ai/dsh-client-product-analytics`. It is read with the ungated `ctx.get('productAnalytics')` and imported as `import type {}`, exactly as upstream does, so it adds no bundled edge — `verify:bundle` confirms the produced externals still match the installed official bundle name for name. > **Reinstalling matters.** `pnpm install` over an existing `node_modules` left the 0.1.7 packages in > `.pnpm`, so two copies of `dsh-typert-protocol` resolved and `tsc` failed with a `RemoteFailure` > mismatch on two `DirectoryBrowseError` lines — code that is upstream's verbatim. Deleting > `node_modules` and reinstalling resolved it; the lockfile itself was already clean. Copied files (23): ``` client/index.ts client/navigation.ts client/shortcuts.ts client/stores.ts client/tree.ts client/pin-order.ts client/locales.ts client/WorkspacePicker.tsx client/WorkspacePicker.module.css client/contract/slots.ts client/rows/WorkspaceBrowser.tsx client/rows/WorkspaceBrowser.module.css client/rows/Rows.tsx client/rows/Rows.module.css client/rows/AnimatedRows.tsx client/rows/AnimatedRows.module.css client/session-actions/ArchiveSession.tsx client/session-actions/ForkSession.tsx client/session-actions/PinSession.tsx client/session-actions/RenameSession.tsx client/session-actions/RowActionToast.tsx client/session-actions/derived.ts css-modules.d.ts ``` ## Why vendored The official package does not export its component, and every data seam that could have let us swap behaviour underneath it is closed by an explicit invariant — see `scripts/probe-approach-b.mjs` for the reproduction: 1. `ctx.slots.provideRoot` rejects a duplicate root standard hook (`useWorkspaces`), so the browser's data source cannot be replaced. 2. `ctx.provide` / `ctx.set` refuse a second provider for the `workspaces` service, so the controller cannot be replaced. 3. A replacement entry cannot re-declare the browser's child slots (`…session.menu.item` etc.), and `renderSlot` is authorised per entry, so it cannot render the official actions either. Keeping the UI 1:1 therefore means owning a copy of it. ## De-branding The vendored tree started as a **byte-identical copy** of upstream — verified by hash, file by file, against `packages/client/ui-workspace/src/` at `dsh-v0.1.7-rc.2`. It is no longer byte-identical: project grouping needed a grouping seam, and the patches below are it. Every one is **structural and optional**: with no override supplied, the code behaves exactly as upstream. Most adaptation still lives *outside* this directory: | Where | Adaptation | Reason | |---|---|---| | `src/client/index.ts` | wrapper that re-exports `apply` / `inject` and passes the grouping observable | gives project logic one seam to hook, and keeps the loader id this plugin's | | `tsdown.config.ts` | specifier classification and the CSS Modules virtual loader | builds the tree as an independent plugin rather than as an official package | | `cordis.patch.yml` | disables the official `ui-workspace` row | two claimants of the single slot, and two providers of `uiWorkspace`, would be a hard startup error | ### Patches inside this directory Kept minimal and listed here so a re-sync is mechanical. Each is marked in the source with a comment naming the seam. | File | Patch | Re-sync action | |---|---|---| | `tree.ts` | adds `GroupSource`, `owningSourceKey`, and `groupBySource` (a line-for-line twin of `groupByWorkspace`); `deriveGroups` takes an optional 6th `sources` parameter | re-apply on top of the new `groupByWorkspace` | | `tree.ts` | `deriveSearchResults` takes an optional 9th `sources` parameter and labels a result row from it, so a search result names its project the way the tree does; the `sources === undefined` branch is upstream's, unchanged (see below) | add the parameter, the `groupBySession` map, and the branch in `labelOf` | | `contract/slots.ts` | adds a mandatory `grouping` hook to `WorkspaceBrowserInjected.hooks`, plus the `GroupSource` type import | re-add the one field + import | | `contract/slots.ts` | adds a mandatory `createOpensSession` hook to the same `hooks` block, and `createProject` returns `{ projectId }` rather than `void` (both **behaviour-relevant**, see below) | re-add the hook field and the return type | | `client/index.ts` | `ProjectActions.createProject` returns `{ projectId }` rather than `void`, and `apply` takes a sixth `createOpensSessionOverride` argument threaded into that hook | re-add the return type, the parameter, and its two defaults | | `shortcuts.ts` | `installWorkspaceShortcuts` takes a fifth argument, `projectModel: boolean`; with it the `workspace.add` command is relabelled and re-aliased to the project dialog (`project.add` / `new project`) and its directory-flow availability check is skipped (**behaviour change**, see below). Called with `false`, every branch is upstream's | re-add the parameter and the three `projectModel` branches; the call site is `client/index.ts` (`projectActions !== undefined`) | | `rows/WorkspaceBrowser.tsx` | consumes `useGrouping`, threads `groupingOverride` into `SessionTree`, uses it for `ungroupedMemberIds` / `expandedGroups` / the two `owningGroupKey` call sites | re-apply the same six edits | | `rows/WorkspaceBrowser.tsx` | `onCreate` drops its `if (group.workspaceId !== undefined)` guard and always expands (**behaviour change**, see below) | remove the guard again | | `rows/WorkspaceBrowser.tsx` | `onCreate` files what a row creates: a project row under itself through `assignSession`, the Ungrouped bucket under **nothing** through `unassignSession` (see below) | re-apply the three-way `file` dispatch | | `rows/WorkspaceBrowser.tsx` | the header's create dialog opens a Session after adding a caller-supplied project — the official add-workspace flow — filing it under the new project and gated on the `createOpensSession` hook (**behaviour change**, see below) | re-apply the `startSession` call, the hook read, and the closed-dialog ordering | | `rows/WorkspaceBrowser.tsx` | expansion is routed by key ownership: caller-supplied keys go through `setProjectExpanded` / `projectExpansion`, every other key through the view store (see below) | re-apply `isCallerOwned` / `recordExpansion` / `hasExpansion` and the merge in `expandedGroups` | | `rows/WorkspaceBrowser.tsx` | caller-supplied groups get the same two-mode member ordering the Workspace rows get, from `orderedProjects` + the `orders` hook; `commitSessionDrag` resolves a caller key after the Workspace lookup; `saveSessionOrder` and the order menu dispatch by key ownership (see below) | re-apply `orderedProjects` / `allProjectOrders`, the `?? groupingOverride?.find(...)` in `commitSessionDrag`, and the two dispatches | | `rows/WorkspaceBrowser.tsx` | a Session can be dragged **between** groups: `DragState.overGroupKey` names the target, a row drop is positional and a header drop is not, and `commitCrossGroupDrag` files it through `assignSession` / `unassignSession` before writing the order (see below) | re-apply `overGroupKey`, `canReceiveDrag`, `insertIntoTargetOrder`, `commitCrossGroupDrag`, and the `groupDrop` wiring | | `rows/Rows.tsx` | a Session row's `dragover` / `drop` call `stopPropagation`, so the row is the target rather than the enclosing group section; `ProjectRowItem` takes an optional `groupDrop` that makes the **header row** — not the section — the cross-group hit target (the highlight stays on the section, painted by the region) | re-add the two calls and the `groupDrop` prop and handlers | | `tree.ts` | under a grouping override the Ungrouped bucket always renders, empty included — it is the drop target that takes a Session back out of a caller-supplied group (see below) | re-apply the `archivedFilter !== 'only'` alternative | | `navigation.ts` | `startSession` takes an optional `beforeOpen` callback and threads it into `openWorkspace`, so a caller can act on the Session that lands (a project row files it). Omitted, the flow is unchanged | re-add the parameter and the pass-through | | `rows/WorkspaceBrowser.tsx` | rename/delete dialogs and the group drag take a `kind`-tagged row (`RowRequest`), so a caller-supplied project row drives the same affordances as a Workspace row; the header's add control runs `createProject` when the composition supplies one, and the dialog titles/labels switch on that kind | re-apply the dispatch, the two dialog blocks, and the drag wiring | | `rows/WorkspaceBrowser.tsx` | a **project** drop resolves its anchor against `groupingOverride`'s order and honours the side the marker showed, so the landed position equals the line the user saw (see below) | re-apply the `projectIds` lookup, the `half` from `activeDrag.over`, and the next-sibling anchor | | `rows/WorkspaceBrowser.tsx` | a **project title is unique**: the create dialog blocks a name another project holds, the rename dialog does too, and the rename alert names the row kind it is warning about (see below) | re-apply `createDuplicate`, the `kind === 'project'` arm of `renameDuplicate`, and the `conflict.projectNamed` dispatch | | `locales.ts` | `conflict.projectNamed` in both dictionaries | add the key | | `rows/Rows.tsx` | labels the Ungrouped bucket by **empty label** rather than missing `workspaceId` | one-line change; a caller-supplied group has no Workspace id but does have a label | | `rows/Rows.tsx` | the row menu's delete label and the menu's aria-label follow `group.kind` | small change; a project's delete removes a record, not a registry entry | | `locales.ts` | project copy (`project.add`, `project.create.*`, `rename.project.title`, `delete.project*`, `field.projectName`, `create`, `actions.project.aria`) in both dictionaries | add the keys | | `navigation.ts` | `startSession` without a target resolves the Host's default Workspace instead of guessing (**behaviour change**, see below) | restore the shipped guess, or re-apply | | `navigation.ts` | the service takes an optional `placeUnscoped` callback, applied only when `beforeOpen` is absent, so the caller can file an unscoped New Session (**behaviour change**, see below) | re-add the parameter and the `beforeOpen ??` composition | | `index.ts` | `apply` takes an optional `groupingOverride`, an optional `ProjectActions`, and optional `expansionsOverride` / `ordersOverride`, forwarding all into the inject face | re-add the parameters and the hook/verb fields | | `rows/Rows.tsx` | adds `sessionRowKey(id)`: a Session row's animation identity is the Session itself (`session:`), kept as one shared constructor so the ordered `rowKeys` array and the rendered attribute cannot drift | re-add the helper and its doc block | | `rows/Rows.tsx` | `SessionNodeItem` takes an optional `rowKey`, and its `data-row-key` is `rowKey ?? sessionRowKey(node.id)` | re-add the prop and the fallback | | `rows/WorkspaceBrowser.tsx` | the grouped view passes `sessionRowKey(node.id)` twice — into `rowKeys` and into the row's `rowKey` — in the same order | re-apply both call sites together | | `rows/AnimatedRows.tsx` | takes `enteringKeys` / `onEnteringKeysDrawn`: a New Session that **reuses a blank Session** keeps its row key, so its entry fade is announced instead of inferred (**behaviour change**, see below) | drop both props and the `arriving` branch | | `contract/slots.ts` | adds `newSessionLanded` to `WorkspaceBrowserInjected['hooks']`: `{ sessionId, seq } \| null`, the Session every New Session flow landed on, sequenced because the reused blank re-announces one id | drop the field | | `navigation.ts` | the service takes an optional `onNewSession` callback, called from `startSession` for every entry, composed with `beforeOpen` rather than replacing it (**behaviour change**, see below). Omitted, the flow is unchanged | re-add the parameter and the two compositions | | `contract/slots.ts` | adds `BaseWorkspaceMissingRequest` / `BaseWorkspaceDialogInjected` / `BaseWorkspaceDialogProps`: the report a New Session raises when its 底层工作区 is gone | re-add the three declarations | | `navigation.ts` | the service takes an optional `onBaseWorkspaceMissing` callback, raised where `startSessionInDefaultWorkspace` previously returned silently (**behaviour change**, see below). Omitted, the flow is unchanged | re-add the parameter and the one call | | `session-actions/BaseWorkspaceMissing.tsx` | **new file**: the `shell.overlay` dialog that reports that case and offers the two repairs | re-add the file and its `shell.overlay` registration | | `locales.ts` | six `baseMissing.*` keys in both dictionaries, kept in one contiguous block rather than editing a shipped key | re-add the block | | `rows/WorkspaceBrowser.module.css` | `.groupDropTarget`: the cross-group highlight painted on the group **section** while the pointer is on the header row (hit testing and highlight answer different questions; the section is the highlight's, the row is the target's) | re-add the one rule | | `rows/WorkspaceBrowser.module.css` | `.baseMissingActions` (the column footer), `.baseMissingBody`, `.baseMissingPath` | re-add the three rules | | `contract/slots.ts` | adds `BaseWorkspaceRoute`: which of three outcomes the caller's 底层工作区 setting resolved to | re-add the union | | `navigation.ts` | the service takes an optional `resolveBaseWorkspace` callback, consulted by an unscoped `startSession` **before** the official default (**behaviour change**, see below). Omitted, the flow is unchanged | re-add the parameter and the two branches | | `index.ts` | `ProjectActions` gains an optional `resolveBaseWorkspace`, passed as the constructor's last argument | re-add the field and the pass-through | | `session-actions/BaseWorkspaceMissing.tsx` | the 重新指定底层工作区 button now consumes the report and calls `chooseBaseWorkspace`, handing the user to the settings card | re-add the `settle()` before the call | | `session-actions/BaseWorkspaceMissing.tsx` | the 重建该工作区 button's action is live (step 3b) | re-add the `run(rebuild)` call | | `session-actions/BaseWorkspaceMissing.tsx` | **two stages in one dialog**: pressing 重建该工作区 switches this card to a confirmation stage (warning icon, the path, 确认重建/返回) rather than opening a second `Modal` | re-add the `stage` state, the focus effect, and both footer branches | | `locales.ts` | the confirmation's five keys in both dictionaries; the single `baseMissing.rebuildHint` is gone, its sentence having moved into the confirmation | re-add the five keys | | `rows/WorkspaceBrowser.module.css` | `.baseMissingConfirm` / `.baseMissingConfirmIcon` | re-add the two rules | | `contract/slots.ts` | `BaseWorkspaceDialogInjected.chooseBaseWorkspace` is documented as wired (step 3a); the type is unchanged | no code change beyond the doc | | `tree.ts` | `GroupSource` gains an optional `directories`, and `sameStringList` is exported | re-add the field and the helper | | `contract/slots.ts` | `WorkspaceBrowserInjected` gains `updateProject?` and `pickDirectory?`, and `createProject` takes a required `directories`; `renameProject?` is **gone** | re-add the two verbs, change the one signature, drop `renameProject` | | `client/index.ts` | `ProjectActions` mirrors the above (`updateProject`, optional `pickDirectory`, `createProject` with `directories`, no `renameProject`), and `browserInjected` forwards them | re-apply the four signature changes and the forwarding block | | `rows/WorkspaceBrowser.tsx` | the project rename dialog becomes an **edit** dialog: it seeds a `renameDirectories` draft from `RowRequest.directories`, renders the shared `DirectoryRows` / `DirectoryHeader`, and commits through `updateProject`; `renameBlocked` compares the list as well as the title (**behaviour change**, see below) | re-apply the target field, the two state values, the two helpers, the comparison, and the block | | `rows/WorkspaceBrowser.tsx` | the create dialog collects source folders: `createDirectories` / `pickingCreateDirectory`, `openCreate` resets the list, and `confirmCreate` sends it | re-apply the two state values, the reset, the helper, the block, and the send | | `rows/WorkspaceBrowser.tsx` | `RowRequest`'s project arm gains an optional `directories`, and the group header's rename action resolves it from `groupingOverride` | re-add the field and the lookup | | `rows/WorkspaceBrowser.tsx` | adds the `DirectoryRows` / `DirectoryHeader` components and imports `IconCloseOutlineRegular` | re-add both components and the icon import | | `rows/Rows.tsx` | the row menu's rename label follows `group.kind` (项目 rows say 编辑项目) while the menu **id** stays `rename` | reuse the label dispatch | | `locales.ts` | `edit.project`, `edit.project.title`, `save`, `field.directories`, `directory.add`, `directory.none`, `directory.remove` in both dictionaries | add the keys | | `rows/WorkspaceBrowser.module.css` | `.directoryBlock` / `.directoryHeader` / `.directoryLabel` / `.directoryEmpty` / `.directoryList` / `.directoryRow` / `.directoryPath` | re-add the block | | `rows/WorkspaceBrowser.module.css` | `.directoryDialog.directoryDialog` widens the project dialogs past the shipped 380px card to **480px** — short of the official directory browser's 680px, which read as loose for a name field plus a few paths — using the same doubled-class specificity trick the official directory browser uses; `.directoryRow` is transparent until hover, matching the official `.row` | re-add the rule and the hover pair | | `rows/WorkspaceBrowser.tsx` | both project dialogs pass `className={css.directoryDialog}`; the add control is the shell `Button` (`variant="ghost" size="sm" icon={}`) and the remove control reuses this file's own `css.iconButton` — a bare `