dsh-claude-style 0.7.2 → 0.9.0
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 +84 -0
- package/README.en.md +7 -7
- package/README.md +7 -7
- package/docs/STYLE.md +85 -132
- package/docs/architecture.md +181 -105
- package/{lib → host}/index.js +42 -1
- package/host/search.js +167 -0
- package/lib/client.js +6804 -5372
- package/lib/model-descriptions.json +337 -5
- package/package.json +4 -3
- /package/{lib → host}/usage.js +0 -0
package/docs/architecture.md
CHANGED
|
@@ -1,150 +1,226 @@
|
|
|
1
1
|
# 架构决策记录
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
操作手册看 README,硬性规则看 AGENTS.md,视觉令牌看 docs/STYLE.md;本文件只管「权衡」。
|
|
3
|
+
本文件只记录**当前有效**的架构决定:每条写决定、理由、代价,以及什么情况下允许重审。操作手册看 README,代理规则与工作流程看 AGENTS.md,视觉令牌看 docs/STYLE.md。
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
维护约束:
|
|
7
6
|
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- 重构或拆分方案若与这里的决定冲突,先修改对应条目、写明原决定为何不再成立,再动手;不静默推翻。
|
|
8
|
+
- 编号是稳定标识,代码注释与其他文档按编号引用:内容随决定更新,编号不重排;一条被另一条整体取代时保留编号,写明由哪一条取代。新决定追加在文末。
|
|
9
|
+
- 事故经过与演变过程写在提交说明里,这里只写结论需要的理由。
|
|
10
|
+
|
|
11
|
+
按主题:
|
|
12
|
+
|
|
13
|
+
- 构建与源码:D1 拼接构建 · D5 模型文案是数据 · D18 源码布局与共用部件层 · D4 输入框样式门控
|
|
14
|
+
- 宿主边界:D2 只做浏览器半边 · D3 宿主选择器纪律 · D19 宿主契约与构建编号 · D10 设置传输 · D11 私有路由与字符串安全
|
|
15
|
+
- 运行时:D6 单一调度器 · D13 功能契约 · D12 快速失败与功能隔离 · D9 样式性能
|
|
16
|
+
- 功能:D14 账号表面 · D15 HDSL 契约 · D16 弹层基准 · D17 权限档位 · D20 滑动高亮 · D21 已归档列表 · D22 搜索面板 · D23 轮次状态行
|
|
17
|
+
- 分发:D7 行为层与皮肤同包 · D8 多主题
|
|
10
18
|
|
|
11
19
|
---
|
|
12
20
|
|
|
13
|
-
## D1.
|
|
21
|
+
## D1. 零构建工具链:构建期逐字拼接成单文件
|
|
14
22
|
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
23
|
+
- **决定**:`scripts/build.mjs` 按 `FRAGMENTS` / `STYLE_FILES` 的顺序把 `src/` 下的 JS 片段与样式表拼成 `lib/client.js`。所有片段共用一个工厂作用域:不写 import/export,顶层名字在整个产物里唯一;React 由构建头部经加载器 `require('react')` 取得,其他宿主包在用到它的功能里直接 `require`。片段用现代语法(`const` / `let`、箭头函数、可选链),基础缩进 4 空格。品牌 SVG、螃蟹的帧条 PNG 与厂商锁定标在构建时内联,字体与模型文案走宿主半边的路由。
|
|
24
|
+
- **构建检查**:`src/` 下每个 `.js` / `.css` 都在两份清单里;`%%TOKEN%%` 全部替换;输入框样式门控(D4);`:has()` 位置(D9);产物能被 `vm.Script` 解析;模型文案文档校验(D5)。产物写入构建编号(D19)。
|
|
25
|
+
- **理由**:DSH 的插件加载器没有相对 require,也没有资产 URL,打包器产出的分块与资产引用无处安放。
|
|
26
|
+
- **代价**:顶层名字共用一个作用域,靠命名约定避免冲突(D18);没有按需裁剪,产物体积靠自律控制。
|
|
27
|
+
- **重审条件**:加载器原生支持 ES module 相对导入与资产 URL。
|
|
19
28
|
|
|
20
|
-
## D2.
|
|
29
|
+
## D2. 只做浏览器半边的皮肤,不改宿主
|
|
21
30
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
31
|
+
- **决定**:一切效果靠 CSS 覆盖与客户端 DOM 覆盖实现,不修改 DSH 引擎、apiproxy 与官方 UI 包。宿主半边(`host/`)只做浏览器做不到的事:提供模型文案、字体、系统用户名、HDSL 契约、用量汇总、删除会话这些私有路由(D11),以及设置表单用的 `Config`(D10)。
|
|
32
|
+
- **理由**:主题是皮肤,宿主升级时要能快速跟进。
|
|
33
|
+
- **代价**:依赖宿主带哈希的类名与界面结构,宿主改版可能打断选择器,由 D3 / D19 的纪律与 smoke、probe 兜住。
|
|
34
|
+
- **重审条件**:宿主开放主题 API,或为需要覆盖的区域提供插槽时,逐步迁移过去。
|
|
26
35
|
|
|
27
|
-
## D3.
|
|
36
|
+
## D3. 宿主选择器纪律
|
|
28
37
|
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
38
|
+
- **决定**:
|
|
39
|
+
- 定位宿主元素的优先顺序:宿主的稳定契约(`[data-slot]` 插槽定位点、宿主自己的 data-* 属性,见 D19)→ 插件在每轮刷新里打的标记 → 带哈希类名的子串匹配。
|
|
40
|
+
- 子串匹配用最长稳定片段。宿主 CSS 模块的类名是 `<哈希>_<局部名>`,按组件定位时写 `[class*="_trailing"]`、`[class*="_row"]`;不带 `_` 的片段会误中别的词(`row` 命中 `…_grow` 曾把输入框高度锁在 28px;`rail` 命中 `trailing`;`tag` 命中 `stage`)与其他插件的类名。只有有意作用于全局的规则(全局字体、角标这类「全局刷子」)才用不带 `_` 的宽匹配。新增子串选择器要在真实页面里比对匹配到的元素,确认没有误伤。
|
|
41
|
+
- 不按界面文字匹配宿主元素:文字随界面语言与宿主版本变化。需要区分的宿主控件由刷新按结构打标记(D19)。
|
|
42
|
+
- 不覆盖 `[class*="viewArea"]` 在会话进行时的布局约定(`flex: 1 0 auto; min-height: auto`),它负责把输入框固定在滚动区底部。
|
|
43
|
+
- 带宿主按钮类名的链接(`_linkButton`,例如设置页的「充值」)是填充或描边按钮,自带文字颜色;全局链接颜色排除这个类名,否则按钮文字会和填充色同色。
|
|
44
|
+
- **理由**:宿主类名只有局部名稳定;界面文字与短片段都已出过实际问题。
|
|
45
|
+
- **代价**:选择器更长;标记要等刷新写入(刷新在同一帧、绘制之前执行)。
|
|
46
|
+
- **重审条件**:宿主为这些区域提供稳定的 data-* 契约时全面迁移。
|
|
33
47
|
|
|
34
|
-
## D4.
|
|
48
|
+
## D4. 输入框样式的构建期门控
|
|
35
49
|
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
50
|
+
- **决定**:输入框相关规则必须写在 `/* @composer-gate */` 标记之下,构建把 `[%%COMPOSER_ATTR%%]` 门控盖到标记以下的每条规则上,漏盖即构建失败。门控属性由输入框功能按「输入框重绘」偏好与当前页面写在 `<body>` 上。
|
|
51
|
+
- **理由**:输入框是性能与正确性最敏感的区域;偏好关闭或功能退役时,这些规则必须整体失效。
|
|
52
|
+
- **代价**:写输入框样式要记得放在标记之下;构建脚本维护门控逻辑。
|
|
53
|
+
- **重审条件**:无(这是安全网)。
|
|
40
54
|
|
|
41
|
-
## D5.
|
|
55
|
+
## D5. 模型文案是数据,不进产物
|
|
42
56
|
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
57
|
+
- **决定**:`src/model-descriptions.json` 在构建时校验后复制到 `lib/`,浏览器第一次绘制选择器时经宿主路由读取。查找顺序:精确条目 → 家族规则 → 档位规则 → 目录自带文本。文案纪律:
|
|
58
|
+
- 文案属于产品线,不属于某个版本:同一条产品线按名字模式映射到同一句,版本迭代与退场都不改。
|
|
59
|
+
- 不写自造的档位前缀(「旗舰档:」),不重复行里已有的模型名。
|
|
60
|
+
- 家族规则有顺序且必须限定范围(例如 `flash` 规则只作用于 deepseek);「最强」「旗舰」这类最高级只允许出现在绑定具体版本号的精确条目里。
|
|
61
|
+
- 同一文档的 `brands` 把模型绑定到厂商锁定标;没有规则认领的模型不画锁定标。
|
|
62
|
+
- **理由**:模型目录变化快,更新文案不应要求改 JS 发版;给版本写的描述最终会挂到别的版本上。
|
|
63
|
+
- **代价**:第一次绘制选择器有一次异步读取;文案规则有学习成本。
|
|
64
|
+
- **重审条件**:宿主的模型目录 API 直接提供本地化文案。
|
|
47
65
|
|
|
48
|
-
## D6.
|
|
66
|
+
## D6. 单一调度器
|
|
49
67
|
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
- **代价**:每个 sync
|
|
53
|
-
- **已知代价与对策**:流式输出期间每帧一次全量 pass 是性能热点,见 D9。
|
|
68
|
+
- **决定**:`src/core/scheduler.js` 持有唯一的 MutationObserver(`<body>` 子树,属性只看 `aria-label` / `aria-selected`),用 requestAnimationFrame 把变化合并成每帧一次刷新,按功能顺序调用各功能的 `sync()`。全局的指针、键盘、焦点、滚动与尺寸监听也只注册在这里,经功能句柄上的钩子分发(D13)。键盘路径只认用户的按键:皮肤自己代发的键盘事件带标记(账号表面对宿主菜单派发的 Escape 带 `__dshHostMenuEscape`),调度器见到标记即跳过——那次代发只送达宿主的菜单,不得顺带触发全局 Esc 路由。刷新状态在调度器开头声明:订阅偏好时,已就绪的设置表单会同步通知并触发 `schedule()`。
|
|
69
|
+
- **理由**:各功能自挂观察者与监听会互相干扰、重复扫描、难以卸载干净。
|
|
70
|
+
- **代价**:每个 `sync()` 必须能快速提前返回,并且只在值变化时写 DOM(同值写入也会让元素样式失效);页面持续变化时每帧都有一次刷新,其开销见 D9。
|
|
54
71
|
|
|
55
|
-
## D7.
|
|
72
|
+
## D7. 行为层与皮肤同包发布
|
|
56
73
|
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
- **重审条件**:出现第二个真实消费者(另一个主题包或宿主官方)需要复用 overrides 层时,把 overrides 抽成独立包,皮肤包依赖它。
|
|
74
|
+
- **决定**:模型选择器、权限分段、账号弹层等行为层与皮肤层共享宿主定位点、调度器与弹层工具,在源码里分目录,发布为同一个包;不想要 Claude 外观的用户在设置里切换品牌。
|
|
75
|
+
- **代价**:「只要行为不要外观」没有独立入口。
|
|
76
|
+
- **重审条件**:出现第二个真实使用者(另一个主题包或宿主官方)需要复用行为层时,把它抽成独立包。
|
|
61
77
|
|
|
62
|
-
## D8.
|
|
78
|
+
## D8. 多主题走单仓库、构建期分包
|
|
63
79
|
|
|
64
|
-
-
|
|
65
|
-
-
|
|
66
|
-
- **代价**:改造时 build.mjs 与目录布局有一次性手术;两个主题之后共享碎片的改动需要双主题回归。
|
|
80
|
+
- **决定**:只有新主题出现行为上的分歧(不同的 DOM 覆盖、不同的输入框结构)时才改造:共用片段留在仓库级 `src/`,主题私有片段收进 `themes/<名字>/`,`build.mjs` 按 `--theme` 参数化,每个主题产出自包含的单文件并各自发包。只换色板与标志时,用现有的品牌切换在包内解决。
|
|
81
|
+
- **代价**:改造时构建脚本与目录有一次性调整;之后改共用片段需要两个主题都回归。
|
|
67
82
|
|
|
68
|
-
## D9.
|
|
83
|
+
## D9. 样式性能:结构判断交给刷新,`:has()` 只放在最后一段
|
|
69
84
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
85
|
+
- **决定**:
|
|
86
|
+
- 依赖 DOM 结构的判断在刷新里算好、写成属性,样式只读属性:输入框形态 `data-composer-variant`、附件 `data-dsh-claude-attachment`、草稿为空 `data-dsh-claude-draft-empty`、控件 `data-dsh-claude-control`(D19)、视图标签条 `data-dsh-view-tabs`。
|
|
87
|
+
- `:has()` 只能出现在选择器的最后一段(`A:has(B)`、`A :has(B)`),后面不能再接后代或兄弟选择器(`A:has(B) C`、`body:not(:has(B)) C`)。构建里的 `checkHasPlacement` 拒绝违规写法。
|
|
88
|
+
- **理由**:在无界面 Chrome 里打开会话、每帧追加一个字(等同流式输出或打字机效果)实测:违规写法单条就让每帧样式重算多出 7–13ms,与括号里测的是结构还是 `:hover` / `:focus-within` 无关;位于最后一段的每条只有 0.1–0.5ms。按本条改完后,整张样式表下每帧样式重算 1.9–2.7ms(不加载样式表时 1.4ms),主线程每帧总耗时 5–7ms。
|
|
89
|
+
- **代价**:状态要等刷新写入(同一帧、绘制之前)。
|
|
90
|
+
- **重审条件**:浏览器对非末段 `:has()` 的失效处理实测不再明显慢于末段写法。
|
|
74
91
|
|
|
75
92
|
## D10. 设置传输只走官方 Config 表单
|
|
76
93
|
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
- **决定**:客户端只有 `configForms` 一条传输:绑定宿主实际提供的命名空间(候选依次为加载器入口 id、包名、`cordis.patch.yml` 插入的 id,以宿主的命名空间目录为准),读写都经表单控制器(值、写队列、修订号栅栏)。命名空间晚到时订阅目录,到达即绑定。宿主半边导出 `Config` 作为 schema:只有 `.volatile()` 字段进表单;schemastery 用顶层 await 加守卫导入,解析不到时 `Config` 为 `undefined`、皮肤照常加载——这是导入规定的唯一例外。设置席位注册为 `plugins.bundle.config`(键为包名);宿主没有 `configForms` 时才注册整页的 `settings.section`。
|
|
95
|
+
- **代价**:`Config` 的顶层 await 让宿主半边晚一步求值(加载器本就等待导入,无实际影响)。
|
|
96
|
+
- **重审条件**:宿主提供不依赖 `Config` 的设置注册方式,或 `configForms` 契约再变。
|
|
97
|
+
|
|
98
|
+
## D11. 私有路由借宿主的请求栅栏;外来字符串只以文本上屏
|
|
99
|
+
|
|
100
|
+
- **决定**:
|
|
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。
|
|
103
|
+
- 用量汇总只读:第三方插件的账本只读不写,自己的缓存放在 `$DSH_HOME/cache/dsh-claude-style/`。
|
|
104
|
+
- 客户端:来自设置、账号服务、系统或其他插件的字符串只用 `textContent` 或元素属性写入;图标复制节点,不重新解析标记。
|
|
105
|
+
- **理由**:插件路由不在宿主自己的栅栏之内;`dsh web` 绑定 `0.0.0.0` 时局域网可直接访问,而页面能驱动执行 shell 命令的智能体。
|
|
106
|
+
- **代价**:路由依赖宿主的 connection 服务。浏览器同源请求带会话 cookie、桌面壳经 `forwardWebRequest` 转发到回环 Host,两种都已核对能通过。
|
|
107
|
+
- **重审条件**:宿主提供自带鉴权的路由注册时迁移过去,删掉本地替身。
|
|
83
108
|
|
|
84
|
-
##
|
|
109
|
+
## D12. 快速失败与功能隔离
|
|
85
110
|
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
111
|
+
- **决定**:
|
|
112
|
+
- 出错的地方直接抛出,不吞错误、不静默回退。宿主服务可能缺席时,用可选链与 `typeof` 检查表达「没有这个服务」,不用 try/catch。
|
|
113
|
+
- 功能隔离:teardown 最先经 `ctx.effect` 注册且幂等;每个功能单独安装,装不上的报告一次并退役;每轮刷新里每个 `sync()` 单独执行,连续失败 3 轮即报告并退役(`ui.retire`)。调度器自身装不上时整体回滚到宿主原样。
|
|
114
|
+
- 退役就是运行该功能自己的 teardown,并交还它接管的宿主界面:页脚接管(`FOOTER_ATTR`)与输入框重绘(`COMPOSER_ATTR`)的门控强制关闭;权限控件替换的宿主控件由它自己的标记把守,随它的 teardown 交还。每个功能的 teardown 是它 DOM 与标记的唯一清理。
|
|
115
|
+
- 需要隔离、但错误必须可见的地方用 `reportError`:`notifyAll` 逐个通知监听函数,某个出错就报告、其余照常;卸载时单个功能失败同样报告后继续。
|
|
116
|
+
- 允许的 catch 只有以下几类,每处写明原因:上述隔离;宿主以抛错表达的预期状态(设置表单的 `set()` 拒绝不属于该配置的字段;`modelDirectories.directoryFor` 对还没建立的会话抛错,下一轮重试;`sessionQuery.readSession` 以 `SESSION_QUERY_CORRUPT_SESSION` / `SESSION_QUERY_SESSION_NOT_FOUND` 表示某个存档读不了或在列出后被删,内容搜索只把这一个会话记为空并记一条警告);Promise 上表示「宿主半边没有应答」或「宿主自己会提示」的失败回调。
|
|
117
|
+
- **理由**:被吞掉的错误会在别处以更难排查的方式出现;隔离只负责不让一个功能拖垮其他功能,失败本身必须留下记录。
|
|
118
|
+
- **代价**:功能失效时界面上没有提示,只有控制台记录;依赖同一宿主服务的功能会一起退役(smoke 的「会话列表故障」用例覆盖)。
|
|
119
|
+
- **重审条件**:宿主提供插件级的错误上报时,把报告接过去。
|
|
90
120
|
|
|
91
|
-
##
|
|
121
|
+
## D13. 功能契约:FEATURES 表与调度钩子
|
|
92
122
|
|
|
93
|
-
- **背景**:`apply()` 依次安装全部特性,teardown 却在最后才交给 `ctx.effect`;每轮 pass 里各 `sync()` 也没有逐个兜底——一个抛错,排在它后面的全部跳过,而且每轮如此。实测:宿主 `remote.account.getProfile()` 返回非 Promise 时 `apply()` 中途抛错,样式表与 body 属性留在页面上、调度器没装上、teardown 没注册(关掉插件也清不掉),前面装好的特性的监听器一并泄漏。effort-picker 那次「Loading plugins…」卡死是同一类问题。
|
|
94
123
|
- **决定**:
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
- **背景**:重构开始时,三个特性碎片都越过 750 行上限——`account-footer.js` 1357 行(账户资料、宿主账号菜单桥接、其他插件页脚条目的镜像、抽屉壳、行构建五份工作挤在一个闭包里)、`model-picker.js` 900 行(模型目录订阅与行构建也塞在里面)、`permissions.js` 868 行(会话统计卡也在里面);`composer/inline.css` 847 行与 `components/model-picker.css` 767 行两条 CSS 同样超限。调度器同时把每个特性的触发调用硬编码在手写清单里:`PASS_FEATURES` 按名列 pass 序、与 entry.js 的安装序分开维护,另有 21 处特性专属调用散在各触发分支里,而且已经与特性漂移——`ui.heroMenu.close` 是被守卫着的死调用(heroMenu 的句柄只有 `{ sync, reposition }`)。`ui` 是跨特性共享的服务注册表,却没有任何文档说明句柄有哪些方法、谁可以读谁。
|
|
104
|
-
- **决定**(重构 Phase 0–3 已落地):
|
|
105
|
-
- **辅助碎片导出顶层 `createX(...)` 工厂**,仿 popover-utils 的 `createHoverIntent`:状态收在工厂自己的闭包里,返回一个小对象;访问器与回调经参数传入(如 `{ isOpen: fn, onChange: fn }`),绝不伸手进别的闭包。特性的 `installX` 负责把工厂接起来(现有十个:`createAccountProfile`、`createHostAccountMenu`、`createAccountRows`、`createFooterMirror`、`createAccountSurface`、`createModelCatalog`、`createModelRows`、`createEffortControl`、`createEffortMatrix`、`createSessionStats`;`createEffortMatrix` 由 `createEffortControl` 接起来)。
|
|
106
|
-
- **拆出多个碎片的特性建一个子目录**(`overrides/account/`、`overrides/model/`、`overrides/effort/`);只拆出一个辅助碎片的特性把它放在特性旁边(`overrides/session-stats.js`)——单文件目录是噪音。FRAGMENTS 里列在特性碎片紧前面;顶层名对整个 bundle 全局唯一、以特性起名(`createAccountProfile`,不是 `createProfile`)。移动就是移动:注释随行、风格与名字不变,只有闭包变量必须变成参数时才改签名。
|
|
107
|
-
- **特性契约**:entry.js 的 FEATURES 表(`{ name, handle?, install }`)统一安装序与 pass 序——pass 序就是安装序过滤出句柄带 `sync` 的特性(`settings` 安装到 `ui.settingsNav`)。scheduler 只认 FeatureHandle 的可选钩子(typedef 在 scheduler.js 头部):`sync` / `owns` + `close('outside')` / `onPointerDown` / `close('escape')` / `close('composer')` / `onInput` / `reposition('viewport' | 'composer')` / `onCopyChange` / `onKey`;没实现的钩子直接跳过,每个特性保住自己原有的关闭路线(permissions 没有外部点击关闭,quickProviders 只在 composer 聚焦时关)。调度器自己的触发源同样不点名特性:时钟每分钟跑一轮整体 pass(hero 问候语这类跟着时钟变的文案靠它翻页),composer 卡片的尺寸变化经 `reposition('composer')` 派发,卡片上的点击聚焦与消息列表跟随都是 composer 特性的钩子。`retire` 按 name 或 handle 匹配:纯 handle 命中只停 sync、不拆安装(settingsNav 的显式分支——失败计数器已拒绝后续 pass,安装继续跑、设置页不卸);退役 `footer` / `composer` 仍强制归还 body 属性(FOOTER_ATTR / COMPOSER_ATTR)。
|
|
108
|
-
- **理由**:拆分前「加一个特性」要改两处清单(entry.js 安装序列 + scheduler 的 PASS_FEATURES)再往各触发分支加调用;现在变成往 FEATURES 表加一行、在句柄上实现钩子——scheduler 不再认识任何具体特性,手写清单无从漂移。拆分把千行闭包变成状态自持的工厂加薄编排,750 行上限重新可守。移动就是移动(注释随行、闭包变量变参数才改签名),搬运提交的 diff 因此可审:搬运里不该出现逻辑改动。本决策不推翻 D1/D6/D12:仍是单文件逐字拼接(D1)、仍是单一调度器统一驱动(D6,钩子只是把硬编码调用变成句柄方法)、特性级失败隔离与 retire 语义原样保留(D12)——它在三者之内工作。
|
|
109
|
-
- **代价**:多一层间接——特性内部状态要经工厂参数表交接,动状态时多过一遍参数;「加一个特性」前要先读这条决策与 scheduler.js 的 typedef。
|
|
110
|
-
- **重审条件**:钩子表继续膨胀(例如第二类键盘事件或第二种观察源进场)时,重审契约粒度——按事件域分组,或让特性自己声明要订阅的触发器。
|
|
124
|
+
- `src/entry.js` 的 FEATURES 表(`{ name, handle?, install }`)同时决定安装顺序与刷新顺序:刷新顺序就是安装顺序中句柄带 `sync` 的那些。
|
|
125
|
+
- 调度器只认功能句柄上的可选钩子(类型定义在 scheduler.js 开头):`sync` / `owns` + `close('outside')` / `onPointerDown` / `close('escape')` / `close('composer')` / `onInput` / `onFocusIn` / `reposition('viewport' | 'composer')` / `onCopyChange` / `onKey`;没实现的钩子直接跳过,每个功能保留自己的关闭方式。
|
|
126
|
+
- `retire` 按 name 或 handle 匹配:只匹配到 handle 时只停止 sync、不拆安装(设置页的 `settingsNav`)。
|
|
127
|
+
- 功能拆出的部分写成顶层工厂 `createX(...)`:状态留在工厂自己的闭包里,访问器与回调经参数传入,不伸手进别的闭包;名字以功能开头(`createAccountProfile`)。
|
|
128
|
+
- **理由**:加一个功能只需在 FEATURES 表加一行、在句柄上实现钩子;调度器不认识任何具体功能,没有要手工同步的清单。
|
|
129
|
+
- **代价**:功能内部状态经工厂参数交接,多一层间接。
|
|
130
|
+
- **重审条件**:钩子继续增多(例如出现第二类键盘事件或第二种观察来源)时,按事件类别分组,或让功能自己声明要订阅的触发器。
|
|
111
131
|
|
|
112
132
|
## D14. 账号表面:一套行模型,两个挂载点
|
|
113
133
|
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
- **重审条件**:宿主为账号菜单提供追加行的插槽时,改用插槽注册,去掉 DOM 追加。
|
|
134
|
+
- **决定**:插件的行(账号头部、其他插件的页脚条目、设置行)只出现在账号弹层里,由 `features/account/surface.js` 每轮判定挂载点。宿主有账号区时(桌面端),宿主的账号行就是入口(插件只打标记供样式重绘),插件把容器插到宿主账号菜单列表的首位,宿主自己渲染它的那几行;宿主没有账号区时(Web),插件自建账号行与弹层。宿主的行不复制、不移动、不转发点击;插件的行被点击后需要收起菜单时,派发一次带 `__dshHostMenuEscape` 标记的 Escape 交给宿主菜单处理(D6:调度器不认代发事件)。账号头部打开封号彩蛋时,覆盖全窗的浮层让指针「离开」行与菜单——悬停收起在浮层打开期间停摆,屏幕退出后页脚保持进入时的样子。
|
|
135
|
+
- **理由**:宿主菜单自带定位、动画、键盘遍历与关闭;插进去的行加入同一套键盘遍历,账号区不依赖宿主菜单的渲染时序与标签文字。
|
|
136
|
+
- **代价**:宿主菜单关闭即卸载,容器每轮重新确认并插入。
|
|
137
|
+
- **重审条件**:宿主为账号菜单提供追加行的插槽时,改用插槽注册。
|
|
119
138
|
|
|
120
139
|
## D15. HDSL 账号契约:宿主半边转发,浏览器半边只认一条回退顺序
|
|
121
140
|
|
|
122
|
-
- **背景**:HDSL 启动器用 `HDSL_` 前缀的环境变量向实例公布「这是谁」:账号显示名、供应商、账号种类、头像来源,以及玩家头像贴图的绝对路径;`HDSL_ACCOUNT_CONTRACT` 是唯一必需键,读不到它整组作废。浏览器半边读不到进程环境,贴图又只有绝对路径,两者都只能由宿主半边转发。启动器的皮肤是一张 64×64(或整数倍)的贴图集,不是成品头像:脸只是其中 (8,8) 起的 8×8 贴图块,帽子层在 (40,8)。
|
|
123
141
|
- **决定**:
|
|
124
|
-
- 宿主半边读 harness 启动时填好的启动环境快照(`ctx.launchEnvironment
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
-
|
|
130
|
-
- **重审条件**:HDSL
|
|
142
|
+
- 宿主半边读 harness 启动时填好的启动环境快照(`ctx.launchEnvironment`),只信 `process` 与 `user-env` 两层(项目目录的 `.env` 会随仓库被克隆),契约版本不为 `1` 时回 `{ contract: false }`。不 import harness 的启动环境包(`link:` 安装的插件解析不到它)。
|
|
143
|
+
- `GET /dsh-claude-style/hdsl` 回账号元数据,不含头像文件路径;`GET /dsh-claude-style/hdsl-skin.png` 回贴图字节,路径只来自环境、永远不来自请求。契约在进程生命周期内不变,读一次并记住。
|
|
144
|
+
- 浏览器半边:昵称 = 自定义昵称 → 官方账号昵称 → HDSL 昵称 → 探测昵称缓存 → 探测昵称 → `User`;头像 = 官方账号头像 → HDSL 头像 → Claude 徽标。两者都经 `src/core/host.js` 的身份存储,欢迎语与账号行只读这一处。
|
|
145
|
+
- 启动器给的是贴图集:按启动器账号列表同样的裁法取头部(脸的 8×8 贴图块按盒子 1/18 内缩,帽子层铺满),方形绘制;画完主动请求一轮刷新。
|
|
146
|
+
- **理由**:契约只需读一次;两条回退链收在一处,各处显示不会各自漂移;裁法与启动器一致,同一个玩家在两处长得一样。
|
|
147
|
+
- **代价**:桌面端的账号行由宿主渲染,HDSL 头像只作用于插件自己的表面;启动器内置的默认形象没有像素可用,回退到徽标。
|
|
148
|
+
- **重审条件**:HDSL 契约升级版本(按指纹取图、账号 UUID)时改接新通道;宿主提供统一的「显示名 / 头像」服务时改接宿主。
|
|
131
149
|
|
|
132
|
-
## D16.
|
|
150
|
+
## D16. 弹层基准:一套外壳、一个停留时长、一次只开一张
|
|
133
151
|
|
|
134
|
-
- **背景**:皮肤的弹层从六处各自长出来(模型选择器、推理强度、权限、账户、会话统计、hero 行的宿主菜单,另加设置页的快捷供应商),每处自己写停留时长与收起宽限。停留原本是 50ms,指针以平常速度扫过就已经超过它,于是位于指针必经之路上的触发器(工作台版面把上下文行贴在输入卡片正上方)看上去没有停留;同时「同时只开一张卡」此前只在模型与推理这一对上硬编码(两侧各自调用对方的 close),换一对弹层就会两张卡叠在一起。
|
|
135
152
|
- **决定**:
|
|
136
|
-
-
|
|
137
|
-
-
|
|
138
|
-
-
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
153
|
+
- 外壳:插件自建的卡片(权限、模型、推理强度、账号、会话统计、快捷供应商)都带 `.dsh-claude-popover-card`,共用底色、边框、圆角、阴影、内边距、层级、开关动画与暗色配色;行、角标、勾号、状态行、主体用中性类名(`dsh-claude-popover-item` 等)。各功能的样式只写位置、宽度与展开方向。
|
|
154
|
+
- 停留与宽限统一为 `shared/popover.js` 的 `POPOVER_OPEN_DELAY` / `POPOVER_CLOSE_DELAY`(100ms);例外连同理由写在使用处(模型选择器两级卡片的收起宽限 150ms;会话统计的展开停留 300ms)。
|
|
155
|
+
- 互斥:`registerPopover(name, close)` 登记每张弹层的关闭方式,`closeOtherPopovers(name)` 在打开前关掉其余的。一个弹层的多层卡片按一项登记;宿主的菜单由驱动它的功能登记。登记表只存关闭函数(弹层未开时是空操作),按名字覆盖登记,热重载后旧一代的函数被新一代替换。
|
|
156
|
+
- hero 行一项登记对应两个宿主菜单,`hero-menu.js` 自己再做一次互斥,判定依据是「已展开的触发器」。
|
|
157
|
+
- **理由**:同一个交互问题分处写必然漂移;共用外观用中性类名,改一个功能的样式不会波及借用它类名的其他功能。
|
|
158
|
+
- **代价**:登记表是模块级状态,功能卸载时必须 `unregisterPopover`。
|
|
159
|
+
- **重审条件**:宿主提供统一的弹层管理(全局互斥或焦点陷阱)时改接宿主。
|
|
143
160
|
|
|
144
161
|
## D17. 权限档位以宿主目录为准,皮肤只做表现层
|
|
145
162
|
|
|
146
|
-
-
|
|
147
|
-
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
|
|
163
|
+
- **决定**:段位与弹层行都按宿主 `permissionPresets` 目录构建:目录里有的档位才画,没有的整条不出现。`PERMISSION_SEGMENTS` 只描述格子,每格列出可绑定的档位、取第一个宿主提供的(部署自己的自动档优先于宿主内置的 Auto review),一个都绑不上就不画。档位名与说明来自皮肤的 `PERMISSION_PRESETS`,不认识的档位用目录自带的名字与说明,机器值永不上屏;档位声明的 `icon` 不画,列表保持纯文字。切换走宿主的 `/permission <preset>`。
|
|
164
|
+
- **理由**:目录变化时不漏档位,第三方档位(例如 auto mode 插件的 `auto-mode`)成为一等条目,接入新档位不用改皮肤代码。
|
|
165
|
+
- **代价**:档位集合变化时重建行与段位;不认识的档位以宿主给的名字出现。
|
|
166
|
+
- **重审条件**:宿主原生支持档位图标、且界面有了统一的档位图形语言时,再考虑画图标。
|
|
167
|
+
|
|
168
|
+
## D18. 源码按功能归档,共用部件单独成层
|
|
169
|
+
|
|
170
|
+
- **决定**:
|
|
171
|
+
- `src/core/`:宿主访问、偏好、模型文案、i18n、调度器。`src/shared/`:多个功能共用的部件,JS 与 CSS 放在一起——`dom.js`(`buildElement`、`createStamp`)、`notify.js`(`notifyAll`)、`popover.js` / `popover.css`(定位、悬停意图、弹层登记表、卡片外壳与行,D16)、`sliding-pill.js` / `sliding-pill.css`(D20)。`src/theme/`:不属于任何功能的全局外观。`src/features/<功能>/`:一个功能的安装器、拆出的工厂与样式表放在一起,主文件以功能命名。`host/`:手写的宿主半边。`lib/`:只放构建产物。
|
|
172
|
+
- 拼接顺序:共用样式表排在所有功能样式表之前,功能自己的规则在同优先级时后出现、取胜。
|
|
173
|
+
- 共用:同一段 DOM 查询或同一种写法出现第 3 次时收进 `shared/`(宿主访问收进 `core/host.js`);功能之间不借用类名,共用外观用中性类名。
|
|
174
|
+
- 文件大小:片段接近 750 行时,在它的功能目录里按职责拆出工厂(D13),不按行数机械切分。
|
|
175
|
+
- 搬动与逻辑改动分开提交:搬动提交里只有位置与路径变化,并用构建产物逐行比对确认。
|
|
176
|
+
- **理由**:一个功能的 JS 与样式放在一起,读代码时只需看一个目录;共用写法只有一份,改动不会漏改。
|
|
177
|
+
- **代价**:路径更深;新文件要登记到构建清单(构建会拒绝漏登的文件)。
|
|
178
|
+
- **重审条件**:D1 的限制解除时,改为真正的模块导入。
|
|
179
|
+
|
|
180
|
+
## D19. 宿主契约与构建编号
|
|
181
|
+
|
|
182
|
+
- **决定**:
|
|
183
|
+
- 插槽定位点:宿主渲染器保证每个插槽外面有 `[data-slot="<key>"]` 包装层,供外部样式定位(ui-renderer 的 scoped-slots)。例如访问模式按钮按 `[data-slot="conversation.input.permission"]` 查找,跳过插件自己插进去的按钮。
|
|
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` 的卡片本就开时挂载、关时摘除,无需经过它。
|
|
186
|
+
- 构建编号:构建取产物内容的哈希写成 `BUILD_ID`,运行时写到 `<body data-dsh-claude-style>` 的值上;页面运行的是哪一版以它为准。热重载会替换插件代码而不重新加载页面,页面的加载时间说明不了代码版本。
|
|
187
|
+
- **理由**:界面文字与哈希类名随语言、版本变化,结构与契约更稳定。
|
|
188
|
+
- **代价**:结构判断依赖宿主 InputBar 的现状,宿主改版时需要核对;smoke 的替身宿主按宿主真实结构搭建,结构变化会先在那里暴露。宿主按 `modalSelector` 仲裁快捷键这一侧不被替身复现,冒烟改查皮肤自己的不变量:关闭的弹层卡片不持有 `role="menu"`。
|
|
189
|
+
- **重审条件**:宿主给这些控件提供自己的 data-* 标记时,直接读宿主的标记。
|
|
190
|
+
|
|
191
|
+
## D20. 分段控件共用滑动高亮
|
|
192
|
+
|
|
193
|
+
- **决定**:视图标签、权限分段、进行中 / 已归档与设置页的分段控件共用 `createSlidingPill`:量出当前项相对控件内边距盒的位置与宽度,写成两个自定义属性,控件的伪元素用 transform 与 width 过渡滑过去。属性与首次位置在同一次调用里写入,所以初次出现不滑入;控件隐藏或没有当前项时撤掉属性,当前项恢复自己的底色。宽度因字体加载而变化时由 ResizeObserver 重新定位。滑动不受「减少动态效果」设置影响(用户要求始终播放)。React 渲染的控件(设置页)在布局副作用里同步,赶在绘制之前。
|
|
194
|
+
- **代价**:每轮刷新读两个元素的位置;在已经排过版的帧里开销很小。
|
|
195
|
+
|
|
196
|
+
## D21. 已归档列表跟随宿主的两份客户端数据
|
|
197
|
+
|
|
198
|
+
- **决定**:侧栏「已归档」是插件自己的平铺列表,订阅宿主 `workspaces.list`(归档集合)与 `sessions.list`(标题、时间、来源),两者都就绪后按宿主「仅已归档」的规则重算:只收已有会话摘要、非子代理、非空白占位的归档会话,按更新时间排序,内容不变时不重建。宿主自带的筛选状态存在 ui-workspace 私有的视图 store 里,不是客户端服务,只能靠点它的菜单改,因此不用。取消归档走 `workspaces.unarchiveSession`;删除走宿主半边的私有路由(D11),失败记录警告。点击已归档行显示宿主的同一条提示:用宿主的 Toast 组件与 `workspace` 文案表里的 `toast.archivedNotOpenable`(宿主的提示通道是私有的)。
|
|
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/{lib → host}/index.js
RENAMED
|
@@ -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
|
|
|
@@ -107,6 +108,12 @@ const SESSION_DELETE_PATH = `${ROUTE_PREFIX}/session-delete`
|
|
|
107
108
|
const USAGE_PATH = `${ROUTE_PREFIX}/usage`
|
|
108
109
|
/** How long a computed roll-up is served before a background refresh is kicked. */
|
|
109
110
|
const USAGE_TTL_MS = 5 * 60 * 1000
|
|
111
|
+
/**
|
|
112
|
+
* Message-content search for the search palette (host/search.js). `?q=` is
|
|
113
|
+
* the query; without one the route only brings its message cache up to date,
|
|
114
|
+
* which the palette asks for as it opens so the first real query is quick.
|
|
115
|
+
*/
|
|
116
|
+
const SESSION_SEARCH_PATH = `${ROUTE_PREFIX}/session-search`
|
|
110
117
|
/** Session ids are the harness's own shape; anything else is refused before it reaches a path. */
|
|
111
118
|
const SESSION_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/
|
|
112
119
|
/** Largest deletion request body read; a real one carries one id. */
|
|
@@ -512,7 +519,8 @@ async function deleteSession(ctx, req, res) {
|
|
|
512
519
|
*/
|
|
513
520
|
export function apply(ctx) {
|
|
514
521
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
515
|
-
|
|
522
|
+
// The copy document is build output beside the client bundle in lib/.
|
|
523
|
+
const file = join(here, '..', 'lib', COPY_FILE)
|
|
516
524
|
const fontsDir = join(here, '..', 'fonts')
|
|
517
525
|
|
|
518
526
|
/**
|
|
@@ -546,6 +554,7 @@ export function apply(ctx) {
|
|
|
546
554
|
const warn = (message) => ctx.logger?.warn?.(`dsh-claude-style: ${message}`)
|
|
547
555
|
const usage = createUsage(ctx)
|
|
548
556
|
const hdsl = createHdslAccount(ctx)
|
|
557
|
+
const sessionSearch = createSessionSearch(ctx)
|
|
549
558
|
|
|
550
559
|
try {
|
|
551
560
|
disposers.push(scope.webServer.register({
|
|
@@ -755,6 +764,38 @@ export function apply(ctx) {
|
|
|
755
764
|
warn(`usage route unavailable: ${error?.message ?? error}`)
|
|
756
765
|
}
|
|
757
766
|
|
|
767
|
+
try {
|
|
768
|
+
// The search palette's message hits. Read-only, behind the same fence
|
|
769
|
+
// as the usage route: the answer quotes the user's own conversations.
|
|
770
|
+
disposers.push(scope.webServer.register({
|
|
771
|
+
kind: 'exact',
|
|
772
|
+
path: SESSION_SEARCH_PATH,
|
|
773
|
+
handler: (req, res) => {
|
|
774
|
+
if (req.method !== 'GET') {
|
|
775
|
+
res.writeHead(405, { allow: 'GET' })
|
|
776
|
+
res.end()
|
|
777
|
+
return
|
|
778
|
+
}
|
|
779
|
+
const refused = refusalOf(ctx, req)
|
|
780
|
+
if (refused !== undefined) {
|
|
781
|
+
sendJson(res, refused, { ok: false, error: refused === 401 ? 'unauthorized' : 'forbidden' })
|
|
782
|
+
return
|
|
783
|
+
}
|
|
784
|
+
const query = (new URL(req.url ?? '/', 'http://local').searchParams.get('q') ?? '').trim().slice(0, QUERY_MAX)
|
|
785
|
+
const answer = query === ''
|
|
786
|
+
? sessionSearch.warm().then(() => ({ sessions: [] }))
|
|
787
|
+
: sessionSearch.search(query)
|
|
788
|
+
void answer.then((value) => {
|
|
789
|
+
sendJson(res, 200, { ok: true, ...value })
|
|
790
|
+
}, (error) => {
|
|
791
|
+
sendJson(res, 500, { ok: false, error: String(error?.message ?? error) })
|
|
792
|
+
})
|
|
793
|
+
},
|
|
794
|
+
}))
|
|
795
|
+
} catch (error) {
|
|
796
|
+
warn(`session search route unavailable: ${error?.message ?? error}`)
|
|
797
|
+
}
|
|
798
|
+
|
|
758
799
|
return () => {
|
|
759
800
|
try {
|
|
760
801
|
usage.dispose()
|