dsh-url-router 0.4.2 → 0.4.3
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/AGENTS.md +65 -4
- package/README.md +13 -5
- package/lib/client.js +939 -82
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
- **段位**:`#/session/<id>`(会话,`uiWorkspace.openSession`)、`#/panel/<panelId>`(主面板,`ctx.layout.selectPanel`)。输入侧接受复数与百分号编码,规范化成单数形式;**不认识的段位一律不碰**,只留一条诊断。
|
|
26
26
|
- **轮次归属必须可证明** ✓:助手行自带轮次 ✓;工具行只有调用 id ✗ → 靠 `[data-turn-start]`(值 = 轮号 ✓)反推 ✓,**但只有"行之前和之后各有一个已渲染边界"时才写** ✓(`turn/<n>/row/…` ✓)。虚拟化下边界常常只渲染一侧 ✗ → 那就**不写轮次** ✓(绝不猜 ✗)。假 DOM 的 `compareDocumentPosition` 用"根的后代序号"实现 ✓(不要用 `ownerDocument` ✗,假件没有 ✓)。
|
|
27
27
|
- **行级路线**:`#/session/<id>/row/<类>/<作用域>/<id>` ✓(身份 = `data-trajectory-row-key` ✓,值是 `%00` 分隔的三元组 ✓ → 拆成路径段 ✓);家系视图的 `data-dshts-turn`(轮号 ✓)按 `turn/<n>` 写 ✓。**点击处理器必须"行优先"** ✓:轨迹行本身就是按钮 ✓,而处理器开头的 `closest([data-chat-turn])` 会直接把非轮次元素挡掉 ✓(历史 bug ✓)。反映**纯被动** ✓(不 preventDefault ✓)。驱动 = 尽力 `scrollIntoView` ✓(虚拟化未渲染的行**不假装** ✓,返回 false ✓)。canonical 里位置的优先级 **行 > 视图 > 轮次** ✓。
|
|
28
|
-
- **会话视图路线**:`#/session/<id>/view/<viewId>`(有工作区时跟在会话 id 之后 ✓)。**`uiConversation` 服务在根插件里读不到** ✗(实测 `ctx.get('uiConversation')` = undefined ✓,它是会话作用域内的 ✓)—— **视图 id 从槽位读** ✓:`ctx.slots.entries('conversation.view')` 的 `options.id`
|
|
28
|
+
- **会话视图路线**:`#/session/<id>/view/<viewId>`(有工作区时跟在会话 id 之后 ✓)。**`uiConversation` 服务在根插件里读不到** ✗(实测 `ctx.get('uiConversation')` = undefined ✓,它是会话作用域内的 ✓)—— **视图 id 从槽位读** ✓:`ctx.slots.entries('conversation.view')` 的 `options.id` ✓,`options.label` 就是页签上印的字 ✓(可能是函数 ✓)。头部页签容器是 **`[data-conversation-tabs]`** ✓、页签是 `button[role="tab"][aria-selected]` ✓(**没有 id 写在 DOM 里** ✗)。
|
|
29
|
+
- **id 与页签按「文字 ↔ label」对齐** ✓(2026-10-27 修):官方头部只渲染 `visibleTabs` ✓,**放不下的视图进「…」菜单** ✗(`visibleTabs = preferred.slice(0, visibleViewLimit)` ✓)—— 所以「页签数 = 注册数」**不成立** ✗(用户 2026-10-27 截图实测:4 个页签 + 「…」,地址栏**不再写视图段** ✗)。现在:选中页签的 `textContent` 去槽位 label 里**唯一匹配** ✓ → 得到 id ✓;匹配不到才退回**位置对齐**,且**只在数量完全相等时** ✓(宁可没有,也不写错 ✗)。
|
|
30
|
+
- **还没做的**:选中视图被折叠进「…」时,DOM 里没有 `aria-selected` 的页签 ✗ → 反映拿不到它(少写视图段 ✓ 不写错 ✓);要修得读「…」按钮的激活态 ✗。默认视图(第一个 ✓)不写进链接 ✓。
|
|
29
31
|
- **视图驱动必须排在工作区/会话分支之前** ✓,否则那两条会先 `return` 把它吞掉 ✓(历史 bug ✓);且**不能被 `source === 'hash'` 挡住** ✓(粘贴视图链接就是要切过去 ✓)。
|
|
30
32
|
- **工作区路线**:`#/workspace/<工作区名>/session/<会话id>`(侧栏形状 ✓)。工作区行是 `data-row-key="workspace:<uuid>"` ✓、**行文本就是工作区名** ✓(无 `data-workspace-*` ✗,实测 ✓);会话所属工作区 = 树里**它之前最近的那个 workspace 行** ✓(`workspaceNameOf()` ✓,行锚点与反映共用 ✓)。名字是**装饰** ✓:`sameRoute` 对 workspace 路线只比**会话 id** ✓(名字过期也能落地 ✓);读不到工作区行就退回 `#/session/<id>` ✓(不猜 ✓)。
|
|
31
33
|
- **行锚点的 href 也要带工作区** ✓(右键"复制链接"直接给出带归属的链接 ✓)。
|
|
@@ -60,9 +62,26 @@
|
|
|
60
62
|
- **观察者的 `attributeFilter` 必须列全所有已发布的页面钩子**:只列 `data-plugin-detail` 曾让任务看板的重试通知饿死(历史 bug,别删名单项)。
|
|
61
63
|
- **未落地的链接要有定时重试**(`LINK_RETRY_MS` = 500ms):只靠 DOM 通知不够,启动后界面会安静下来,一次被吞掉的重试就永远不来了。
|
|
62
64
|
- **切换主面板前先 `beginNavigation()`**(`ctx.layout` 的配套动作,可选面,缺失即跳过)。
|
|
63
|
-
- **设置面板是覆盖层**:`#/settings` ✓(裸段位 ✓,`ROUTE_PATTERN` 支持 `#/<kind>` ✓)与 `#/settings/<分区>` ✓。反映读 `settingsOverlayOf()` ✓(**官方 `data-shortcut-modal="settings"` 优先** ✓,见上条
|
|
65
|
+
- **设置面板是覆盖层**:`#/settings` ✓(裸段位 ✓,`ROUTE_PATTERN` 支持 `#/<kind>` ✓)与 `#/settings/<分区>` ✓。反映读 `settingsOverlayOf()` ✓(**官方 `data-shortcut-modal="settings"` 优先** ✓,见上条 ✓)。
|
|
66
|
+
- **座位是单占的:入口可能是「菜单里的一项」** ✓(2026-10-27 起,2026-10-28 补全):`[data-slot="sidebar.settings"]` 里放的是 `settings.launcher` 的**占位者** ✓ —— 空位时是外壳自己的齿轮(`button[aria-haspopup="dialog"]` ✓,一点就开浮层 ✓);被占位时可能是**账号行**(实测桌面观察器:`haspopup: menu` ✓、标签就是账号名 ✓、座位里**根本没有** dialog 触发器 ✗)→ 一点只弹菜单 ✗,浮层要再点菜单里的「设置」项 ✓。**只点齿轮 = 菜单开着、浮层没开** ✗ → 所有设置链接 8 秒后过期 ✗(用户实测"打不开 `#/settings/账号与余额`" / "打不开 `#/settings/内置插件`" ✓)。
|
|
67
|
+
- **驱动分步、每次调用只走一步、从 DOM 重新判断** ✓(`openSettings`):① 浮层开着 → 点分区(原有逻辑 ✓);② 屏上已经有「设置」项 → 点它 ✓;③ 否则**按一次**启动器 ✓(优先座位里的 `[aria-haspopup="dialog"]` ✓,否则座位里第一个按钮 ✓)。收敛:按启动器 → 点设置项 → 点分区 ✓。
|
|
68
|
+
- **绝不重复点启动器** ✗:它是开关 ✓,重试循环会把菜单开了又关 ✗(与模型页卡片同一个陷阱 ✓)。判据是**每个链接一份状态** ✓(`settingsLauncherPressed` ✓,`dropLink()` 复位 ✓)—— **别用「页面上有没有可见 popover」来决定要不要按** ✗:页面里常驻别的 popover ✓,那样会让驱动**永远不按** ✓ → 链接全部打不开 ✓(2026-10-28 实测踩到,用户当场报"更差了"✓)。
|
|
69
|
+
- **屏上的设置项** ✓:优先在**可见的 `[role="menu"]`** 里找 ✓;不在菜单里时**只认 `role="menuitem"`** ✓(随便一枚叫「设置」的按钮不是它的 ✓)。标签 `/设置|settings/i` **包含**匹配 ✓(`Settings⌥⌘,` ✓ 前缀/后缀都行 ✓)。
|
|
70
|
+
- **「可见」的判据** ✓(`onScreen`):不在 `[hidden]` 祖先里 ✓、`getClientRects().length > 0` ✓ —— popover 关掉后**仍挂在 DOM 里** ✗,`[role="menuitem"]` 照样查得到 ✓,点隐藏项什么也不会发生 ✓,还会让启动器再也按不到 ✗。**别用 `offsetParent` 判断** ✗:`position:fixed` 的弹层它恒为 null ✓(而弹层正是 fixed ✓)。
|
|
71
|
+
- **设置项按标签匹配,不限于 `[role="menuitem"]`** ✓:实测两种菜单都有(`Settings⌥⌘,` ✓ / 账号菜单里的「设置」✓)→ 逐个选择器查(`[role="menuitem"]` / `button` / `[role="option"]` / `a` ✓,**不用逗号列表** ✗ 小 DOM 桩不支持 ✓),标签 `/设置|settings/i` **包含**匹配 ✓(不要求开头 ✗:标签可能带快捷键尾巴或前缀 ✓)。
|
|
72
|
+
- **启动器要用「指针按下」按,不是裸 `click()`** ✓(2026-10-28 实测):座位上的账号行是 popover 触发器 ✓,`element.click()` 对它**没有反应** ✗ —— 观察器日志里那个菜单的内容(`设置⌘,` / `意见反馈` / `退出登录` ✓)**只在读者手点时出现过** ✗,驱动按了却从没开过 ✗ → 链接永远打不开 ✗。现在 `pressElement()` 派发 `pointerdown` / `mousedown` / `pointerup` / `mouseup`(带元素中心坐标 ✓)+ `click` ✓;构造器缺失就跳过 ✓(最小 DOM 仍能跑 ✓)。
|
|
73
|
+
- **`openSettings` 以浮层优先** ✓:浮层开着 → 只点分区 ✓(裸 `#/settings` 什么都不点 ✓);浮层关着才走「屏上的设置项 → 启动器」✓。**旧结构会在裸链接上反复按启动器** ✗(把菜单开了又关 ✗,单测当场抓到 ✓)。
|
|
74
|
+
- **首选:走座位的 store,一次点击都不用** ✓(2026-10-28):`ctx.slots.entries('sidebar.settings')[0].store` → `store.create().actions.openSection(id)` ✓(`dsh-usage` 同款用法 ✓,`actions` 实测是 `{open, close, select, openSection}` ✓,`openSection(id)` 同时置 `activeId` 与 `open = true` ✓)。分区的 **id** 从 `settings.section` 槽位按**标签**映射 ✓ —— 外壳自己就是 `entries('settings.section').map(e => ({id: e.options.id, label: …}))` ✓,所以这是同一个列表 ✓。**store 不在 / 标签映射不上才退回 DOM 重放** ✓。**为什么必须优先**:座位被账号行占位时,驱动**能把菜单按开** ✓(日志实测 `popovers: 设置⌘,/意见反馈/退出登录` ✓)却**点不动那个菜单项** ✗ → 链接永远不落地 ✗(用户实测:overlay 只在他手点时开过 ✓,链接在飞时一次都没开 ✓)。
|
|
75
|
+
- **驱动辅助函数注意作用域** ✗:**模块作用域的函数不能用宿主作用域里的 `attempt`** ✗(`attempt is not defined` ✗ → 被 `guard` 吞掉 → 整段驱动静默中断 ✗ —— 2026-10-28 自己踩到 ✓,单测抓住 ✓)。模块作用域一律 `attemptValue` ✓。
|
|
76
|
+
- **「打不开」那行不能拿原始字符串比** ✓(2026-10-28 修):读者交上来的是**他读到的字** ✓(设置分区就是原始中文 ✓),而 canonical 是**百分号编码** ✓ → 字面比较会把**已经落地**的链接报成打不开 ✗(实测:浮层已经开在那个分区上 ✓,行里却写着「当前 settings:内置插件」✗ —— 用户当场指出"能打开"✓)。现在 `landedFragment()` 先比 `readableFragment()` 两边 ✓(解码后相等即落地 ✓),再比**路由形状** ✓:设置分区按**前缀** ✓(裸 `#/settings` 被任意分区满足 ✓、命名分区必须同一个 ✓)、面板页按**前缀** ✓、会话/工作区仍按会话 id ✓(工作区名是装饰 ✓)。
|
|
64
77
|
- **设置分区的身份 = 显示名**(实测无稳定 id ✗:导航项只有 `aria-current="true"` ✓,无 `id`/`data-*`/`aria-controls` ✗,右侧内容区无 `data-*` ✗)。反映:在覆盖层里找 `aria-current="true"` **且 `closest('nav') !== null`** 的按钮 ✓,取它的标签文字 ✓;驱动:按标签(先精确、再忽略大小写)点 ✓。**换语言/改文案会让旧分区链接失效** ✗(重新点一次即写出新链接 ✓)。
|
|
65
|
-
-
|
|
78
|
+
- **没有 ARIA 的行:一条很窄的类名兜底** ✓(2026-10-28):实测有的行**只有样式类** ✗(`dsh-client-ui-market` 的过滤 chip:`…_filterChip` / `…_filterChipOn` ✓,一个 `aria-*`、`data-*` 都没有 ✗ —— 上面那排 `Web 插件/皮肤/创意工坊` 反而有 `role="tab"` + `aria-selected` ✓,所以只有它写进了链接 ✓)。兜底规则:类名**以** `On` / `Active` / `Selected` / `Current` / `Checked` **结尾** ✓、同一父元素下**恰好一个**命中 ✓、该行至少两个控件 ✓ —— 命中不了就不算一行 ✓(**别把这条放宽** ✗:它是"猜",只配在完全没有契约时兜一下 ✓;哪天真有了 `aria-pressed` 就该删掉 ✓)。
|
|
79
|
+
- **控件的标签只取它自己的文字** ✓(`controlLabelOf` ✓):实测 chip 读作 `宠物 12` ✓,`12` 是嵌套徽标里的计数 ✗ —— 计数会变 ✗,不是身份 ✗。所以只拼控件**自己的文本节点** ✓(没有就退回 `textContent` ✓),链接写成 `…/宠物` ✓,数字变了也不失效 ✓。
|
|
80
|
+
- **AI 给链接 = 一条可用的入口** ✓(2026-10-29 实测确认):模型没有 DOM ✗,但它可以把链接**写进回复** ✓ —— 读者点一下就等价于"粘进地址栏" ✓:点击改的是 `location.hash` ✓ → `hashchange` ✓ → `reconcile('hash')` ✓ → **驱动照样发生** ✓(用例:`a settings link that arrives as a plain hash change opens the panel` ✓,实测**当场**就打开 ✓,连重试都不用 ✓)。所以「让 AI 提供链接来触发行为」**今天就有** ✓,不需要工具 ✗。
|
|
81
|
+
⇒ **只有"AI 自己动手、读者一下都不点"才需要宿主半区** ✗:本插件宿主半区是空操作 ✓(零依赖 ✓),要做工具得先找到一条**宿主→页面**的通道 ✓ —— 扫过 app 包清单(有 `dsh-client-connection` ✓ / `dsh-client-modules` ✓)但**没有公开文档** ✗,属于"要先逆向确认"的活 ✓。**别为了这个打破"只有浏览器半区"** ✗,除非确认通道存在 ✓。
|
|
82
|
+
- **查询结果不是数组** ✗(2026-10-28 血的教训):`querySelectorAll` 给的是 **NodeList** ✗,而**假 DOM 当年返回真数组** ✗ → 于是一整类 bug 在测试里全绿 ✓、在真机上整段静默跳过 ✗。**`attemptValue(() => x.querySelectorAll(...))` 后面接 `Array.isArray` 的写法一律是死的** ✗ —— 要么在 attempt **里面**就展开 ✓(`[...x.querySelectorAll(...)]` ✓),要么直接 `for...of` 迭代 ✓。这一轮一次抓出**三处** ✓:设置行的扫描(`settingsRowsOf` ✗ —— 它正是"连页签层也没了"的真凶 ✓,`.24` 用 `for...of` 所以是好的 ✓)、`visibleMenuOf` ✗、`settingsMenuItemOf` ✗(后两处 = "设置项按不动"的另一半原因 ✓)。**假 DOM 现在返回 NodeList-like** ✓(`nodeListOf` ✓)—— **别改回真数组** ✗,它就是为了让这类 bug 跑得出来 ✓。
|
|
83
|
+
- **设置路线是任意层的** ✓(2026-10-28):`#/settings/<分区>[/<行 1 的选中>[/<行 2 的选中>…]][/provider/<id>]` ✓。**一行** = 同一个父元素下、都带同一个标记(`aria-selected` ✓ 或 `aria-pressed` ✓)且**恰好一个为 `true`** 的一组控件 ✓(实测:`Web 扩展 / 创意工坊` 里还有一行 `编辑推荐 / 皮肤 / 宠物 / 插件 / 预设` ✓,再下面还有 `全部 / 角色扮演` ✓)。反映 = 按**文档顺序**把每行的选中标签拼起来 ✓(`compareDocumentPosition` 排序 ✓);驱动 = **逐行点** ✓,且**每一行只在前一行匹配到的那一行之后找** ✓ —— `皮肤` 既是分区页签又是子行标签 ✗,游标把它们分开 ✓;**每次调用只推进一层** ✓(点开的那一行要等下一次渲染 ✓,重试接上 ✓)。
|
|
84
|
+
- **设置分区的身份 = 显示名**,页签/子行/筛选同理 ✓(`[role="tab"]` 的 `aria-selected` ✓、开关行的 `aria-pressed` ✓),身份都是**标签文字** ✓;`sameRoute` 与 `landedFragment` 都按**前缀**比 ✓(少写几层也算落地 ✓、多出的层是细化 ✓)。
|
|
66
85
|
- **`aria-selected` 也必须观察** ✓(与 `aria-current` 同理:切换页签只改它 ✓)。
|
|
67
86
|
- **设置链接按"前缀"落地** ✓:裸 → 分区 → 页签逐级 ✓;命名了页签的链接必须看到那个页签 ✓,只命名到分区的链接被该分区任意页签满足 ✓。
|
|
68
87
|
- **`aria-current` 必须在 DOM 观察者的 `attributeFilter` 里** ✓:切换分区只改这一个属性 ✓,漏了它地址栏就永远停在第一次看到的分区(历史 bug ✓)。
|
|
@@ -105,9 +124,37 @@
|
|
|
105
124
|
- **调试不在产品里** ✓:`dev/inspect/` 是一个**独立观察器插件** ✓(`private: true`,靠根 `files` 白名单不进 npm ✓),只读公开契约(`#dsh-url-router-titlebar`、`data-dsh-url-router-platform`、`[class*="titleRow"]`、`.dshpm-trigger`)✓。判据三条:`bars` 恒为 1 ✓、`onTop` ✓、`leaves` 有没有被压住 ✓。**别再把调试代码写回 `lib/client.js`** ✗ —— 发布物必须是跑过测试的那一份 ✓。
|
|
106
125
|
- **假 DOM 不支持 `#id` 选择器** ✗:mac 侧的新代码对 `getElementById` / `querySelectorAll` 全部用了可选调用 ✓,测试里缺失即跳过 ✓。
|
|
107
126
|
|
|
127
|
+
## 电源菜单路线(2026-10-27 起)
|
|
128
|
+
|
|
129
|
+
用户明确要求(2026-10-27):`#/power` = **打开电源菜单**,只开不关 ✓。
|
|
130
|
+
|
|
131
|
+
- **段位** ✓:`#/power`(无身份 ✓,一个应用只有一个菜单 ✓)。
|
|
132
|
+
- **反映** ✓:`powerMenuOpenOf()` —— 契约属性 `data-dsh-power-menu`(值 `open` ✓ 或空 ✓)优先 ✓,其次是这个插件的 `.dshpm-menu` / `.dshpm-trigger[aria-expanded="true"]`(`getElementsByClassName` ✓,假 DOM 没有就跳过 ✓)。菜单开着时 canonical **就是它** ✓(盖在所有表面之上 ✓),关掉后自动回到底下那个界面 ✓。
|
|
133
|
+
- **驱动** ✓:点 `.dshpm-trigger`(或 `data-dsh-power-trigger`)**一次** ✓;**已经开着就不点** ✗ —— 它是开关 ✓,重试循环会把菜单开了又关(与模型页卡片同一个陷阱 ✓)。
|
|
134
|
+
- **动作段:`#/power/shutdown` / `#/power/restart`** ✓(2026-10-29 用户明确要求 ✓)。**动作是命令,不是地方** ✓ —— 所以它有一条**全仓唯一的来源闸** ✓:只认**读者亲手敲进地址栏**的 fragment ✓(`typedClaim` ✓,由 `driveTypedFragment` 升旗 ✓、**每一次 hash 读都降旗** ✓)→ 聊天消息 / README / **AI 回复**里的链接点了**不会关掉应用** ✓(用例 `a power action that arrives as a hash change is not run` ✓)。**一次 claim 只发一次** ✓(`powerActionSent` ✓,`dropLink` 复位 ✓);**boot 的 hash 不触发** ✓(boot 走 `safeReadLink` ✗ 不是 typed ✓)→ 重启后残留的 `#/power/shutdown` 不会变成重启循环 ✓。
|
|
135
|
+
- **动作的 id 必须能过 canonical** ✓(2026-10-29 自己踩到 ✓):`routeHash('power', …)` 原来**无条件**返回 `#/power` ✗ → 地址栏拿"规范化后的 wanted"(= 裸 `#/power` ✗)去和屏幕比 ✗ → 一条**已经发出**的命令被判「打不开」✓。现在 `routeHash` 保留动作段 ✓(`#/power/restart` ✓),并且 `arrived()` **先**判"这是不是一条命令" ✓(是就直接算落地 ✓)—— 因为屏幕上永远不会有东西等于一条命令 ✓(应用正在退出 ✓)。
|
|
136
|
+
- **先走它自己的 UI,端点只做兜底** ✓(2026-10-29 用户要求 ✓):`#/power/<动作>` 的驱动是**两级** ✓ —— ① 开它自己的菜单 ✓ → 点它发布的**那一项** ✓;② **菜单给 `POWER_ACTION_MENU_LIMIT = 4` 次驱动机会**(≈2 秒 ✓)都没交出那一项 → 走端点 ✓。这样它的流程会跑 ✓(它自己的遮罩 ✓,将来它若加二次确认也会经过 ✓),而且**永远不会静默 no-op** ✓(这是最重要的性质 ✓)。
|
|
137
|
+
- **绝不按标签点不可逆的项** ✗(2026-10-29 血的教训 ✓):`dsh-power-menu-mac` 的两项只有标签 `关闭DSH` / `重启DSH` ✓,**差一个字** ✗ —— 用户报"`#/power/restart` 激活的是关闭" ✓。猜标签点一个**会结束应用**的按钮,猜错不可恢复 ✗ → **只认契约属性** ✓(`data-dsh-power-action="<动作>"` ✓,限定在 `powerMenuOf()` = `[data-dshpm-menu]` ✓ 或 `.dshpm-menu` ✓ 里 ✓),**没有契约属性就整条走端点** ✓ —— 端点 URL 里带着动作名 ✓,**结构上不可能串** ✓。要给 A 路径加契约:给它的两个 `MenuItem` 各加一行 `'data-dsh-power-action': 'shutdown' | 'restart'` ✓。
|
|
138
|
+
- **焦点:真机制在插件那边,修法也在那边** ✓(2026-10-29 ✓):`dsh-power-menu-mac` 原来有个 `useEffect(…, [open])` 在菜单打开时调 **`first.focus()`** ✓ —— 那是**「关闭DSH」** ✗,读者的地址栏**失焦** ✗,他**再按一次 Enter**(以为没反应 ✓)就由浏览器**原生激活焦点上的按钮** ✓ → **关闭** ✓。日志佐证 ✓:`warnings: []` ✓(我们两条路都会先写一行 ✓,一行都没有 ✓ → **不是我们点的** ✓),`attempts.inner` 停在 2 ✓(我们没走到动作 ✓)。
|
|
139
|
+
⇒ **我们在自己这边抢过一次焦点,撤了** ✗:`.35` 里 `openPowerMenu()` 之后调 `host.focusAddressBar()` ✓ —— 插件那边改好之后(删掉 `first.focus()` ✓、把 `onKeyDown` 挂到**触发按钮**上 ✓、开菜单时 `triggerRef.current?.focus()` ✓),**再抢就会把它的方向键导航弄死** ✗(`onMenuKeyDown` 靠 `document.activeElement` 找下标 ✓,焦点不在触发按钮上就收不到键 ✓)。所以 `.36` 撤掉 ✓,并留一条用例盯着"**打开菜单后不动焦点**" ✓(防止以后又去抢 ✓)。
|
|
140
|
+
**教训**:别人的插件修好之后,自己的"绕过/对抗"要**跟着撤** ✗ —— 否则两边的修法会互相打架 ✓。
|
|
141
|
+
- **跑之前拿地址栏对一次,但只拒绝"写着另一个动作"** ✓(2026-10-29 ✓):判据是 `typed.kind === 'power' && typed.id !== '' && typed.id !== link.id` ✓ —— **不能**要求"地址栏必须写着这个动作" ✗:菜单一开 ✓,屏上的 canonical 就是 `#/power` ✓(裸的 ✓),严格版会把**我们自己刚打开的菜单**当成 claim 过期 ✓ → 拒绝 ✓ → 读者要**按两次回车** ✓(用户实测 ✓)。现在:claim `restart` + 地址栏 `shutdown` → **仍然拒绝** ✓(这才是"要重启却关机"那条要防的 ✓);claim `restart` + 地址栏 `#/power` 或别的界面 → **放行** ✓(那是菜单自己的状态 ✓ 或菜单已关 ✓)。用例:`… is not refused by the bar showing its own menu` ✓ 与 `… is still refused when the bar names another action` ✓。
|
|
142
|
+
- **每条命令留一行记录** ✓:动作名 + 走的哪条路 ✓。**必须同时进 `report().warnings`** ✓(`note()` 助手 ✓)—— 只 `host.warn` 等于**没有记录** ✗:桌面窗口没有控制台 ✓,观察器读的是 `report()` ✓(2026-10-29 实测:`host.warn` 的行在日志里**一条都看不到** ✗,当时只能靠用户回报 ✓)。这是**不可逆**动作 ✓,必须有据可查 ✓。
|
|
143
|
+
- **自己写入的回声必须认出来** ✓(2026-10-29 第三次踩到,这次是真凶 ✓):`driveTypedFragment` 写完 hash ✓ → **同步**读一次 ✓(claim 是"手打" ✓)→ 但**命令型链接立刻落地** ✓(`arrived()` 对命令恒真 ✓)→ `dropLink()` 丢掉 claim ✓ → **一个 task 之后**浏览器送来**我们自己写入的回声** `hashchange` ✓ → 老代码在这里**无条件降旗** ✓ → 新建的 claim 是"非手打" ✗ → 动作分支静默早退 ✓ → **读者要按两次回车** ✓✓✓(用户实测 ✓;第二次因为 hash 没变 ✗ → 没有回声 ✗ → claim 干净 ✓)。修法 ✓:`driveTypedFragment` 记下 `typedEcho` ✓,`onHashChange` 里 `typedClaim = (当前 hash 的可读形态 === typedEcho 的可读形态)` ✓ —— **自己写入的回声仍是读者的** ✓。
|
|
144
|
+
⇒ **这个分支以前完全不留痕迹** ✗ → 这一轮只能靠"两次回车"的现象反推 ✓。现在它记一行 `ignoring power action "x": this fragment did not come from the address bar` ✓(`powerIgnored` 每 claim 一次 ✓)✓。**教训**:静默早退的分支必须留痕 ✓,否则下一次还是靠猜 ✓。
|
|
145
|
+
- **「手打」要按 claim 记** ✗(2026-10-29 自己踩到 ✓):`typedClaim` 只活一次读取 ✓ → 重试时已经降旗 ✓ → 命令**失去权威** ✓ → 兜底永不触发 ✓(单测当场抓到 ✓)。现在另有 `linkedTyped` ✓:**claim 建立时**记下它是不是手打的 ✓,整个 claim 期间有效 ✓。
|
|
146
|
+
⇒ **但按 claim 记会漏掉一种情况** ✗(同日第二次踩到 ✓,用户当场报"没反应"✓):同一条链接**先以 hash 形式来过**(读者点过一条聊天里的链接 ✓)→ claim 是"非手打" ✓;随后读者**手打同一条** ✓ → `linked.id === parsed.id` ✓ → **不建新 claim** ✗ → `linkedTyped` 保持 false ✗ → **手打的命令被自己拒了** ✓。修法:`driveTypedFragment()` **重新授权当前 claim** ✓(`if (linked !== undefined) linkedTyped = true` ✓,并重置 `powerActionSent` / `powerActionAttempts` ✓)—— **手打就是权威** ✓,不管同一个 fragment 之前干过什么 ✓。用例 `a typed power action re-arms a claim that arrived from somewhere else` ✓(**把修复关掉它确实会红** ✓,验过 ✓)。
|
|
147
|
+
- **端点有两个包、两条路径,必须都试** ✓(2026-10-29 血的教训 ✓):桌面版装的是 **`dsh-power-menu-mac`** ✓,它的 base 是 **`/api/dsh-power-menu-mac`** ✓(`lib/index.js` 里 `const BASE = '/api/dsh-power-menu-mac'` ✓,另有 `GET …/health` 与 `GET …/plan` ✓);而普通包 `dsh-power-menu` 用 `/api/dsh-power-menu` ✓。**打错那条 = 静默 404 = 什么都不发生** ✓ —— 用户看到的就是"不能重启" ✓(我自己踩到 ✓:读了 `dsh-power-menu` 就写死了路径 ✓,**没先确认他装的是哪个包** ✗)。现在 `POWER_ACTION_BASES` 按序尝试 ✓(mac 优先 ✓,非 ok 就换下一个 ✓),全失败才记一条诊断 ✓(桌面窗口没有控制台 ✓,静默 no-op 是最坏的答案 ✓)。
|
|
148
|
+
- **实现 = 电源插件自己公开的端点** ✓:`POST <base>/{shutdown,restart}` ✓(读两个包的 `lib/client.js` 得到 ✓)—— **一次 `fetch`,不碰 DOM、不看语言、不等菜单渲染** ✓。比"开菜单再点那一项"轻得多 ✓,也**不需要它改任何东西** ✓(早先提的 `data-dsh-power-action` 建议**作废** ✓)。
|
|
149
|
+
- **菜单项本身仍然永不点击** ✓(裸 `#/power` 照旧只开菜单 ✓,一条老硬约束 ✓)—— 只有**显式动作段 + 手工输入**这一条路会执行 ✓。
|
|
150
|
+
- **代价(用户已知并接受)** ✓:浮层是瞬时状态 ✓ —— 地址栏会随开合来回跳 ✓,最近列表会记一条 `power:` ✓。
|
|
151
|
+
- **它读的是别的插件的 DOM** ✓(第一个这种)✓:所以契约属性优先、类名兜底 ✓。要往电源插件里加契约就加 `data-dsh-power-menu` / `data-dsh-power-trigger` ✓,别改类名 ✗。
|
|
152
|
+
- 用例:`an open power menu names itself in the address bar` ✓、`a power link opens the menu, and never presses anything inside it` ✓、`a power link on an already open menu presses nothing` ✓。
|
|
153
|
+
|
|
108
154
|
## 地址栏偏好契约(2026-10-26 起,改之前先读)
|
|
109
155
|
|
|
110
|
-
-
|
|
156
|
+
- **三个偏好在 `localStorage`** ✓:`dsh-url-router-settings` = `{selectAllOnFocus, hoverReveal, recentLimit}` ✓(默认 `true` / `false` / `8` ✓);`dsh-url-router-recent` = `[{hash, at, source}]`,最新在前 ✓,`source` 是 `visit` | `paste` ✓。它们**不属于 profile** ✓(是"这台机器这个窗口"的偏好 ✓),所以没有进 `dsh.profile`,也没有宿主接口 ✓。
|
|
157
|
+
- **悬浮才显示** ✓(2026-10-27):`hoverReveal` 为开时,bar 带 `data-dsh-url-router-hover` ✓,**只降透明度**(`opacity:0` ✓,**别动 `pointer-events`** ✗ —— 元素还得能被悬浮到,那正是"传感器" ✓)。`:hover` 不用 CSS 判定:JS 在 bar 的 `mouseenter` / `mouseleave` 上写 `data-dsh-url-router-shown` ✓(列表是 bar 的子元素 ✓,指针进列表不会触发父级 `mouseleave` ✓);输入框 `focus` 也点亮 ✓;`blur` 走 **0ms 超时**再收 ✓(列表项的 `mousedown` 会 `preventDefault`,但同一次任务里布尔的焦点状态还没落定 ✓);列表开着不收 ✓。偏好切换通过 `subscribeSettings` 实时应用 ✓(不必重启 ✓)。**mac 与 Windows 两套样式表都要加** ✓(两个常量各自一份 ✓)。
|
|
111
158
|
- **首次点击全选** ✓:未聚焦时的 `mousedown` 里 `preventDefault` + `focus()` + `select()` ✓ —— 只 preventDefault 不 select 没用 ✓,只 select 不 preventDefault 也没用 ✓(按下落光标那条默认行为会把选区抹掉 ✓)。**已聚焦时再点必须是普通光标行为** ✓(否则拖选与定位全废 ✓)。`focus` 事件那条路也要选 ✓(Tab / 脚本聚焦 ✓)。
|
|
112
159
|
- **当前链接不能当过滤词** ✗:框里平时就写着当前界面 ✓ —— 按 `input.value` 过滤会在聚焦瞬间把列表清空 ✓(踩过 ✓)。**只有读者真的编辑过(`input` 事件)才过滤** ✓(`narrowed` 标志 ✓)。
|
|
113
160
|
- **最近列表的身份 = 界面** ✓,不是坐标 ✓:`recentIdentity()` 把 `turn` / `row` / `view` / `pane` / 面板内部页都归并到所在表面 ✓(`workspace` 形状取里面的会话 id ✓)。否则在一个对话里点几十行就把列表刷满 ✓。**当前屏幕不列出** ✓。
|
|
@@ -133,6 +180,20 @@
|
|
|
133
180
|
- **任务详情** ✓:行是 `li > button`(`aria-label` = 任务名 ✓,`aria-expanded` = 是否展开 ✓,`aria-describedby` 末尾是 `-metadata-<任务 id>` ✓ 实测 ✓);详情是 `<aside aria-label="Task details">` ✓,页签是 `button[data-detail-tab="rule|records"]` + `aria-selected` ✓(实测 ✓)。**任务 id 只能从 `aria-describedby` 的末段取** ✓(`useId` 前缀不稳定 ✓)。
|
|
134
181
|
- **筛选先于任务** ✓:筛完才可能有点得到的行 ✓,所以驱动顺序固定 ✓。
|
|
135
182
|
|
|
183
|
+
## 子代理路线(2026-10-26 起)
|
|
184
|
+
|
|
185
|
+
子代理会话**也是会话** ✓(能打开、有轮次、有链接 ✓),但只有 id 的链接看不出它是谁的子代理 ✗(用户 2026-10-26 原话)✓。
|
|
186
|
+
|
|
187
|
+
- **段位** ✓:`#/session/<子会话 id>/subagent-of/<父>` ✓(工作区形状同样适用:`#/workspace/<名>/session/<子>/subagent-of/<父>` ✓)。`<父>` **默认写父会话的标题** ✓(目录里的 `title` ✓,读起来就是「谁的子代理」✓),没有标题 / 标题恰好是保留字时**退回父会话 id** ✓。
|
|
188
|
+
- **身份 = 子会话** ✓(打开的是它 ✓);**父是装饰** ✓ —— 落地只比子会话 id ✓,父写错/过期/改成标题都照样落地 ✓(和 workspace 名一个道理 ✓)。**代价**:URL 里没有父 id 了,不能靠改 URL 跳到父会话 ✗(要跳就点聊天里的子代理卡片 ✓)。
|
|
189
|
+
- **父段的两种形态要分得清** ✓:`isSessionIdLike(父段)`(uuid,可带 `session-` 前缀)→ 按**地址对象**打开 ✓;否则当它是**标题** ✓ → 用子会话自己的 id 打开 ✓(实测裸 id 也能开 ✓)。**别把标题当 `parentSessionId` 传** ✗。
|
|
190
|
+
- **标题不能变成位置** ✓:`ROUTE_RESERVED_TOKENS`(turn / row / view / pane / subagent-of / provider / filter / task / rule / records / session / workspace / settings / panel)里的标题一律退回 id ✓ —— 下游解析器按**段名**找位置(`indexOf`),标题恰好叫 `turn` 就会被当成轮次 ✗。标题还会 `trim` + 折叠空白 + 截到 `SUBAGENT_TITLE_LIMIT = 80` ✓。
|
|
191
|
+
- **反映** ✓:父 id 先从目录取 ✓(`list.byId[子].parentSessionId` ✓,`SessionListEntry` 自带 ✓),再兜底 `ctx.sessions.subagentAddress(子 id)` ✓;标题从 `list.byId[父].title` ✓(**别读侧栏行文本** ✗ —— 行文本带着相对时间尾巴,实测是「测试子代理回复数字18h」✓)。拿不到就是普通会话路线 ✓(不猜 ✓)。
|
|
192
|
+
- **驱动** ✓:`uiWorkspace.openSession(target)` 的 `target` 是 **`SessionTarget = SessionId | SubagentAddress`** ✓ —— 链接带了父,就传**地址对象** ✓ `{ parentSessionId, childSessionId, mode }` ✓,`mode` 从目录回读(`one-shot` / `continuable` / `unknown`)✓,回读不到就 `unknown` ✓(官方在读取子历史时解析 ✓)。**别用裸 id 打开子代理** ✗。
|
|
193
|
+
- **最近列表的身份仍是子会话** ✓(`recentIdentity` 已经是 `session:<id>` ✓,装饰段不参与 ✓)。
|
|
194
|
+
- **子代理不出现在侧栏** ✓(`origin === 'subagent'` 的会话被列表过滤 ✓)→ 它没有行锚点 ✓,链接来自聊天里的子代理卡片或手写 ✓。
|
|
195
|
+
- 用例:`a subagent conversation says whose subagent it is, by the parent's title` ✓、`a parent with no title (or a reserved one) falls back to its id` ✓、`a subagent link naming the parent id opens the child through its address` ✓、`a subagent link whose decoration is a title opens the child by its id` ✓。真机(隔离宿主 + 真实数据)实测 ✓:粘裸子会话 id → 子会话打开(7 个轮次)✓、地址栏**自己补回** `/subagent-of/测试子代理回复数字1` ✓;粘带父 id 的链接 → 地址栏落地成同一条 ✓。
|
|
196
|
+
|
|
136
197
|
## 右栏 pane 的两条修正(2026-10-26 起)
|
|
137
198
|
|
|
138
199
|
- **收起的右栏不算 pane** ✓:收起来时页签仍挂在 DOM 里 ✗(实测:关掉右栏后 `active` 仍返回上一个页签 ✗),所以 canonical 先问 `[data-rightbar-collapsed]` ✓(值为 `true` 或空即收起 ✓),收起时**不写 `pane/…`** ✓。会话与面板两条分支都要判 ✓(只改一条会漏 ✓)。
|
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ dsh plugin --profile web add "link:/path/to/dsh-url-router"
|
|
|
42
42
|
| `#/panel/schedules/task/<任务 id>` | 自动化任务里某条任务的详情(规则页) | 面板自己的任务行 |
|
|
43
43
|
| `#/panel/schedules/task/<任务 id>/records` | 同一条任务的「运行记录」页(默认的 `rule` 不写) | 面板详情里的页签 |
|
|
44
44
|
| `#/settings/<分区>/provider/<提供商 id>` | 模型页里点了「编辑」展开的那张提供商卡片(id 是配置里的 key,不是显示名) | 卡片自己的「编辑」按钮 |
|
|
45
|
+
| `#/power` | 电源菜单(浮层)。链接只能**打开**它,关闭/重启永远要人点 | 电源按钮 |
|
|
46
|
+
| `#/session/<子会话 id>/subagent-of/<父标题>` | 子代理会话:一眼看出它是谁的子代理(父是装饰,打开的是子会话;父没标题时退回父 id) | 聊天里的子代理卡片 |
|
|
45
47
|
|
|
46
48
|
输入侧宽容:`sessions`/`panels` 复数、百分号编码都接受,一律规范化成上面的形式。**不认识的段位不碰**,只留一条诊断(留给以后的路由器,或者别的插件写的深链形态 ✓ —— 一个 fragment 只能有一个写者 ✓,所以读到不属于本插件的形态时会**明确说一句**,而不是静默 ✗)。
|
|
47
49
|
|
|
@@ -79,13 +81,14 @@ macOS 的标题栏是**原生**的(左边红黄绿三个灯),页面里没
|
|
|
79
81
|
- 输入什么 ✓:任何 `#/…` 路由(会话 / 工作区 / 视图 / 轮次 / 行 / 面板 / 设置三层 / 右栏 pane)、**裸会话 id**、或**整条链接**(只取它的 fragment)✓;回车写 hash → 浏览器触发 `hashchange` → 落地走的就是"粘贴链接"那条已验证的路 ✓。
|
|
80
82
|
- 它同时是**反映** ✓:本插件每次写地址栏都会同步进这个框 ✓,所以它显示的就是"现在在哪" ✓。Escape 还原 ✓;认不出的文本不动地址栏 ✓。
|
|
81
83
|
|
|
82
|
-
###
|
|
84
|
+
### 设置页:地址栏的三个偏好
|
|
83
85
|
|
|
84
|
-
在**插件管理器**里找到本插件那一行 → 右侧的配置箭头(也就是 `#/panel/plugins/dsh-url-router/dsh-url-router
|
|
86
|
+
在**插件管理器**里找到本插件那一行 → 右侧的配置箭头(也就是 `#/panel/plugins/dsh-url-router/dsh-url-router`)就是本插件的设置页。三个偏好跟着这台机器走(`localStorage`),改完立刻生效,不用重启:
|
|
85
87
|
|
|
86
88
|
| 偏好 | 默认 | 说明 |
|
|
87
89
|
| --- | --- | --- |
|
|
88
90
|
| 首次点击地址栏时全选 | 开 | 像浏览器地址栏:**第一次**点进去,框里原来的链接整条选中,直接输入就替换它;已经聚焦时再点,才是放光标。关掉后就是一个普通文本框。 |
|
|
91
|
+
| 悬浮时才显示地址栏 | 关 | 平时隐去(只降透明度,位置和尺寸都不变),鼠标移到地址栏所在的位置才出现;输入框有焦点、或最近列表开着时不会消失。 |
|
|
89
92
|
| 记住最近链接的条数 | 8 | **下拉框里能看到的条数**(地址栏里正显示的那个界面不算 —— 它已经在你眼前了,离开之后才会回到列表)。最低 **0**(= 不记忆,地址栏也不再弹最近列表),**没有上限**。 |
|
|
90
93
|
|
|
91
94
|
最近列表的用法与浏览器一致:点地址栏就弹出来,输入即过滤,Esc 关掉;条目有两个来源 —— **用过的界面**(本插件写进地址栏的每一个界面 ✓)和**粘贴过的链接**(在地址栏里粘贴任何本插件认得的链接或裸会话 id ✓,落地与否都先记住 ✓)。
|
|
@@ -105,6 +108,8 @@ macOS 的标题栏是**原生**的(左边红黄绿三个灯),页面里没
|
|
|
105
108
|
- **只动 fragment**:路径与查询串从不改写(启动 `?token=…` 能活下来)。
|
|
106
109
|
- **唯一写者**:本插件拥有 fragment,请**不要**和别的写 fragment 的插件同时启用(例如 `dsh-session-url`),两者会互相覆盖。
|
|
107
110
|
- **只用 `replaceState`**:浏览面板不污染后退栈。
|
|
111
|
+
|
|
112
|
+
- **`#/power` 是唯一的浮层链接** ✓:菜单是瞬时状态(点外面就关),所以它只承诺“能打开”,不承诺“能停留” ✓;开着时地址栏写它 ✓,关掉后回到底下那个界面 ✓。**关闭/重启永远不驱动** ✗。
|
|
108
113
|
- **链接落地前,挡在前面的表面先让开** ✓:主面板选中时地址栏读到的就是面板 ✓,设置浮层在屏时读到的就是 `#/settings/<分区>` ✓ —— 这两种情况下会话**其实已经在背后打开了** ✗,屏幕上却像"没反应" ✓。所以驱动任何非设置链接之前 ✓,先 `beginNavigation()` + `selectPanel(null)` 让面板退场 ✓,再用**官方自己那条 document 级 Escape** 把设置浮层请走 ✓(模态栈是官方内部的私有注册表 ✗ 外部拿不到 ⇒ Escape 是浏览器侧唯一可用的官方关闭路径 ✓,也就是你按 Esc 走的那条 ✓)。**一条链接只按一次** ✓:不会在你正在操作页面时乱按键 ✓,代价只是万一那次没关掉、你再粘一次 ✓。设置自己的链接**不动**浮层 ✓。
|
|
109
114
|
- **轮次信息:能证明才写** ✓。助手行自己的身份里就带轮次(`row/assistant/80/32` = 第 80 轮第 32 项 ✓);**工具行不带** ✗(只有调用 id ✓)→ 于是我从视图的**轮次边界** `[data-turn-start]` 反推 ✓,**但只在行夹在两个已渲染边界之间时才写** ✓(`turn/82/row/tool/call/…` ✓)。视图是虚拟化的 ✓,边界可能只渲染了一侧 ✗ → 那时**宁可不写轮次** ✓,也绝不猜一个错的轮次 ✗。
|
|
110
115
|
- **轨迹里的每一行都有自己的链接** ✓:`#/session/<id>/row/<类>/<作用域>/<id>`(例:`row/tool/call/call_ce3a…`、`row/assistant/80/32` = 第 80 轮第 32 项 ✓;有工作区时跟在会话 id 之后 ✓)。身份来自视图自己发的 **`data-trajectory-row-key`** ✓(值是 `<类>%00<作用域>%00<id>` ✓,我把它拆成可读路径 ✓);你家 Trajectory Plus 发的是 **`data-dshts-turn`**(轮号 ✓)→ 那种行按轮次写(`turn/<n>` ✓)。点一行就反映进地址栏 ✓(**纯被动**:不 preventDefault ✓)。粘贴行链接会打开那个对话并**尽力滚到那一行** ✓ —— 视图是虚拟化的 ✓,**没渲染出来的行不会被假装滚到** ✗(不会骗你 ✓)。
|
|
@@ -119,7 +124,7 @@ macOS 的标题栏是**原生**的(左边红黄绿三个灯),页面里没
|
|
|
119
124
|
|
|
120
125
|
- **轮次链接是"位置",而且现在真能跳** ✓:点某一轮,地址栏记下 `#/session/<id>/turn/<n>` ✓;**粘贴它会打开那个对话、把历史翻页回那一轮、再滚到它** ✓(真机实测:贴 `/turn/118` → 窗口变成 118→121、目标在视口 true、最顶可见正是 118 ✓)。用的是**官方已有公开契约** ✓:`sessions.retainScope(id, source)` 拿到会话控制器 ✓ → `session.projections.faceOf('turnOutline')` 读到那一轮的 `seq` ✓ → `session.loadThrough(seq)` 翻页回去 ✓(完事 `reference.release()` ✓)→ 再按官方视口同款选择器 `[data-chat-turn]:not([hidden])` 滚到它 ✓。座位高度/相交都读不到时**不算到位** ✓,宁可继续重试也不假报 ✗。
|
|
121
126
|
- **右栏 pane 已闭环** ✓:`#/session/<id>/pane/<kind>` 与 `#/panel/<面板>/pane/<kind>` ✓(真机验证:贴链接开出对应页签 ✓、地址栏跟着切换 ✓)。页签种类由右栏自己注册 ✓(实测 7 种:`guide` / `subagentchat` / `changes-review` / `plan` / `files` / `text` / `terminal` ✓)。
|
|
122
|
-
- **设置面板已纳入**:`#/settings` ✓、**`#/settings/<分区>`** ✓、**`#/settings/<分区>/<页签>`** ✓(页签靠标准 `role="tab"` + `aria-selected` ✓,分区内任何页签都吃这一套 ✓)。分区**没有稳定 id** ✗(实测:导航项上只有 `aria-current="true"` ✓,父链是 `nav` ✓,右侧内容区一个 `data-*` 都没有 ✗;官方内部的 `openSection(id)` 也不对外 ✗)→ 所以**分区的身份就是它的显示名** ✓:反映读「`aria-current="true"` 且属于 `nav`」的那个按钮的**标签文字** ✓,驱动按标签点
|
|
127
|
+
- **设置面板已纳入**:`#/settings` ✓、**`#/settings/<分区>`** ✓、**`#/settings/<分区>/<页签>`** ✓(页签靠标准 `role="tab"` + `aria-selected` ✓,分区内任何页签都吃这一套 ✓)。分区**没有稳定 id** ✗(实测:导航项上只有 `aria-current="true"` ✓,父链是 `nav` ✓,右侧内容区一个 `data-*` 都没有 ✗;官方内部的 `openSection(id)` 也不对外 ✗)→ 所以**分区的身份就是它的显示名** ✓:反映读「`aria-current="true"` 且属于 `nav`」的那个按钮的**标签文字** ✓,驱动按标签点 ✓。**入口可能是「齿轮 → 菜单 → 设置」两级** ✓:`settings.launcher` 座位是单占的 ✓,空位时齿轮一点就开浮层 ✓,被账号行占位时一点只弹菜单 ✗(实测桌面:座位里没有齿轮 ✗)——现在分步走 ✓(浮层 → 屏上的设置项 → **按一次**启动器 ✓),**只点真正在屏上的项** ✓(关掉的菜单还挂在 DOM 里 ✗)、**启动器每个链接只按一次** ✓(它是开关 ✗,重复按会把菜单关掉 ✗)。含义:**换语言或官方改文案后,旧的分区链接会失效** ✗(但你重新点一次,地址栏就会写出新链接 ✓ —— 自洽、不过期 ✓)。裸 `#/settings` 只要面板开着就算落地 ✓,随后地址栏自动升级为具体分区 ✓。
|
|
123
128
|
- **设置页分区的身份只有显示名**(上一条的含义,别当成 bug 去找官方要 id ✗):官方那个 `openSection(id)` 里的 id **不对外** ✗,DOM 上也没有任何 `data-*` ✓ —— 所以链接里写的就是用户读到的那行字 ✓,官方改文案或换语言后旧链接失效是**设计如此** ✓。
|
|
124
129
|
- **会话内的轮次与行锚点已纳入** ✓:`#/session/<id>/turn/<n>` ✓(**反映 + 翻页 + 滚动 ✓ 全链路已闭环**)、`#/session/<id>/row/<类>/<作用域>/<id>` ✓(反映 + 尽力滚动 ✓,含可证明时的轮次归属 ✓)。轮次跳转用的是官方公开 API ✓,**没有等待任何新接缝** ✓(`uiChat.scrollToTurn` 那条请求已降级为锦上添花 ✓,讨论见 <https://github.com/deepseek-ai/deepseek-harness/discussions/9339> ✓)。
|
|
125
130
|
- **技能中心 / SSH 的内部页**:需要它们各自发布"当前实体 id + 按 id 打开"的契约(任务看板就是这么做的)。
|
|
@@ -160,6 +165,8 @@ Listed in the community plugin index: [zhu1090093659/dsh-community-plugins](http
|
|
|
160
165
|
| `#/panel/schedules/task/<id>` | one automation task's detail (its rules tab) |
|
|
161
166
|
| `#/panel/schedules/task/<id>/records` | that task's records tab (the default `rule` is not written) |
|
|
162
167
|
| `#/settings/<section>/provider/<provider id>` | the provider card whose editor is open in Models (the id from the config, not the display name) |
|
|
168
|
+
| `#/power` | The power menu (a popover). A link can only **open** it, shutdown and restart stay human actions | the power button |
|
|
169
|
+
| `#/session/<child id>/subagent-of/<parent title>` | a subagent conversation, naming whose subagent it is (the parent is decoration and falls back to its id when untitled; the child opens) |
|
|
163
170
|
| `#/panel/plugins/<package>` | an installed plugin's page |
|
|
164
171
|
| `#/panel/plugins/<item>` | a built-in plugin's page (`shell`, `agent-loop`, `subagent`, `web-search`) |
|
|
165
172
|
| `#/panel/plugins/<package>/<component>` | one component page inside that plugin's page |
|
|
@@ -182,13 +189,14 @@ The Windows desktop window draws its own title bar **inside the page**: the prel
|
|
|
182
189
|
- Accepts any `#/…` route, a bare conversation id, or a whole link (only its fragment is used); Enter writes the hash, the browser fires `hashchange`, and the link lands through the very same path a pasted link takes.
|
|
183
190
|
- It also **reflects**: every write this plugin makes to the address bar is mirrored into the box, so it always names what is on screen. Escape restores it; text that names no route is refused.
|
|
184
191
|
|
|
185
|
-
### Settings: the address bar's
|
|
192
|
+
### Settings: the address bar's three preferences
|
|
186
193
|
|
|
187
|
-
Find this plugin's row in the **plugin manager** and press its configure control (or open `#/panel/plugins/dsh-url-router/dsh-url-router` directly) — that page is this plugin's settings.
|
|
194
|
+
Find this plugin's row in the **plugin manager** and press its configure control (or open `#/panel/plugins/dsh-url-router/dsh-url-router` directly) — that page is this plugin's settings. All three preferences live on this machine (`localStorage`) and take effect the moment they change, no restart:
|
|
188
195
|
|
|
189
196
|
| Preference | Default | Meaning |
|
|
190
197
|
| --- | --- | --- |
|
|
191
198
|
| Select the whole link on the first click | on | Like a browser's address bar: the **first** click selects everything already in the box, so typing replaces the old link; a further click once the box is focused places the caret. Turned off, it is an ordinary text field. |
|
|
199
|
+
| Show the bar on hover | off | Hidden until the pointer is over it — only the opacity drops, so the spot it occupies and its size stay the same. A focused field or an open recent list keeps it visible. |
|
|
192
200
|
| Remembered links | 8 | How many links the drop-down lists (the screen already in the address bar does not count — it is where you are; it returns to the list once you leave it). The floor is **0** (no memory, and no recent list), and there is **no ceiling**. |
|
|
193
201
|
|
|
194
202
|
The recent list behaves like a browser's: focusing the bar opens it, typing narrows it, Escape closes it. Entries come from two places — **the screens this plugin named** (every view it wrote into the address bar) and **the links pasted into the bar** (anything it can open, remembered whether or not it lands).
|