DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

DDDMUC /

DDDMUC/dsh-url-router

Verified

DSH Web GUI plugin: one URL for every surface — the address bar names what is on screen (a conversation or a main panel) and a link opens it. Single owner of the URL fragment.

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@e8e6b207

dsh-url-router

DSH 地址栏路由插件(Web 与 Windows 桌面版)—— 每个界面都有自己的链接:地址栏写着屏幕上的东西,链接点开就是那个界面。 给 DeepSeek Harness (dsh) 补上"可寻址":点侧栏会话,地址栏变成 #/session/<id>;点插件面板,变成 #/panel/plugins;点进某个插件、某个组件、某个任务,地址栏跟着一起走。把这条链接贴到新标签页、或者对着侧栏任意一行右键,就是浏览器自己的行为——无需复制按钮、无需自定义菜单。桌面版(Windows)自己没有地址栏,本插件就把它画在标题栏那条空白里。

它是地址栏的唯一写者:只改 URL 的 fragment,路径与查询串(例如启动时的 ?token=…)原样保留;只用 replaceState,浏览面板不会往后退栈里塞历史。只有浏览器半区——零依赖、无宿主路由、无遥测。

中文 · English


中文

安装

# 通过插件管理器(推荐)
dsh plugin --profile web add dsh-url-router

# 重启 dsh web,之后硬刷新浏览器

开发时也可以直接挂工作树:

dsh plugin --profile web add "link:/path/to/dsh-url-router"

已登记进社区插件索引 ✓ —— zhu1090093659/dsh-community-plugins 的 community.json 是本插件在 DSH Workshop 商店与 dsh-market.com 清单里的唯一数据源 ✓(登记条目 dsh-url-router,ui/panel ✓)。

路由表

地址 界面 由谁打开
#/session/<id> 主视图里的那个会话 uiWorkspace.openSession(id)
#/panel/<panelId> 主面板(plugins、task-board、skill-explorer、ssh …) layout.selectPanel(id)
#/panel/plugins/<包名> 某个已安装插件的页面 面板自己的列表行控件
#/panel/plugins/<官方项 id> 某个官方内置插件的页面(shell、agent-loop、subagent、web-search) 面板自己的列表行控件
#/panel/plugins/<包名>/<组件 id> 插件页面里的某个组件页 面板自己的组件行控件
#/panel/task-board/task/<任务 id> 任务看板里某个任务的详情 任务看板自己的 dsh-taskboard-open-task 窗口事件
#/panel/task-board/new 任务看板的新建任务表单 任务看板自己的 dsh-taskboard-new-task 窗口事件
#/panel/schedules/filter/<all|active|inactive> 自动化任务的状态筛选(已开启 / 已结束;默认的 all 不写进链接) 面板自己的筛选按钮(按位置,语言无关)
#/panel/schedules/task/<任务 id> 自动化任务里某条任务的详情(规则页) 面板自己的任务行
#/panel/schedules/task/<任务 id>/records 同一条任务的「运行记录」页(默认的 rule 不写) 面板详情里的页签
#/settings/<分区>/provider/<提供商 id> 模型页里点了「编辑」展开的那张提供商卡片(id 是配置里的 key,不是显示名) 卡片自己的「编辑」按钮
#/power 电源菜单(浮层)。链接只能打开它,关闭/重启永远要人点 电源按钮
#/session/<子会话 id>/subagent-of/<父标题> 子代理会话:一眼看出它是谁的子代理(父是装饰,打开的是子会话;父没标题时退回父 id) 聊天里的子代理卡片

输入侧宽容:sessions/panels 复数、百分号编码都接受,一律规范化成上面的形式。不认识的段位不碰,只留一条诊断(留给以后的路由器,或者别的插件写的深链形态 ✓ —— 一个 fragment 只能有一个写者 ✓,所以读到不属于本插件的形态时会明确说一句,而不是静默 ✗)。

面板内部页是面板自己的词汇表,本插件不发明:插件管理器发布 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"。

右栏 pane 是页面旁边的一层,不是页面本身 ✓:面板一旦有自己的页(任务详情、插件页…),地址栏就只写那一页(#/panel/schedules/task/<id>/records),旁边开着右栏也不会多出 pane/…;只有面板没有自己的页时才写 pane/<kind> ✓。右栏收起时那一层直接不写(收起的右栏会把页签留在 DOM 里,旧版因此把 pane/client.js 这种已经不在屏上的东西写进了地址栏);pane 的 kind 优先读右栏控制器里的 {kind},因为 Files 页签在打开文件后标题会变成文件名(client.js),拿标题当 kind 就是错的。

每一行都是真链接

侧栏的会话行、插件面板的列表行(已安装组与官方组)都是整行覆盖的透明真锚点:

  • 右键 → 浏览器原生菜单(在新标签页中打开 / 复制链接 / 另存为)
  • 中键、Cmd/Ctrl + 单击 → 原生新标签页
  • 左键单击 → 在本页打开(preventDefault 后调官方导航,不新增历史记录)

桌面版的地址栏

DSH 桌面版(Windows)的标题栏是画在网页里的:preload 给 <html> 打上 data-windows-titlebar、给出 --dsh-windows-titlebar-height,菜单条本身是页面里的一个 shadow DOM 固定元素([data-windows-menu])。那条 strip 里的空白是页面空间,而桌面版自己没有地址栏 —— 于是本插件把地址栏放在那儿:

  • 只在 html[data-windows-titlebar] 存在时出现 ✓(纯 Web 版什么都不画 ✓)。
  • 位置:紧跟 shell 自己的菜单条右侧 ✓、右端避开窗口按钮 ✓;默认撑满中间;右边缘有握把 —— 拖它可以把地址栏改短(右边留白),长度按这条 strip 的比例记住,窗口缩放时等比;双击握把恢复撑满 ✓。高度取 --dsh-windows-titlebar-height ✓;-webkit-app-region: no-drag 保证能点 ✓。

macOS 桌面的地址栏

macOS 的标题栏是原生的(左边红黄绿三个灯),页面里没有 Windows 那条 strip 可以借。但会话页顶部有一行常驻的标题行(面包屑 + 模式标签 + 右侧按钮 + 电源按钮),地址栏就落在那行里:

  • 判定:shell 给 <html> 打上 data-platform=darwin(或 UA 是 Mac)并且(UA 里有 Electron/<x> 或页面跑在 dsh-app: 协议上)→ 只有桌面 App 满足,浏览器里开 Web 版不画 ✓。(实测桌面版会把 UA 里的 Electron/<x> 抹掉,所以真正生效的是 dsh-app: 那条 ✓。)
  • 位置:先量这一行里所有真正占位的东西(有自己文字的元素、按钮、图标),按左边界排序并合并重叠 → 得到若干空白段 → 取最宽的那一段,右端贴着头部按钮、左端跟着走,两边各留 25px 内缩 ✓。
  • 宽度:默认 700px(一条会话链接的完整长度);左右两条边都是握把(悬停 / 拖动时各显出一条 2px 竖线)—— 拖哪条动哪条,另一条钉住不动;双击任一握把恢复默认 ✓。长度按窗口宽度的比例记在 localStorage(dsh-url-router-bar-width = {ratio, shift}),所以拖过一次之后,把窗口拉大拉小,地址栏等比跟着变 ✓。上限永远是“这一次量到的空白段”(不会越过头部按钮 / 右栏面板 ✓),下限 120px ✓;左边缘的偏移以空白段左端为基准(shift),换界面仍贴在那条线上 ✓。拖动期间地址栏不会因为鼠标离开就消失 ✓ —— “离开就收起”只对没在拖的时候生效 ✓。
  • 和电源按钮同一行(垂直居中于它 ✓),position: fixed —— 所以既不遮挡也不挤压任何东西 ✓。
  • 没有电源按钮的界面(插件页、设置页、面板页):竖线照样是那条常量(和电源按钮同心)✓,横线在这条线上量空白段 —— 不再“按当前屏幕那一行重新量” ✗(那条路把框放到过面包屑行和页面标题行上 ✓)。永不隐藏 ✓:一段空白都不够宽时退到右端一段体面宽度,也不 display:none ✓。
  • 只有一个框 ✓:挂载前先按 id 摘掉旧的 bar 和样式表 ✓,所以 effect 重跑多少次(热重载、重复 apply)都只有一个 ✓。
  • Windows 那条路径完全不变,data-windows-titlebar 优先,两种形态互斥 ✓。
  • 输入、反射、拒识、Escape 恢复的逻辑与 Windows 版共用同一份代码 ✓。
  • 输入什么 ✓:任何 #/… 路由(会话 / 工作区 / 视图 / 轮次 / 行 / 面板 / 设置三层 / 右栏 pane)、裸会话 id、或整条链接(只取它的 fragment)✓;回车写 hash → 浏览器触发 hashchange → 落地走的就是"粘贴链接"那条已验证的路 ✓。
  • 它同时是反映 ✓:本插件每次写地址栏都会同步进这个框 ✓,所以它显示的就是"现在在哪" ✓。Escape 还原 ✓;认不出的文本不动地址栏 ✓。

设置页:地址栏的三个偏好

在插件管理器里找到本插件那一行 → 右侧的配置箭头(也就是 #/panel/plugins/dsh-url-router/dsh-url-router)就是本插件的设置页。三个偏好跟着这台机器走(localStorage),改完立刻生效,不用重启:

偏好 默认 说明
首次点击地址栏时全选 开 像浏览器地址栏:第一次点进去,框里原来的链接整条选中,直接输入就替换它;已经聚焦时再点,才是放光标。关掉后就是一个普通文本框。
悬浮时才显示地址栏 关 平时隐去(只降透明度,位置和尺寸都不变),鼠标移到地址栏所在的位置才出现;输入框有焦点、或最近列表开着时不会消失。
记住最近链接的条数 8 下拉框里能看到的条数(地址栏里正显示的那个界面不算 —— 它已经在你眼前了,离开之后才会回到列表)。最低 0(= 不记忆,地址栏也不再弹最近列表),没有上限。

最近列表的用法与浏览器一致:点地址栏就弹出来,输入即过滤,Esc 关掉;条目有两个来源 —— 用过的界面(本插件写进地址栏的每一个界面 ✓)和粘贴过的链接(在地址栏里粘贴任何本插件认得的链接或裸会话 id ✓,落地与否都先记住 ✓)。

输入时还会搜索界面上的字 ✓(2026-11-07):输入框里有字时,下拉框先列界面上的字 —— 屏幕上那些本来就能变成链接的行,比如插件页的「智能体团队」、侧栏的会话标题。输入「智能体」就提示「智能体团队」,点一下就打开 #/panel/plugins/<包名> ✓。只列真能变成链接的字:正文里一句普通的话不会给一个编出来的链接 ✓。三组各带小标题:界面上的字 ✓(可路由的行,最多 6 条)、消息 ✓(会话里的轮次,标成「第 N 轮」,点一下跳到那一轮,最多 8 条 —— 同一句话在多个轮次出现时都会列出来)、用过的界面 ✓。左键跳过去、右键照样复制绝对 URL ✓。

左键 = 跳过去,右键 = 复制链接 ✓:右键那一下会吃掉浏览器菜单 ✓,直接把完整 URL(路径 + 启动 ?token=… + fragment ✓)放进剪贴板 ✓,粘到新标签页或发给别人就能开 ✓;被复制的那一条会短暂显示「已复制 ✓」✓,地址栏右端也会闪一下「已复制链接」✓(列表不会因此关掉、光标也不会跑 ✓)。

  • 一次切换只算一次访问 ✓(2026-11-07):一次导航会写好几次 fragment(先选面板再开它的页、先写会话再写位置),只有最后落地的那一屏记一条 —— 没停留过的屏不会挤到你刚离开的那屏前面 ✓。
  • 身份是界面,不是坐标 ✓:#/session/<id>/turn/3/row/… 这类点位链接归并到它所在的会话 ✓,不会用同一个对话把列表刷满 ✓;同一界面再次出现只是提到最前 ✓,不重复 ✓。
  • 当前屏幕不列出来 ✓(它不是一个"回去"的地方)。
  • 条数就是下拉框里能看到的条数 ✓:磁盘上多留一个缓冲位给"当前界面" ✓,所以设 8 就真能看到 8 条 ✓(不是 7 条 ✗)。调小会立刻裁掉多余的 ✓;调到 0 会清空并不再记录 ✓。
  • 会话条目显示侧栏里的对话标题 ✓(读不到标题时才只显示链接 ✓);面板 / 设置等段位显示中文类别 + 链接本身 ✓。
  • 页面里还有「清空记录」按钮 ✓。
  • 存在这两个键里:dsh-url-router-settings 与 dsh-url-router-recent ✓ —— 换浏览器配置就没了,符合"这台机器、这个窗口"的语义 ✓。
  • 粘链接不会按到开关 ✓:插件行的启用开关就在配置控件旁边,两个都是 <button> ✓ —— 驱动组件页时只点非 role="switch" 的控件 ✓,所以开不到的组件页链接会按时过期并留一条诊断 ✓,不会顺手把插件关掉 ✓(实测踩到过 ✓)。

它守的规矩

  • 只动 fragment:路径与查询串从不改写(启动 ?token=… 能活下来)。

  • 唯一写者:本插件拥有 fragment,请不要和别的写 fragment 的插件同时启用(例如 dsh-session-url),两者会互相覆盖。

  • 只用 replaceState:浏览面板不污染后退栈。

  • #/power 是唯一的浮层链接 ✓:菜单是瞬时状态(点外面就关),所以它只承诺“能打开”,不承诺“能停留” ✓;开着时地址栏写它 ✓,关掉后回到底下那个界面 ✓。关闭/重启永远不驱动 ✗。

  • 链接落地前,挡在前面的表面先让开 ✓:主面板选中时地址栏读到的就是面板 ✓,设置浮层在屏时读到的就是 #/settings/<分区> ✓ —— 这两种情况下会话其实已经在背后打开了 ✗,屏幕上却像"没反应" ✓。所以驱动任何非设置链接之前 ✓,先 beginNavigation() + selectPanel(null) 让面板退场 ✓,再用官方自己那条 document 级 Escape 把设置浮层请走 ✓(模态栈是官方内部的私有注册表 ✗ 外部拿不到 ⇒ Escape 是浏览器侧唯一可用的官方关闭路径 ✓,也就是你按 Esc 走的那条 ✓)。一条链接只按一次 ✓:不会在你正在操作页面时乱按键 ✓,代价只是万一那次没关掉、你再粘一次 ✓。设置自己的链接不动浮层 ✓。

  • 轮次信息:能证明才写 ✓。助手行自己的身份里就带轮次(row/assistant/80/32 = 第 80 轮第 32 项 ✓);工具行不带 ✗(只有调用 id ✓)→ 于是我从视图的轮次边界 [data-turn-start] 反推 ✓,但只在行夹在两个已渲染边界之间时才写 ✓(turn/82/row/tool/call/… ✓)。视图是虚拟化的 ✓,边界可能只渲染了一侧 ✗ → 那时宁可不写轮次 ✓,也绝不猜一个错的轮次 ✗。

  • 轨迹里的每一行都有自己的链接 ✓:#/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 ✓)。粘贴行链接会打开那个对话并尽力滚到那一行 ✓ —— 视图是虚拟化的 ✓,没渲染出来的行不会被假装滚到 ✗(不会骗你 ✓)。

  • 会话视图(对话 / 轨迹 / 轨迹plus …)也进地址栏 ✓:#/session/<id>/view/<viewId>(有工作区时是 #/workspace/<名>/session/<id>/view/<viewId> ✓)。视图 id 是真的 id ✓(来自槽位 conversation.view 的 id 字段 ✓,按 order 排序后与头部页签一一对应 ✓):实测本机是 chat / trajectory / surface(Trajectory Plus ✓)/ plus2(Trajectory Plus 2 ✓)。默认视图(第一个 ✓)不写进链接 ✓,所以普通对话链接保持短 ✓。粘贴视图链接会真的切过去 ✓(与其它覆盖层不同:这是链接存在的意义 ✓)。

  • 工作区是"它住在哪",所以写在链接最前面 ✓:#/workspace/<工作区名>/session/<会话id>(侧栏本身就是这个形状 ✓)。工作区名是给眼睛看的 ✓ —— 决定打开哪个会话的永远是会话 id ✓,所以名字过期/写错也照样打开对的会话 ✓(地址栏随后自己纠正成真实工作区 ✓)。读不到工作区行(没有侧栏 / 树里没有)就不猜 ✓,退回 #/session/<id> ✓。

  • 作用域由视图决定,不靠猜:主面板是应用级界面(列的是本机插件,不是某个对话),所以它的链接不带会话 id。

  • 链接落地即交权:视图一旦变成链接描述的那个界面,链接立刻失去权威——这样你在面板里继续点进去时,地址栏不会被拽回上一层。

  • 绝不抛错:官方 selectPanel 对未注册的面板 id 是抛异常的;所有宿主面调用都被容纳,失败只留一条诊断并回落到屏幕上真实的视图。

  • 侧栏扫描有预算 ✓:一帧内的多次 DOM 变更合并成一次扫描 ✓;只带文本/普通元素的批次直接跳过 ✓(流式输出不会一帧扫一次 ✓);工作区归属一趟遍历解决 ✓(不是每行走一遍树 ✗);已挂锚的行不再重挂 ✓,锚点被视图丢掉时下一趟自动补回 ✓。

已知限制

  • 轮次链接是"位置",而且现在真能跳 ✓:点某一轮,地址栏记下 #/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]) 滚到它 ✓。座位高度/相交都读不到时不算到位 ✓,宁可继续重试也不假报 ✗。
  • 右栏 pane 已闭环 ✓:#/session/<id>/pane/<kind> 与 #/panel/<面板>/pane/<kind> ✓(真机验证:贴链接开出对应页签 ✓、地址栏跟着切换 ✓)。页签种类由右栏自己注册 ✓(实测 7 种:guide / subagentchat / changes-review / plan / files / text / terminal ✓)。
  • 设置面板已纳入:#/settings ✓、#/settings/<分区> ✓、#/settings/<分区>/<页签> ✓(页签靠标准 role="tab" + aria-selected ✓,分区内任何页签都吃这一套 ✓)。分区没有稳定 id ✗(实测:导航项上只有 aria-current="true" ✓,父链是 nav ✓,右侧内容区一个 data-* 都没有 ✗;官方内部的 openSection(id) 也不对外 ✗)→ 所以分区的身份就是它的显示名 ✓:反映读「aria-current="true" 且属于 nav」的那个按钮的标签文字 ✓,驱动按标签点 ✓。入口可能是「齿轮 → 菜单 → 设置」两级 ✓:settings.launcher 座位是单占的 ✓,空位时齿轮一点就开浮层 ✓,被账号行占位时一点只弹菜单 ✗(实测桌面:座位里没有齿轮 ✗)——现在分步走 ✓(浮层 → 屏上的设置项 → 按一次启动器 ✓),只点真正在屏上的项 ✓(关掉的菜单还挂在 DOM 里 ✗)、启动器每个链接只按一次 ✓(它是开关 ✗,重复按会把菜单关掉 ✗)。含义:换语言或官方改文案后,旧的分区链接会失效 ✗(但你重新点一次,地址栏就会写出新链接 ✓ —— 自洽、不过期 ✓)。裸 #/settings 只要面板开着就算落地 ✓,随后地址栏自动升级为具体分区 ✓。
  • 设置页分区的身份只有显示名(上一条的含义,别当成 bug 去找官方要 id ✗):官方那个 openSection(id) 里的 id 不对外 ✗,DOM 上也没有任何 data-* ✓ —— 所以链接里写的就是用户读到的那行字 ✓,官方改文案或换语言后旧链接失效是设计如此 ✓。
  • 会话内的轮次与行锚点已纳入 ✓:#/session/<id>/turn/<n> ✓(反映 + 翻页 + 滚动 ✓ 全链路已闭环)、#/session/<id>/row/<类>/<作用域>/<id> ✓(反映 + 尽力滚动 ✓,含可证明时的轮次归属 ✓)。轮次跳转用的是官方公开 API ✓,没有等待任何新接缝 ✓(uiChat.scrollToTurn 那条请求已降级为锦上添花 ✓,讨论见 https://github.com/deepseek-ai/deepseek-harness/discussions/9339 ✓)。
  • 技能中心 / SSH 的内部页:需要它们各自发布"当前实体 id + 按 id 打开"的契约(任务看板就是这么做的)。
  • 和别的 fragment 写者互斥(例如写 #msg= 的"跳到消息"插件):一个 URL 只有一个 fragment ✓,两个写者只能轮流拥有 ✗。本插件不假装合并 ✓ —— 它认出外来 fragment ✓、绝不覆盖 ✓,并明确说"只能启用一个写者" ✓(而不是互相静默擦掉对方的链接 ✗)。
  • 链接同名不跨机:会话 id 只在同一台机器、同一个 DSH_HOME 下有意义。

开发

手写 JS 直接进 lib/(无构建步骤),npm test 直接跑交付产物(107 条用例,node --test)。改之前请读 AGENTS.md 里的路由与行链接契约。

排障

浏览器 Console 里调用 window.__dshUrlRouter.report() ✓,它会列出:当前地址栏 ✓、本插件认为屏幕在哪个界面 ✓、正在落地的链接与重试次数 ✓、每一层读到了多少个钩子 ✓、右栏 pane 的真实 kind 列表 ✓、它拒绝认领的段位 ✓、以及 compat(与当前 DSH 版本不兼容的层,见下)✓ 和 work(reconcile / 摆放次数,看性能用)✓。它绝不出错、也不写任何东西 ✓。

兼容自检 ✓:官方一改 DOM 契约,插件不会报错,只会静默不再给某一层写链接 ✗。所以渲染期会做五项检查(设置浮层标记、插件管理器钩子、自动化筛选组、会话页签容器、侧栏会话行)✓:屏幕上确实出现了那一层、但本插件要读的钩子没了 → 每个界面只警告一次 ✓,措辞统一为「与当前 DSH 版本不兼容」✓,同时记进 report().compat ✓。它只告警、不改行为 ✓。

English

A URL router for the DSH Web GUI — every surface gets its own link. Click a conversation and the address bar becomes #/session/<id>; click a panel (Plugins, Task Board, …) and it becomes #/panel/<panelId>; walk into a plugin, a component, or a task and the bar follows. Paste the link into another tab, or right-click any sidebar row for the browser's own "open in new tab / copy link".

It is the single writer of the fragment: path and query string (a launch ?token=…) are never touched, and it only ever calls replaceState. Browser half only — no dependencies, no host route, no telemetry.

Install

dsh plugin --profile web add dsh-url-router
# restart `dsh web`, then hard-refresh the browser

Listed in the community plugin index: zhu1090093659/dsh-community-plugins keeps community.json, the single source for the DSH Workshop store catalog and the dsh-market.com manifest (entry dsh-url-router, ui/panel).

Routes

Route Surface
#/session/<id> the conversation the main view shows
#/panel/<panelId> a main-column panel
#/panel/schedules/filter/<all|active|inactive> the automation list's status filter (the default all is not written)
#/panel/schedules/task/<id> one automation task's detail (its rules tab)
#/panel/schedules/task/<id>/records that task's records tab (the default rule is not written)
#/settings/<section>/provider/<provider id> the provider card whose editor is open in Models (the id from the config, not the display name)
#/power The power menu (a popover). A link can only open it, shutdown and restart stay human actions
#/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)
#/panel/plugins/<package> an installed plugin's page
#/panel/plugins/<item> a built-in plugin's page (shell, agent-loop, subagent, web-search)
#/panel/plugins/<package>/<component> one component page inside that plugin's page
#/panel/task-board/task/<taskId> one task's detail on the task board
#/panel/task-board/new the task board's new-task form

A kind it does not know is left alone with a single diagnostic (a fragment has exactly one writer, so a foreign shape is named rather than silently ignored). Panel inner pages are the panel's own vocabulary: the plugin manager publishes data-plugin-detail / data-plugin-item-detail / data-plugin-row-detail, the task board publishes data-dsh-taskboard-open-task / data-dsh-taskboard-new-task.

Every row is a real link

Conversation rows and both plugin-list groups carry a transparent full-row anchor: right-click gives the native menu, middle/Cmd-click opens a native new tab, and a plain left click navigates in place without adding a history entry.

The desktop address bar

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:

  • Drawn only when html[data-windows-titlebar] is present (nothing at all on the plain web app).
  • 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.
  • 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.
  • 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.
  • 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.

Settings: the address bar's three preferences

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:

Preference Default Meaning
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.
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.
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.

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).

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.

  • 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.
  • The screen on display is not offered (it is not somewhere to go back to).
  • 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.
  • A conversation entry shows the conversation title from the sidebar when the sidebar has it; other surfaces show their category and the link itself.
  • The page also has a clear button.
  • 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.

Rules it keeps

  • Fragment only. Path and query are never rewritten.
  • One writer. Do not run another fragment writer (dsh-session-url) beside it.
  • replaceState only, so panels never fill the Back stack.
  • Scopes are declared, not guessed. A panel is application-level and carries no session id.
  • A link stays authoritative until it lands, then hands the address bar back.
  • Never throws. Refused navigations are contained with one diagnostic.

Limits

Still open: Skill Center / SSH inner pages, which need their own "publish the current id + accept an open request" contract the way the task board did. Session links are meaningful on the same machine and DSH_HOME only.

—/ 5

No ratings yet

Verified DSH bundle

Commit e8e6b207a673

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout