dsh-claude-style 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.en.md +4 -4
- package/README.md +4 -4
- package/docs/STYLE.md +62 -13
- package/docs/architecture.md +37 -10
- package/host/index.js +94 -20
- package/host/search.js +167 -0
- package/lib/client.js +3869 -2360
- package/lib/model-descriptions.json +264 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `dsh-claude-style` are documented here, newest first.
|
|
4
4
|
|
|
5
|
+
## [0.9.1] - 2026-09-27
|
|
6
|
+
|
|
7
|
+
[中文](#cn-0.9.1) | [English](#en-0.9.1)
|
|
8
|
+
|
|
9
|
+
<h3 id="cn-0.9.1">问题修复</h3>
|
|
10
|
+
|
|
11
|
+
- 修复 **已归档列表里的会话删不掉**:点「删除」后宿主半边只删存储目录、不动归档记录,存储目录已经不在的会话更是直接被拒——列表那一行永远留着,重载页面后还在。现在删除成功时宿主半边在同一请求里把该会话从归档集合里移除;归档集合里存储目录已消失的条目(幽灵)也按删除成功处理并移出归档集合,浏览器随即重拉一次会话列表基线,已删除的会话不会再靠旧摘要顶在已归档列表或会话树里。旧版宿主半边对目录缺失的会话回 404 时,浏览器同样移除该行。
|
|
12
|
+
|
|
13
|
+
<h3 id="en-0.9.1">Bug Fixes</h3>
|
|
14
|
+
|
|
15
|
+
- **Fixed archived sessions refusing to disappear when deleted**: the delete button removed the stored directory while the archive record kept the id, and a session whose directory was already gone was refused outright — the row stayed on the list and survived a page reload. A successful delete now also drops the session from the archive set in the same request, an archive entry whose stored directory has vanished is answered as deleted and leaves the set, and the browser re-pulls the session baseline right after, so a deleted session no longer lingers on the archived list or in the session tree on a stale summary. Against an older host half that still answers 404 for a missing directory, the browser removes the row just the same.
|
|
16
|
+
|
|
17
|
+
**Full Changelog**: [v0.9.0...v0.9.1](https://github.com/Nwflower/dsh-claude-style/compare/v0.9.0...v0.9.1)
|
|
18
|
+
|
|
19
|
+
## [0.9.0] - 2026-09-26
|
|
20
|
+
|
|
21
|
+
[中文](#cn-0.9.0) | [English](#en-0.9.0)
|
|
22
|
+
|
|
23
|
+
<h3 id="cn-0.9.0">新增功能</h3>
|
|
24
|
+
|
|
25
|
+
- **侧栏搜索框与搜索面板**:鼠标移到侧栏上时,左上角的品牌标志淡出、换成一个淡入的搜索按钮(右端标出宿主的搜索快捷键),点它在窗口上方打开搜索面板;宿主的搜索快捷键(桌面端 Ctrl+K,Web 端 Ctrl+Alt+K,在设置里改过的以改后的为准)也打开这个面板,侧栏「工作区」标题行原来的搜索图标不再显示。面板打开与关闭时连同背后的遮罩一起淡入淡出。面板能搜历史会话、项目、插件、当前会话可用的 Skill 与快捷键。会话按标题、所属工作区名和对话内容搜:你和模型说过的每句话都算(工具调用的参数与子代理会话不算),句子中间的几个中文字也能搜到,不分大小写,空格多几个少几个都行;行下显示命中的那一句,命中的字加粗。插件新增只读的私有路由 `/dsh-claude-style/session-search` 供面板查询,与其他私有路由一样先过宿主的请求校验;宿主进程启动后第一次打开面板时它会把全部会话读一遍(本机 128 个会话约 16 秒,这期间面板显示「正在搜索」),之后每次查询约 0.1 秒。顶部的「全部 / 会话 / 项目 / 插件 / Skill / 快捷键」切换分类;搜索框为空时列出最近的五个会话和「新会话 / 插件 / 设置 / 键盘快捷键」几个操作。↑↓ 选择,Enter 打开,Tab 与 Shift+Tab 切换分类,Esc 关闭。选中会话即打开该会话;选中项目在这个工作区开一个新会话;选中插件打开它在插件页的详情;选中 Skill 把 `/名称 ` 放到当前会话输入框的开头;选中快捷键打开宿主的快捷键列表,并筛到这一条。侧栏收起成窄条时没有搜索框。
|
|
26
|
+
- **进行中、已停止与失败的轮次改用 Claude Code 式的状态行**:原来宿主把「深度求索中,用时 N 秒」「已停止」「处理失败」排在这一轮的最前面,工作过程往下越排越长、状态却停在上方。现在这一行跟在这一轮工作的末尾(这一轮的页脚、排队中的消息仍在它下面),画成 Claude Code 的样子。进行中是一个转动的火花标记加「用时 · 输出 tokens · 当前动作」,例如「34秒 · 1.2k tokens · 思考中…」,当前动作依次有等待模型、思考中、思考了 N 秒、输出中、准备调用工具与运行工具中;停止或失败后火花静止,文字为「已停止 · 17秒 · 179 tokens」「处理失败 · 12秒 · 300 tokens」这样,历史里的这些轮次也一样。tokens 是这一轮已完成各步上报的输出用量,正在输出的那一步结束后计入。正常结束的一轮仍是宿主原来的「用时 N 秒」,在原来的位置,折叠与展开照旧。
|
|
27
|
+
|
|
28
|
+
### 体验优化
|
|
29
|
+
|
|
30
|
+
- **像素螃蟹改用 Claude Code 的原版动画**:工作台首页输入卡片上的螃蟹原来是按 Claude Code 的样子手绘的十一个姿势;现在换成 Claude Code 自己的钓鱼动画,逐帧照搬(43 帧、每帧 80 毫秒,约 3.4 秒):眨眼、举竿甩过卡片边沿、跳一下侧过身钓鱼、收竿转回正面,像素边缘清晰。鱼竿仍用随主题变化的灰色,触发方式(点击、指针移开、闲置时隔二三十秒自己来一次,减少动态效果时只认点击)不变。
|
|
31
|
+
- **对话中输入框的发送键改成 Claude Code 的回车图标**:原来是等宽字体里的「↵」字符,有内容时变成陶土橙;现在是按 Claude Code 画的回车箭头(上沿一小段、沿右侧向下、再沿底边向左到箭头),线条清晰,输入框有内容时用正文墨色,空着时仍是浅灰。
|
|
32
|
+
- **桌面端侧栏的品牌行上移 10px**:桌面端的侧栏从 40px 高的标题栏下方开始,品牌标志因此比 Web 端低出一截;现在品牌行(连同其下的新会话、插件与会话列表)上移 10px,悬停时换上的搜索框仍整个露在标题栏下方,与标题栏里的收起按钮也不重叠。Web 端不变。
|
|
33
|
+
- **用量面板的倍数趣味行从二十九本书扩到六十五本**:书单补入《黄色壁纸》《变形记》《化身博士》《野性的呼唤》《时间机器》《绿野仙踪》《黑暗之心》《浮士德》《螺丝在拧紧》《伊索寓言》《彼得·潘》《君主论》《隐形人》《安徒生童话》《世界大战》《善恶的彼岸》《白牙》《沉思录》《格林童话》《格列佛游记》《瓦尔登湖》《鲁滨逊漂流记》《神曲》《包法利夫人》《查拉图斯特拉如是说》《奥德赛》《双城记》《远大前程》《理想国》《罪与罚》《利维坦》《安娜·卡列尼娜》《卡拉马佐夫兄弟》《堂吉诃德》《基督山伯爵》《悲惨世界》,长度都是全文以 o200k_base 分词器实测的值(英文原著或通行英译本,《浮士德》为德文原文)。书单仍从《道德经》排到《追忆似水年华》,用量越过的书更密,趣味行落到的那本书离实际用量更近。
|
|
34
|
+
|
|
35
|
+
### 问题修复
|
|
36
|
+
|
|
37
|
+
- 修复 **开启「自动弹出弹层」后进入封号彩蛋页会立刻退出**:桌面端从账号菜单点开彩蛋页时,全窗浮层让指针「离开」菜单,悬停收起随后代宿主菜单按下的 Escape 被插件的键盘路由当成了用户按键,约 0.1 秒就把刚打开的彩蛋页关掉。现在这次代发的 Escape 只送达宿主菜单:彩蛋页开着时悬停收起同时停摆,页面保持打开,退出后侧栏与账号菜单恢复进入前的样子。
|
|
38
|
+
- 修复 **启用皮肤后带输入框的会话页上快捷键失灵**:皮肤常驻页面的模型选择器、推理强度与权限弹层卡片在关闭时仍标注 `role="menu"`,宿主据此把快捷键仲裁给一个看不见的「前景菜单」,连按 Esc 停止智能体、Ctrl+W 关闭页面与标签页菜单的 Esc 都不再生效(Web 端受影响的面更宽)。现在这一角色标注随卡片开合写入与撤销:卡片打开时才存在,快捷键的仲裁恢复如常。
|
|
39
|
+
|
|
40
|
+
<h3 id="en-0.9.0">New Features</h3>
|
|
41
|
+
|
|
42
|
+
- **A sidebar search box and search palette**: while the pointer is over the sidebar, the brand mark at its top left fades out into a search button (the host's search shortcut shown at its right end), and pressing it opens a search palette near the top of the window; the host's search shortcut (Ctrl+K on the desktop, Ctrl+Alt+K on the web, or whatever it has been rebound to in settings) opens the same palette, and the search icon the sidebar's Workspace heading used to carry is gone. The palette fades in and out together with the mask behind it. The palette searches past sessions, projects, plugins, the skills the current session offers and keyboard shortcuts. Sessions match by title, by workspace name and by what was said in them: every message between you and the model counts (tool-call arguments and subagent sessions do not), a few Chinese characters from the middle of a sentence match, case does not matter and neither do extra spaces; the matching line shows under the row with the match in bold. The plugin adds a read-only private route, `/dsh-claude-style/session-search`, for these queries, behind the host's request check like its other private routes; the first time the palette opens after the host process starts, the route reads every session once (about 16 s for 128 sessions here, while the palette shows "Searching…"), and each query after that takes about 0.1 s. The palette has All / Sessions / Projects / Plugins / Skills / Shortcuts across the top to narrow the list; with the box empty it lists the five most recent sessions and the New session, Plugins, Settings and Keyboard shortcuts actions. ↑↓ moves, Enter opens, Tab and Shift+Tab switch the category, Esc closes. Picking a session opens it; a project starts a new session in that workspace; a plugin opens its details on the plugins page; a skill puts `/name ` at the head of the current session's input; a shortcut opens the host's shortcut list filtered to that entry. The collapsed sidebar rail has no search box.
|
|
43
|
+
- **A Claude Code style status line for running, stopped and failed turns**: the host used to put "Deep diving for N s", "Stopped" and "Failed" at the very top of the turn, so the work grew downward while its status stayed above it. The line now follows the end of the turn's work (the turn's footer and queued messages still sit below it) and looks like Claude Code's. While the turn runs it is a turning spark, then elapsed time · output tokens · what the model is doing, for example "34s · 1.2k tokens · Thinking…", the action being one of waiting for the model, thinking, thought for N s, writing, preparing a tool call and running tools; once the turn is stopped or fails the spark stands still and the line reads like "Stopped · 17s · 179 tokens" or "Failed · 12s · 300 tokens", for such turns in the history too. The token count is the output usage the turn's finished steps report; the step still streaming joins it once it finishes. A turn that finishes normally keeps the host's own "Took N s" in its place, and folding works as before.
|
|
44
|
+
|
|
45
|
+
### Improvements
|
|
46
|
+
|
|
47
|
+
- **The pixel crab plays Claude Code's own animation**: the crab on the Studio home page's composer card used to be eleven hand-drawn poses after Claude Code's; it now plays Claude Code's own fishing animation frame for frame (43 frames of 80 ms, about 3.4 s) — a blink, the rod raised and cast past the card's edge, a hop into a side-on stance to fish, the rod reeled in as it turns back — with crisp pixel edges. The rod keeps its theme-following grey, and what sets it off (a click, the pointer leaving it, on its own every half minute or so, only a click under reduced motion) is unchanged.
|
|
48
|
+
- **The in-conversation composer's send button uses Claude Code's return glyph**: it used to be the "↵" character from the monospace font, turning clay once there was text; it is now Claude Code's drawn return arrow (a short stub along the top, down the right side, back along the bottom to the arrowhead) in crisp strokes, in the body ink once the box holds text and still pale grey while it is empty.
|
|
49
|
+
- **The desktop sidebar's brand row sits 10px higher**: the desktop sidebar starts below the 40px titlebar, which left the brand mark lower than on the web; the brand row (and the New session, Plugins and session list below it) now rises 10px, with the search box that takes its place on hover still wholly below the titlebar and clear of the collapse button there. The web build is unchanged.
|
|
50
|
+
- **The usage panel's yardstick line grows from twenty-nine to sixty-five books**: the list adds The Yellow Wallpaper, Metamorphosis, The Strange Case of Dr Jekyll and Mr Hyde, The Call of the Wild, The Time Machine, The Wonderful Wizard of Oz, Heart of Darkness, Faust, The Turn of the Screw, Aesop's Fables, Peter Pan, The Prince, The Invisible Man, Andersen's Fairy Tales, The War of the Worlds, Beyond Good and Evil, White Fang, Meditations, Grimms' Fairy Tales, Gulliver's Travels, Walden, Robinson Crusoe, The Divine Comedy, Madame Bovary, Thus Spoke Zarathustra, The Odyssey, A Tale of Two Cities, Great Expectations, The Republic, Crime and Punishment, Leviathan, Anna Karenina, The Brothers Karamazov, Don Quixote, The Count of Monte Cristo and Les Misérables, each sized by feeding its full text to the o200k_base tokenizer (the English original or the standard English translation; Faust in the German original). The list still runs from Tao Te Ching to In Search of Lost Time; with the books set closer together, the one the line lands on sits nearer the real usage.
|
|
51
|
+
|
|
52
|
+
### Bug Fixes
|
|
53
|
+
|
|
54
|
+
- **Fixed the account-hold easter egg closing itself right after it opened while "Open popovers on hover" is on**: on the desktop, opening the page from the account menu made the full-window overlay "leave" the menu, and the hover close that followed pressed Escape on the host's behalf — which the skin's keyboard routing took for the user's own key and dismissed the page about 0.1 s in. That dispatched Escape now reaches only the host's menu, the hover close stands down while the page is open, and the sidebar and its account menu come back as they were once the page is dismissed.
|
|
55
|
+
- **Fixed shortcuts going dead on conversation pages with an input box while the skin is enabled**: the model picker, reasoning-effort and permission popover cards the skin keeps in the page carried their `role="menu"` mark while closed, so the host arbitrated its shortcuts to an invisible "foreground menu" — pressing Esc twice to stop the agent, Ctrl+W to close a page and the Esc of the tab menu all stopped working (the Web build loses a wider set of them). The mark now rides the card's open state: it exists only while a card is open, and shortcut arbitration goes back to normal.
|
|
56
|
+
|
|
57
|
+
**Full Changelog**: [v0.8.0...v0.9.0](https://github.com/Nwflower/dsh-claude-style/compare/v0.8.0...v0.9.0)
|
|
58
|
+
|
|
5
59
|
## [0.8.0] - 2026-09-26
|
|
6
60
|
|
|
7
61
|
[中文](#cn-0.8.0) | [English](#en-0.8.0)
|
package/README.en.md
CHANGED
|
@@ -66,11 +66,11 @@ Keep only one theme enabled at a time. After installation, **restart `dsh web`**
|
|
|
66
66
|
2. **Composer** — the input box is rebuilt from the ground up: a permission segmented control (Read / Edit / Auto / Yolo) with its tier list under it, a model trigger carrying the vendor lockup, and the toolbar and status stats laid out on a single line, with the send and stop buttons unified into 7px rounded rectangles. The stats sentence and the model trigger share the same font size and color. The permission tiers come from the host's own catalog: a deployment draws exactly the tiers it configures, and a tier a third-party plugin registers (the auto mode plugin's `auto-mode`, say) appears in the list and takes the Auto slot; the list stays text-only, so a tier's declared glyph is not drawn. Switching tiers slides the highlight from the old tier to the new one, and every segmented control on the settings page does the same.
|
|
67
67
|
3. **Model picker** — a new two-level popover. The first level lists the official service plus the quick providers selected in settings; "More models" opens a second level grouped by provider. Every row carries its vendor lockup and a description, and the footer holds the reasoning-effort slider (stepless — it snaps to the nearest level on release) together with the "More models" entry. The second level aligns its bottom edge with the first, providers already shown on the first level are not repeated, and models without thinking support get no slider. Hovering opens the popover after a 100 ms dwell and closes it after 150 ms; the gap between the two cards does not count as leaving.
|
|
68
68
|
4. **Workspace** — the sidebar's "Workspace" heading becomes an **Active / Archived** segmented control. Active keeps the host's session tree, while Archived is the skin's own flat list (title, time, and per-row unarchive and delete icon buttons). Rows match the host's session rows measurement for measurement (row x=12 / width 251 / height 28 / title x=36). Switching between the two views slides the highlight from one segment to the other.
|
|
69
|
-
5. **Sidebar** — the New session and Plugins rows take Claude's real shape: no background at rest, a background on hover, the "+" of New session set inside a circular chip, and both icons rotating 90° clockwise on hover (four-fold symmetric glyphs, so they land back on themselves). The account drawer and the ban-screen easter egg remain as they were.
|
|
69
|
+
5. **Sidebar** — the New session and Plugins rows take Claude's real shape: no background at rest, a background on hover, the "+" of New session set inside a circular chip, and both icons rotating 90° clockwise on hover (four-fold symmetric glyphs, so they land back on themselves). The account drawer and the ban-screen easter egg remain as they were. On the desktop the brand row sits 10px higher than the host draws it, level with the web build. While the pointer is over the sidebar, the brand mark at its top left fades into a search box (the collapsed rail has none); pressing it, or the host's search shortcut (Ctrl+K on the desktop, Ctrl+Alt+K on the web), fades in a search palette near the top of the window, and the Workspace heading's own search icon is gone: it searches past sessions (by title, workspace name and what was said in them: every message between you and the model counts, a few Chinese characters from the middle of a sentence match, case does not matter, and the matching line shows under the row with the match in bold; the first time the palette opens after the host starts, every session is read once, which takes a dozen seconds or so), projects, plugins, the skills the current session offers and keyboard shortcuts, with All / Sessions / Projects / Plugins / Skills / Shortcuts across the top, and with the box empty it lists the five most recent sessions and the New session, Plugins, Settings and Keyboard shortcuts actions. ↑↓ moves, Enter opens, Tab and Shift+Tab switch the category, Esc closes; a session opens, a project starts a new session in that workspace, a plugin opens its details on the plugins page, a skill puts `/name ` at the head of the current session's input, and a shortcut opens the host's shortcut list filtered to that entry.
|
|
70
70
|
6. **Account area** — the nickname and the picture each follow one fixed order. Nickname: the custom nickname → the signed-in account's name → the HDSL launcher's account name → the last probed system user → the freshly probed system user → `User`. Picture: the signed-in account's avatar → the HDSL launcher's avatar → the Claude mark. The account profile is fetched from the server only at startup, on hot reload, and on sign-in and sign-out — never polled. The HDSL contract is read once by the host half and forwarded over two read-only routes (`GET /dsh-claude-style/hdsl` for the metadata, `GET /dsh-claude-style/hdsl-skin.png` for the avatar image; neither carries the avatar file's absolute path, and both sit behind the host's request fence). A contract version that is missing or unknown voids the whole group, and a deleted avatar file falls back to the mark. What the launcher publishes is a skin sheet rather than a finished avatar, so that picture is cropped into the head first — the face's 8×8 texel block inset by 1/18 of the box with the hat layer over the whole box, drawn square so no rounding shaves its pixels. The account popover remains the host's own: the host's account row is the entry point, and the popover lists the account header, the other plugins' footer entries, and the host's own Settings / Feedback / Sign out, each keeping its original copy, order, and click behavior. On hosts without an account area (Web), the plugin builds a popover with the same look. The card uses the main canvas's ivory background and spans the sidebar's width.
|
|
71
|
-
7. **Conversation views** — in the desktop Windows titlebar mode, the Conversation / Trajectory control sits centered and always visible on the titlebar row (the same row as the New session button). On wide windows with a short title it stays on the title's row; otherwise it keeps its own row below the title. Switching views slides the highlight from the old tab to the new one.
|
|
72
|
-
8. **Home layouts** — the new-conversation page comes in two layouts, Studio by default and switchable in the settings page at any time: **Classic** is the centered headline over the composer card, the headline drawn from a pool of greetings for the time of day (a few each for morning, midday, afternoon, evening and night, plus a few for any hour) and redrawn each time the new-conversation page comes back; **Studio** pins the greeting to the top left as one fixed line ("What's up next, <user>?"), docks the composer in the conversation's single-line form at the window's bottom edge with a row of hairline context chips above it, and sets the greeting and a 480px usage panel against the composer's left edge: an Overview tab with six stat cells (sessions, messages, total tokens, active days, peak hour, favorite model) and a twenty-six-week per-day heat grid of square cells in equal columns, a Models tab stacking each day's per-model tokens into one bar over a ranked model list (swatch, name, input and output, share; past six rows it folds behind a "show more" row, and the open list folds back with "show less"), and All / 30d / 7d range pills at the top right, which the peak hour and the multiplier line follow too; once the picked range's total passes one book, a multiplier line appears under the grid, naming one of
|
|
73
|
-
9. **The pixel crab** — in the Studio layout, Claude Code's pixel crab stands on the new-conversation composer card's top edge, near its right end (the Classic layout has none). When it is clicked or the pointer leaves it (and on its own every half minute or so while the page is open), it plays Claude Code's fishing routine
|
|
71
|
+
7. **Conversation views** — in the desktop Windows titlebar mode, the Conversation / Trajectory control sits centered and always visible on the titlebar row (the same row as the New session button). On wide windows with a short title it stays on the title's row; otherwise it keeps its own row below the title. Switching views slides the highlight from the old tab to the new one. For running, stopped and failed turns the status line follows the end of the turn's work (the footer and queued messages sit below it) and looks like Claude Code's: while running, a turning spark, then elapsed time · output tokens · what the model is doing (waiting for the model, thinking, thought for N s, writing, preparing a tool call, running tools); once stopped or failed, a still spark and "Stopped / Failed · duration · output tokens". A turn that finishes normally keeps the host's own look and place.
|
|
72
|
+
8. **Home layouts** — the new-conversation page comes in two layouts, Studio by default and switchable in the settings page at any time: **Classic** is the centered headline over the composer card, the headline drawn from a pool of greetings for the time of day (a few each for morning, midday, afternoon, evening and night, plus a few for any hour) and redrawn each time the new-conversation page comes back; **Studio** pins the greeting to the top left as one fixed line ("What's up next, <user>?"), docks the composer in the conversation's single-line form at the window's bottom edge with a row of hairline context chips above it, and sets the greeting and a 480px usage panel against the composer's left edge: an Overview tab with six stat cells (sessions, messages, total tokens, active days, peak hour, favorite model) and a twenty-six-week per-day heat grid of square cells in equal columns, a Models tab stacking each day's per-model tokens into one bar over a ranked model list (swatch, name, input and output, share; past six rows it folds behind a "show more" row, and the open list folds back with "show less"), and All / 30d / 7d range pills at the top right, which the peak hour and the multiplier line follow too; once the picked range's total passes one book, a multiplier line appears under the grid, naming one of sixty-five books (Tao Te Ching to In Search of Lost Time) drawn afresh each time the new-conversation page comes back. The numbers are rolled up by the plugin's host half: with `dsh-cost-meter` installed it reads that plugin's ledger cache directly (read-only — their file is never written), and without it the host half folds the local session logs itself, caching per log by size and modification time. Both sources answer each model's four buckets and each day's per-model tokens, with one model served by several providers counted as one row; the ledger keeps no hours, so the peak hour is always folded per day from the local session logs; when the roll-up cannot answer, the panel falls back to the projection block each host session row carries. The panel draws its skeleton first and does not shift when the numbers land. Before a workspace is picked (a fresh start, no session yet), the usage panel and the Read / Edit / Auto / Yolo permission segments still show; the segments rest on the preset a new session starts in, and pressing them opens the workspace picker, the same as pressing the input box, whose dashed "pick a workspace" ring now outlines the single-line input box alone. When the window is too short for the whole panel, the panel scrolls between the greeting and the composer with its bottom edge fading out, and the page no longer runs past the window. The workspace picker is drawn as Claude Code's folder menu: a narrow card of plain text rows set closer together, with no folder glyph in front of each folder and no plus sign on "Add workspace…"; the current workspace keeps the accent check.
|
|
73
|
+
9. **The pixel crab** — in the Studio layout, Claude Code's pixel crab stands on the new-conversation composer card's top edge, near its right end (the Classic layout has none). When it is clicked or the pointer leaves it (and on its own every half minute or so while the page is open), it plays Claude Code's fishing routine, frame for frame from Claude Code's own animation: a blink, the rod raised and cast down past the card's edge, a hop into a side-on stance for a spell of fishing, and the rod reeled in as it turns back — about three and a half seconds. With reduced motion requested (Windows' "Show animations in Windows" turned off, for one), it moves only when clicked.
|
|
74
74
|
|
|
75
75
|
## Disabling and uninstalling
|
|
76
76
|
|
package/README.md
CHANGED
|
@@ -66,11 +66,11 @@ dsh plugin --profile web add Nwflower/dsh-claude-style # GitHub 源
|
|
|
66
66
|
2. **输入框** —— 输入框整块重做:权限分段控件(只读 / 编辑 / 自动 / Yolo)与它下方的档位列表、带品牌锁定标的模型触发器、工具栏与状态统计同一行排版,发送键与停止键统一成 7px 圆角。统计句与模型触发器共用一套字号与颜色。权限档位取自宿主的权限目录:宿主配置了哪几档就画哪几档,第三方插件注册的档位(例如 auto mode 插件的 `auto-mode`)会自动出现在列表里并占用「自动」那一格;列表保持纯文字,档位自己声明的图标不画。切换档位时,高亮块从原档位平滑滑到新档位;设置页里的各组分段控件也是如此。
|
|
67
67
|
3. **模型选择器** —— 全新的两级弹层:一级列官方服务与设置页勾选的快捷供应商,「更多模型」二级按供应商分组;每行带厂商锁定标与说明文案,底部是推理等级推条(无极滑动,松手对齐最近档位)与「更多模型」入口。二级底边与一级对齐,一级已显示的供应商不再重复;模型不支持思考时不画推条。悬停停留 100ms 打开、离开 150ms 关闭,两卡之间的空隙不算离开。
|
|
68
68
|
4. **工作区** —— 侧栏「工作区」标题改成 **进行中 / 已归档** 分段控件:进行中沿用宿主的会话树,已归档是皮肤自己的平铺列表(标题、时间、每行「取消归档」与「删除」两个图标按钮)。行排版与宿主的会话行逐项一致(行 x=12 / 宽 251 / 高 28 / 标题起点 x=36)。切换两种视图时,高亮块在两段之间平滑滑动。
|
|
69
|
-
5. **侧栏** —— 新会话与插件两行取 Claude 真机形状:默认无底色、hover 才有底色,新会话的「+」套一枚圆形底,两行图标 hover 顺时针转 90
|
|
69
|
+
5. **侧栏** —— 新会话与插件两行取 Claude 真机形状:默认无底色、hover 才有底色,新会话的「+」套一枚圆形底,两行图标 hover 顺时针转 90°(四重对称图形,转回原位);底部账户抽屉、封号彩蛋一并保留。桌面端的品牌行比宿主原样上移 10px,不再比 Web 端低出一截。鼠标移到侧栏上时,左上角的品牌标志淡出、换成一个搜索框(侧栏收起成窄条时没有),点它或按宿主的搜索快捷键(桌面端 Ctrl+K,Web 端 Ctrl+Alt+K)在窗口上方淡入搜索面板,「工作区」标题行原来的搜索图标不再显示:搜历史会话(按标题、所属工作区名和对话内容:你和模型说过的每句话都算,句子中间的几个中文字也能搜到,不分大小写,行下显示命中的那一句并把命中的字加粗;宿主启动后第一次打开面板时要把全部会话读一遍,约十几秒)、项目、插件、当前会话可用的 Skill 与快捷键,顶部「全部 / 会话 / 项目 / 插件 / Skill / 快捷键」切换分类,搜索框为空时列出最近的五个会话与「新会话 / 插件 / 设置 / 键盘快捷键」几个操作。↑↓ 选择、Enter 打开、Tab 与 Shift+Tab 切换分类、Esc 关闭;选中会话即打开,选中项目在该工作区开新会话,选中插件打开它在插件页的详情,选中 Skill 把 `/名称 ` 放到当前会话输入框开头,选中快捷键打开宿主的快捷键列表并筛到这一条。
|
|
70
70
|
6. **账号区** —— 昵称与头像各走一条固定顺序。昵称:自定义昵称 → 官方账号昵称 → HDSL 启动器昵称 → 上次探测到的系统用户名 → 本次探测到的系统用户名 → `User`;头像:官方账号头像 → HDSL 启动器头像 → Claude 徽标。官方账号资料只在启动、热重载、登录与登出时向服务器读取,不轮询;HDSL 契约由宿主半边读一次后经两条只读路由转发(`GET /dsh-claude-style/hdsl` 回账号元数据、`GET /dsh-claude-style/hdsl-skin.png` 回头像图片,都不含头像文件的绝对路径,并过宿主的请求栅栏),契约版本读不到或不认识时整组忽略,头像文件被删除时回落到徽标。启动器给的是皮肤贴图而不是成品头像,因此账号位置的头像由贴图裁出:脸部 8×8 贴图块按盒子 1/18 内缩、帽子层铺满整盒,方形绘制不被圆角裁切。账号弹层沿用宿主自带的那一个:宿主的账号行就是入口,弹层里依次是账号头部、其它插件的页脚条目与宿主自己的「设置 / 意见反馈 / 退出登录」,各行的文案、顺序与点击行为保持宿主原样;宿主没有账号区的环境(Web)由插件自建同样外观的弹层。弹层卡片为象牙白背景(与主画布同色)、宽度与侧栏一致。
|
|
71
|
-
7. **对话视图** —— 对话 / 轨迹控件在桌面 Windows
|
|
72
|
-
8. **首页版面** —— 新会话页有两套版面,默认是工作台,在设置页随时切换:**经典**是居中大标题加输入卡片,标题按时段从一组问候里抽一句(早上、午间、下午、晚上、深夜各有几句,另有几句不分时段),每次回到新会话页换一句;**工作台**把标题移到左上并固定为一句「What's up next, 用户名?」,输入框改用对话内的单行样式贴住窗口底部、上方一行细边框上下文胶囊,标题与一块 480px 窄栏用量面板贴着输入框左缘排布:概览页签是六个数字格(会话数、消息数、Token 总量、活跃天数、高峰时段、最常用模型)与 26 周按天热力图(等分列方格),模型页签是每天一根按模型颜色堆叠的柱子加一份模型排行(色块、模型名、输入与输出、占比,超过六行折成「再显示 N 个」,展开后可「收起」),右上角的「全部 / 30 天 / 7
|
|
73
|
-
9. **像素螃蟹** —— 工作台版面的新会话页上,输入卡片上沿、靠右的位置站着 Claude Code 的像素螃蟹(经典版面没有)。点它一下、或指针从它身上移开时(页面开着时也会每隔二三十秒自己来一次),它会照 Claude Code
|
|
71
|
+
7. **对话视图** —— 对话 / 轨迹控件在桌面 Windows 标题栏模式下居中常显在标题栏那一行(与新会话按钮同一行);宽窗口且标题较短时它停在标题那一行,否则留在标题下方自己那一行。切换视图时,高亮块从原标签平滑滑到新标签。进行中、已停止与失败的轮次,状态行跟在这一轮工作的末尾(页脚与排队中的消息在它下面),画成 Claude Code 的样子:进行中是转动的火花标记加「用时 · 输出 tokens · 当前动作」(等待模型、思考中、思考了 N 秒、输出中、准备调用工具、运行工具中),停止或失败后火花静止、文字为「已停止 / 处理失败 · 用时 · 输出 tokens」;正常结束的一轮保留宿主原来的样子与位置。
|
|
72
|
+
8. **首页版面** —— 新会话页有两套版面,默认是工作台,在设置页随时切换:**经典**是居中大标题加输入卡片,标题按时段从一组问候里抽一句(早上、午间、下午、晚上、深夜各有几句,另有几句不分时段),每次回到新会话页换一句;**工作台**把标题移到左上并固定为一句「What's up next, 用户名?」,输入框改用对话内的单行样式贴住窗口底部、上方一行细边框上下文胶囊,标题与一块 480px 窄栏用量面板贴着输入框左缘排布:概览页签是六个数字格(会话数、消息数、Token 总量、活跃天数、高峰时段、最常用模型)与 26 周按天热力图(等分列方格),模型页签是每天一根按模型颜色堆叠的柱子加一份模型排行(色块、模型名、输入与输出、占比,超过六行折成「再显示 N 个」,展开后可「收起」),右上角的「全部 / 30 天 / 7 天」切换统计范围(高峰时段与趣味行一并跟随);所选范围内的用量超过一本书时,热力图下方出现倍数趣味行,书目共六十五本(《道德经》到《追忆似水年华》),每次回到新会话页随机换一本。数据由插件的宿主半边汇总:装了 `dsh-cost-meter` 时直接读它的账本缓存(只读,不写对方文件),没有时读本机会话日志自己算,并按日志的文件大小与修改时间增量缓存;两种来源都按模型给出四类 token、按天给出各模型的 token,同一模型经不同提供方的用量合成一行;账本不记小时,高峰时段始终由本机会话日志按天算出;汇总不可用时面板改用宿主会话列表每行自带的投影值。数字没到之前面板先画骨架,位置在填充前后不变。还没选工作区时(刚打开、尚无会话),用量面板与 Read / Edit / Auto / Yolo 权限分段照常显示,分段停在新会话的默认权限上,点它和点输入框一样打开工作区选择;表示「先选工作区」的虚线框只框住单行输入框本身。窗口高度放不下整块面板时,面板在问候语与输入框之间自己滚动、底边渐隐,页面不会被撑出窗口。工作区选择菜单照 Claude Code 的文件夹菜单绘制:窄卡片、纯文字行排得更紧,每行前面的文件夹图标与「添加工作区…」前的加号都不画,当前工作区仍以强调色勾号标出。
|
|
73
|
+
9. **像素螃蟹** —— 工作台版面的新会话页上,输入卡片上沿、靠右的位置站着 Claude Code 的像素螃蟹(经典版面没有)。点它一下、或指针从它身上移开时(页面开着时也会每隔二三十秒自己来一次),它会照 Claude Code 的样子钓一次鱼:眨眨眼,举起鱼竿甩过卡片边沿,跳一下侧过身钓一会儿,再收竿转回正面,逐帧照搬 Claude Code 的动画,前后约三秒半。系统要求减少动态效果时(例如 Windows 关掉了「在 Windows 中显示动画」),它只在被点击时才动。
|
|
74
74
|
|
|
75
75
|
## 停用与卸载
|
|
76
76
|
|
package/docs/STYLE.md
CHANGED
|
@@ -200,6 +200,52 @@ properties on the card (`--dsh-claude-hero-menu-x` / `--dsh-claude-hero-menu-y`,
|
|
|
200
200
|
written by the same pass that stamps it) which `features/hero-menu/hero-menu.css` reads
|
|
201
201
|
with `!important`; that declaration outranks the host's plain inline value.
|
|
202
202
|
|
|
203
|
+
## Search · 搜索
|
|
204
|
+
|
|
205
|
+
**Sidebar box.** The box takes the brand's place in the logo row while the
|
|
206
|
+
pointer is over the sidebar: 32px tall, 8px radius, a 1px `#E8E6DC` hairline on
|
|
207
|
+
the ivory canvas fill (dark: `#2E2C29` on `#1E1E1D`), a 16px search glyph in the
|
|
208
|
+
secondary label, the "Search" label in the tertiary label at 14px, and the
|
|
209
|
+
host's search shortcut as 20px keycaps at the right end. It is a button like
|
|
210
|
+
the New session and Plugins rows below it: the pointer cursor, and on hover the
|
|
211
|
+
same plate those rows take (`--dsh-claude-hover-bg`, dark `rgba(255, 255, 255,
|
|
212
|
+
0.08)`) laid over its opaque fill. Box and brand share one grid cell and
|
|
213
|
+
cross-fade over 0.16s, the timing the workspace heading and its segmented
|
|
214
|
+
control trade places with; the box is excluded from the window drag region. In
|
|
215
|
+
the desktop titlebar mode the brand row rises 10px, since the sidebar column
|
|
216
|
+
starts under the 40px titlebar — as far as it goes with the box still wholly
|
|
217
|
+
below that edge, which clips the column.
|
|
218
|
+
|
|
219
|
+
**Palette.** A modal card set 8vh from the top, 760px wide at most, 16px radius,
|
|
220
|
+
on the same ivory fill (dark `#1E1E1D`) under the host's mask. From the top: a
|
|
221
|
+
17px borderless input with a 28px close button; the category chips (32px tall,
|
|
222
|
+
8px radius, 14px, tertiary at rest and primary when current) sharing one
|
|
223
|
+
sliding highlight at `rgba(20, 20, 19, 0.06)`; the list, capped at
|
|
224
|
+
`min(520px, 62vh)`, with 13px tertiary section captions and 40px rows (8px
|
|
225
|
+
radius, 15px title, an 18px glyph in the secondary label, a 13px tertiary detail
|
|
226
|
+
after the title and an optional 13px tertiary excerpt line under it, the match
|
|
227
|
+
inside it in the primary label at weight 500 with no fill); a hairline-topped footer
|
|
228
|
+
of hints with 20px keycaps. The input text, the chips' text, the captions and
|
|
229
|
+
the row glyphs all start on one vertical line 24px in from the card edge. The
|
|
230
|
+
highlighted row takes `rgba(20, 20, 19, 0.05)` (dark `rgba(250, 249, 245, 0.07)`)
|
|
231
|
+
and shows the Enter glyph; the others hide it. The card fades in and settles
|
|
232
|
+
from 6px above at 98.5% scale over 0.18s; on close card and mask fade out
|
|
233
|
+
together over 0.14s before the modal unmounts.
|
|
234
|
+
|
|
235
|
+
## Turn status · 轮次状态行
|
|
236
|
+
|
|
237
|
+
A running, stopped or failed turn's status line sits after the turn's work (its
|
|
238
|
+
footer and queued messages below it), 4px under the last row, with no rule under
|
|
239
|
+
it: a 14px Claude spark in the brand clay (`--dsw-alias-brand-primary`,
|
|
240
|
+
`#D97757`), 8px gap, then the text in the tertiary label at the host's secondary
|
|
241
|
+
content size (13px, 24px line), one line with an ellipsis. Parts are joined by
|
|
242
|
+
` · `: `elapsed · N tokens · action` while the turn runs, `Stopped` or `Failed`
|
|
243
|
+
(the host's words) `· duration · N tokens` after; the token part is left out
|
|
244
|
+
until a finished step has reported usage. While the turn runs the spark turns a
|
|
245
|
+
full circle every 2.4s, breathing down to 78% at the half turn, in every motion
|
|
246
|
+
setting, like the sidebar's background-work ring; a stopped or failed turn's
|
|
247
|
+
spark stands still. A turn that finishes normally keeps the host's own control.
|
|
248
|
+
|
|
203
249
|
## Home layouts · 首页版面
|
|
204
250
|
|
|
205
251
|
The new-conversation page has two arrangements. `homeLayout` (settings: Home
|
|
@@ -256,9 +302,10 @@ bold figure — the figure stays barely above its own label, which is also what
|
|
|
256
302
|
lets a long model id such as deepseek-v4.1-flash sit on one line; the favourite
|
|
257
303
|
model is a name, not a figure, and keeps the regular weight. Messages are the
|
|
258
304
|
settled calls. Once the picked range's total passes one
|
|
259
|
-
book, the yardstick line appears under the grid:
|
|
305
|
+
book, the yardstick line appears under the grid: sixty-five books from Tao Te
|
|
260
306
|
Ching (7.6k tokens) to In Search of Lost Time (1.6M), the public-domain ones
|
|
261
|
-
measured by feeding their full text to the o200k_base tokenizer
|
|
307
|
+
measured by feeding their full text to the o200k_base tokenizer (the English
|
|
308
|
+
original or the standard English translation; Faust in German) and the rest
|
|
262
309
|
estimated (1.3 tokens a word for English, 0.96 tokens a character for modern
|
|
263
310
|
Chinese prose). The line states the rounded multiple as a share of the book
|
|
264
311
|
("~2× the tokens in Moby-Dick"), never as an excess over it.
|
|
@@ -315,18 +362,20 @@ placeholder, a zero day draws the grid's own base tone.
|
|
|
315
362
|
## The composer crab · 输入卡片上的螃蟹
|
|
316
363
|
|
|
317
364
|
On the studio layout's new-conversation page, Claude Code's pixel crab stands on
|
|
318
|
-
the composer card's top edge
|
|
319
|
-
|
|
365
|
+
the composer card's top edge. Its frames are Claude Code's own animation, cut to
|
|
366
|
+
34×23 cells and drawn at 2px a cell, crisp (68×46px); the resting crab is 48×32px
|
|
367
|
+
with its feet on the card's edge and the right arm 8px inside the single-line
|
|
320
368
|
card's right edge, so the feet stand where its 18px corner starts to round. The
|
|
321
|
-
classic layout keeps its centred hero without the crab. The shell is the clay
|
|
322
|
-
themes, the side-on back is `#b9603f`, the eyes
|
|
323
|
-
tertiary label ink. When the crab is clicked,
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
down
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
369
|
+
classic layout keeps its centred hero without the crab. The shell is the clay
|
|
370
|
+
accent `#d97757` in both themes, the side-on back is `#b9603f`, the eyes
|
|
371
|
+
`#141413`, and the rod takes the tertiary label ink. When the crab is clicked,
|
|
372
|
+
when the pointer leaves it, and on its own every 25–45 seconds while the page is
|
|
373
|
+
in view, it plays Claude Code's fishing routine, 43 frames of 80ms (3.44s): a
|
|
374
|
+
blink, the rod raised overhead and cast down past the card's edge, a hop into a
|
|
375
|
+
side-on stance, a spell of fishing, and the rod reeled in as it turns back to
|
|
376
|
+
face front. With reduced motion requested, only a click plays it: the pointer
|
|
377
|
+
passing by and the idle timer leave it still. Only the crab takes the pointer;
|
|
378
|
+
the room the rod swings through does not.
|
|
330
379
|
|
|
331
380
|
## Implementation notes
|
|
332
381
|
|
package/docs/architecture.md
CHANGED
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
- 构建与源码:D1 拼接构建 · D5 模型文案是数据 · D18 源码布局与共用部件层 · D4 输入框样式门控
|
|
14
14
|
- 宿主边界:D2 只做浏览器半边 · D3 宿主选择器纪律 · D19 宿主契约与构建编号 · D10 设置传输 · D11 私有路由与字符串安全
|
|
15
15
|
- 运行时:D6 单一调度器 · D13 功能契约 · D12 快速失败与功能隔离 · D9 样式性能
|
|
16
|
-
- 功能:D14 账号表面 · D15 HDSL 契约 · D16 弹层基准 · D17 权限档位 · D20 滑动高亮 · D21 已归档列表
|
|
16
|
+
- 功能:D14 账号表面 · D15 HDSL 契约 · D16 弹层基准 · D17 权限档位 · D20 滑动高亮 · D21 已归档列表 · D22 搜索面板 · D23 轮次状态行
|
|
17
17
|
- 分发:D7 行为层与皮肤同包 · D8 多主题
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
21
|
## D1. 零构建工具链:构建期逐字拼接成单文件
|
|
22
22
|
|
|
23
|
-
- **决定**:`scripts/build.mjs` 按 `FRAGMENTS` / `STYLE_FILES` 的顺序把 `src/` 下的 JS 片段与样式表拼成 `lib/client.js`。所有片段共用一个工厂作用域:不写 import/export,顶层名字在整个产物里唯一;React 由构建头部经加载器 `require('react')` 取得,其他宿主包在用到它的功能里直接 `require`。片段用现代语法(`const` / `let`、箭头函数、可选链),基础缩进 4 空格。品牌 SVG 与厂商锁定标在构建时内联,字体与模型文案走宿主半边的路由。
|
|
23
|
+
- **决定**:`scripts/build.mjs` 按 `FRAGMENTS` / `STYLE_FILES` 的顺序把 `src/` 下的 JS 片段与样式表拼成 `lib/client.js`。所有片段共用一个工厂作用域:不写 import/export,顶层名字在整个产物里唯一;React 由构建头部经加载器 `require('react')` 取得,其他宿主包在用到它的功能里直接 `require`。片段用现代语法(`const` / `let`、箭头函数、可选链),基础缩进 4 空格。品牌 SVG、螃蟹的帧条 PNG 与厂商锁定标在构建时内联,字体与模型文案走宿主半边的路由。
|
|
24
24
|
- **构建检查**:`src/` 下每个 `.js` / `.css` 都在两份清单里;`%%TOKEN%%` 全部替换;输入框样式门控(D4);`:has()` 位置(D9);产物能被 `vm.Script` 解析;模型文案文档校验(D5)。产物写入构建编号(D19)。
|
|
25
25
|
- **理由**:DSH 的插件加载器没有相对 require,也没有资产 URL,打包器产出的分块与资产引用无处安放。
|
|
26
26
|
- **代价**:顶层名字共用一个作用域,靠命名约定避免冲突(D18);没有按需裁剪,产物体积靠自律控制。
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
|
|
66
66
|
## D6. 单一调度器
|
|
67
67
|
|
|
68
|
-
- **决定**:`src/core/scheduler.js` 持有唯一的 MutationObserver(`<body>` 子树,属性只看 `aria-label` / `aria-selected`),用 requestAnimationFrame 把变化合并成每帧一次刷新,按功能顺序调用各功能的 `sync()`。全局的指针、键盘、焦点、滚动与尺寸监听也只注册在这里,经功能句柄上的钩子分发(D13
|
|
68
|
+
- **决定**:`src/core/scheduler.js` 持有唯一的 MutationObserver(`<body>` 子树,属性只看 `aria-label` / `aria-selected`),用 requestAnimationFrame 把变化合并成每帧一次刷新,按功能顺序调用各功能的 `sync()`。全局的指针、键盘、焦点、滚动与尺寸监听也只注册在这里,经功能句柄上的钩子分发(D13)。键盘路径只认用户的按键:皮肤自己代发的键盘事件带标记(账号表面对宿主菜单派发的 Escape 带 `__dshHostMenuEscape`),调度器见到标记即跳过——那次代发只送达宿主的菜单,不得顺带触发全局 Esc 路由。刷新状态在调度器开头声明:订阅偏好时,已就绪的设置表单会同步通知并触发 `schedule()`。
|
|
69
69
|
- **理由**:各功能自挂观察者与监听会互相干扰、重复扫描、难以卸载干净。
|
|
70
70
|
- **代价**:每个 `sync()` 必须能快速提前返回,并且只在值变化时写 DOM(同值写入也会让元素样式失效);页面持续变化时每帧都有一次刷新,其开销见 D9。
|
|
71
71
|
|
|
@@ -98,8 +98,8 @@
|
|
|
98
98
|
## D11. 私有路由借宿主的请求栅栏;外来字符串只以文本上屏
|
|
99
99
|
|
|
100
100
|
- **决定**:
|
|
101
|
-
- 宿主半边的私有路由(`/username`、`/usage`、`/hdsl`、`/hdsl-skin.png`、`POST /session-delete`)处理前先调 `connection.requestRejection(req)`,拒绝即回 401/403;宿主没有该服务时,插件自带的替身只放行回环请求(回环 Host、无跨站标记、Origin 与 Host 一致)。模型文案与字体是公开的静态资产。
|
|
102
|
-
- 删除会话另加四道:只接受 POST;id 必须符合宿主的会话 id 格式;正在打开的会话拒绝;目录解析后必须留在会话根目录内。删除发生在宿主半边,浏览器只提交一个 id
|
|
101
|
+
- 宿主半边的私有路由(`/username`、`/usage`、`/session-search`、`/hdsl`、`/hdsl-skin.png`、`POST /session-delete`)处理前先调 `connection.requestRejection(req)`,拒绝即回 401/403;宿主没有该服务时,插件自带的替身只放行回环请求(回环 Host、无跨站标记、Origin 与 Host 一致)。模型文案与字体是公开的静态资产。
|
|
102
|
+
- 删除会话另加四道:只接受 POST;id 必须符合宿主的会话 id 格式;正在打开的会话拒绝;目录解析后必须留在会话根目录内。删除发生在宿主半边,浏览器只提交一个 id。删除成功时宿主半边同时经 `workspaceRegistry.unarchiveSession` 把该 id 从归档集合里移除;归档集合里存储目录已消失的条目(幽灵)同样按删除成功应答并移出归档集合,否则归档列表会一直留着这行。会话根目录读不了是故障,按 500 应答。
|
|
103
103
|
- 用量汇总只读:第三方插件的账本只读不写,自己的缓存放在 `$DSH_HOME/cache/dsh-claude-style/`。
|
|
104
104
|
- 客户端:来自设置、账号服务、系统或其他插件的字符串只用 `textContent` 或元素属性写入;图标复制节点,不重新解析标记。
|
|
105
105
|
- **理由**:插件路由不在宿主自己的栅栏之内;`dsh web` 绑定 `0.0.0.0` 时局域网可直接访问,而页面能驱动执行 shell 命令的智能体。
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
- 功能隔离:teardown 最先经 `ctx.effect` 注册且幂等;每个功能单独安装,装不上的报告一次并退役;每轮刷新里每个 `sync()` 单独执行,连续失败 3 轮即报告并退役(`ui.retire`)。调度器自身装不上时整体回滚到宿主原样。
|
|
114
114
|
- 退役就是运行该功能自己的 teardown,并交还它接管的宿主界面:页脚接管(`FOOTER_ATTR`)与输入框重绘(`COMPOSER_ATTR`)的门控强制关闭;权限控件替换的宿主控件由它自己的标记把守,随它的 teardown 交还。每个功能的 teardown 是它 DOM 与标记的唯一清理。
|
|
115
115
|
- 需要隔离、但错误必须可见的地方用 `reportError`:`notifyAll` 逐个通知监听函数,某个出错就报告、其余照常;卸载时单个功能失败同样报告后继续。
|
|
116
|
-
- 允许的 catch 只有以下几类,每处写明原因:上述隔离;宿主以抛错表达的预期状态(设置表单的 `set()` 拒绝不属于该配置的字段;`modelDirectories.directoryFor`
|
|
116
|
+
- 允许的 catch 只有以下几类,每处写明原因:上述隔离;宿主以抛错表达的预期状态(设置表单的 `set()` 拒绝不属于该配置的字段;`modelDirectories.directoryFor` 对还没建立的会话抛错,下一轮重试;`sessionQuery.readSession` 以 `SESSION_QUERY_CORRUPT_SESSION` / `SESSION_QUERY_SESSION_NOT_FOUND` 表示某个存档读不了或在列出后被删,内容搜索只把这一个会话记为空并记一条警告);Promise 上表示「宿主半边没有应答」或「宿主自己会提示」的失败回调。
|
|
117
117
|
- **理由**:被吞掉的错误会在别处以更难排查的方式出现;隔离只负责不让一个功能拖垮其他功能,失败本身必须留下记录。
|
|
118
118
|
- **代价**:功能失效时界面上没有提示,只有控制台记录;依赖同一宿主服务的功能会一起退役(smoke 的「会话列表故障」用例覆盖)。
|
|
119
119
|
- **重审条件**:宿主提供插件级的错误上报时,把报告接过去。
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
|
|
123
123
|
- **决定**:
|
|
124
124
|
- `src/entry.js` 的 FEATURES 表(`{ name, handle?, install }`)同时决定安装顺序与刷新顺序:刷新顺序就是安装顺序中句柄带 `sync` 的那些。
|
|
125
|
-
- 调度器只认功能句柄上的可选钩子(类型定义在 scheduler.js 开头):`sync` / `owns` + `close('outside')` / `onPointerDown` / `close('escape')` / `close('composer')` / `onInput` / `reposition('viewport' | 'composer')` / `onCopyChange` / `onKey`;没实现的钩子直接跳过,每个功能保留自己的关闭方式。
|
|
125
|
+
- 调度器只认功能句柄上的可选钩子(类型定义在 scheduler.js 开头):`sync` / `owns` + `close('outside')` / `onPointerDown` / `close('escape')` / `close('composer')` / `onInput` / `onFocusIn` / `reposition('viewport' | 'composer')` / `onCopyChange` / `onKey`;没实现的钩子直接跳过,每个功能保留自己的关闭方式。
|
|
126
126
|
- `retire` 按 name 或 handle 匹配:只匹配到 handle 时只停止 sync、不拆安装(设置页的 `settingsNav`)。
|
|
127
127
|
- 功能拆出的部分写成顶层工厂 `createX(...)`:状态留在工厂自己的闭包里,访问器与回调经参数传入,不伸手进别的闭包;名字以功能开头(`createAccountProfile`)。
|
|
128
128
|
- **理由**:加一个功能只需在 FEATURES 表加一行、在句柄上实现钩子;调度器不认识任何具体功能,没有要手工同步的清单。
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
|
|
132
132
|
## D14. 账号表面:一套行模型,两个挂载点
|
|
133
133
|
|
|
134
|
-
- **决定**:插件的行(账号头部、其他插件的页脚条目、设置行)只出现在账号弹层里,由 `features/account/surface.js` 每轮判定挂载点。宿主有账号区时(桌面端),宿主的账号行就是入口(插件只打标记供样式重绘),插件把容器插到宿主账号菜单列表的首位,宿主自己渲染它的那几行;宿主没有账号区时(Web
|
|
134
|
+
- **决定**:插件的行(账号头部、其他插件的页脚条目、设置行)只出现在账号弹层里,由 `features/account/surface.js` 每轮判定挂载点。宿主有账号区时(桌面端),宿主的账号行就是入口(插件只打标记供样式重绘),插件把容器插到宿主账号菜单列表的首位,宿主自己渲染它的那几行;宿主没有账号区时(Web),插件自建账号行与弹层。宿主的行不复制、不移动、不转发点击;插件的行被点击后需要收起菜单时,派发一次带 `__dshHostMenuEscape` 标记的 Escape 交给宿主菜单处理(D6:调度器不认代发事件)。账号头部打开封号彩蛋时,覆盖全窗的浮层让指针「离开」行与菜单——悬停收起在浮层打开期间停摆,屏幕退出后页脚保持进入时的样子。
|
|
135
135
|
- **理由**:宿主菜单自带定位、动画、键盘遍历与关闭;插进去的行加入同一套键盘遍历,账号区不依赖宿主菜单的渲染时序与标签文字。
|
|
136
136
|
- **代价**:宿主菜单关闭即卸载,容器每轮重新确认并插入。
|
|
137
137
|
- **重审条件**:宿主为账号菜单提供追加行的插槽时,改用插槽注册。
|
|
@@ -182,9 +182,10 @@
|
|
|
182
182
|
- **决定**:
|
|
183
183
|
- 插槽定位点:宿主渲染器保证每个插槽外面有 `[data-slot="<key>"]` 包装层,供外部样式定位(ui-renderer 的 scoped-slots)。例如访问模式按钮按 `[data-slot="conversation.input.permission"]` 查找,跳过插件自己插进去的按钮。
|
|
184
184
|
- 控件标记:输入框的刷新按宿主 InputBar 的结构给按钮打 `data-dsh-claude-control`——`commands`(工具行里唯一打开 listbox 的按钮)、`stop` / `send`(提交区的主按钮,按图标区分:停止画 rect,提交箭头画 path;排队与插话是同一个提交按钮换了文字)、`access`(权限插槽里的按钮)。样式写成 `button[data-dsh-claude-control="…"]`,优先级与按文字匹配时相同。
|
|
185
|
+
- 弹层角色随开合写撤:宿主把文档里每个 `[role="menu"]`(ui-primitives 的 `modalSelector`,与 `[role="dialog"][aria-modal="true"]` 同列)当作占据前景的菜单——快捷键派发、`closeTopModal`、Esc-Esc 停止序列与 dock 标签菜单都按它仲裁。皮肤为量宽高而常驻 `<body>` 的弹层卡片关闭时只是视觉上藏起来,仍会命中这份查询,让宿主把整套快捷键判给一个看不见的菜单;因此 `role="menu"` 经 `setMenuPopoverOpen`(src/shared/popover.js)与 `data-open` 同写同撤——卡片开着才持有角色,折起即交还。宿主自己的菜单关闭即从文档摘除,皮肤以撤销属性达到同一效果;`quickProviders` 的卡片本就开时挂载、关时摘除,无需经过它。
|
|
185
186
|
- 构建编号:构建取产物内容的哈希写成 `BUILD_ID`,运行时写到 `<body data-dsh-claude-style>` 的值上;页面运行的是哪一版以它为准。热重载会替换插件代码而不重新加载页面,页面的加载时间说明不了代码版本。
|
|
186
187
|
- **理由**:界面文字与哈希类名随语言、版本变化,结构与契约更稳定。
|
|
187
|
-
- **代价**:结构判断依赖宿主 InputBar 的现状,宿主改版时需要核对;smoke
|
|
188
|
+
- **代价**:结构判断依赖宿主 InputBar 的现状,宿主改版时需要核对;smoke 的替身宿主按宿主真实结构搭建,结构变化会先在那里暴露。宿主按 `modalSelector` 仲裁快捷键这一侧不被替身复现,冒烟改查皮肤自己的不变量:关闭的弹层卡片不持有 `role="menu"`。
|
|
188
189
|
- **重审条件**:宿主给这些控件提供自己的 data-* 标记时,直接读宿主的标记。
|
|
189
190
|
|
|
190
191
|
## D20. 分段控件共用滑动高亮
|
|
@@ -194,6 +195,32 @@
|
|
|
194
195
|
|
|
195
196
|
## D21. 已归档列表跟随宿主的两份客户端数据
|
|
196
197
|
|
|
197
|
-
- **决定**:侧栏「已归档」是插件自己的平铺列表,订阅宿主 `workspaces.list`(归档集合)与 `sessions.list`(标题、时间、来源),两者都就绪后按宿主「仅已归档」的规则重算:只收已有会话摘要、非子代理、非空白占位的归档会话,按更新时间排序,内容不变时不重建。宿主自带的筛选状态存在 ui-workspace 私有的视图 store 里,不是客户端服务,只能靠点它的菜单改,因此不用。取消归档走 `workspaces.unarchiveSession`;删除走宿主半边的私有路由(D11
|
|
198
|
+
- **决定**:侧栏「已归档」是插件自己的平铺列表,订阅宿主 `workspaces.list`(归档集合)与 `sessions.list`(标题、时间、来源),两者都就绪后按宿主「仅已归档」的规则重算:只收已有会话摘要、非子代理、非空白占位的归档会话,按更新时间排序,内容不变时不重建。宿主自带的筛选状态存在 ui-workspace 私有的视图 store 里,不是客户端服务,只能靠点它的菜单改,因此不用。取消归档走 `workspaces.unarchiveSession`;删除走宿主半边的私有路由(D11),宿主半边在删除目录的同时把 id 移出归档集合,浏览器删除成功后重拉一次会话列表基线,让已删除会话的旧摘要立刻离开投影、不再顶着会话树里的行;其余失败记录警告。点击已归档行显示宿主的同一条提示:用宿主的 Toast 组件与 `workspace` 文案表里的 `toast.archivedNotOpenable`(宿主的提示通道是私有的)。
|
|
198
199
|
- **代价**:宿主「仅已归档」的规则变化时需要同步。
|
|
199
200
|
- **重审条件**:宿主把视图筛选或提示通道开放为客户端服务时改用宿主的。
|
|
201
|
+
|
|
202
|
+
## D22. 搜索面板:宿主的 Modal、宿主的数据与导航
|
|
203
|
+
|
|
204
|
+
- **决定**:
|
|
205
|
+
- 侧栏搜索框插在宿主品牌行(`[data-slot="sidebar"]` 里的 `_logoRow`)的品牌旁边,只在这一行带宽版品牌时放置,侧栏收起成窄条时不放;这一行改成网格,品牌与搜索框同占第一格,两者的淡入淡出交给样式表的 `[data-slot="sidebar"]:hover`,不另外监听指针。
|
|
206
|
+
- 宿主的搜索快捷键(`session.search`)没有可替换的公开入口,它的效果是展开宿主自己的侧栏搜索并聚焦其输入框。宿主的这块搜索因此保持挂载、只从视觉与布局里拿掉;调度器的 `onFocusIn` 钩子见到焦点落进它的输入框时,搜索面板打开,并经宿主自己的清除按钮把它收回。这样快捷键改绑后也跟着走。
|
|
207
|
+
- 面板是宿主 ui-primitives 的 `Modal`(headless):遮罩、焦点归还、Esc 与模态层都用宿主的,面板开着时宿主的快捷键把它当作前景对话框;卡片里的行由皮肤自己搭。它是模态对话框,不套 D16 的弹层外壳,但登记进弹层互斥表。宿主的 `Modal` 关闭即卸载,没有退场动画,所以关闭时面板先给遮罩层打上标记、保持挂载淡出,淡出结束后再卸载;标记在卸载之后才撤,否则卡片与遮罩会重播一帧入场动画。
|
|
208
|
+
- 数据读宿主的客户端服务:`sessions.list` 与 `workspaces.list`(去掉已归档、子代理与空白占位)、`remote.pluginInventory` 与 `remote.pluginManager`、当前会话的 `remote.skills`、`shortcuts.catalog`。远程命名空间在面板打开时用 `ctx.get('remote.<名字>')` 读:在根上下文里直接读 `remote` 的子属性,cordis 会以「没有 inject」拒绝。
|
|
209
|
+
- 选中后走宿主自己的导航:`uiWorkspace.openSession` / `startSession`、`pluginNavigation.openBundle`、`layout.selectPanel`,Skill 经会话输入的 `setDraft` 与 `focus`;设置页与快捷键列表经它们在插槽登记里声明的 store(`slots.entries(key)` 条目上的 `store.create()`)打开。宿主的快捷键命令没有公开的执行入口,所以快捷键行打开快捷键列表,不代为执行命令。
|
|
210
|
+
- 匹配沿用宿主侧栏搜索的子串规则,排序为标题开头、标题中的词开头、标题其他位置、第二个键(路径、包名、描述、别名)。宿主 `/` 菜单的子序列排序在标题、路径和描述上会命中大量无关结果,因此不用。
|
|
211
|
+
- 消息内容由宿主半边的 `/session-search` 路由搜(host/search.js)。宿主自己的内容索引(`sessions.search`,SQLite FTS5)在出厂的 Web 组合里是关的(`openAt: never`);就算打开,它的 `unicode61` 分词只按整词匹配,一串不带空格的中文是一个词,句子中间的几个字永远搜不到。路由改经宿主的 `sessionQuery.readSession` 读原始日志,只取用户与助手消息里的文本块(助手消息里的工具调用参数不算),匹配规则照搬 `sessionQuery` 自己 `text` 过滤的写法:字面、不分大小写、空白可伸缩。每个会话的消息文本连同变化标记留在内存里:存档用持久化后端 `list()` 给的修订号(JSONL 是文件 stat),宿主开着的会话用日志长度 `seq`;搜索时只重读标记变了的会话,子代理会话不读。面板打开时先发一次不带查询的请求,让宿主半边提前读完;每个会话只回最新的一处命中,附一行摘录与命中位置,面板把命中的字加粗。
|
|
212
|
+
- **理由**:面板要做的每件事宿主都已有一条路:模态层、列表数据、打开会话与页面。借用这些路,面板的行为与宿主各处一致,也不重复实现。内容搜索是唯一的例外:宿主的索引对中文不可用,读日志与匹配规则仍用宿主的,只把逐字匹配放在插件这边。
|
|
213
|
+
- **代价**:依赖上面这些服务、store 与品牌行结构的现状,宿主改名或收回时需要跟进;smoke 只覆盖放置、打开与卸载,数据与导航在真实页面核对。内容搜索在进程启动后的第一次要把全部会话读一遍(本机 128 个会话约 16 秒),之后每次查询约 0.1 秒;消息文本常驻宿主进程内存,大小约等于全部对话的文字量。
|
|
214
|
+
- **重审条件**:宿主提供自己的全局搜索或命令面板时,改为打开宿主的;宿主的内容索引能按子串匹配中文、并在出厂组合里打开时,内容搜索改回 `sessions.search`。
|
|
215
|
+
|
|
216
|
+
## D23. 轮次状态行:宿主的控件、宿主的数据
|
|
217
|
+
|
|
218
|
+
- **决定**:
|
|
219
|
+
- 宿主把每一轮的「过程」控件(ui-chat 的 turn-process:进行中显示「深度求索中,用时 N 秒」,停止与失败后显示「已停止」「处理失败」)排在这一轮的最前面。进行中、已停止与失败的轮次里,这个控件改做状态行、排到这一轮的工作之后;正常结束的一轮保留宿主的控件与位置,它负责折叠这一轮的工作。
|
|
220
|
+
- 皮肤不另建元素,也不移动节点:对话列本就是纵向 flex,刷新按文档顺序给列里的行写内联的 `--dsh-claude-turn-order`,样式表只对带这个属性的行取 `order: var(--dsh-claude-turn-order)`。第一个要移动的控件之前的行不写、保持 0;每个要移动的控件排在它这一轮最后一行(页脚 `turn-tail` 除外)之后,从那里起的行(页脚、后面的轮次、排队中的消息)逐级加一层,所以历史里多处停止或失败的轮次各自排对。标记与顺序每轮刷新重算,不再需要的当场撤掉。
|
|
221
|
+
- 控件改画成 Claude Code 的状态行:火花标记加一行文字。进行中为「用时 · 输出 tokens · 当前动作」,火花转动;已停止或失败为「宿主的已停止 / 处理失败 · 用时 · 输出 tokens」,火花静止。状态写在按钮的 `data-dsh-claude-turn-state`(`live` / `stopped` / `failed`),文字写在 `data-dsh-claude-turn-status`,样式表用 `attr()` 画出来;宿主自己的标签(和它每秒跳动的时钟)留在 DOM 里、只是不显示,React 的节点不被改写。
|
|
222
|
+
- 数字只读宿主的聊天快照:`uiConversation.binding(会话 id).target('chat')`,会话 id 取对话区外层的 `data-conversation-session`,轮次的状态与结束原因取快照里这一轮的 `status` 与 `turn/end` 的 `reason.kind`(`aborted` / `error`)。用时取这一轮 `turn/start` 到现在(进行中)或到 `turn/end`(已结束);输出 tokens 是这一轮已经结束的各步上报的 `usage.outputTokens` 之和,正在输出的那一步结束后才计入,不做估算;当前动作按最新一步的助手输出判断(它在输出过程中原地增长):最后一块是推理为「思考中」,推理之后开始输出正文或工具调用时为「思考了 N 秒」,没有推理时为「输出中」或「准备调用工具」;这一步没在输出时,这一轮有进行中的工具调用为「运行工具中」,否则为「等待模型」。快照不带推理的时间戳,「思考了」的时长是本页面看到推理出现到结束的时间。时长的写法(进行中秒数不补零,结束后补零)与「已停止」「处理失败」两个词用宿主 `chat` 文案表里的原文,与宿主自己的显示一致。
|
|
223
|
+
- 状态行没有自己的计时器:宿主的时钟每秒改写一次标签文字,这次改动触发的刷新顺带更新状态行。
|
|
224
|
+
- **理由**:状态行在工作过程的末尾,正在发生的事、停在哪里、为何失败都与它的状态挨在一起,和 Claude Code 一样;借用宿主的控件与数据,完成、停止、失败与折叠都还是宿主的行为。
|
|
225
|
+
- **代价**:依赖对话列是纵向 flex、行上的 `data-chat-flow-kind` / `data-chat-turn` 与聊天快照的现状;读屏与 Tab 键仍按文档顺序先经过这个控件;历史里有停止或失败的轮次时,它之后的每一行都带一个内联顺序值。
|
|
226
|
+
- **重审条件**:宿主提供可替换的轮次状态插槽,或把这个控件移到这一轮末尾时,改用宿主的。
|
package/host/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import { homedir, userInfo } from 'node:os'
|
|
|
33
33
|
import { dirname, join, resolve, sep } from 'node:path'
|
|
34
34
|
import { fileURLToPath } from 'node:url'
|
|
35
35
|
import { createUsage } from './usage.js'
|
|
36
|
+
import { QUERY_MAX, createSessionSearch } from './search.js'
|
|
36
37
|
|
|
37
38
|
export const name = 'dsh-claude-style'
|
|
38
39
|
|
|
@@ -90,7 +91,10 @@ const HDSL_LAYERS = ['process', 'user-env']
|
|
|
90
91
|
* controller archives and unarchives, and the agent protocol's session delete is
|
|
91
92
|
* the host delegating to an ACP agent that owns the storage. The archived row's
|
|
92
93
|
* delete button therefore comes here, where the stored session directory under
|
|
93
|
-
* the harness home
|
|
94
|
+
* the harness home is removed and the id is dropped from the workspace
|
|
95
|
+
* registry's archive set — the stored-directory miss included, so an archive
|
|
96
|
+
* entry whose storage is already gone leaves the set instead of pinning its
|
|
97
|
+
* row to the list. The path is also spelled in
|
|
94
98
|
* src/constants.js (SESSION_DELETE_ROUTE) for the browser half; keep the two in
|
|
95
99
|
* step.
|
|
96
100
|
*/
|
|
@@ -107,6 +111,12 @@ const SESSION_DELETE_PATH = `${ROUTE_PREFIX}/session-delete`
|
|
|
107
111
|
const USAGE_PATH = `${ROUTE_PREFIX}/usage`
|
|
108
112
|
/** How long a computed roll-up is served before a background refresh is kicked. */
|
|
109
113
|
const USAGE_TTL_MS = 5 * 60 * 1000
|
|
114
|
+
/**
|
|
115
|
+
* Message-content search for the search palette (host/search.js). `?q=` is
|
|
116
|
+
* the query; without one the route only brings its message cache up to date,
|
|
117
|
+
* which the palette asks for as it opens so the first real query is quick.
|
|
118
|
+
*/
|
|
119
|
+
const SESSION_SEARCH_PATH = `${ROUTE_PREFIX}/session-search`
|
|
110
120
|
/** Session ids are the harness's own shape; anything else is refused before it reaches a path. */
|
|
111
121
|
const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/
|
|
112
122
|
/** Largest deletion request body read; a real one carries one id. */
|
|
@@ -444,13 +454,37 @@ function readRequestBody(req, limit) {
|
|
|
444
454
|
})
|
|
445
455
|
}
|
|
446
456
|
|
|
457
|
+
/**
|
|
458
|
+
* Drop one id from the workspace registry's archive set, durably.
|
|
459
|
+
*
|
|
460
|
+
* The registry is read at request time: it mounts with the workspace domain and
|
|
461
|
+
* can complete after this plugin. A host without one has no archive set to
|
|
462
|
+
* update — and no archived list to delete from — while a failing unarchive
|
|
463
|
+
* propagates: the route answers 500, the row stays, and the next delete
|
|
464
|
+
* retries through the miss branch.
|
|
465
|
+
*/
|
|
466
|
+
async function unarchiveSession(ctx, sessionId) {
|
|
467
|
+
const registry = ctx.get?.('workspaceRegistry')
|
|
468
|
+
if (typeof registry?.unarchiveSession !== 'function') return
|
|
469
|
+
await registry.unarchiveSession(sessionId)
|
|
470
|
+
}
|
|
471
|
+
|
|
447
472
|
/**
|
|
448
473
|
* Delete one stored session: the directory named by the id under one of the
|
|
449
|
-
* sessions root's working-directory directories
|
|
474
|
+
* sessions root's working-directory directories, and the id's entry in the
|
|
475
|
+
* workspace registry's archive set.
|
|
450
476
|
*
|
|
451
477
|
* The id never reaches a path unchecked — it must match the harness's own
|
|
452
478
|
* shape, the lookup is by exact name, and the resolved directory must stay
|
|
453
479
|
* inside the sessions root.
|
|
480
|
+
*
|
|
481
|
+
* The archive set can name a session whose stored directory is already gone —
|
|
482
|
+
* an earlier removal took the directory while the registry entry stayed, and
|
|
483
|
+
* the stale session summary keeps its row on the archived list. The miss
|
|
484
|
+
* answers success too: the registry's unarchive runs no existence check and
|
|
485
|
+
* resolves without writing for an id that is not archived, so the entry leaves
|
|
486
|
+
* the set either way and the row is gone for good instead of coming back on
|
|
487
|
+
* the next reload.
|
|
454
488
|
*/
|
|
455
489
|
async function deleteSession(ctx, req, res) {
|
|
456
490
|
const refused = refusalOf(ctx, req)
|
|
@@ -476,34 +510,41 @@ async function deleteSession(ctx, req, res) {
|
|
|
476
510
|
}
|
|
477
511
|
const root = resolve(sessionsRoot(ctx))
|
|
478
512
|
let dir = null
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
513
|
+
// The root listing is the storage's own answer, so a failure there is a
|
|
514
|
+
// fault and propagates to the route's 500 answer; a candidate that stats as
|
|
515
|
+
// absent is the OS's "not under this entry" — the expected state the scan
|
|
516
|
+
// moves past.
|
|
517
|
+
for (const entry of readdirSync(root)) {
|
|
518
|
+
const candidate = join(root, entry, sessionId)
|
|
519
|
+
let stat
|
|
520
|
+
try {
|
|
521
|
+
stat = statSync(candidate)
|
|
522
|
+
} catch {
|
|
523
|
+
continue
|
|
524
|
+
}
|
|
525
|
+
if (stat.isDirectory()) {
|
|
526
|
+
dir = candidate
|
|
527
|
+
break
|
|
492
528
|
}
|
|
493
|
-
} catch {
|
|
494
|
-
dir = null
|
|
495
529
|
}
|
|
496
|
-
if (dir === null
|
|
497
|
-
|
|
530
|
+
if (dir === null) {
|
|
531
|
+
await unarchiveSession(ctx, sessionId)
|
|
532
|
+
sendJson(res, 200, { ok: true, ghost: true })
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
if (!resolve(dir).startsWith(root + sep)) {
|
|
536
|
+
sendJson(res, 403, { ok: false, error: 'session directory outside the sessions root' })
|
|
498
537
|
return
|
|
499
538
|
}
|
|
500
539
|
try {
|
|
501
540
|
rmSync(dir, { recursive: true, force: true })
|
|
502
541
|
} catch (error) {
|
|
542
|
+
// A storage fault answers 500 with the OS error — nothing is swallowed.
|
|
503
543
|
sendJson(res, 500, { ok: false, error: String(error?.message ?? error) })
|
|
504
544
|
return
|
|
505
545
|
}
|
|
506
|
-
|
|
546
|
+
await unarchiveSession(ctx, sessionId)
|
|
547
|
+
sendJson(res, 200, { ok: true, ghost: false })
|
|
507
548
|
}
|
|
508
549
|
|
|
509
550
|
/**
|
|
@@ -547,6 +588,7 @@ export function apply(ctx) {
|
|
|
547
588
|
const warn = (message) => ctx.logger?.warn?.(`dsh-claude-style: ${message}`)
|
|
548
589
|
const usage = createUsage(ctx)
|
|
549
590
|
const hdsl = createHdslAccount(ctx)
|
|
591
|
+
const sessionSearch = createSessionSearch(ctx)
|
|
550
592
|
|
|
551
593
|
try {
|
|
552
594
|
disposers.push(scope.webServer.register({
|
|
@@ -756,6 +798,38 @@ export function apply(ctx) {
|
|
|
756
798
|
warn(`usage route unavailable: ${error?.message ?? error}`)
|
|
757
799
|
}
|
|
758
800
|
|
|
801
|
+
try {
|
|
802
|
+
// The search palette's message hits. Read-only, behind the same fence
|
|
803
|
+
// as the usage route: the answer quotes the user's own conversations.
|
|
804
|
+
disposers.push(scope.webServer.register({
|
|
805
|
+
kind: 'exact',
|
|
806
|
+
path: SESSION_SEARCH_PATH,
|
|
807
|
+
handler: (req, res) => {
|
|
808
|
+
if (req.method !== 'GET') {
|
|
809
|
+
res.writeHead(405, { allow: 'GET' })
|
|
810
|
+
res.end()
|
|
811
|
+
return
|
|
812
|
+
}
|
|
813
|
+
const refused = refusalOf(ctx, req)
|
|
814
|
+
if (refused !== undefined) {
|
|
815
|
+
sendJson(res, refused, { ok: false, error: refused === 401 ? 'unauthorized' : 'forbidden' })
|
|
816
|
+
return
|
|
817
|
+
}
|
|
818
|
+
const query = (new URL(req.url ?? '/', 'http://local').searchParams.get('q') ?? '').trim().slice(0, QUERY_MAX)
|
|
819
|
+
const answer = query === ''
|
|
820
|
+
? sessionSearch.warm().then(() => ({ sessions: [] }))
|
|
821
|
+
: sessionSearch.search(query)
|
|
822
|
+
void answer.then((value) => {
|
|
823
|
+
sendJson(res, 200, { ok: true, ...value })
|
|
824
|
+
}, (error) => {
|
|
825
|
+
sendJson(res, 500, { ok: false, error: String(error?.message ?? error) })
|
|
826
|
+
})
|
|
827
|
+
},
|
|
828
|
+
}))
|
|
829
|
+
} catch (error) {
|
|
830
|
+
warn(`session search route unavailable: ${error?.message ?? error}`)
|
|
831
|
+
}
|
|
832
|
+
|
|
759
833
|
return () => {
|
|
760
834
|
try {
|
|
761
835
|
usage.dispose()
|