ice-ai-lab/dsh-plugin-pi-ui0

dsh-plugin-pi-ui

pi-web-inspired sidebar chrome for the DeepSeek Harness Web GUI: temporary sessions at the top, a one-directory session list under a working-directory picker, a draft composer that creates nothing until you send, and a compact file explorer that takes over the right column's Files tab.

包名
dsh-plugin-pi-ui
版本
0.1.1
许可证
MIT
最近更新
2026年9月11日

安装

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:ice-ai-lab/dsh-plugin-pi-ui

description: "pi-web-inspired sidebar chrome for the DeepSeek Harness Web GUI: temporary sessions at the top, a one-directory session list under a working-directory picker, a draft composer that creates nothing until you send, and a compact file explorer that takes over the right column's Files tab." kind: "package-reference"

dsh-plugin-pi-ui

English | 中文

Summary

This package reshapes the Web GUI's left sidebar into blocks that read as one system, riding the harness --dsw-* design tokens so it follows every shipped theme:

  1. Temporary sessions at the top. The newest disposable scratch sessions, five at a time with a 查看更多 footer that loads ten more per press. The + button starts a draft; the 🗑 cleans up scratch directories no session is using.
  2. The working directory below it, anchored at the column's middle. A compact dropdown picks the directory being worked in — and the shipped session list under it always shows that one directory's sessions.
  3. A compact file explorer in the right column, where its Files tab otherwise shows the shipped workspace-files view.

Cutting across all three: nothing exists until you send. Every "new session" gesture opens a draft composer and stops there. No scratch directory is minted and no session row appears until the first message is actually sent — then the directory and the session are created together, or an existing provisional blank session in the named directory is reused.

Table of Contents


Install

The plugin is a plain out-of-tree package: a node half, a browser half, and a loader patch. Add it to a profile and restart dsh web.

# From a profile with this package available:
dsh plugin --profile web add dsh-plugin-pi-ui

The package declares dsh.bundle.patch, so the command above appends it to dsh.profile.bundles and its own cordis.patch.yml applies on the next boot. The equivalent hand-written row goes in the profile's own patch layer:

# ~/.dsh/profiles/web/cordis.patch.yml
- insert:
    - id: pi-ui
      name: 'dsh-plugin-pi-ui'

Use exactly one of the two routes: the same row id inserted twice fails boot. For a local checkout, a link dependency keeps edits live across restarts:

// ~/.dsh/profiles/web/package.json
{
  "dependencies": {
    "dsh-plugin-pi-ui": "link:/absolute/path/to/dsh-plugin-pi-ui"
  }
}

A restart of dsh web is required. The browser half is composed into window.__DSH_BOOT__ at boot, so a newly added client plugin cannot appear in an already-running server even though its node half loads live. After restarting, reload the page.

To remove it again, drop the row from dsh.profile.bundles (or the profile patch), remove the dependency, and restart. Scratch directories stay on disk; remove /scratch/ when you no longer want them.

Use this plugin

The column at a glance

[ 临时会话                         1  🗑  + ]
                     ⋮  (the column's middle)
[ 工作区  [ 📁 …/my-project ▾ ]        + ]
[ that one directory's sessions …        ]

The shipped full-width New Session button, the search magnifier, and the add-workspace button stand down while the column is wide; the plugin supplies the controls in the table below. In the collapsed rail the plugin draws nothing and every shipped control returns.

ControlGesture
+ in the 临时会话 headerOpens the draft composer for a temporary session. The click creates nothing; sending mints the scratch directory and the session together. If the session cannot be created, the fresh directory is deleted again, so no orphan folder is left behind.
🗑 in the 临时会话 headerDeletes scratch directories that no session is rooted in (shown only when one exists). It confirms first and never touches a directory in use.
the dropdown between 工作区 and its +The working-directory picker: your directories, Open folder…, and Remove working directory…. Picking a directory with sessions opens its provisional blank session, else its newest; picking one with no sessions yet hands you the draft composer instead of minting a session.
+ at the 工作区 row's right edgeThe draft composer for a new session in the directory shown. The click creates nothing; the first message creates the session, reusing that directory's provisional blank session when one is already there.
Remove working directory…Lists every registered directory with its session count and removes the one you pick. Removal touches the registry alone: the directory, its files, and its session logs all stay put — those sessions simply stop being grouped under it.

A temporary session row re-opens that scratch session. Temporary sessions are deliberately absent from the dropdown — that control stays a directory picker — and they are never registered as workspaces, so they do not accumulate in the durable workspace registry.

Nothing exists until you send

Every start gesture ends in the same place: a draft composer instead of a new session. That is what keeps the sidebar free of "新会话" rows for sessions nobody ever wrote in, and it is why a temporary session's directory only appears under /scratch/ at the moment you send.

The draft composer is the shipped blank-state card with two live twins laid over it: an editable input box over the card's own input area, and a send control over the card's (inert) send circle. Everything else about that card is the shipped card, drawn by the shipped component: the dashed border, the attach control, the permission select, the model seat, and the send button's own look. The plugin adds no hint line and no controls of its own. Type, then press Enter or the send circle.

The session list always shows one working directory

The region below 工作区 is a working directory's session list, and it shows exactly one directory's sessions at a time — the directory the picker shows: the current session's own workspace when it has one, else the one you last chose. A temporary session carries no working directory, so it neither changes nor appears in that list. There is no toggle and no "show everything" state: the picker is the selector.

The picker belongs to you, not to the selected session. The directory you chose last is remembered in the browser, so moving in and out of temporary sessions never blanks it out. Searching (the section header's magnifier) still searches every session, whatever the list is showing.

Settings

Settings → General gains two switches:

  • File explorer — the right column's explorer, in place of the shipped file view. Turning it off restores the shipped view immediately, with no reload.
  • Working directory switcher — the plugin's sidebar chrome as a whole.

Both are in-memory: they reset on reload. That is deliberate — a persisted preference would want the settings service rather than a client-local store.

Understand the implementation

Why the chrome is an overlay

The shipped sidebar declares no seat above its browsing region, and the only seats inside its brand row (sidebar.brand.mark / sidebar.brand.name) are single cells a shipped package already occupies — registering there would shadow the product brand, and both sit inside a `` that starts a session. So this plugin registers into shell.overlay, the frame-wide additive list seat, and positions its surfaces over the sidebar column.

A seat is given no geometry, so the plugin measures. It resolves the column from its semantic CSS-module class suffixes (the build keeps them: the runtime class is _regionArea) and writes one custom property onto it:

[class*="regionArea"] {
  padding-top: var(--dshpi-reserve-top, 0px);
}

The shipped region reserves that band, so the temporary list sits above the list's own content instead of covering it. Three further marks are deliberate and narrow:

  • data-dshpi-wide on the column — only while the column is wide — hides the shipped full-width New Session button, the header actions, and the search button, and reserves the brand's right padding so the row's own text stays clear of the controls that replace it. The collapsed rail keeps everything.
  • data-dshpi-focus plus a one-rule stylesheet shows exactly one session group, described below.
  • data-dshpi-header-controls keeps the CSS hook meaningful while the header row has no plugin-drawn control.

Every other shipped element is left alone.

The one-directory session list

The browsing region is a single seat that ui-workspace already occupies and its group rows carry no workspace identity, so the plugin cannot filter the list's data. What it can rely on is the list's own order: groupByWorkspace emits one group per workspace in Host order and appends the ungrouped bucket last, so a group's position is its workspace's position. Marking the column and revealing exactly that one position with an nth-of-type rule is declarative and immune to a React re-render undoing it.

The focused group's folder header is hidden while it is expanded, so the region reads as a plain session list rather than a folder with one child. That hiding is conditional on the group being expanded because a folded group keeps its header as the only control that can unfold it — and since the fold state is persisted and there is no toggle to escape with, leaving that control in place is what stops a collapsed group from stranding a directory with no way back. That same reasoning is why the dropdown carries Remove working directory…: with every other group hidden and the focused header hidden while expanded, the shipped rename/delete gestures are out of reach.

The draft composer

In the blank state the shipped card is inert: ui-conversation only activates it for a session that has a workspace (its own chip title), and the shipped model seat is not even rendered without a session. The plugin therefore twins the two controls a draft needs and leaves the row alone:

  • the input box is measured from [data-composer-card] / [data-composer-input] and covered by a transparent-bordered textarea filled with the card's own --dsw-specific-input-major (so the shipped placeholder underneath cannot show through), using the card's own font size, line height and caret color;
  • the send control is measured from the card's primary button and covered by a live twin of the same size, radius and fill.

Measurement is one rAF-coalesced pass fed by a 400 ms poll, a window resize listener, and a body-subtree MutationObserver, so the twins follow the hero's entrance animation, a sidebar collapse, and a window resize.

Start gestures and the pending target

A start gesture sets an in-memory pending target and clears the current selection, which is what puts the draft composer on stage:

TargetCreated on send
scratch (the temporary +, the default)POST /dsh-pi-ui/scratch mints a directory under the scratch root, then the session is created with that cwd. A refused session creation deletes the fresh directory again.
workspace (the workspace +, a directory with no sessions)The session is created with that workspaceId — nothing is written to disk. An existing provisional blank session there is adopted instead of duplicated.
cwd (a session-less directory the plugin can see)The session is created with that cwd.

The target lives only in memory and is retired as soon as any session opens, so a reload before the first send simply drops the draft.

The explorer as a right-column tab type

ctx.sidebarRightTabs.register(...) claims the shipped file view's kind — the files kind — at extension priority, the band a type from outside the product may use to take a builtin kind over, and the keyed sidebar.right.pane.tab seat supplies the body under this type's own id. A guide entry keeps the column's "new tab" page listing it, and because that entry replaces the builtin's, the column's default page is still the same kind.

The tree is rooted at the current session's working directory — for a temporary session, its scratch directory — and lists one level at a time through this package's node half. Clicking a file opens it through the harness resource address, so it lands in the right column exactly as the shipped tree's rows do. Turning the Settings switch off unregisters the type and the shipped view resumes, with no reload.

The node half

Three plain exact HTTP routes, because an out-of-tree plugin has no codegen step for a generated Remote namespace:

RouteMethodBounds
/dsh-pi-ui/treeGETRead-only, direct children only, resolved through the harness fs backend, refused when the resolved target escapes the session working directory, capped at 800 entries. Never reads file contents.
/dsh-pi-ui/scratchGET / POSTPOST takes no caller input: the directory name is generated server-side, under the scratch root only.
/dsh-pi-ui/scratch/deletePOSTAccepts names only. Each must be a single path segment, is re-checked for containment after resolution, and the scratch root itself is never a deletion target. Caps 200 names and 64 KiB per request.

Like the rest of the plugin surface in this ecosystem, these routes are not behind the browser-session cookie. Treat them as local-only and do not expose the server beyond loopback.

What is persisted

Only one client-local key: dsh-pi-ui.workspace, the working directory the picker last showed. Everything else — the pending target, the two Settings switches, the measured geometry — is in memory. Temporary sessions need no bookkeeping at all: they are recognised by matching a session's working directory against the canonical scratch root.

Further exploration

  • @deepseek-ai/dsh-client-ui-slots and the renderer's seat semantics — what a single, list, keyed, or chain seat promises a registration.
  • @deepseek-ai/dsh-client-ui-sidebar-right — the tab registry this plugin's explorer registers into, including the priority bands.
  • @deepseek-ai/dsh-client-ui-workspace — the browser whose session groups this plugin scopes one at a time.
  • @deepseek-ai/dsh-host-directory-picker-browse plus its client half — compose them to replace the OS folder chooser with pi-web's in-app browser. The plugin's Open folder… tries the in-app primitives first and falls back to the OS chooser, so no plugin change is needed.
  • dsh --profile web --dump-config — the composed profile tree, the fastest way to see which bundles and patch layers are in play.

Model experience

The plugin registers no tools and no commands, so a model sees only what the harness already gives it. What this package changes about that:

  • A temporary session's working directory is /scratch/-/, created at send time. The ordinary sandbox policy scopes the session there exactly as it scopes any other session.
  • The session list, the picker, the draft composer, and the explorer are all UI-only surfaces; none of them writes to a session log, and none of them reaches a model.
  • The node half's three routes are HTTP, not tools, and are never offered to a model.

Safety note. The scratch root lives inside the harn