dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
在 DSH Web 侧边栏的工作区列表下面加一个「最近」区块,把所有工作区的最新会话平铺成一条时间线,行与悬停卡片复用外壳自己渲染的会话行,滚到底继续加载更早的会话;同时把工作区列表本身折叠为最近有记录的 5 个文件夹。需要 DSH ^0.2.0-rc.1。
English | 简体中文
DSH Web GUI 插件:在左侧边栏「工作区」列表下面加一个 最近 区块,把各个项目里最新的会话打散成一条时间线。上面按文件夹找项目,下面直接点最近干的活。

Codex 的侧边栏有两个视角:项目(文件夹)和最近(时间线)。DSH 的侧边栏只有项目视角——想回到刚才那个会话,得先想起它在哪个工作区里、再展开那个文件夹。
这个插件补上第二个视角:所有工作区的会话按更新时间排成一条平铺列表,行的长相、悬停行为和浮层都与上面工作区列表里的会话行逐元素一致——状态点、标题、距今时间(等待你响应时换成「待审批 / 计划待审 / 待回答」)、置顶标记,悬停时时间与标记让位给该行的操作(⋯ 菜单 / 归档 / 置顶);浮层是同款会话卡片(完整标题、距今时间、运行状态),并且多一行「文件夹图标 + 项目名」——平铺列表跨项目,这一行是唯一能看出会话属于哪个项目的地方。点击即打开会话。列表不折叠:滚到已渲染部分的末尾就继续加载更早的会话——整栏一条滚动条,从工作区树一路滚进历史。
0 8px / --dsw-radius-md / --dsw-alias-interactive-bg-hover)。transition,所以连过渡动画也没有:指针进入和离开都是一步到位,鼠标底下不会有东西滑动或跳一下。--dsw-* 语义 token 与外壳自带的 StateDot、细箭头图标,跟随明暗主题与品牌主题;折叠钮的尺寸、缩进、颜色与外壳自己的「展开其余 N 个会话」完全一致。@deepseek-ai/dsh-client-ui-primitives 的 HoverCard(工作区树会话卡片同一组件、同一份 244px 深色卡面),浮层内文字沿用它的固定浅色值,明暗主题下都不反色。sidebar.footer.action 列表槽位;行操作走外壳公开的 uiWorkspace 服务;工作区列表的折叠在已发布的外壳里没有对应槽位或服务,因此由本插件在 DOM 层补上(见设计说明),不修改、也不需要重新构建 DSH 前端。在跑 GUI 的 profile 目录里(通常是 ~/.dsh/profiles/web),从 GitHub 装:
dsh plugin --profile web add github:ttmouse/dsh-recent
改本插件源码时用本地克隆(link: 会跟随你的改动):
dsh plugin --profile web add link:/path/to/dsh-recent
该命令会把依赖写进 profile 的 package.json,并自动把 dsh-recent 追加到 dsh.profile.bundles(也可以手工做这两步):
{
"dependencies": { "dsh-recent": "link:/path/to/dsh-recent" },
"dsh": { "profile": { "bundles": ["…", "dsh-recent"] } }
}
然后重启 dsh web 并刷新页面——bundle 列表在启动时读取,热重载只覆盖已加载插件的源码变更:
# 停掉当前的 dsh web,再重新启动
dsh web
pnpm install
pnpm run typecheck # tsc --noEmit(src + tests)
pnpm test # 单元测试;构建过后还会校验 lib/client.js 的加载器契约
pnpm run build # lib/index.js + lib/invariant.js + lib/client.js + lib/types
改完源码必须重新 pnpm run build:宿主服务的是 lib/client.js,不是源码。
sidebar.footer.action 是 @deepseek-ai/dsh-client-ui-sidebar 长期声明的 list 槽位,渲染位置正是工作区树与「设置」之间——需求要的位置。它同时被收窄到 56px 图标栏(wide: false)时,本区块渲染为 null:一条列表在导轨里没有意义。useSessions/useSessionStatus/useWorkspaces 选择器钩子),会话操作走注册时注入的 RecentActions 面(src/client/sessionActions.ts,绑定 uiWorkspace),不在组件里订阅任何外部源。uiWorkspace.openSession(它同时清掉中间列选中的面板)。uiWorkspace 是注册所需的 cordis 服务,侧边栏外壳本身就依赖它。@deepseek-ai/dsh-client-ui-primitives 导出的 relativeTime(刚刚 / N分钟 / N小时 / N天 / N个月 / N年),每 30 秒刷新一次,所以同一会话在两个界面上读到的年龄一定一致。ui-workspace 的会话行实现——16×20 状态格、14/20 标题、10/16 三级色时间、置顶标记、悬停时换出的三个 16px 图标按钮,高亮色 --dsw-alias-interactive-bg-hover 同时用于悬停行与当前会话;浮层沿用外壳会话卡片的元素与取值(标题 / 距今时间 / 状态行),只多一行项目名——卡片本身给扩展内容留了位置(外壳的 sidebar.session.row.hover 座位),所以这一行插在时间之后、状态行之前,状态行仍是卡片的最后一行。项目名取工作区标题,会话不属于任何工作区时退回目录名、再退回「未分组」。卡片可点击复制标题,与外壳会话卡片同一交互。uiWorkspace(archiveSession / pinSession / forkSession)执行。外壳自己的行操作槽位(sidebar.workspaces.session.row.action、sidebar.workspaces.session.menu.item)按「声明即独占」规则已被 ui-workspace 声明,插件不能再声明或渲染它们,所以这里复刻的是外观与行为,而不是槽位本身。宿主以「会话仍在运行」拒绝归档时(WorkspaceArchiveError + workspace/session-active)弹出停止并归档确认;其余失败只记日志——插件没有自己的通知面。_groupSection,但既不提供「少显示几个」的能力,也没有对应槽位、store 或配置项,所以插件从外面补:把落选的分组置为 display: none,在最后一个保留的组后面插一行折叠钮。React 每次重渲染都会重写这份列表,因此用一个挂在 document.body 上的 MutationObserver 自愈,并用「先比对再写」保证自己的写入不会触发自己;观察回调先按记录过滤(只处理落在侧栏列内的变更),对话流式输出不会走到 apply()。列表容器的定位靠「第一个已渲染的 _groupSection 的父节点」,所以单列表/搜索渲染(没有 group 包装)天然不受影响。data-row-key="workspace:<工作区 id>"(未分组那一组的 key 是空串),这是分组唯一稳定的地址(类名是 hash,结构归 React)。没有历史记录的分组排在所有有记录的分组之后,所以默认折叠时它只会在名额有剩时才出现;列表本身没超过 5 个时不做任何裁剪。未分组那一组因此不再特殊:它默认不展示,只有在自己的会话足够新、挤进前 5 时才出现。折叠钮按渲染顺序插在最后一个保留组之后,落选的分组留在原位只是被隐藏,所以展开时顺序不变。wide 由 false 变 true)时工作区列表回到只显示 5 个、最近区块回到第一页,和 Codex 一致。position:relative; display:block 的 span,槽位自己还带 2px 间距)。所以折叠层按 data-row-key="session:<id>" 在分组里找行,并且隐藏/还原的是槽位本身——只隐藏行会留下槽位的间距;自己那行折叠钮插在最后一个保留行的槽位之后(与外壳自己的溢出钮同层、同度量,margin-top 也照它归零)。每组只留一个展开入口:外壳已经渲染「展开其余 N 个会话」时不再补第二个,而且点击它同时展开两层——插件在 document 上监听那个 data-row-key,顺手把自己的折叠状态也放开。排序用会话目录里的更新时间,目录里没有记录的行排最后。overflow-y: auto 列表),成为这一栏的最后一段内容——所以整栏只有一条滚动条,滚过工作区树就自然滚到「最近」,再往下就是更早的会话。区块因此不占底部固定高度、也不做内部滚动,行多高就多高。IntersectionObserver(root 就是那个滚动容器,底部提前 320px 触发)盯着它;哨兵进入视野就把渲染窗口再加一页。追加后哨兵被推出触发带,观察器自然安静下来,等下一次滚动——所以是一页一页地长,而不是一次铺完。区块折起(表头箭头)时不渲染哨兵,也就不会偷偷加载。flex: 1 会把列表撑到栏底、把区块顶到看不见的地方,所以折叠层要把列表的 flex: 1 换成 flex: 0 1 auto。区块搬进滚动容器后这个问题不存在了(区块是列表内容的一部分,不会落在栏底之外),这层覆盖随之删掉,外壳的布局不再被本插件改动。sidebar.panellist 走面板形态)。_groupSection / _footArea 两个 CSS Module 后缀和「第一个 group 的父节点」定位,分组的身份则靠标题行上的 data-row-key="workspace:<id>";外壳若改名或改结构,折叠会静默失效(退化成外壳原本的完整列表),不会报错也不会破坏列表——identity 读不到的分组一律排到最后,等于按列表顺序裁。折叠按分组计数(未分组那一组也算一个),不按会话数。区块所在的滚动容器也走这条定位:外壳若改成不给列表自己的 overflow-y,区块会跟着那次解析落到别的元素上。data-row-key="session:<id>" 定位、靠 _groupSection 划归属(嵌套分组不串行);外壳若去掉行上的 key 或改掉槽位结构,会话折叠会静默退回外壳自己的配额(5 个空闲会话 + 运行中豁免),不会报错。顺带一提,外壳自己那行的数字只看它自己没渲染的部分,点开时会比它承诺的多露出插件折叠的那几条。ui-workspace 的内部 store 驱动,插件拿不到入口,所以最近行的 ⋯ 菜单比外壳的会话菜单少一项「重命名」。要找很久以前的会话,搜索仍然比滚到底快。displayTitle 的回退值(通常是项目目录名或会话 id)。