dsh-url-router 0.4.0 → 0.4.2
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 +85 -2
- package/README.md +72 -5
- package/lib/client.js +1784 -87
- package/package.json +4 -3
package/AGENTS.md
CHANGED
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
## 桌面地址栏契约(2026-10-24 起,改之前先读)
|
|
84
84
|
|
|
85
85
|
- **形态**:Windows 桌面版的标题栏是**页面里画的** ✓ —— preload(`app.asar` 的 `lib/preload-app.cjs`)跑 `syncWindowsAppearance()`:给 `<html>` 打 `data-windows-titlebar`、设 `--dsh-windows-titlebar-height: 40px`,并用 shadow DOM 画菜单条(宿主 `[data-windows-menu]`,`position:fixed; top:0; left:var(--dsh-windows-menu-start,48px); z-index:1100; -webkit-app-region:no-drag`)。⇒ 那条 strip 是**页面空间** ✓,插件可以往里画 ✓。
|
|
86
|
-
-
|
|
86
|
+
- **门控**:Windows 那条路径只在 `documentElement.getAttribute('data-windows-titlebar') !== null` 时执行 ✓;Web 版**一行都不执行** ✓。macOS 走另一条分支 ✓(见下节 ✓),两者靠 `macInHeader = !windowsStrip && macDesktopWindow(root)` 互斥 ✓,`place()` 里就是一个 if/else ✓ —— **不是两个各自独立挂载的组件** ✓。
|
|
87
87
|
- **定位**:`left = 48 + 菜单宿主宽度 + 8` ✓(宽度用宿主 `getBoundingClientRect()` 量 ✓,**不要**读 shadow 内部 ✗);`right = TITLEBAR_CAPTION_WIDTH = 138px` 是 Windows 窗口按钮宽度常量 ✓(改缩放/换系统可能要调 ✓);高度一律走 CSS 变量 ✓,不要写死 40 ✓。
|
|
88
88
|
- **交互**:`-webkit-app-region: no-drag` 必须留 ✓ —— strip 本身是拖拽区 ✗,去掉它输入框点不动 ✗。
|
|
89
89
|
- **落地只写 hash** ✓:`window.location.hash = fragment` ✓,绝不自己调路由内部 ✗ —— 浏览器触发 `hashchange` ✓,落地就是"粘贴链接"那条已验证的路 ✓。
|
|
@@ -91,6 +91,86 @@
|
|
|
91
91
|
- **假 DOM 不支持 `#id` 选择器** ✗ → 地址栏带 `data-dsh-part="url-router-address-bar"` ✓,测试用属性选择器 ✓(行锚点当年同理 ✓)。
|
|
92
92
|
- **验收**:布局可在 Web 版上用 `document.documentElement.setAttribute('data-windows-titlebar','')` + 那个 CSS 变量预演 ✓(同一套 CSS ✓),但 `dsh-app://` 下的 `replaceState` / `hashchange` 行为**只能真机确认** ✓。
|
|
93
93
|
|
|
94
|
+
## macOS 地址栏契约(2026-10-25 起,改之前先读)
|
|
95
|
+
|
|
96
|
+
- **形态**:macOS 没有页面里画的 strip ✓(标题栏是原生的,红黄绿三个灯在左上 ✓)。借的是会话页顶部那行**常驻标题行** ✓:面包屑 + 模式标签 + 右侧一排按钮 + 电源按钮 ✓。bar 仍是 `position:fixed` 挂在 body 上 ✓ —— 和 Windows 同一套挂载方式 ✓。
|
|
97
|
+
- **判定**:`data-platform=darwin`(或 UA 是 Mac)**且**(UA 有 `Electron/<x>` **或**协议是 `dsh-app:`)✓。**实测桌面版把 UA 里的 `Electron/` 抹掉了** ✓(UA 形如 `… @deepseek-ai/dsh-desktop/0.2.1-rc` ✓),所以真正生效的是 `dsh-app:` 那条 ✓ —— 别以为 UA 分支在兜底 ✓。
|
|
98
|
+
- **定位 = 量空白段,不是探矩形** ✓:`macLeaves()` 走一遍行内**所有真正占位的元素** ✓(有自己文字、或 `button/a/input/[role=button]/[role=tab]/img/svg`、或没有子元素 ✓)→ 只留和电源按钮同一行的 ✓ → 按左边界排序并**合并重叠** ✓ → 求所有空白段 ✓ → 取**最宽**那段 ✓ → `fitBar()` 右对齐(贴着头部按钮)+ 两侧 `MAC_BAR_INSET = 25` 内缩 ✓。
|
|
99
|
+
- **宽度必须封顶** ✓(`MAC_BAR_MAX_WIDTH = 1150`):空白段的左边界就是**会话标题文字的右边界** ✓,不封顶的话每换一个会话框就变一次宽 ✓。
|
|
100
|
+
- **「探测矩形 + 一撞就隐藏」那套别再写** ✗:它把候选框放在电源按钮左侧 ✓,必然横跨头部那排按钮 ✓,于是每次都被判"不空"然后整框 `display:none` ✓ —— 这就是"框怎么都不出来"的历史 bug ✓。
|
|
101
|
+
- **没有电源按钮的界面**(插件页 / 设置页 / 面板页 ✓):**按当前屏幕那一行重新量** ✓,不再沿用会话页的空白段 ✗(用户 2026-10-26 的截图:沿用旧空白段正好横压在自动化任务详情的「规则 / 任务运行记录」页签上 ✓)。做法:记住上一行的 `top` ✓,在 `[top, top + 26]` 这条带里扫**控件与标题**(`button` / `[role=tab]` / `[role=button]` / `a` / `input` / `textarea` / `select` / `h1..h3` ✓,`bandSpans()` ✓),把交点合并成占用段 ✓,**取最宽的空段**放框 ✓(`fitBar()` 右对齐 + 内缩 ✓);一段都不够宽就**隐藏** ✓(宁可不画,也不压在可点的东西上 ✓)。
|
|
102
|
+
- **窗口宽度仍要一起存** ✓(`{top, viewport, gap}` ✓):头部按钮是**右对齐**的 ✓,窗口一变它们就移动 ✓。宽度差超过 `MAC_BAR_RESIZE_SLACK = 8` 就**不猜** ✓,先隐藏,等回会话页重新量 ✓。
|
|
103
|
+
- **`MutationObserver` / `requestAnimationFrame` 一律从 `window` 取** ✓(`window.MutationObserver` / `window.requestAnimationFrame` ✓):假 DOM 与置换过 document 的宿主里,裸全局读会静默拿不到 ✓(踩过 ✓)。
|
|
104
|
+
- **只有一个框** ✓:挂载前先 `getElementById` 摘掉同 id 的旧 bar 和旧样式表 ✓ —— effect 重跑(热重载 / 重复 apply)不会多一个 ✓。
|
|
105
|
+
- **调试不在产品里** ✓:`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
|
+
- **假 DOM 不支持 `#id` 选择器** ✗:mac 侧的新代码对 `getElementById` / `querySelectorAll` 全部用了可选调用 ✓,测试里缺失即跳过 ✓。
|
|
107
|
+
|
|
108
|
+
## 地址栏偏好契约(2026-10-26 起,改之前先读)
|
|
109
|
+
|
|
110
|
+
- **两个偏好在 `localStorage`** ✓:`dsh-url-router-settings` = `{selectAllOnFocus, recentLimit}` ✓(默认 `true` / `8` ✓);`dsh-url-router-recent` = `[{hash, at, source}]`,最新在前 ✓,`source` 是 `visit` | `paste` ✓。它们**不属于 profile** ✓(是"这台机器这个窗口"的偏好 ✓),所以没有进 `dsh.profile`,也没有宿主接口 ✓。
|
|
111
|
+
- **首次点击全选** ✓:未聚焦时的 `mousedown` 里 `preventDefault` + `focus()` + `select()` ✓ —— 只 preventDefault 不 select 没用 ✓,只 select 不 preventDefault 也没用 ✓(按下落光标那条默认行为会把选区抹掉 ✓)。**已聚焦时再点必须是普通光标行为** ✓(否则拖选与定位全废 ✓)。`focus` 事件那条路也要选 ✓(Tab / 脚本聚焦 ✓)。
|
|
112
|
+
- **当前链接不能当过滤词** ✗:框里平时就写着当前界面 ✓ —— 按 `input.value` 过滤会在聚焦瞬间把列表清空 ✓(踩过 ✓)。**只有读者真的编辑过(`input` 事件)才过滤** ✓(`narrowed` 标志 ✓)。
|
|
113
|
+
- **最近列表的身份 = 界面** ✓,不是坐标 ✓:`recentIdentity()` 把 `turn` / `row` / `view` / `pane` / 面板内部页都归并到所在表面 ✓(`workspace` 形状取里面的会话 id ✓)。否则在一个对话里点几十行就把列表刷满 ✓。**当前屏幕不列出** ✓。
|
|
114
|
+
- **条数 = 下拉框里能看到的条数** ✓,**当前屏幕不占名额** ✓(用户 2026-10-26 明确纠正过:按存储条数算会永远少一条 ✓)。做法:磁盘多留一个缓冲位(`recentKeep() = limit + 1` ✓,当前屏幕最多占一条 ✓),`recentEntries()` 先砍掉当前屏幕再取 `limit` ✓。默认 8 ✓、**0 = 关闭** ✓(不写盘 ✓、不弹列表 ✓)、**无上限** ✓(`normaliseSettings` 只做非负取整 ✓,别加"顺手"的上限 ✗)。调小要**立刻**裁掉 ✓(`trimRecent()` ✓),不能等下一次写入 ✓;**`recentEntries()` 纯读** ✓(`report()` 会调它 ✓,绝不能写盘 ✗)。
|
|
115
|
+
- **最近列表的两种点击** ✓:左键 = `driveTypedFragment(hash)` + `settle`(照旧 ✓);**右键 = 复制链接** ✓。右键必须 `preventDefault`(吃掉浏览器菜单 ✓),复制的是**绝对 URL**(`origin + pathname + search + hash` ✓ —— 只复制 fragment 在新标签页里打不开 ✓),路径与启动 `?token=…` 一起带上 ✓。反馈两处 ✓:行内文本临时变「已复制 ✓」✓、bar 右端的 `[data-dsh-url-router-toast]` 闪「已复制链接」✓(列表关了也看得见 ✓)。
|
|
116
|
+
- 复制优先 `navigator.clipboard.writeText` ✓(**它是异步的,失败时再走 fallback** ✓);没有该服务(`dsh-app:` 这类非常规协议可能没有 ✓)或它拒绝时,用**临时 textarea + `execCommand('copy')`** ✓ —— 这条会抢焦点 ✗,所以用 `quietly()` 把 `focus`/`blur` 事件屏蔽掉 ✓(否则 `blur` 会把列表关掉 ✗、`focus` 会重选整条链接 ✗),复制完再把焦点还给输入框 ✓。
|
|
117
|
+
- **别把 fallback 当实参提前求值** ✗:`attemptValue` 只有一个参数 ✓,写成 `attemptValue(fn, fallback())` 会让 fallback **每次都执行** ✓(真机表现:每次右键都闪一下、焦点跑到 body、列表被关掉 ✗ —— 2026-10-26 实测踩到 ✓)。
|
|
118
|
+
- `copyBySelection` 也会临时 `input.focus()` 回来 ✓,卸载时清掉 toast / copied 的定时器 ✓。
|
|
119
|
+
- **粘贴先记,落地再记一次** ✓:`paste` 事件里只要 `titlebarFragmentOf()` 认得出就先记 ✓(已经在屏上的链接不会触发 `reflect` 写入 ✗,否则它永远记不上 ✓)。`visit` 记在 `publishRoute()` ✓ —— 那是"地址栏真的被写了"的唯一入口 ✓。
|
|
120
|
+
- **假 DOM**:`input.select` / `input.focus` / `document.activeElement` / `removeChild` 全部可选调用或判型 ✓,假 DOM 已补齐这几个 ✓(`select()` 记次数、`focus()` 写 `activeElement` ✓)。
|
|
121
|
+
- **设置页 = 插件管理器里那一行** ✓(不是独立的设置分区 ✓):`ctx.slots.inject('plugins.row.config', …)` + `ctx.slots.register({name:'plugins.row.config', key: ROW_CONFIG_KEY}, Card)` ✓,`ROW_CONFIG_KEY = 'dsh-url-router#dsh-url-router'` ✓ —— 键的形状是 `<包名>#<row id>` ✓(官方 `rowConfigKey()` 实测 ✓),本插件包名与行 id 恰好同名 ✓,于是那个页面的 URL 正好是 `#/panel/plugins/dsh-url-router/dsh-url-router` ✓。
|
|
122
|
+
- 组件收到 `{view}` ✓:`'summary'` 只要一行字 ✓(但行页的写法是 `description ?? renderSlot('…', {view:'summary'})` ✓ —— **有 description 的行根本不显示 summary** ✓,本插件有 description ✓,所以 summary 目前看不到 ✓,别以为注册失败 ✗);其余(页面传 `'page'`)画设置本身 ✓,容器是 `[data-plugin-config]` ✓。
|
|
123
|
+
- 页面 DOM 标记:`data-plugin-row-detail` = 那个 key ✓(反映侧读的就是它 ✓)。
|
|
124
|
+
- **没有 slot 服务 / 没有 React / 没有 DOM 就跳过** ✓(`startSettingsCard()` 逐项判型 ✓):组合里缺插件管理器不算失败 ✓。
|
|
125
|
+
- **驱动组件页时绝不能点启用开关** ✓(2026-10-26 实测踩到 ✓):插件行的**第一个 `<button>` 可以是 `role="switch"` 的启用开关** ✗ —— 旧的 `row.querySelector('button').click()` 把插件**关掉**了 ✓(probe profile 里真的被写成 `disabled: true` ✗)。现在走 `openControlOf()` ✓:跳过 `role="switch"` ✓,找不到就**不点** ✓,让链接按时过期 ✓。**以后任何"按行打开某页"的代码都得走它** ✓。
|
|
126
|
+
|
|
127
|
+
## 自动化任务面板(2026-10-26 起,改之前先读)
|
|
128
|
+
|
|
129
|
+
- **段位** ✓:`#/panel/schedules[/filter/<all|active|inactive>][/task/<任务 id>[/<tab>]]` ✓。默认的 `filter/all` 与默认的 `rule` 页签**不写进链接** ✓,但 `filter/all` 作为**输入**要归一化掉(`normalisePanelInner()` ✓),否则那条链接永远等不到一个会写 `all` 的页面 ✓。
|
|
130
|
+
- **筛选项没有值,只有位置** ✓:官方源码把 `["all","active","inactive"]` 映射成三个按钮 ✓,按钮上只有 `aria-pressed` ✓(实测 ✓)—— 所以链接里的值就是**位置对应的固定 token** ✓,与语言无关 ✓。识别那组按钮:`[role="group"]` 下**恰好三个按钮、每个都有 `aria-pressed`** ✓,且不在右栏里 ✓。
|
|
131
|
+
- **模型页里展开的编辑卡** ✓:`#/settings/<分区>/provider/<provider id>` ✓(id 是配置里的 key ✓,不是显示名 ✓)。卡片**没有 data 钩子** ✗(官方源码里就是 `<li class=rowCard>` + 哈希类名 ✓),所以:**展开的判定 = 这个 `li` 里出现了 `input` / `textarea`** ✓;**身份来自动作按钮的无障碍名** ✓ `providerCopy(t('editProvider'), target)` = `<本地化标签> <显示名> (<provider id>)` ✓ → 只取**括号里的 id** ✓(与语言无关 ✓)。驱动 = 找 `aria-label` 末尾是 `(<id>)` 的**第一个**按钮 ✓(卡片动作里编辑在删除之前 ✓);落地:命名了 provider 的链接必须看到同一张卡展开 ✓,只命名分区的链接照旧被任意卡片满足 ✓。
|
|
132
|
+
- **`aria-pressed` 必须在 DOM 观察者的 `attributeFilter` 里** ✓:按下筛选只改这一个属性 ✓,漏了它地址栏就停在第一次的筛选上 ✓(实际踩到 ✓)。
|
|
133
|
+
- **任务详情** ✓:行是 `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
|
+
- **筛选先于任务** ✓:筛完才可能有点得到的行 ✓,所以驱动顺序固定 ✓。
|
|
135
|
+
|
|
136
|
+
## 右栏 pane 的两条修正(2026-10-26 起)
|
|
137
|
+
|
|
138
|
+
- **收起的右栏不算 pane** ✓:收起来时页签仍挂在 DOM 里 ✗(实测:关掉右栏后 `active` 仍返回上一个页签 ✗),所以 canonical 先问 `[data-rightbar-collapsed]` ✓(值为 `true` 或空即收起 ✓),收起时**不写 `pane/…`** ✓。会话与面板两条分支都要判 ✓(只改一条会漏 ✓)。
|
|
139
|
+
- **kind 优先读控制器** ✓:`sidebarRight.active()` 返回的是活的 tab 描述符 `{ id, kind, contentId, title }` ✓(实测 ✓),而 DOM 上只有标题 ✗ —— Files 页签在打开文件后标题会变成**文件名**(`client.js` ✗),拿标题当 kind 就会写出 `pane/client.js` ✓(用户截图里就是这个 ✓)。所以顺序是**控制器 → DOM 标签 → 注册表** ✓;DOM 标签匹配不到注册表时,只有在**没有注册表**的情况下才把它当 kind ✓,有注册表就当它是标题、返回 undefined ✓。
|
|
140
|
+
- **面板自己的页盖过 pane** ✓(用户 2026-10-26 明确要求"让位"✓):canonical 先取 `panelPage()` ✓,**只有它为空时才写 `pane/<kind>`** ✓(旧版把 pane 接在页后面 ✗,用户看到的仍然是 `…/task/<id>/pane/client.js` ✗)。**驱动面板页时要把 `pane/<kind>` 尾巴摘掉** ✓,否则会把面板页当成 `files` 页去点 ✓(路由器自己写出的链接就会踩到 ✓)。**代价**:手写"页 + pane"的链接会因为 canonical 永远不写那条尾巴而按时过期 ✓(路由器自己已经不会再写这种链接 ✓)。
|
|
141
|
+
- **面板链接按前缀落地** ✓(`sameRoute` 的面板分支 ✓):链接可以比屏幕上的页**更宽**(`plugins/<包>` 被 `plugins/<包>/<组件>` 满足 ✓),但不能更窄 ✓;链接没写 pane 就忽略 pane ✓,写了就要对上 ✓。精确比较会让"页面已经打开"的链接一直等下去 ✗。
|
|
142
|
+
|
|
143
|
+
## 性能契约(2026-10-26 起)
|
|
144
|
+
|
|
145
|
+
- **DOM 通知一帧一次** ✓:`onDomChange` 的处理器被 `requestAnimationFrame` 合并 ✓(一个任务里几十次 mutation 只读一次状态 ✓),没有帧的宿主(测试 / 极简 DOM)**同步执行** ✓ —— 假 DOM 的断言依赖这一点 ✓。实测(隔离宿主):20 个连发任务从 **20 次 reconcile → 8 次**(= 跨了 8 帧,每帧最多一次)✓。
|
|
146
|
+
- **地址栏摆放的短路** ✓:有电源按钮的界面(会话页)先比一个形状签名(行矩形 + 按钮矩形 + 视口宽 + 行文本长度)✓,没变就**不重新走一遍行内元素** ✓(那一遍每个叶子一次 `getBoundingClientRect`,流式回答时每帧都在跑 ✗)。**无电源按钮的界面不缓存** ✓(那条带里放的是页面自己的内容,行不动它也会变 ✓)。
|
|
147
|
+
- **最近列表一次扫描** ✓:`sessionTitles()` 一次建 `data-row-key → 标题` 的 Map ✓,`recentLabel(hash, titles)` 复用 ✓ —— 别按条目各扫一遍侧栏 ✗(以前 8 条 × 2 次 ✓)。
|
|
148
|
+
- **`report().work`** ✓:`{ reconciles, placements }` 两个计数器,用来区分"被合并"和"每批都跑" ✓(对照实验就靠它 ✓)。
|
|
149
|
+
- **别在 mutation 回调里同步做重活** ✗:所有"读整个页面"的动作都要走这个合并路径 ✓,或像摆放那样先做形状短路 ✓。
|
|
150
|
+
- **`characterData` 不在观察范围** ✓(流式文本更新本来就不触发 ✓),`attributeFilter` 只列真正改变路由的属性 ✓。
|
|
151
|
+
|
|
152
|
+
## 链接驱动的小护栏(2026-10-26 起)
|
|
153
|
+
|
|
154
|
+
- **已经被隐藏的行** ✓:自动化任务的链接若指向被当前筛选藏起来的任务,`clickScheduleTask()` 找不到行时会先按回「全部」✓(下一轮重试就能找到 ✓),而不是干等到 8 秒过期 ✓。
|
|
155
|
+
- **已经展开的卡** ✓:`clickSettingsProvider()` 发现目标卡片**已经展开**就**不再点** ✓ —— 官方的编辑按钮是开关 ✓,重试循环会把卡片开了又关 ✗。
|
|
156
|
+
- **卸载时取消排队的帧** ✓(`cancelAnimationFrame(placeFrame)` ✓),别让已摘掉的 bar 在下一帧写样式 ✗。
|
|
157
|
+
|
|
158
|
+
## 兼容自检(#6,2026-10-26 已实现)
|
|
159
|
+
|
|
160
|
+
官方改契约不会抛错,只会让某一层**静默失去名字** ✗ —— 所以渲染期要做一次"屏幕在、钩子没了"的检查 ✓。
|
|
161
|
+
|
|
162
|
+
- **判据 = 弱信号 + 强钩子** ✓:弱信号证明"那个界面确实在屏上"(别的、还认得的东西 ✓),强钩子是本插件真正要读的东西 ✓。两者同时成立才算不兼容 ✓ —— 只用强钩子会误报(界面本来就没开 ✓)。
|
|
163
|
+
- **五项检查**(`compatibilityProblems()` ✓,全部只读 ✓):
|
|
164
|
+
1. `settings-overlay`:`role="dialog"` + `aria-modal="true"` + `nav` 里 ≥3 个按钮 ✓,但没有 `data-shortcut-modal="settings"` ✓(这个标记历史上就挪过一次 ✓)。
|
|
165
|
+
2. `plugin-manager`:`[data-plugin-panel]` 在 ✓,但六种钩子(`data-plugin-package` / `data-plugin-item` / `data-plugin-row` / `data-plugin-detail` / `data-plugin-item-detail` / `data-plugin-row-detail`)一个都没有 ✓。
|
|
166
|
+
3. `schedules-filter`:自动化面板在(`activePanelId === 'schedules'` ✓)且页面上有 `[role="group"]` ✓,但没有哪一组是"三个 `aria-pressed` 按钮" ✓。
|
|
167
|
+
4. `conversation-tabs`:**注册的会话视图 ≥2 个** ✓(单视图组合没有页签 ✓)、屏上有 `[data-chat-turn]` ✓、但没有 `[data-conversation-tabs]` ✓。
|
|
168
|
+
5. `sidebar-rows`:`[data-slot="sidebar.settings"]` 在(真侧栏 ✓)、目录里有会话 ✓、但 `[data-row-key^="session:"]` 为 0 ✓。
|
|
169
|
+
- **每项只报一次** ✓(`observed.compat` 记名字 ✓),**每秒最多扫一次** ✓(`COMPAT_EVERY_MS = 1000` ✓,在 `reconcile` 里跑 ✓)。
|
|
170
|
+
- **措辞统一** ✓:`与当前 DSH 版本不兼容:<name> —— 期望 <expected>,实际 <found>。这一层的链接会静默失效,请升级 DSH 或本插件` ✓(`console.warn` ✓,桌面版没有控制台,所以同时进 `report().compat` ✓)。
|
|
171
|
+
- **只告警、绝不改行为** ✓(检查里不写任何东西、不影响 canonical ✓)。
|
|
172
|
+
- **新增一层路由时同步加一条检查** ✓:弱信号选"这一层旁边还稳的东西" ✓,不要拿待检查的钩子自己当弱信号 ✗(那样它一坏就静默跳过 ✓,等于没查 ✓)。
|
|
173
|
+
|
|
94
174
|
## 跨机开发日志
|
|
95
175
|
|
|
96
176
|
改动记录在私有库 [DDDMUC/repo-devlogs](https://github.com/DDDMUC/repo-devlogs) 的 **`dsh-url-router/`** 文件夹(`HANDOFF.md` 最新一轮在最上面;macOS 端写 `WORKLOG-macos.md`,条目以 `[macOS]` 开头)。按该库规矩,每条先写 `**运行环境**`(设备 / 应用 / 服务商与模型),再写做了什么、动了哪些文件、怎么验证、遗留问题。
|
|
@@ -98,7 +178,7 @@
|
|
|
98
178
|
## 待办设计(用户 2026-10-10 提的一批,未实现,设计已定)
|
|
99
179
|
|
|
100
180
|
- **外部 fragment 写者共存(#4)**:`dsh-milestone` 之类的插件会在同一条 URL 的**同一个 fragment** 里写 `#msg=…` ✓。一个 fragment 只能有一个写者 ✗ → 现在的行为是"互不认账、静默覆盖" ✗。要做:① 自己的写入**只碰自己的段位** ✓(已经是这样 ✓);② 读到**不属于本插件语法**的 fragment 时,**留一条明确诊断**(点出对方形态 + 说明互斥),不要静默 ✓;③ 真共存需要**统一裁决**(见下条的接缝)✓。
|
|
101
|
-
-
|
|
181
|
+
- ~~**兼容边界自检(#6)**~~:**已实现(2026-10-26,见「兼容自检」小节)** ✓。
|
|
102
182
|
- **路由体检(#7)**:一个 `window.__dshUrlRouter.report()` ✓(无 UI 依赖 ✓,绝不抛错 ✓),列出:当前 canonical 路由 ✓、正在落地的链接与重试次数 ✓、每一层界面**读到多少个钩子** ✓(会话行 / 工作区行 / 视图 tab 与 id 列表 / 轮次 / 轨迹行 / 轮次边界 / 插件行 / 官方项 / 设置覆盖层与分区 / 任务看板 / 右栏 pane ✓)、已发出的告警 ✓、以及被"不碰"的段位 ✓。
|
|
103
183
|
- **对外接缝(#8)**:`ctx.urlRouter`(服务)或同名窗口事件 ✓,让第三方把"当前界面"写成链接 ✓,并由本插件**统一裁决** fragment 所有权 ✓ —— 这同时也是 #4 的真正解法 ✓。
|
|
104
184
|
- **面板内层页面(#5)**:技能中心 / SSH 需要各自发契约 ✓(`data-*` + 接受同名窗口事件 ✓);任务看板是现成先例 ✓,可作为 PR 往上游推 ✓(需要各仓 owner 同意 ✓)。
|
|
@@ -111,6 +191,9 @@
|
|
|
111
191
|
- **座位/视口判据用官方同款选择器** ✓:`[data-chat-turn]:not([hidden])` ✓,并自己排除 `closest('[hidden]')` ✓ —— **虚拟列表会把未载入的轮次也渲染进 DOM** ✗(只查"存在"会假报就位 ✗,实测 ✗)。
|
|
112
192
|
- **`sameRoute` 对 turn 的落地判据 = 目标真在视口** ✓(`routeKey` 比法会让 canonical 一带上 turn 就判等 → 链接永不驱动 ✗,死锁实测 ✓)。
|
|
113
193
|
- **drive 会重入 reconcile** ✓,重入可能已 `dropLink()` → **守门后立刻 `const link = linked`,体内一律用快照** ✗(否则 `reading 'inner'` ✓,实测 ✗)。
|
|
194
|
+
- **官方方法绝不拆出来单独调用** ✗(2026-10-26 rc.3 实测):`loadThrough` 是**原型方法** ✓,内部读 `this.openState` ✓ —— 旧代码 `const load = session.loadThrough; load(seq)` 让 `this` 丢了 ✓ → 每次抛 `Cannot read properties of undefined (reading 'openState')` ✓,被 `attempt` 吞掉 → **轮次跳转静默失效** ✗(没有诊断位 ✓,只有"链接 8 秒后过期" ✓)。现在一律 `session.loadThrough(seq)` ✓。**同类风险**:`openTab` / `openTabIn` / `setActive` / `toggleExpanded` 这些也都要带接收者 ✓。
|
|
195
|
+
- **翻页之后要自己滚** ✓(2026-10-26 补):官方聊天是 `loadThrough(seq).then(() => scrollToTurn(turn))` 两步 ✓(`ChatNavigation.request()` ✓),它那个 navigation 对象外部拿不到 ✗ → 我们只做前半步时,座位确实加载了 ✓,但虚拟列表**不会自己滚过去** ✓,紧接着就把刚加载的轮次裁掉 ✓(实测:座位在 1.8 秒出现、`top=-43325`、之后消失 ✗)。现在 `loadThrough` 的 promise 一 resolve 就用**最多 25×50ms** 的耐心找座位并 `scrollIntoView({block:'center'})` ✓(座位要等聊天下一次 commit 才渲染 ✓)。实测:跳 60 轮前的轮次 **2.8 秒**落地 ✓、可见 ✓。
|
|
196
|
+
- **已经满足的轮次链接要保留轮号** ✓:座位本来就在屏上时链接会立刻落地 ✓,旧代码落地后写回的是"纯会话"canonical ✗ → 粘贴的 `turn/<n>` 一落地就把轮号丢了 ✗。现在落地时先把轮次记成 anchor ✓ 再写 canonical ✓。
|
|
114
197
|
- `loadThrough` 是**公开 slot 契约** ✓(`session-controller` 1885 注释 "page backwards until the window covers seq" ✓)。官方 `uiChat.scrollToTurn(sessionId, turn)` 那条请求(讨论 #9339 ✓)**已降级为锦上添花** ✓(不阻塞 ✓)。
|
|
115
198
|
|
|
116
199
|
## 已实现:#4 外部 fragment 共存 / #7 路由体检(2026-10-11)
|
package/README.md
CHANGED
|
@@ -38,10 +38,16 @@ dsh plugin --profile web add "link:/path/to/dsh-url-router"
|
|
|
38
38
|
| `#/panel/plugins/<包名>/<组件 id>` | 插件页面里的某个组件页 | 面板自己的组件行控件 |
|
|
39
39
|
| `#/panel/task-board/task/<任务 id>` | 任务看板里某个任务的详情 | 任务看板自己的 `dsh-taskboard-open-task` 窗口事件 |
|
|
40
40
|
| `#/panel/task-board/new` | 任务看板的新建任务表单 | 任务看板自己的 `dsh-taskboard-new-task` 窗口事件 |
|
|
41
|
+
| `#/panel/schedules/filter/<all\|active\|inactive>` | 自动化任务的状态筛选(已开启 / 已结束;默认的 `all` 不写进链接) | 面板自己的筛选按钮(按位置,语言无关) |
|
|
42
|
+
| `#/panel/schedules/task/<任务 id>` | 自动化任务里某条任务的详情(规则页) | 面板自己的任务行 |
|
|
43
|
+
| `#/panel/schedules/task/<任务 id>/records` | 同一条任务的「运行记录」页(默认的 `rule` 不写) | 面板详情里的页签 |
|
|
44
|
+
| `#/settings/<分区>/provider/<提供商 id>` | 模型页里点了「编辑」展开的那张提供商卡片(id 是配置里的 key,不是显示名) | 卡片自己的「编辑」按钮 |
|
|
41
45
|
|
|
42
46
|
输入侧宽容:`sessions`/`panels` 复数、百分号编码都接受,一律规范化成上面的形式。**不认识的段位不碰**,只留一条诊断(留给以后的路由器,或者别的插件写的深链形态 ✓ —— 一个 fragment 只能有一个写者 ✓,所以读到不属于本插件的形态时会**明确说一句**,而不是静默 ✗)。
|
|
43
47
|
|
|
44
|
-
面板内部页是**面板自己的词汇表**,本插件不发明:插件管理器发布 `data-plugin-detail` / `data-plugin-item-detail` / `data-plugin-row-detail`,任务看板发布 `data-dsh-taskboard-open-task` / `data-dsh-taskboard-new-task
|
|
48
|
+
面板内部页是**面板自己的词汇表**,本插件不发明:插件管理器发布 `data-plugin-detail` / `data-plugin-item-detail` / `data-plugin-row-detail`,任务看板发布 `data-dsh-taskboard-open-task` / `data-dsh-taskboard-new-task`,并接受同名窗口事件;自动化任务面板的三态筛选只有 `aria-pressed`(按钮上没有值),所以链接里写的是官方那三个固定值的**位置**(`all` / `active` / `inactive`),换语言不影响;任务详情用行上的 `aria-expanded` 与 `aria-describedby` 末段的 id 认,页签用它自己的 `data-detail-tab="rule|records"`。
|
|
49
|
+
|
|
50
|
+
**右栏 pane 是页面旁边的一层,不是页面本身** ✓:面板一旦有自己的页(任务详情、插件页…),地址栏就**只写那一页**(`#/panel/schedules/task/<id>/records`),旁边开着右栏也不会多出 `pane/…`;只有面板没有自己的页时才写 `pane/<kind>` ✓。右栏收起时那一层**直接不写**(收起的右栏会把页签留在 DOM 里,旧版因此把 `pane/client.js` 这种已经不在屏上的东西写进了地址栏);pane 的 kind 优先读右栏控制器里的 `{kind}`,因为 Files 页签在打开文件后标题会变成文件名(`client.js`),拿标题当 kind 就是错的。
|
|
45
51
|
|
|
46
52
|
### 每一行都是真链接
|
|
47
53
|
|
|
@@ -55,11 +61,45 @@ dsh plugin --profile web add "link:/path/to/dsh-url-router"
|
|
|
55
61
|
|
|
56
62
|
DSH 桌面版(Windows)的标题栏是**画在网页里的**:preload 给 `<html>` 打上 `data-windows-titlebar`、给出 `--dsh-windows-titlebar-height`,菜单条本身是页面里的一个 shadow DOM 固定元素(`[data-windows-menu]`)。那条 strip 里的空白是**页面空间**,而桌面版**自己没有地址栏** —— 于是本插件把地址栏放在那儿:
|
|
57
63
|
|
|
58
|
-
- 只在 `html[data-windows-titlebar]` 存在时出现
|
|
64
|
+
- 只在 `html[data-windows-titlebar]` 存在时出现 ✓(纯 Web 版什么都不画 ✓)。
|
|
59
65
|
- 位置:紧跟 shell 自己的菜单条右侧 ✓、右端避开窗口按钮 ✓;高度取 `--dsh-windows-titlebar-height` ✓;`-webkit-app-region: no-drag` 保证能点 ✓。
|
|
66
|
+
|
|
67
|
+
### macOS 桌面的地址栏
|
|
68
|
+
|
|
69
|
+
macOS 的标题栏是**原生**的(左边红黄绿三个灯),页面里没有 Windows 那条 strip 可以借。但会话页顶部有一行**常驻的标题行**(面包屑 + 模式标签 + 右侧按钮 + 电源按钮),地址栏就落在那行里:
|
|
70
|
+
|
|
71
|
+
- 判定:shell 给 `<html>` 打上 `data-platform=darwin`(或 UA 是 Mac)**并且**(UA 里有 `Electron/<x>` **或**页面跑在 `dsh-app:` 协议上)→ 只有桌面 App 满足,浏览器里开 Web 版不画 ✓。(实测桌面版会把 UA 里的 `Electron/<x>` 抹掉,所以真正生效的是 `dsh-app:` 那条 ✓。)
|
|
72
|
+
- 位置:先量这一行里**所有真正占位的东西**(有自己文字的元素、按钮、图标),按左边界排序并合并重叠 → 得到若干空白段 → 取**最宽的那一段**,右端贴着头部按钮、左端跟着走,两边各留 25px 内缩 ✓。
|
|
73
|
+
- 宽度封顶 **1150px**:空白段的左边界是跟着**会话标题长度**跑的 ✓,不封顶的话换个会话框就变一次宽 ✓;放不下时按实际空白缩 ✓。
|
|
74
|
+
- 和电源按钮**同一行**(垂直居中于它 ✓),`position: fixed` —— 所以既不遮挡也不挤压任何东西 ✓。
|
|
75
|
+
- 没有电源按钮的界面(插件页、设置页、面板页):**按当前屏幕那一行重新量** ✓ —— 扫描这一行上的控件与标题,取**最宽的空段**放框(自动化任务详情的「规则 / 任务运行记录」页签不会再被压住 ✓);一段都不够宽就**隐藏** ✓,宁可不画也不压在可点的东西上 ✓。窗口尺寸变过(> 8px)时旧数据一律不信 ✓,等回到会话页重新量 ✓。
|
|
76
|
+
- **只有一个框** ✓:挂载前先按 id 摘掉旧的 bar 和样式表 ✓,所以 effect 重跑多少次(热重载、重复 apply)都只有一个 ✓。
|
|
77
|
+
- Windows 那条路径完全不变,`data-windows-titlebar` 优先,两种形态互斥 ✓。
|
|
78
|
+
- 输入、反射、拒识、Escape 恢复的逻辑与 Windows 版共用同一份代码 ✓。
|
|
60
79
|
- 输入什么 ✓:任何 `#/…` 路由(会话 / 工作区 / 视图 / 轮次 / 行 / 面板 / 设置三层 / 右栏 pane)、**裸会话 id**、或**整条链接**(只取它的 fragment)✓;回车写 hash → 浏览器触发 `hashchange` → 落地走的就是"粘贴链接"那条已验证的路 ✓。
|
|
61
80
|
- 它同时是**反映** ✓:本插件每次写地址栏都会同步进这个框 ✓,所以它显示的就是"现在在哪" ✓。Escape 还原 ✓;认不出的文本不动地址栏 ✓。
|
|
62
81
|
|
|
82
|
+
### 设置页:地址栏的两个偏好
|
|
83
|
+
|
|
84
|
+
在**插件管理器**里找到本插件那一行 → 右侧的配置箭头(也就是 `#/panel/plugins/dsh-url-router/dsh-url-router`)就是本插件的设置页。两个偏好跟着这台机器走(`localStorage`),改完立刻生效,不用重启:
|
|
85
|
+
|
|
86
|
+
| 偏好 | 默认 | 说明 |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| 首次点击地址栏时全选 | 开 | 像浏览器地址栏:**第一次**点进去,框里原来的链接整条选中,直接输入就替换它;已经聚焦时再点,才是放光标。关掉后就是一个普通文本框。 |
|
|
89
|
+
| 记住最近链接的条数 | 8 | **下拉框里能看到的条数**(地址栏里正显示的那个界面不算 —— 它已经在你眼前了,离开之后才会回到列表)。最低 **0**(= 不记忆,地址栏也不再弹最近列表),**没有上限**。 |
|
|
90
|
+
|
|
91
|
+
最近列表的用法与浏览器一致:点地址栏就弹出来,输入即过滤,Esc 关掉;条目有两个来源 —— **用过的界面**(本插件写进地址栏的每一个界面 ✓)和**粘贴过的链接**(在地址栏里粘贴任何本插件认得的链接或裸会话 id ✓,落地与否都先记住 ✓)。
|
|
92
|
+
|
|
93
|
+
**左键 = 跳过去,右键 = 复制链接** ✓:右键那一下会吃掉浏览器菜单 ✓,直接把**完整 URL**(路径 + 启动 `?token=…` + fragment ✓)放进剪贴板 ✓,粘到新标签页或发给别人就能开 ✓;被复制的那一条会短暂显示「已复制 ✓」✓,地址栏右端也会闪一下「已复制链接」✓(列表不会因此关掉、光标也不会跑 ✓)。
|
|
94
|
+
|
|
95
|
+
- 身份是**界面**,不是坐标 ✓:`#/session/<id>/turn/3/row/…` 这类点位链接归并到它所在的会话 ✓,不会用同一个对话把列表刷满 ✓;同一界面再次出现只是**提到最前** ✓,不重复 ✓。
|
|
96
|
+
- 当前屏幕**不列出来** ✓(它不是一个"回去"的地方)。
|
|
97
|
+
- 条数就是下拉框里能看到的条数 ✓:磁盘上多留一个缓冲位给"当前界面" ✓,所以设 8 就真能看到 8 条 ✓(不是 7 条 ✗)。调小会**立刻**裁掉多余的 ✓;调到 0 会清空并不再记录 ✓。
|
|
98
|
+
- 会话条目显示侧栏里的**对话标题** ✓(读不到标题时才只显示链接 ✓);面板 / 设置等段位显示中文类别 + 链接本身 ✓。
|
|
99
|
+
- 页面里还有「清空记录」按钮 ✓。
|
|
100
|
+
- 存在这两个键里:`dsh-url-router-settings` 与 `dsh-url-router-recent` ✓ —— 换浏览器配置就没了,符合"这台机器、这个窗口"的语义 ✓。
|
|
101
|
+
- **粘链接不会按到开关** ✓:插件行的启用开关就在配置控件旁边,两个都是 `<button>` ✓ —— 驱动组件页时只点**非 `role="switch"`** 的控件 ✓,所以开不到的组件页链接会按时过期并留一条诊断 ✓,**不会**顺手把插件关掉 ✓(实测踩到过 ✓)。
|
|
102
|
+
|
|
63
103
|
### 它守的规矩
|
|
64
104
|
|
|
65
105
|
- **只动 fragment**:路径与查询串从不改写(启动 `?token=…` 能活下来)。
|
|
@@ -88,11 +128,13 @@ DSH 桌面版(Windows)的标题栏是**画在网页里的**:preload 给 `<
|
|
|
88
128
|
|
|
89
129
|
### 开发
|
|
90
130
|
|
|
91
|
-
手写 JS 直接进 `lib/`(无构建步骤),`npm test` 直接跑交付产物(
|
|
131
|
+
手写 JS 直接进 `lib/`(无构建步骤),`npm test` 直接跑交付产物(107 条用例,`node --test`)。改之前请读 [AGENTS.md](AGENTS.md) 里的路由与行链接契约。
|
|
92
132
|
|
|
93
133
|
### 排障
|
|
94
134
|
|
|
95
|
-
浏览器 Console 里调用 `window.__dshUrlRouter.report()` ✓,它会列出:当前地址栏 ✓、本插件认为屏幕在哪个界面 ✓、正在落地的链接与重试次数 ✓、**每一层读到了多少个钩子** ✓、右栏 pane 的**真实 kind 列表**
|
|
135
|
+
浏览器 Console 里调用 `window.__dshUrlRouter.report()` ✓,它会列出:当前地址栏 ✓、本插件认为屏幕在哪个界面 ✓、正在落地的链接与重试次数 ✓、**每一层读到了多少个钩子** ✓、右栏 pane 的**真实 kind 列表** ✓、它**拒绝认领**的段位 ✓、以及 `compat`(与当前 DSH 版本不兼容的层,见下)✓ 和 `work`(reconcile / 摆放次数,看性能用)✓。它绝不出错、也不写任何东西 ✓。
|
|
136
|
+
|
|
137
|
+
**兼容自检** ✓:官方一改 DOM 契约,插件不会报错,只会**静默不再给某一层写链接** ✗。所以渲染期会做五项检查(设置浮层标记、插件管理器钩子、自动化筛选组、会话页签容器、侧栏会话行)✓:屏幕上确实出现了那一层、但本插件要读的钩子没了 → **每个界面只警告一次** ✓,措辞统一为「与当前 DSH 版本不兼容」✓,同时记进 `report().compat` ✓。它**只告警、不改行为** ✓。
|
|
96
138
|
|
|
97
139
|
## English
|
|
98
140
|
|
|
@@ -114,6 +156,10 @@ Listed in the community plugin index: [zhu1090093659/dsh-community-plugins](http
|
|
|
114
156
|
| --- | --- |
|
|
115
157
|
| `#/session/<id>` | the conversation the main view shows |
|
|
116
158
|
| `#/panel/<panelId>` | a main-column panel |
|
|
159
|
+
| `#/panel/schedules/filter/<all\|active\|inactive>` | the automation list's status filter (the default `all` is not written) |
|
|
160
|
+
| `#/panel/schedules/task/<id>` | one automation task's detail (its rules tab) |
|
|
161
|
+
| `#/panel/schedules/task/<id>/records` | that task's records tab (the default `rule` is not written) |
|
|
162
|
+
| `#/settings/<section>/provider/<provider id>` | the provider card whose editor is open in Models (the id from the config, not the display name) |
|
|
117
163
|
| `#/panel/plugins/<package>` | an installed plugin's page |
|
|
118
164
|
| `#/panel/plugins/<item>` | a built-in plugin's page (`shell`, `agent-loop`, `subagent`, `web-search`) |
|
|
119
165
|
| `#/panel/plugins/<package>/<component>` | one component page inside that plugin's page |
|
|
@@ -130,11 +176,32 @@ Conversation rows and both plugin-list groups carry a transparent full-row ancho
|
|
|
130
176
|
|
|
131
177
|
The Windows desktop window draws its own title bar **inside the page**: the preload marks `<html data-windows-titlebar>` and publishes `--dsh-windows-titlebar-height`, and its menu is a fixed shadow-DOM element in the page (`[data-windows-menu]`). That strip is page space, and the desktop window has **no address bar of its own** — so this plugin puts one there:
|
|
132
178
|
|
|
133
|
-
- Drawn only when `html[data-windows-titlebar]` is present (nothing at all on the web app
|
|
179
|
+
- Drawn only when `html[data-windows-titlebar]` is present (nothing at all on the plain web app).
|
|
180
|
+
- On the **macOS desktop** build there is no in-page strip to borrow, so the bar becomes a floating pill instead: pinned just under the native title bar at the window's top-right, 380×30, rounded, blurred and shadowed. It is recognised by the shell's `<html data-platform="darwin">` marker (or a Mac user agent) together with the window being the application — Electron in the user agent or the `dsh-app:` scheme — so the web app in a browser never draws it. It sits inside the 48px the shell reserves above its frame (`--dsh-frame-top-clearance`), clear of the traffic lights. Both shapes share one code path; the Windows marker wins when present.
|
|
134
181
|
- Placed right after the shell's own menu and before the window buttons, at `--dsh-windows-titlebar-height`, with `-webkit-app-region: no-drag` so it stays clickable.
|
|
135
182
|
- 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.
|
|
136
183
|
- 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.
|
|
137
184
|
|
|
185
|
+
### Settings: the address bar's two preferences
|
|
186
|
+
|
|
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. Both preferences live on this machine (`localStorage`) and take effect the moment they change, no restart:
|
|
188
|
+
|
|
189
|
+
| Preference | Default | Meaning |
|
|
190
|
+
| --- | --- | --- |
|
|
191
|
+
| 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. |
|
|
192
|
+
| 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
|
+
|
|
194
|
+
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).
|
|
195
|
+
|
|
196
|
+
**Left click opens it, right click copies it.** The right click swallows the browser menu and puts the **whole URL** on the clipboard — path, launch query (`?token=…`) and fragment — so it opens in a new tab or on another machine's window. The copied row briefly says "已复制 ✓" and the bar flashes "已复制链接"; the list stays open and the caret stays put.
|
|
197
|
+
|
|
198
|
+
- Identity is the **screen**, not the position: a link into `#/session/<id>/turn/3/row/…` folds into its conversation, so one conversation cannot fill the list; seeing the same screen again moves it to the front instead of adding it twice.
|
|
199
|
+
- The screen on display is not offered (it is not somewhere to go back to).
|
|
200
|
+
- The count is what the drop-down lists: one spare entry is kept on disk for the screen on display, so 8 really shows 8 links (not 7). Lowering the count trims immediately; 0 clears the list and stops remembering.
|
|
201
|
+
- A conversation entry shows the conversation title from the sidebar when the sidebar has it; other surfaces show their category and the link itself.
|
|
202
|
+
- The page also has a **clear** button.
|
|
203
|
+
- **A pasted link never presses a switch.** A plugin row's enable switch sits right beside its configure control and both are `<button>`s, so the driver clicks only controls that are not `role="switch"`: a component page that cannot be opened expires with a diagnostic instead of silently disabling the plugin.
|
|
204
|
+
|
|
138
205
|
### Rules it keeps
|
|
139
206
|
|
|
140
207
|
- **Fragment only.** Path and query are never rewritten.
|