omdsh-sidechat
English | 中文
在 DeepSeek Harness 网页界面的任何位置按 ⌘L,就地开一条独立的对话。它带上你正看着的东西作为锚点,问的话和得到的回答都留在它自己的 session 里;在没保存之前,这条对话不会出现在左侧边栏——让它在工作区下面落一行的,是面板右下角的保存按钮。想带上你正在跑的那条对话的上下文也行——标题栏上有分叉按钮,但默认不开:侧边对话默认独立,除非你点名要它成为一根 fork。
它提供什么
| 界面 | 从哪来 |
|---|---|
| 在任何位置生效的 ⌘L | 本插件自己在 window 上装的监听器;在文本框里、在任何挂了 data-omdsh-sidechat-yield 的子树里让位,装了 omdsh-shortcuts 时整个交出去 |
| 一块可拖动、召唤到应用之上的面板 | shell.overlay 上 order 100 的条目:落在选区旁边,位置写进 localStorage 跨刷新记住 |
| 会话标题栏工具位上的一个图标 | conversation.session.header.utilities 这个 list 座位,order 105,排在 omdsh-sidepanel 两个开关的里侧 |
| 那一排不在时的同一个图标 | shell.overlay 上 order -8 的替身,在空白会话和 Code 模式下占住那个角落 |
sidechat 服务——registerAnchorSource、summonChord、setSummonChord | 浏览器侧的 ctx.reflect.provide;知道自己在哪的面板可以直接说出来 |
Chat 工作区里一条被藏起来的侧边会话——或者当前会话的一根 fork 分支——以及一个把它放进侧边栏的保存按钮 | ISessions.fork、host 的 session.create(开新的)、IWorkspaces.archiveSession(藏起来)和 ISession.prompt,都是浏览器本来就握着的面——没有宿主侧 |
sidechat.open 命令 | shortcut 服务在场时注册给 omdsh-shortcuts,图标的 tooltip 也从那里读回当前绑定 |
harness 的输入框只有一个,在对话列底部,而它属于你正在进行的那件事。于是每次「这个函数到底在干嘛」,你都要付两次代价:把手从正在看的地方挪开、滚回输入框,以及把这个问题塞进那条对话的上下文里——它会一直待在那儿,占着窗口,影响后面每一轮。
这个插件把两次代价都删掉:在原地召唤一条自己的对话。
它是一条完整的会话,有多轮,有历史,住在 Chat 工作区里(嵌入上下文时则作为 fork 落在源会话的工作区)。在保存之前它不会出现在侧边栏;保存与否,它都是自己的一条——只不过恰好是从你正指着的那一行旁边召唤出来的。
它显示什么
一问一答,以及工作时的一行字。就这些。
harness 自己的对话列是一个工作台:推理、工具调用、结果、计划、重试,每一步都摊开,因为那一列是你监工的地方。这里不是。这里是问一句、读答案,中间那整套装置压缩成三个词:
你:这个 slot 的 scope 为什么是 session?
Deep diving...
因为视图要跟着当前会话走,而 …text块是回答;reasoning块永远不显示,不是折叠,不是藏在展开条后面——是丢掉;tool-call也不显示,而且一个只调了工具、没说话的步骤不产生任何一条记录,否则记录里会长出一串空回答;- 模型在忙、还没吐出正文时,就是一行
Deep diving...。哪个工具在跑,恰恰是这个界面答应不带的东西。
这条规则是纯函数(transcript.ts),所以它有自己的一组用例,逐条对应上面这几句话。
样式上一个字节都不是自己发明的:回答走 harness 自己的 MarkdownText,你自己的话走 MessageText,每一个颜色都是 alias 变量。所以它跟着 dsh 的主题走,而这个包并不知道当前是哪个主题。
它住在哪
一条自己的 session,落在宿主托管的 Chat 工作区里——就是 omdsh-chatmode 创建、omdsh-sidepanel 用来推导模式的那一个,这里按用户读到的标题去认,而不是靠 import。标题是个产品事实(它就是侧边栏里的分组标题),三个包因此能在它身上达成一致,谁也不依赖谁。
没装 chat 插件的部署没有这个工作区,那就退到当前会话所在的工作区里开第二条 session。上下文照样是分开的,只是两条对话共享一个目录。这个插件也不会因此偷偷依赖上那一个。
嵌入上下文的时候,它是一条 fork。 sessions.fork 是 harness 对「开一条带着这段历史的对话」的官方动词——host 侧的 live-session fork API,harness 自己的对话列也在用它。子会话拿到源会话的完整历史作为种子、继承它的工作目录,血统记在 parentSessionId 上,所以侧边栏里它嵌套在源会话下面。这个插件不拷贝任何东西,也不往被监工的那条里写一个字——fork 切出来的就是一根只读分支。
保存之前是藏起来的。 不管哪条路开出来的会话,一连上就被归档——归档是工作区注册中心自己的动词:日志和工作区的记账都原样保留,只是所有分组界面都不再画它。它不是轻量草稿纸,而是一条真实落盘的 session,只是侧边栏约好不显示。唯一的例外是 host 拒绝归档:会话保持可见,按「已保存」对待——Save 本来要给它造的那一行,它已经有了。
跨刷新记住:会话 id、它 fork 自哪一条、嵌入开关的当前偏好、以及是否已保存,都在 localStorage 的同一条记录里(旧版本写的裸 id 照读,当作「没嵌入、偏好关、已保存」;保存功能之前的记录没有保存标记,按已保存读——那时候的会话从不被藏)。记住的 id 会先拿会话列表验一遍——被删掉的对话不是错误,是一条结束了的对话。
记住的还有它说过的话。 harness 只为「在台上」的那条会话拉历史,而侧边这条按设计永远不上台——所以面板在绑定时自己去把这个窗口要来(transcript-source.ts)。少了这一步,记住的对话回来时是活的,但空着:刷新之后说的都在,刷新之前说的一句不剩。
标题栏上的四个按钮
嵌入上下文 —— 那个分叉图标,也是这条对话的状态:它显示这条侧边对话当前是否带着另一条会话的上下文,并且自己就是开关。
- 亮着(品牌蓝):这条侧边对话是当前会话的一根 fork,上下文就在窗口里。按下去就是断开——嵌入的上下文从窗口里消失,开一条不带它的新对话;被丢下的那根 fork 留在暗处,和任何没保存的侧边对话一样。
- 暗着:这条对话是独立的。按下去就是嵌入——fork 当前正在监工的那条,开一条带着它上下文的新对话。
- Code 模式下变灰:那一列是终端,没有「当前会话」可以嵌入。按钮不可按,也永远不会 fork。
两个方向都以开一条新会话落地,因为一个会话的上下文就是它的历史——既没法事后拼进去,也没法原地摘掉。每个模式都默认暗着——也就是独立——打开嵌入靠的就是这个按钮;Code 里永远灰着;没装 omdsh-basemode 的部署没有模式系统,按 Chat/Work 处理。
在 Chat 窗口中展现 —— 跳到主界面里的这条对话。它本来就是一条普普通通的会话(独立会话在 Chat 工作区,fork 在它源会话的工作区),所以「展现」就只是导航(sessions.open):没有导出,没有复制,同一批消息没有第二种呈现。想接着长聊、想看完整的推理和工具调用,就去那边——那边是工作台,这里是侧边。还没连上会话时这个按钮是禁用的。没保存的会话这样打开后,侧边栏里依旧没有它的行——希望它留在列表里,就先在面板里按保存。
新对话 —— 见下。
关闭 —— 只收起面板,对话本身一动不动。
保存
面板右下角的那个控件——状态行旁边一个小小的 保存——是页脚里唯一和提问无关的东西。
按下去之前,这条侧边对话在任何模式下都没有侧边栏里的行。按下它,就是给藏起来的那条对话切一根 fork:子会话带着它的历史、继承它的工作目录和工作区,不被藏起来——面板接着往子会话里说。藏起来的原会话留在暗处。harness 没有「取消隐藏」这个动词,所以保存出来的对话本质上是一根新分支;从面板的视角什么都没丢,侧边栏里的行落在对话自己的工作区下面——独立对话在 Chat,嵌入对话在源会话的工作区。
保存过之后,控件显示 已保存 加一个对勾,在这条对话活着期间一直如此。它在两种时候拒绝:会话还空着(没什么可保存的,而且工作区账本里躺着的一条空白 session,正是 harness 留给 New Session 复用的东西),或者回答还在进行(fork 是最后一个已完成轮次的快照——此刻保存会丢掉正在飞的这一轮);按钮在这两种状态下本来就是禁用的。host 拒绝保存时,对话留在暗处,状态行上会说一句;按钮继续提供下一次机会。
新对话
面板标题栏上的那个按钮。按下去开一条新的,旧的留在原地——没保存的留在暗处,保存过的留在侧边栏里。新对话服从嵌入开关的当前偏好:开关开着,开出来的就是当前会话的一根新 fork;关着,才是下面说的那种空白会话。
独立会话这条路的底层,是在主工作区里调 session.create 开一条全新的会话,外加本插件自己的空白复用——什么都没说就连按两下,你还是待在同一条空对话里,而不是在 Chat 里撒下一串被藏起来的空壳。这跟 harness 自己的 New Session 是同一条规矩,只是范围圈在本插件自己的对话上:这里有意绕开了 harness 的 connectWorkspace 复用,因为它可能把用户正看着的那条会话递回来,而一个马上要藏会话的面板,绝不能把用户的那条藏起来。fork 没有这种复用:一根分支本来就是新的。
锚点从哪来
锚点是一条关于位置的事实:路径、行号范围、你选中的文字。三样可以各自缺席。
优先问注册进来的锚点源(后注册的先问),都没有答案时,落到内置的那一个——读浏览器自己的选区:
- 选中的文字就是引用;
- 从选区往上找最近的
data-omdsh-anchor,那是路径; - 从选区首尾各自往上找最近的
data-omdsh-anchor-line,那是行号。
两个属性都是可选的,缺一个降一级。全都没有时锚点为空——面板照样召唤,照样问得出去,只是不带位置。
这两个属性是公开约定,和 omdsh-sidepanel 伸手去够 #root 和 [data-slot="conversation"] 是同一种做法:是发布出来的锚点,不是类名、也不是 DOM 形状;不在就跳过,不是错误。想让自己的面板可锚定,加一个属性就完了。
公开的属性一共四个,另外两个管的是「这个事件归谁」而不是「这一行在哪」:data-omdsh-sidechat-yield 标记一棵自己收着召唤键的子树,data-omdsh-sidechat 标记本插件自己的浮层——正是它把「草稿框里选中的文字」读成「这个人在重读自己写的东西」,而不是一段引用;也正是它让召唤键在面板内部变成关闭,而不是让位给光标底下那个 textarea。
面板如果知道得比 DOM 能表达的更多,还可以直接说:
ctx.sidechat.registerAnchorSource(() => ({ origin: 'element', path: currentFile }))它把什么发出去
两条对话之间只过去一样东西:锚点,作为文本。上下文不在这里传——它走 fork:切分支的时候,源会话的整个历史已经是新会话自己的历史了,之后问的每一句都是这根分支上普普通通的一句。PromptContentPart 只有 text 和 image,所以锚点没法作为结构传,那就让它长成一个人会手写的样子:
/w/proj/src/client/apply.ts:199-205
children: { 'conversation.view': { kind: 'list', scope: 'session' },
这个 slot 的 scope 为什么是 session?注意那是绝对路径。 这里有两个工作目录,而且通常不是同一个:锚点来自你正在看的工作区,独立会话住在 Chat;一根 fork 则继承源会话的工作目录,两个目录重合,路径自然落成相对形式。所以路径先按来源还原成绝对,再按接收方相对化——两个目录相同,自然得到相对路径;不同,自然得到绝对路径,两种情况都不用特判。接收方缩不短的绝对路径不是降级,而是正确答案:它是唯一还能指对文件的形式。
引用会被夹断,不会被灌进去。超过 60 行掐掉中间,超过 4096 字节掐掉尾部,两种都会在文本里写明白掐了多少,而且在按下回车之前就显示在面板里。
夹断的标记是英文的,不跟界面语言走:那两行是给 agent 读的,不是给人读的。
草稿是单行、以 / 开头、且不带锚点时,走 session.command();命令没匹配上就照原样当文本发。带着锚点时不走命令路径——命令只吃一行,把你指着的东西悄悄丢掉,是这个界面绝不能干的事。
排队还是插话
| 侧边对话 | Enter | ⌘⏎ |
|---|---|---|
| 空闲 | 直接发 | 直接发 |
| 正在回答 | queue(排到本轮之后) | steer(插进本轮) |
注意这一栏说的是侧边对话自己忙不忙。你的主会话在跑,恰恰是这个插件存在的场景,它绝不会被读成「侧边对话在忙」。
发送失败(模型不支持、会话已经关了)保留错误码。问题本身已经进了记录,那是比任何提示条都好的回执,而且一分钟后还读得到。
它坐在哪
shell.overlay,ui-layout 的全框浮层。这是个 list 座位,所以坐上来不占别人的位子:omdsh-sidepanel 的两块面板已经在这一层,大家按 order 排开(面板 -10,omdsh-sidepanel 的替身 -9,[本插件图标的替身](#这一排不在的时候它去哪) -8,面板 100——前三个是家具,面板是召唤出来的)。
- 第一次出现在选区旁边:默认落在选区下方,放不下就翻到上面,钳进视口;没有选区时落在屏幕上方三分之一处居中。
- 之后就不再自己动了。 位置只被两件事改变:你拖它,或者窗口小到装不下它(那时被拉回视口内——否则标题栏跑到屏幕外,就再也拖不回来了)。召唤只是把它叫回来,不是重新摆一次。位置跨刷新记住,所以「第一次」是真的第一次。
- 拖标题栏移动。用 pointer capture 而不是 window 监听:指针跑得比面板快也不脱手,而且手势底下不会顺手选中一片文字。标题栏里那几个按钮不是把手。
- 没有遮罩。遮罩会盖住你正要问的那段代码。整层本来就是穿透的,面板自己开
pointer-events就够了。 - 点面板外不关,
Escape关,发送成功不关——回答就要在这里出现。 - 锚点跟着选区变,面板不跟。 开着面板再拖一段新的,锚点那一行实时换,窗口一动不动。你打字时还在滑来滑去的窗口,就得靠人去追了。
- 关掉不清空对话。再打开还是刚才那一条——这才叫对话。从头开始是「新对话」的活,而它做成一个按钮,正是为了让这件事必须有意为之。
标题栏那个图标
会话标题栏右侧的工具位上有一个图标,点它就召唤。位置和 omdsh-sidepanel 的两个开关是同一排,并且排在它们里侧(105 对它们的 110);那是个 list 座位,所以这是纯加法:sidepanel 一行没改,也没被 import。
没绑快捷键时它就是唯一入口;tooltip 里带着当前绑定(mac ⌘L,别处 Ctrl+L),用鼠标找到这个功能的人可以就此不用鼠标。
难的不是这个按钮,是按它的时候别把选区弄没了。按下按钮会在 click 处理器跑起来之前就收起选区、移走焦点——不拦住这一步,从这里点开的面板永远锚定到空。所以在 pointerdown 上调了 preventDefault,跟格式工具栏的按钮是同一个动作。
这一排不在的时候,它去哪
这一排并不总在屏幕上,而它缺席的两种情况都很日常:
- 空白会话:harness 会为 hero 把整个标题栏清掉——于是你看到的第一屏反而没有入口;
- Code 模式:
omdsh-codemode用终端遮住了整个conversation座位,标题栏一起没了。快捷键在那儿也是让给终端的,所以没有替身的话,只要 Code 模式开着,这个面板就够不着。
所以同一个按钮在浮层上有一个替身,占住工具位那一排的角落——用标题栏自己量出来的 padding、那一排自己的高度——于是标题栏来了又走的时候图标不会跳。面板在 Chat / Work / Code 三种模式下都在,行为只有一处不同:嵌入上下文在 Chat 和 Work 里可开关、默认关,在 Code 里关闭且按钮变灰——那一列是终端,没有可嵌入的会话。入口在三种模式下都在。
两者永远不会同时出现,而替身等的是标题栏那一份的挂载回报,不是把「harness 什么时候藏标题栏」这条私有规则再推导一遍。这份回报同时也是重新测量的信号:Code 模式接管时中栏的几何一模一样,光看它的盒子什么都看不出来。
这个角落是共用的——omdsh-sidepanel 开关的替身也在那儿,omdsh-usage 的花费条会从里侧贴过来。把它们排开不靠任何注册表,靠的是这条规则:只测真正占着角落的那一个(右边缘最靠右的),然后贴到它里侧。 反过来取最左边的话,测到的会是那些自己已经贴进来的面,而其中一个又在测这一个,于是两边会一直互相往外推。角落空着的组合什么都测不到,图标就直接占住它。
那个键
这一节说的是「没装键位层」的情形。 装了 omdsh-shortcuts 之后,下面这些全部让位,键归那个插件——见[换一个键](#换一个键)。
harness 没有键位注册中心(ui-commands 是斜杠命令的契约,不是 keybinding),所以单独跑的时候,这个插件在 window 上自己装了一个监听器。规矩:
- 只拿 ⌘L(外加 Escape,且只在面板开着时);
- 在文本里让位:input、textarea、contenteditable,以及任何挂了
data-omdsh-sidechat-yield的子树。在那里面事件既不消费也不preventDefault; - 关着的时候什么都不消费;
- 在自己的面板里是切换,否则让位规则会把这个键交给面板自己的 textarea。
⌘L 同时是浏览器的地址栏快捷键,这是有意做的取舍,上面几条规矩就是为它付的账。Ctrl+L 在终端里是清屏——终端挂上 data-omdsh-sidechat-yield 就把这个键完整收回去。
换一个键
对使用者来说,答案是 omdsh-shortcuts。 把它装在这个插件旁边,召唤就变成它那份文档里普普通通的一行,在设置面板里改绑,不写代码也不用刷新:
dsh plugin --profile web add @omdsh-plugins/omdsh-shortcuts装上之后这边会发生什么,写在下一小节。本节剩下的部分是给插件作者的路子——一次服务调用,给那些想把这个键收走的包。
CmdOrCtrl+L 只是默认值:
ctx.sidechat.setSummonChord('CmdOrCtrl+Shift+K') // 改绑
ctx.sidechat.summonChord() // → 当前绑定
ctx.sidechat.setSummonChord(null) // 交出这个键写法是 Electron accelerator 语法——omdsh-shortcuts 的 MenuItem.accelerator 用的就是这一套,将来键位搬到原生菜单项上时不需要翻译层。
null 是完全交出(原生菜单抢走这个键时,页面里再有个 handler 跟它抢,是最难查的 bug);改绑立刻生效,旧键在同一刻释放;写错的 accelerator 抛异常,而不是被忽略。
装了键位插件的时候
有键位层的组合里,键该由一个地方决定——两个监听器抢同一个键,正是那个地方要防的事。所以只要 shortcut 服务在场(由 omdsh-shortcuts 发布),这个插件就把键交出去(setSummonChord(null),此后它自己的监听器一个事件都不消费),改为注册一条命令 sidechat.open。快捷键于是变成那边文档里普普通通的一行,而 tooltip 照样教这个键:绑定是从那边的总机里读回来的,每次变动都重新读一遍,所以在设置面板里改绑,不刷新也能落到图标上。
在那边还没有给 sidechat.open 绑上键之前,入口就是标题栏那个图标——这是诚实的状态,不是坏掉的状态:内置的 ⌘L 之所以消失,正是因为键位现在归别人决定了。
没装那个插件,这里什么都不变。 这次交接跑在 apply 里起的受限 fiber 上,绝不写进顶层 inject:别的插件有没有提供某个服务,是组合的属性;一个 loader entry 去等一个没人组合的服务,会永远停在 pending——那会让启动自检失败、整页跟着挂掉,而不只是少一个功能。所以没有键位层的组合保留内置的 ⌘L;运行时把键位层卸掉,也会把这个键还回来,而不是让面板一个键都不剩。
没有宿主侧
src/index.ts 是一个空 apply()。
面板通过 ISession.prompt 提问、通过 SessionFace(它本身就是 ObservableSnapshot<ConversationSnapshot>)读回答、通过 ISessions.fork 或运行时自己的 session.create 安家——藏起来和保存走 IWorkspaces.archiveSession 和再一次 fork——全是浏览器本来就握着的公开面。模式来自 omdsh-basemode 发布的 sessionModes 服务,在受限 fiber 上按名去够,够不着就按「没有模式系统」处理。锚点则是拿浏览器自己的选区拼的,不读文件系统。所以没有路由要开,没有工作目录要设栅栏,也没有这个插件需要新够到的东西。
空 apply() 存在的唯一理由,是让这个包成为 Loader entry——dsh-client-modules 扫描 dsh.client 时,找的就是这个集合。
运行时依赖:零。它画界面用的每一样东西,都是 harness 已经发布出来的面。
安装
npx @omdsh-plugins/omdsh-plughub add omdsh-sidechat这就是插件中心的安装器,只是入 口从按钮换成了 argv。它从这套集合的 registry 里解析出这个插件、从它的 GitHub 仓库装上,并把那条 pnpm 构建白名单写好——裸的 dsh plugin add github:… 会把这一步留给你,而那条记录里带着 pnpm 解析出来的 commit,只能从报错里抄,事先 写不出来。
dsh plugin --profile web add @omdsh-plugins/omdsh-sidechat 现在还不是那条命令:这个 包不在 npm 上,pnpm 会回 ERR_PNPM_FETCH_404。同样的安装也可以是一个按钮—— 只要 profile 里已经有插件中心,它就在设置 → 插件 → 插件中心里这个插件的卡片 上。
或者从 checkout 装:
pnpm install && pnpm run build
dsh plugin --profile web add "$PWD"
dsh web --port <n>卸载是同一条路:
dsh plugin --profile web remove @omdsh-plugins/omdsh-sidechat它需要一个有浏览器的界面:这里的一切都在浏览器侧,而那一侧的 inject 只写 harness 自己的服务(slots、sessions、workspaces、locale)。在没有浏览器的界面上——TUI、headless——客户端那一半根本不会被拉取,宿主那一半是空的,这是对的:这里没有东西要跑。
每一个伴生插件都是可选的,缺谁都有答案,不会致命。没有 omdsh-chatmode 就没有 Chat 工作区,独立侧边对话改在当前会话所在的工作区里开;没有 omdsh-basemode 就没有模式系统,嵌入按 Chat/Work 处理(永远可嵌入、默认关、按钮可用);没有 omdsh-shortcuts 就保留内置的 ⌘L;没有 omdsh-sidepanel,标题栏那个角落只是空一点。它们都不出现在顶层 inject 里,所以全部没装的 profile 照样能起来,这个插件也照样能用。
它不注册任何设置命名空间。 这里没有一样东西是表单画得出来的——唯一可调的是那个键,而它要么是内置默认值,要么是 omdsh-shortcuts 文档里的一行——所以插件中心会列出这个包,但不提供任何控件,这比摆一张空表单诚实。
命令
pnpm install
pnpm run harness:local ../../deepseek-harness # 先在那边 pnpm run build
pnpm run build
pnpm run typecheck
pnpm run test
pnpm run check:harness-pin # 只要还链着就报错
pnpm run harness:npm # 提交前切回 registry pinlink: 的路径是参数,永远不是提交进去的值——它按声明它的 manifest 解析,写死一次就把一台机器的目录结构烤进包里,而且失败得悄无声息。pnpm run check:harness-pin 就是拦这个的。
规格刻意保持在 registry pin 下也能跑:纯逻辑那几个模块对 harness 的 import 全是 import type,所以裸 clone pnpm install && pnpm test 就行。transcript.ts 把内容分类器作为参数接收而不是 import,正是为了守住这一条——那条显示规则是这个包里最值得检验的东西。嵌入的决策同理是纯函数(embed.ts),也有自己的一组用例。
已知限制
- 它永远不碰你正在跑的那条对话。 fork 只读源会话的历史,问题从不会发进主会话;反过来也一样——这里没有办法把问题发进主会话,只能切一根自己的分支。
- 嵌入是切分支那一刻的快照。 fork 之后,源会话又说的每一句都不会流进来;要跟上,就再按一次嵌入按钮,切一根新的。
- 保存也是一次 fork。 harness 没有「取消隐藏」的动词,所以保存是切一根分支,面板继续在分支上说话;藏起来的原会话——包括保存之后说的话——不在侧边栏里,这个面板也没有办法删掉它。
- 没保存的会话是看不见,不是不存在。 它们是侧边栏约好不画的真实落盘会话;按新对话会把旧的留在暗处,侧边栏里没有能点回去的行,只有面板自己的 localStorage 记录还够得着它。
- 不显示推理,也不显示工具调用。 是丢掉而不是折叠。想看的时候,标题栏那个按钮会把你带到摊开这一切的工作台上。
- 它不读文件。 锚点只是页面已经显示给你的东西;这里没有自己的
@补全,也没法附上页面没在显示的文件。 - 一次一个锚点,一个文件。 横跨两个文件的引用就是两个问题;图片则完全不收——那是 composer 的能力,而这里不是 composer。
- 单靠它自己,这个键改不了。 想改绑,要么装
omdsh-shortcuts,要么从别的插件里调setSummonChord;这个包自己不注册任何设置命名空间。 - 一个 frame 只有一块面板。 它是单个浮层条目,只记一个位置、一条对话,所以没有第二块侧边对话可以并排摆着。