kjx-talesofai/dsh-plugin-session-palette ↗★ 0
dsh-plugin-session-palette
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. 适合拥有大量历史会话,需要通过标题、工作区或目录快速模糊检索并切换会话的用户。
インストール
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:kjx-talesofai/dsh-plugin-session-paletteドキュメント
README 全文を読む ↗Usage
- Press ⌘K. The palette opens with your sessions already listed.
- Type to narrow by title, workspace name, or the directory the workspace lives in (fuzzy-matched, ordered-subsequence — the same ranker the host's own
/menu uses). The directory matters because a renamed workspace no longer shows its folder, and the folder is what you remember. - ↑/↓ or hover to move the highlight; the right-hand pane previews that session's last turn.
- ⏎ opens it. Tab switches to the Pinned or Archived drawer.
- ⌘⇧R renames the highlighted session right there — the search field becomes the title field, ⏎ saves, esc cancels.
- ⌘⇧P pins, ⌘⇧⌫ archives, and both toggle — so the same chord restores.
- In an open conversation, double-click the title in the header to rename it there — the same editor, floated over the title's own box.
- ⌘⇧T opens the tag picker for the highlighted session: type to narrow, type a name nothing matches to create it, ⏎ toggles, esc done. Tags show as glyphs on the row and as chips in the preview; clicking a chip filters the list (several = any of them).
Tags
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.
- Where they live: the plugin's Config, next to
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. - Identity is the id, derived from the label when the tag is created and stable afterwards, so renaming a tag later cannot orphan the sessions that point at it.
- The filter means "any of": tags are how you group work that spans workspaces and dates, so narrowing on two tags should show both groups, not their intersection.
- One picker, attached to the item. The sidebar's session menu row (with a tag icon, styled by the shipped
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.
Finding your way around a long list
A palette that asks you to browse needs landmarks. Three, in order of how much they do:
- Sticky group headings. After two screens the only thing left was a wall of identical rows; the heading now stays at the top of the scrollport, so "this is yesterday" is always on screen.
- A position, not just a total. The footer reads
37 / 340— the whole point is answering "am I lost?", which340 sessionscannot. - Jumps that always work. ⌘↑/⌘↓ go to the ends from anywhere (Home/End already did, but only once the caret reached an edge), and PgUp/PgDn move a screenful — clamped, because a page that jumps back to the top reads as a bug.
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.
Renaming
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:
- ⌘⇧R, not ⌘R —
page.refreshowns ⌘R, and ⌘⌥G is the host's ownsession.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. - The search field, not a second input — an input inside a
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 tosessionPaletteinretainInfo, 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.
How it works
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.