dsh-session-tabs

DSH PLUGINUI 与外观

仓库 rifkyazizf/dsh-session-tabs

UI 与外观 未标注验证等级 安全审查:未标注 可信度:未标注 可安装插件

为 DeepSeek Harness 网页界面加上 JetBrains 风格的会话标签栏,每个聊过天的会话保留一个标签,仅浏览过的会话只占一个可替换的预览标签。

一句话选型结论

按资料字段整理

dsh-session-tabs 是UI 与外观类的 DSH 可安装插件,未做沙盒验证,暂无安全审查结论,列出 5 项功能,没有现成安装命令,需要按仓库 README 接入。验证通过不等于安全:插件会读写文件、执行命令或联网,装前先到仓库确认它做了什么;涉及密钥或系统权限的,建议先在隔离环境试装。

分类UI 与外观 收录类型可安装插件 验证等级未标注验证等级 安全审查未标注
验证等级 UNKNOWN 未标注验证等级
GitHub 星标 1 复刻 0 · 未关闭 issue 未标注
最近更新 2026-10-08 创建于 2026-10-08
README 19.2 KB 正文体量,越大说明说明书写得越细

安装

可安装插件
在 DSH 里执行,装完用 dsh plugin list 确认已挂上;标了 L4 / L5 的命令已在沙盒里真实跑通
  1. 1
    确认版本与运行环境

    仓库没有标注兼容版本与 Node.js 要求,先确认你的 DSH 版本能装,再继续。

  2. 2
    执行安装命令

    在 DSH 里执行上面的命令;多包仓库要注意区分具体子包名,别装错包。

  3. 3
    确认已挂载

    用 dsh plugin list 查看列表里是否出现这个插件,出现即挂载成功。

  4. 4
    试跑并检查权限

    加载后跑一个最小用例,确认它能正常工作;同时看它申请了哪些文件、命令或网络权限。

兼容 DSH 版本未标注版本不匹配时安装会报错
Node.js 要求未标注来自仓库 package.json 的 engines 字段
前置依赖无需要同时安装的同级依赖
npm 包名未发布到 npm可用 npm view 查看包信息

能做什么

5 项功能
字段来自项目仓库与公开索引,中文说明由本站标注

dsh-session-tabs:为 DeepSeek Harness 对话面板加上会话标签条

  • 会话标签栏置于对话顶部
  • 发过消息的会话保留标签
  • 仅打开的会话共用预览标签
  • 标签按使用顺序排列
  • 标签过多时横向滚动

上游标签

ai-codingcoding-agentdeepseekdeepseek-harnessdeveloper-toolsdshdsh-pluginelectronpluginsession-tabstabs

验证等级与安全审查

未标注验证等级
自动化流程结论,不是人工审核
验证等级未标注验证等级上游未给出验证结论,安装前请自行确认
安全审查未标注静态分析结论,未标注不代表已通过审查
仓库健康度未标注按仓库活跃度与最近更新时间给出
可信度未标注综合仓库活跃度与验证结果

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 列出了 5 项功能,能直接看出它解决什么问题。

先想清楚的地方

  • 安全审查还没结论,装之前先看仓库改了什么、要什么权限。
  • 没有现成安装命令,要按仓库 README 手动接入。
  • 验证通过不等于安全:插件会读写文件、执行命令或联网,装前先到仓库确认它做了什么。

更适合

  • 需要在UI 与外观场景里给 DSH 补能力的人。
  • 能接受在本地装依赖、愿意自己确认权限的人。

不太适合

  • 只使用 DSH 官方内置能力、不接受第三方插件的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
插件验证等级安全审查星标兼容版本安装命令
dsh-session-tabs当前 未做沙盒验证 未标注 1 未标注 无
nuwa-skill 未做沙盒验证 未标注 3.8 万 未标注 有
awesome-dsh-plugin L4 · 沙盒安装通过 低风险 1.8 万 0.1.1-rc.2 有
deepseek-balance-whale-widget L4 · 沙盒安装通过 低风险 4,492 0.1.7-rc.2 等 2 个 有
dsh-better-sidebar L4 · 沙盒安装通过 低风险 4,112 0.1.7-rc.2 等 10 个 有
dsh-deep-whale L4 · 沙盒安装通过 低风险 2,470 未标注 有

仓库档案

UI 与外观
字段来自项目仓库与公开索引,中文说明由本站标注
作者 / 维护方rifkyazizf仓库 dsh-session-tabs · 默认分支 未标注
分类UI 与外观interface
收录类型可安装插件声明了 dsh.bundle、可用 dsh plugin add 安装的条目
开发语言JavaScript仓库主语言
许可证MIT商用或二次分发前请确认条款原文
星标 / 复刻 / issue1 / 0 / 未标注收录时的数值,非实时
创建 / 更新2026-10-08 · 2026-10-08最后推送 未标注
本站收录2026-10-11首次进入本站目录的时间

README 全文

19.2 KB
作者原文,中文版优先;只做安全转义,不改内容

dsh-session-tabs

JetBrains-style session tabs for the DSH Web GUI: one tab per chat session, in a
strip at the top of the conversation panel.

│ ● Add DeepSeek…  × │ ● Fix the login bug │ ● vendor sync │

A session you sent a message in keeps its tab. A session you merely
opened is one disposable tab that the next open replaces, so clicking
through the sidebar never leaves a trail of tabs.

The rule

This is the editor-tab behaviour from JetBrains, with sessions in the role of
files:

Session Tab
You sent a message in it An ordinary tab that stays
You only opened it The single "preview" tab, replaced by the next open

The stick signal is not tracked by this plugin. The session list already
carries it as blank:

> A blank session is the selected Workspace's provisional New Session row.

blank === false therefore means the session has real content. The host sets it
on the first accepted prompt (promptAttempted, set synchronously before the
first await) and its reducer deliberately refuses to re-blank a session
afterwards:

handleBlank(blank) {
if (blank === this.blankBit) return;
if (blank && (this.promptAttempted || this.running)) return;  // sticky
...
}

So "sent a message" is the host's own durable notion of it, already computed and
already sticky. Delegating to it means the strip cannot disagree with the rest of
the UI about whether a session has been used, and there is no local bookkeeping
to drift or to persist.

Two properties follow, and both are tested:

  • Order is engagement order, not recency. Revisiting an old tab does not
    reshuffle the strip under the pointer.
  • The preview is always last, because it is the one that will be replaced.

Subagent sessions are excluded: they are reached through their parent's
breadcrumb rather than by switching the conversation, so a tab for one would
misrepresent what the strip does. Nothing is capped or recycled: every engaged
session keeps a tab in engagement order, and the row scrolls sideways when the
tabs outgrow it (plain wheel included), with the active tab kept in view.

Placement — the first row of the conversation

The strip is registered in conversation.input.dock — a list seat above the
composer, shared with the todo, queue, and goal docks — and drawn in a row this
plugin anchors ahead of every row the conversation already has, so the tabs are the
first thing in the conversation:

div.root                            ← the conversation column (flex column)
├── div[data-session-tabs-host]     ← the strip's row (inserted by this plugin)
├── header                          ← title row + Chat/Trajectory view tabs
└── div[data-conversation-content]  ← transcript + composer

Why it is not a seat

The app declares no seat above the conversation content:

  • conversation.header is a single seat declared and occupied by the
    shipped ui-conversation plugin. A second registration at the same priority
    throws single slot "conversation.header" already has a registration …;
    registering at apply time — before the seat exists — throws
    slot "conversation.header" is not declared, which fails the plugin's whole
    fiber and aborts web boot.
  • Its only unoccupied child, conversation.header.leading, sits inside the
    title bar (the window drag region), so it cannot hold a full-width row.
  • The header's other children (…session.header.actions, .utilities) are narrow
    control clusters beside the title.

Placement is therefore done in the DOM: the dock registration supplies the seat
and the session props, and the component inserts a host row as the conversation
column's first child, then renders the strip into it with createPortal (the shell
exposes react-dom as a platform seed word, so a client bundle may require it).
The column is reached from the seat's own placeholder, so it is always the column
that seat belongs to. The host is re-anchored whenever it leaves the document, so a
re-render of the conversation column cannot strand the strip in a detached node.

The row sits outside the header's data-window-drag region, so chips click and
scroll normally; window dragging keeps the title row and the sidebar.

Why the memory is the whole order, not the visible tabs

stepTabs returns the strip and the order to remember next, which is the full
engagement order — never the window that happened to be rendered. That looks like a
detail; it is the difference between a still strip and a rotating one:

Remembered What 6 re-renders of 10 engaged sessions show
the visible tabs (a window) 5 distinct windows — the strip rotates on every re-render
the full engagement order 1 window, unchanged

Typing re-renders the strip constantly (the session store ticks), so a rotating
strip is exactly the flicker a user sees while typing. verify.mjs replays this
loop through stepTabs — the function the component calls — and fails if more than
one window appears.

Why the memory is module state, not a ref

Switching sessions remounts the conversation Factory occurrence — its key carries
the session generation — so a ref dies the moment you open another session. A
dismissal kept in a ref therefore came back on the very next click elsewhere, and the
engagement order died with it, reshuffling the tabs on every switch. dismissedTabs
and engagementOrder are module state for that reason, and verify.mjs drives real
remounts (fresh refs, retained module memory) to assert both survive.

A page reload would still wipe them, and the shell reloads its page whenever the app
restarts, so the same memory is written to localStorage under
dsh.session-tabs.v1 — the dsh..v1 convention shipped client plugins use (the
conversation's transcript width, the right sidebar's layout) — and read back on load.
That key lives in the dsh-app://app origin, which the desktop registers as
standard + secure, so it survives restarts. A blocked store, a missing key, or a
corrupt payload all read as a cold start rather than an error, and the offline suite
covers the round trip, the payload validation, and a seeded "page load".

The scrollbar is the shell's

The strip sets no scrollbar-width/scrollbar-color, on purpose. ui-theme
installs unscoped ::-webkit-scrollbar rules (--dsh-scrollbar-width = 5px, a
rounded thumb in --dsw-alias-scrollbar-bg-l1, a hover colour from
--dsw-alias-scrollbar-hover-l1), so the bar matches the transcript's and the
sidebar's. Chromium ignores those rules for any element that sets the standard
properties, and the strip used to set scrollbar-width: thin — which is why it had a
fat native macOS bar while the rest of the app had a thin themed one. If a profile
disables ui-theme, the strip's bar goes native along with every other scrollbar.

The frame is browser-style, and why the rail is paint

The strip is drawn as a browser tab strip rather than a row of chips: a bar a shade
off the pane's own surface, the tabs sitting on a hairline rail along its bottom,
and the active tab punched down to the pane colour so it reads as connected to the
row beneath it. The chip is 30px tall with 8px shoulders and square feet
(borderRadius: "8px 8px 0 0") on a 36px bar.

Three things about how that is drawn are deliberate, and each was forced by how CSS
composites a scroller:

  • The bar's fill is a translucent neutral (rgba(127,127,127,0.12)), not a
    surface token. The shell's surfaces are raised above the pane in dark mode —
    --dsw-alias-bg-base is the pane, and --dsw-alias-bg-layer-1…3 are lighter
    than it — so no single surface token can be "a bar slightly off the pane" in both
    themes: one theme would erase the bar, the other would paint a raised card. A 12%
    neutral over the pane reads as a raised bar in the dark theme and a recessed one
    in the light theme, from one value.
  • The active chip is filled with --dsw-alias-bg-base — the pane's own colour.
    That, not a border or a shadow, is what merges the tab into the row below.
  • The rail is the strip's bottom row of background, not a border-bottom.
    A child cannot paint over its parent's border, and the strip clips its own
    overflow because it is the horizontal scroller — so the active chip could never
    cover a border to break the rail under itself. Painted as the strip's own
    background, the chips are composited on top of it: the active chip's opaque fill
    hides the rail beneath it, while the gaps between tabs and the inactive tabs keep
    it. The strip carries no bottom padding for the same reason, so a chip's box
    reaches the rail.

Degradation

Conversation shape Where the strip draws
Main column whose parent has a header child the anchored row — the column's first row
Embedded occurrence (subagent preview — no header) nothing; a dock strip there would be noise
Unrecognized shape (no [data-conversation-content] ancestor) the dock it was registered in

The last row is the safety net: a future app version can move the strip, but not
lose it — and none of these paths can fail web boot.

Why the registration waits for the declaration

conversation.input.dock is declared by the Conversation Factory, whose entry
mounts after this plugin's two services (slots, sessions) resolve, so the
registration is made inside ctx.slots.inject("conversation.input.dock", …): the
callback runs when the seat is declared, now or later, and never runs if it is
not. A bare ctx.slots.register({ name: "conversation.input.dock" }, …) at
apply time throws slot "conversation.input.dock" is not declared (a parent entry's children table must declare it), and a throw inside apply fails the
plugin's whole fiber — the desktop shell then replaces the app with its
"could not start" dialog.

Layout

package.json    dsh.client declaration (platform: web) + local package metadata
lib/index.js    node half — empty apply(), gives the Loader a host-side row
lib/client.js   browser half — the strip (inlined policy) + top-row anchoring
lib/policy.js   the recycle rule, imported by verify.mjs
verify.mjs      policy + bundle contract + rendered chips (node verify.mjs)

lib/client.js cannot import lib/policy.js: a client bundle is a
window.__ModuleLoader__ bundle that may require only the shell seeds react
and react/jsx-runtime, and the browser half ships as-is with no bundler. The
policy is therefore inlined verbatim, and verify.mjs asserts the two copies
agree — on every constant, on the whole deriveTabs derivation across its
fixtures, and on chipLabel, fitLabel, and graphemes across their cases — so
editing one without the other fails the run.

Titles and widths

Every chip is the same width (TAB_WIDTH, 190px, border-box) regardless of its
title: a strip whose tabs widen and narrow with their text reads as noise, and one
busy title would shove its neighbours sideways. Inside that fixed box the label takes
whatever the state dot and the reserved close slot leave (flex: 1 1 auto,
min-width: 0) and ellipsizes there — so a longer title, or a larger shell font,
trims the label instead of resizing the tab.

The label text is still cut first, on word boundaries when a whole word is
reasonably close to the limit, so a chip reads as a phrase rather than a chopped
token:

Session title Chip
Add DeepSeek peak/off-peak session tabs Add DeepSeek…
a_very_long_single_token_filename a_very_long_single…
Fix the login bug Fix the login bug

The full title stays reachable as the chip's tooltip, so truncation never hides
which session a chip is.

The tab context menu

Right-click a chip for the shell's own menu material (MenuSurface +
MenuItemButton from dsh-client-ui-primitives, portaled to the body and clamped
into the viewport):

Row What it does
Rename session edits the title in place — the chip's label slot becomes an input, seeded with the full title; Enter commits, Escape and blur abandon
Close all left dismisses every tab before this one
Close all right dismisses every tab after it
Close all others dismisses every other tab

Two rules keep it honest:

  • Bulk closes act on tabs, never sessions. They only add ids to the dismissal
    set (closeRangeIds), which is the same memory the × uses.
  • The active tab is never left behind. If a bulk close dismisses the session
    being read, the conversation follows the tab that was kept — so the strip and the
    transcript cannot disagree about which session is up.
  • The right-clicked tab is never closed by any of the three, and a row with nothing
    to do is disabled rather than hidden, so the menu keeps its shape.

Rename goes through sessions.rename(sessionId, title) — the service call the
sidebar's own rename dialog ends at. That dialog itself cannot be raised by a
third-party plugin (its request store is plugin-local state), which is why the strip
carries its own inline editor.

Interactions

  • Click a chip — focus that session (ctx.uiWorkspace.openSession, looked
    up per click). This is not ctx.sessions.open: the sessions service has no
    such method, which is why an early build's chips looked dead while the sidebar
    navigated fine.
  • Click × — drop the tab from the strip, and re-render immediately: the
    dismissal set is a ref (it is only read while deriving the strip), so the
    handler also bumps a render ticket — without it the chip lingered until an
    unrelated store tick and the × looked dead. The session itself is untouched;
    deleting a session stays the sidebar's job. Closing the active tab first
    selects a neighbour, so the strip is never left pointing at a session it no
    longer shows.
  • The × is a reserved slot, not an insertion. A closable chip always carries
    the control's 16px (plus gap) and toggles only its visibility: always visible on
    the active chip, visible on hover for the rest. Appending it on hover widened
    that chip and pushed every chip after it sideways under the cursor. The last
    remaining tab and the preview carry no slot at all — neither can be closed.
    Hovering the × lights a small circular background under it (a tighter highlight
    than the chip's own), and that hover changes colours only: closeControlStyle
    is asserted to return identical geometry in every state.
  • The last remaining tab is not closable, since an empty strip has no way
    back. A preview tab is not closable either — it is already disposable.
  • A running session shows a green dot on its chip.

Verify

node ~/.dsh/plugins/dsh-session-tabs/verify.mjs

Covers the recycle rule (browsing leaves exactly one tab; engaging promotes it;
engaged tabs survive a browse; order is stable; subagents excluded; the cap
holds), the fixed-point memory (a replayed re-render loop that must not move the
strip), the fitted chip labels (the pixel fit, its word-boundary and stub rules,
grapheme-safe cuts, the canvas measurer's font plumbing and its no-canvas
fallback, and the character budget that stands in before layout), the
wheel-to-sideways mapping, the bundle contract
and inline-copy parity, the
dock registration (the seat and list id it claims, and that apply cannot reach
register outside the declaration wait), the placement decision (anchored row,
embedded silence, dock fallback), and the rendered chips including the
close-control rules.

Mutation-tested rather than trusted: disabling the preview rule fails 19
assertions, breaking truncation fails 8, registering the wrong seat fails 1,
reinstating a fixed tab window fails 2, a dead or vertical-only wheel mapping fails
2 and 1, a clipped (overflow: hidden) strip fails 1, and each placement guard
fails its own — a row inserted below the existing ones 1, a host anchored in an
embedded occurrence 1, a missing header check 1, a row that can stretch the
conversation column 1, an unrecognized shape that stops falling back to the dock 1,
a missing anchor marker 1, and registering without waiting for the declaration (the
bug that broke web boot) 2.
fails 2.

Install

The plugin goes into the desktop profile, which the DeepSeek Harness app
owns. The dsh plugin command refuses that profile, so edit its files directly.

  1. Quit DeepSeek Harness.
  2. Add the package to dependencies in ~/.dsh/profiles/desktop/package.json:
"dsh-session-tabs": "github:rifkyazizf/dsh-session-tabs"
  1. Install it from the profile directory:
cd ~/.dsh/profiles/desktop && pnpm install
  1. Append the loader row to ~/.dsh/profiles/desktop/cordis.patch.yml:
- insert:
- id: ui-session-tabs
name: 'dsh-session-tabs'
  1. Start DeepSeek Harness. The tab strip shows at the top of the conversation panel.

To pin a version, append a tag or commit to the spec, for example
github:rifkyazizf/dsh-session-tabs#a8f106b. To update, run pnpm update dsh-session-tabs in the profile directory.

For local development, clone the repo and point the dependency at the clone with
"file:/path/to/dsh-session-tabs". The profile is watched live
(patchReload: live) and DSH's client-HMR row polls the client bundle, so edits
to lib/client.js hot-reload in an open tab. A new plugin row needs one page
refresh the first time.

The strip is registered in conversation.input.dock (list seat, id
session-tabs, order 10) and drawn in its own row under the conversation header.

Disable or remove

To disable the strip, comment out the ui-session-tabs insert in
~/.dsh/profiles/desktop/cordis.patch.yml. To uninstall, also remove the
dependency and run pnpm install in the profile directory. Changes take effect
on the next page refresh.

Known gaps

  • The strip scrolls rather than recycling. With very many engaged sessions the
    row keeps every tab and scrolls; the × on a chip is how tabs are pruned. While it
    is scrolling, the shell's themed 5px horizontal bar is laid out in the strip's last
    row, so the row grows by 5px and the active tab's feet rest on the bar rather than
    on the rail. That row cannot be reclaimed — a custom ::-webkit-scrollbar consumes
    layout space, and the strip has to stay a scroller, since a fixed tab width is the
    whole point of the chip contract (see Titles and widths).
  • Memory lives in localStorage, not on the host. Dismissals and the
    engagement order survive a reload and an app restart, but clearing site data — or
    reaching the GUI on a different origin — starts fresh. The engaged set itself is
    durable on the host (its blank bit), so only which tabs you closed and their
    order are local.
  • A dismissal lasts until you revisit that session. Closing a tab hides it while
    something else is current; navigating back to that session (sidebar included)
    brings it back — that is the rule, not a bug.
  • No drag-to-reorder, and no middle-click close. Right-click offers rename and
    the three bulk closes only — no pin, fork, or archive (those stay in the sidebar).
  • The menu is English-only. The plugin registers no locale dictionaries, unlike
    the shipped client plugins, which pass their rows through ctx.locale.

License

MIT

常见问题

6 问
答案由本站依据条目字段整理,不引用上游原文
dsh-session-tabs 怎么安装?

本站没有抓到该条目的安装命令,请到仓库主页确认安装方式;这类条目多数是生态应用或尚未发布安装包的项目。

dsh-session-tabs 的验证等级是什么意思?

未标注验证等级。上游未给出验证结论,安装前请自行确认

dsh-session-tabs 安全吗?

未标注。

dsh-session-tabs 装之前要先确认什么?

先看它会读写哪些文件、执行哪些命令、连接哪些网络地址;验证等级只说明装得上、跑得起来,不等于安全。涉及密钥或系统权限的,建议先在隔离环境试装。

dsh-session-tabs 用的是什么许可证?

上游标注为 MIT。商用或二次分发前请到仓库确认许可证原文。

dsh-session-tabs 有哪些功能?

会话标签栏置于对话顶部、发过消息的会话保留标签、仅打开的会话共用预览标签、标签按使用顺序排列、标签过多时横向滚动。

同名或近似条目

8 条
不同作者的仓库分开列,请确认你要装的是哪一个
UI 与外观
按星标排序,可作替代方案

nuwa-skill

nuwa-skill

女娲 DSH plugin 只需输入一个名字,就自动调研、提炼并验证任何人的心智模型、决策启发式与表达 DNA,生成可直接调用的思维视角 S

未标注验证等级 3.8 万 星

awesome-dsh-plugin

awesome-dsh-plugin

A curated list of plugins for DeepSeek Harness (dsh) · DeepSeek Harnes

L4 · 沙盒安装通过 1.8 万 星

deepseek-balance-whale-widget

DeepSeek-Balance-Whale-Widget

DSH 界面右下角的常驻小鲸鱼余额挂件,实时显示 DeepSeek 账户余额、今日已用与每轮对话消耗,支持拖拽吸附、Q 弹动画与音效,开箱即

L4 · 沙盒安装通过 4,492 星

dsh-better-sidebar

DSH-better-sidebar

面向 DeepSeek Harness 的服务化侧边栏工作台插件,把文件编辑、终端、Git 变更、子代理任务与侧边对话整合进右侧栏与底部面板

L4 · 沙盒安装通过 4,112 星

dsh-deep-whale

dsh-deep-whale

DSH Web 鲸鱼娘皮肤系列(深海女仆工坊 maid-atelier)。

L4 · 沙盒安装通过 2,470 星

deepseek-design

deepseek-design

可视化设计工作室,支持网站、App 原型、海报、信息卡、报告和杂志的模板创建、元素编辑、选区级 AI 草稿衔接与导出。

L4 · 沙盒安装通过 1,438 星

插件的元信息来自作者仓库;分类、验证等级与安全结论的中文说明为中文整理,最近核验 —。