dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Make Cmd+K open a Spotlight-style session palette instead of focusing the sidebar search box — with in-place renaming and up to three tags per session.
Press ⌘K and get a Spotlight-style session palette instead of a search box in the corner — recent first, running and waiting sessions on top, a preview of each session's last turn before you commit to opening it, two ways to rename a session in place — ⌘⇧R on the highlighted row, or a double click on the title at the top of an open conversation — and tags: up to three per session, clickable to filter.
dsh-plugin-session-palette — ⌘K 弹出会话面板:最近优先,正在运行与等待你的置顶,预览最后一个 turn,⌘⇧R 或双击标题就地改名,还能给会话打标签(最多 3 个,点标签就筛选)
它是什么:DSH Web 客户端的会话切换器。按 ⌘K,屏幕上方弹出一个面板:左边是会话列表(按「需要你 / 活跃 / 今天 / 昨天 / 过去 7 天 / 过去 30 天 / 更早」分组),右边是高亮那条的预览——包含它最后一个 turn 的你的提问和模型回复。方向键选,回车进。
为什么做:DSH 原本的 ⌘K 是「展开侧边栏 → 展开搜索框 → 把焦点丢进左下角那个输入框」。要切换会话,视线得从正文移到屏幕角落,而且只能看到一个平铺列表。VS Code 的 ⌘P、Sublime 的 ⌘P、macOS 的 ⌘空格 都是把它做成视线所在处弹出的浮层——因为切换会话是高频动作,值得一次专门的界面。
四个视图(Tab 向前循环、⇧Tab 向后):
| 视图 | 回答的问题 | 内容 |
|---|---|---|
| 最近(默认) | 现在在发生什么 | 全部会话,按活跃度 + 时间分组 |
| 工作区 | 我不记得标题,只记得那个目录 | 按目录分组,组头就是工作区名,最近碰过的排前面 |
| 置顶 | 我留下过什么 | 你置顶的,按置顶顺序(最近置顶在前) |
| 归档 | 我收起来过什么 | 已归档的,行变暗 |
抽屉,不是分组——这是这个插件最核心的设计判断。一个 25 天前置顶的会话和今天没有任何关系,把它钉在「今天」上面占着首屏就是噪音。「怕忘了」的意思是必须找得到,不是必须一直看见。所以置顶和归档各是一个带计数的可切换视图:计数负责提醒你里面还有东西,抽屉负责在你需要时把它给你;而它们完全不影响「最近」的排序。
和一个搜索的区别:这不是「更好的搜索」,是「更快地切回去」。所以它打开时不输入任何东西就已经列好了会话——⌘K 之后直接按方向键就能跳。
怎么装(在 DSH「添加插件」里填,不需要 npm 账号、也无需发布):
| 填入 | 形式 |
|---|---|
| GitHub | github:kjx-talesofai/dsh-plugin-session-palette#v0.8.1(当前可用) |
| 本地目录 | 绝对路径,例如 /path/to/dsh-plugin-session-palette |
装完重启桌面端(host 半边只在启动时加载)。
怎么用:
| 按键 | 动作 |
|---|---|
| ⌘K | 打开 / 收起面板 |
| ↑ ↓ | 移动高亮(循环),鼠标划过也会移动 |
| Home End | 首 / 末(输入框光标已在两端时) |
| ⏎ | 打开高亮的会话 |
| ⌘⇧R | 给高亮的会话改名(复用上面那个输入框,⏎ 保存 / esc 取消) |
| 双击标题 | 在已经打开的会话里,双击顶部那个名字 → 就地弹出输入框改名 |
| ⌘⇧T | 给高亮的会话打标签(右栏变成标签选择器:打字筛/新建、⏎ 加或去掉、esc 完成) |
| Tab / ⇧Tab | 循环三个视图 |
| ⌘⇧P | 置顶 / 取消置顶 |
| ⌘⇧⌫ | 归档 / 恢复 |
| ⌘↑ / ⌘↓ | 跳到第一条 / 最后一条(光标在哪儿都生效) |
| PgUp / PgDn | 翻一屏(到底就停,不回头) |
| esc | 关闭 |
注意事项:
cordis.patch.yml,重启后还在。每个会话最多 3 个;标签是「文字 + 可选 emoji + 颜色」,颜色只用主题 token,所以亮/暗色都跟着走。删除「dev」?3 个会话会失去它。 确认后,标签从词表和每一个挂着它的会话上一起移除(一次原子写入,不会留下指向已消失标签的分配);删除后的提示条里有撤销,会把标签和那些会话一起放回来。标题 🔥🧪),因为扫列表时眼睛停在标题上,不是停在右边缘。37 / 340),超过一屏的跳转有 ⌘↑/⌘↓ 和 PgUp/PgDn。9月12日),不是 23 天前——"上个月那个"是日期问题。crumbCurrent 这个类名后缀),所以它是这一版里唯一依赖宿主 DOM 的地方:DSH 哪天把这个类名改了,双击会静默失效(⌘K → ⌘⇧R 那条路不受影响)。侧边栏那排会话的双击归宿主管(它弹自己的改名框),这里刻意不碰。updatedAt 记的是你最后一次输入的时间,模型回复不会推进它。写"2 小时前"会骗人。卸载:DSH「插件」列表里卸载 dsh-plugin-session-palette → 重启;或「Restore All」恢复按键默认值。无需清理任何偏好——它没写过。
老实说(实测范围):只在 macOS(Apple Silicon)+ DSH Desktop 0.2.1-rc.2 上端到端跑过(0.8.0 的改名、标签、长列表都是在这个版本上验的)。Windows / Linux 未测;DSH 升级后上游契约可能变化,见下方 Compatibility。
DSH's ⌘K expands the sidebar, expands the search field, un-collapses the rail if needed, and then focuses the input — with a 300 ms delay when the rail has to slide open first. Switching sessions is a high-frequency action, and the cost lands in the worst place: your eyes have to leave the conversation and find a field in the corner.
Every tool that solves this well solves it the same way — VS Code, Sublime, Spotlight — with an overlay at the point of attention. This is that overlay.
The second problem is ordering. The host sorts sessions by updatedAt, which is max(createdAt, lastPromptAt): the last time the user wrote, not the last time anything happened. An agent reply does not move it. So a session whose agent just finished, or one that is blocked waiting for your approval, can sit below a dozen conversations you merely touched more recently. The palette ranks live state above the clock — waiting on you, then running, then finished-but-unread — and only then falls back to recency buckets.
Tab cycles. Each view answers a different question about the same set, which is why they are views rather than sections of one list.
| View | Question | Ordering |
|---|---|---|
| Recents | What is happening now? | Needs-you → Active → Today → Yesterday → Previous 7 days → Previous 30 days → Older |
| Workspaces | I remember the folder, not the title | One group per workspace folder, freshest group first, recency inside |
| Pinned | What did I keep? | Pin order, most recently pinned first |
| Archived | What did I put away? | Most recently active first; rows are dimmed |
Two consequences worth stating plainly:
Use DSH's own Add plugin dialog. No npm account, no publishing.
| Field | Value |
|---|---|
| GitHub | github:kjx-talesofai/dsh-plugin-session-palette#v0.8.1 |
| Local directory | an absolute path, e.g. /path/to/dsh-plugin-session-palette |
Restart the desktop app afterwards: the Host half is only loaded at startup. The browser half reloads with the page.
This plugin is not published to npm, and npm install is not the install path — a plain npm install of the tarball would also drag in the whole Harness as a peer dependency.
/ menu uses). The directory matters because a renamed workspace no longer shows its folder, and the folder is what you remember.A tag is id + label + optional emoji + colour. Five colours, each a theme token, so light and dark follow without a second palette. A session carries at most three; the ceiling is refused out loud ("Three tags is the limit") rather than truncated, because a fourth tag the user cannot see is worse than a no.
takeOver. Both tag fields are .volatile(), which is load-bearing twice — only volatile fields are accepted on write, and the Loader compares volatile fields out of its config diff, so saving a tag updates the running plugin instead of remounting it. Without that, the act of tagging would close the palette it was done from.MenuItemButton) opens the palette with that session highlighted and the picker already up, instead of growing a second tag UI to keep in step.The editor is a popover-like block inside the preview pane, under the session's own tag chips: its own input, its own list, its own Escape. The search field keeps searching sessions the whole time, and the list is never frozen — moving the highlight simply closes the editor, because an editor belongs to the row it was opened on. That is the corrected model; the first version made the search field filter tags while the list was locked, and it was unusable for exactly the reason you would expect: two meanings on one field, and a target that could not be moved.
This needs one DSH restart. The tag store is new host-side Config, and the Host reads index.js at startup — so until DSH restarts, the picker says "Tags need one DSH restart" and refuses to write. It never pretends to save.
A palette that asks you to browse needs landmarks. Three, in order of how much they do:
37 / 340 — the whole point is answering "am I lost?", which 340 sessions cannot.Two smaller ones: rows older than a week show the date you worked on them rather than 23d (a duration is not what you remember about last month), and the card is 1180px × 88vh — this surface exists for browsing, so it should not be shy about space.
The highlight is already the thing you are about to act on, so a rename edits that row and nothing else. ⌘⇧R turns the search field into a title field: same place, same line height, focus never moves, the query stays applied and the highlight stays put. ⏎ saves, esc restores the query, and a rejected write leaves the draft in place with the host's reason on its own line underneath.
Three decisions are worth naming:
page.refresh owns ⌘R, and ⌘⌥G is the host's own session.rename. Both are chords the Electron main process intercepts before the page ever sees them, so a handler on either would never run and the host would act behind the palette instead. Shift+R is unclaimed by every shipped command, which is a property of the current catalog rather than a promise.role="option" row is invisible to the accessibility tree, and a second text box would have to fight the row's focus handling. One field, two jobs, and its ARIA role changes with the job.sessions.using, not a direct call — the host refuses to rename a session that is not live in its store, so the write goes through a retained reference (the same path the shipped sidebar uses). The retain is attributed to sessionPalette in retainInfo, and it lasts for exactly one call.The bytes you type are not the bytes stored: the host normalizes whitespace and shortens a title past maxTitleBytes (80 in the shipped bundle). The toast therefore reports the title the host accepted, and Undo is only offered when there was a title to go back to.
The Chrome-free parts: the palette is one component registered into the frame-wide shell.overlay seat, rendering role="dialog" aria-modal="true" — which is what makes it a real modal for the host's Escape arbitration and for background-shortcut gating. The toast is a second entry in the same seat so it outlives the palette.
Taking ⌘K is the interesting part, and it writes no preference:
| Step | Mechanism |
|---|---|
| 1 | ctx.shortcuts.register refuses any default that overlaps session.search, and there is no unregister API — so the chord cannot be claimed by registering a command |
| 2 | ctx.shortcuts.registerFixed performs no conflict check, so a fixed binding on ⌘K is accepted |
| 3 | That fixed binding makes session.search conflicted, which removes meta+KeyK from the Electron main process's natively intercepted set |
| 4 | With the chord no longer intercepted, the DOM keydown reaches the page, where fixed observers run before configurable dispatch — and consume it |
Steps 2–4 were verified end to end against a running app before the palette was written. A fixed binding is deliberately not user-rebindable, which is why the plugin also registers a normal, unbound command (session.palette) that you can attach your own chord to in Settings → Shortcuts.
The preview comes from the host's turnOutline projection, which already carries every turn's prompt (≤50 code points) and response (≤120) into the session list — so for most sessions it costs no request at all. Measured coverage was about two thirds; the rest are the sessions you have not touched in a long time, and those are read lazily, one at a time, after the highlight settles.
No tag editor yet. Tags are created from the picker (label only, colour assigned in a ring) and can be deleted there; there is still no UI for emoji, colour or renaming a tag — that is the next release. The Config page in Plugin management shows the raw data in the meantime.
No content search yet. Matching covers the session title, the workspace name and the workspace path. The message-body search the shipped sidebar performs is not wired in.
No query language yet. @workspace, is:archived, after:7d and filter chips are designed but not implemented.
No settings UI yet. The one switch (takeOver) lives in your profile's cordis.patch.yml:
- id: session-palette
name: 'dsh-plugin-session-palette'
config:
takeOver: false # hand ⌘K back to the shipped sidebar search
The fixed chord is not user-rebindable. That is a property of the mechanism, not an oversight; the unbound companion command is the escape hatch.
Archiving a session with running work is refused by the Host, and the palette reports it as a plain failure rather than offering the shipped stop-and-archive flow.
A rename briefly opens the session. Renaming requires a retained reference, and retaining starts the session's history open. Rename is an explicit one-shot action, so this is the same cost the shipped sidebar pays for its own row rename — but it is not free on a session you have never opened.
The rename chord is the fixed part, not the title. ⌘⇧R is a local handler, so it only works while the palette has focus, and it is not rebindable.
The header double click depends on one class name suffix. crumbCurrent is a CSS-module class, so the hash changes between builds and the suffix does not; if DSH ever renames the suffix the gesture stops working silently. Nothing else breaks, and ⌘K → ⌘⇧R still renames.
No session deletion, ever — archived sessions are recoverable, which is what the Archived drawer is for.
Verified against DSH 0.2.1-rc.2 on macOS (Apple Silicon), in a real profile. The plugin depends on the following upstream contracts, each of which was checked against the shipped bundles rather than assumed:
| Contract | Where it comes from |
|---|---|
shell.overlay as a root-scoped list slot with standard session hooks | dsh-client-ui-layout, dsh-client-ui-renderer, dsh-client-ui-session |
registerFixed / observeFixedInput, and fixed bindings being excluded from native interception | dsh-client-shortcuts |
| The dotted command-id pattern (a bad id silently disables every keyboard command) | dsh-client-shortcuts |
ctx.sessions.list, refreshProjections, ctx.uiWorkspace, ctx.workspaces | dsh-api-session-controller, dsh-client-ui-workspace, dsh-api-workspace-controller |
sessions.using → reference.binding.session.rename, and the host's "not live in this store" refusal | dsh-api-session-controller, dsh-session-title |
| Native interception keyed on exact chord + modifiers, unmatched chords reaching the main frame | lib/main.js (before-input-event) |
turnOutline (per-turn prompt/response previews) | dsh-session-turn-outline |
retainedBy.mainView as "the current session" (the header's title span is found by its crumbCurrent class suffix — still <hash>_crumbCurrent in 0.2.1-rc.2) | dsh-client-ui-workspace, dsh-client-ui-conversation |
Upstream may change any of these. A failure in the shortcut contract is the one to watch: it is silent rather than loud.
npm test # 113 tests, no install step required
There is deliberately no build step: client.js is the shipped browser bundle, and the Host half is a single index.js.
The suite evaluates the browser half as a script in node:vm (so the artifact really is a script-executable module-loader bundle), drives the real component through a small hooks runtime, and reads the Host half as text — never importing it, because that would need @deepseek-ai/schemastery and make a bare checkout fail.
| Path | Ships to users | Purpose |
|---|---|---|
client.js | ✓ | The browser half — component, styles, dictionaries, shortcut claim |
index.js | ✓ | The Host half — the Config schema and a no-op apply |
cordis.patch.yml | ✓ | The bundle row that mounts the package |
locale/*.json | ✓ | Plugin-manager display metadata (en / zh) |
assets/icon.svg | ✓ | Plugin icon |
tests/plugin.test.mjs | ✗ | Contract + rendered-behaviour suite |
docs/plan.md | ✗ | The design record: what was verified, and what was got wrong |
probe/ | ✗ | A separate diagnostic bundle that empirically proved the ⌘K mechanism |
probe/ is kept as a regression harness for the shortcut mechanism: if a future DSH changes the fixed-binding conflict semantics or Electron's interception, re-running it answers that in one press instead of a re-derivation. It is a separate package, sets nothing by default, and is excluded from the tarball.
Remove dsh-plugin-session-palette from DSH's plugin list and restart. There is nothing to clean up — the plugin writes no keybinding preference, no browser storage, and no profile state beyond its own optional config block.
MIT © 2026 Jiaxin Kou
Built by hypersampling · GitHub