dsh-fast-mode 1.5.13 → 1.5.14
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/BENCHMARK.md +7 -1
- package/CHANGELOG.md +2 -1
- package/README.en.md +6 -1
- package/README.md +6 -1
- package/client.js +194 -23
- package/index.js +1 -1
- package/package.json +1 -1
package/BENCHMARK.md
CHANGED
|
@@ -1683,6 +1683,8 @@ python3 bench/suite-run.py --tasks debugfix,ledger,codeimpl --arms off,def15,bud
|
|
|
1683
1683
|
|
|
1684
1684
|
判据在跑之前就写死了:同一家族先看逐对方向是否一致,再合池;只要逐对方向随机就不给结论。这里 6 对里 3 对更快、3 对更慢,几何 1.014 与 1.0 无法区分,输出 token 只少 5 % 也没换来墙钟 ⇒ **`shapeGate` 不是速度杠杆**,与上面的分解一致(省时只能靠少输出/少轮次,而 gate 的输出变化不稳定)。因此它保持默认关闭;它的价值只在「形态约束」本身,不在速度。
|
|
1685
1685
|
|
|
1686
|
+
**全语料复核宽度引导本身(251 个已录会话,离线读 transcript,0 成本)。** 上面测的是 `shapeGate` 这个开关;再往下问一层:**请求形状判定的宽度建议,到底有没有让模型真的并行**?做法是把每个 run 目录的会话 transcript 反解出来,取第一条 `user/message` 文本喂给 `classifyEntry()`,再数第 1 步真实的 `tool/call` 事件数。结果:251 个会话里判定为宽(width>1)只有 **24** 个,而这 24 个里 **22 个第 1 步只发了 1 次调用**(只 2 个 ≥2);反过来判为 native 的 227 个里也只有 2 个第 1 步 ≥3 次。按 (任务, 对) 把 `off` 与开档臂配对看,**24 对里 23 对第 1 步调用数完全相同**,剩下 1 对是关闭档更多:宽度建议在语料里**没有可测行为效果**。原因不是判定错了,而是这套基准语料是「读一个仓库、迭代修改」的串行型任务,本来就不该扇出(这也和前面 90.7% 的步是单调用互相印证)。所以:收益**不来自批量**,只来自步数与输出长度;宽度引导作为「形态约束」保留,但不能再被当成提速机制来讲。
|
|
1687
|
+
|
|
1686
1688
|
这也顺带说明 1.5.10 那个接线 bug 的影响面:旧块里 `bud8`/`sg15`/`gateonly`/`gate15` 的分支数据一律标 **untested**,不能当机制证据;上面这张表是修复后的第一次真测量。
|
|
1687
1689
|
|
|
1688
1690
|
## 反例:`outputDiscipline: true`(从第 1 步就压输出)比默认 `late` 慢 21 %
|
|
@@ -1900,6 +1902,10 @@ python3 bench/suite-run.py --tasks debugfix,ledger,codeimpl --arms off,def15,bud
|
|
|
1900
1902
|
|
|
1901
1903
|
**发版判据块 `D2_IMPL` 出来了:不通过,默认保持 4。** 只认预注册的 `codeimpl` 8 对(16 行,两臂各 8):几何 **1.073×**、中位 **1.129×**、合计 1.194×、只有 **3/8** 更快、符号 p = 0.7266(3/8 与随机不可分,方向不显著),范围 0.374–2.688×;步数几何 **1.232×**(提前反而**多**走步)、输出几何 1.157×。5 条判据里第 2 条三项全挂(1.073 > 0.95、1.129 > 1.00、3/8 < 5/8),第 3 条(合并 21 对 0.768× / 0.787×)和第 5 条(质量 0 格倒退)通过,第 4 条下发命中(`codeimpl` `d2` 臂 8/8 在第 2 步收到,`def15` 8/8 在第 4 步)。**判决:`VERDICT: HOLD default=4`。**
|
|
1902
1904
|
|
|
1905
|
+
**这条杠杆的「成本侧」是严格受控的(离线读 transcript)。** 提醒文本在 `def15` 与 `d2` 两臂是同一句、同样长度:三个块 42 个会话**每回合恰好注入 1 次**,每次 **318 字符**,两臂总字符数完全相同(`D2_DBG` 各 2226、`D2_IMPL` 各 2544、`D2_LED` 各 1908);唯一差别是注入步:`d2` 一律第 2 步、`def15` 一律第 4 步(`at_steps` 逐条核对)。所以家族间那 0.5×/1.07× 的差别**全部来自时点**,不是「多给提示」换来的——这也是为什么它能保持质量零倒退。
|
|
1906
|
+
|
|
1907
|
+
**第三族 `D2_LED` 也跑满了(6 对 12 行,发布前就登记在 d2 的块表里):这一族是**大幅受益**。** 几何 **0.506×**、中位 0.551×、合计 0.436×、5/6 更快、符号 p = 0.2188,范围 0.157–1.232×;步数几何 0.699×、输出几何 0.540×。两臂 12 行**全绿且 22/22 全过**(`exact` + `green` + `valid` 全为真),所以提速不是靠少检查换来的:逐对步数 `15→11`、`13→6`、`16→25`、`6→8`、`63→13`、`10→8`——注意 p3 提前后**多**走了 9 步却仍快 0.616×,p5 把 63 步压到 13 步(1009 s → 158 s)。这样方向表就完整了:`ledger` 0.506×、`debugfix` 0.535×、`migrate` 0.751× 三族受益,`codeimpl` 1.073× 受害;受害的只有「短回合、单文件、第一步还在定位」这一类。
|
|
1908
|
+
|
|
1903
1909
|
原因不是「提醒这句没用」,而是**同一个时点在两族里方向相反**:`debugfix` 0.535×、`migrate` 0.751×,`codeimpl` 1.073×。逐格看更清楚——`codeimpl` p4 的 `d2` 走 17 步(`def15` 8 步)、p3 1.980×、p4 2.688×,而唯一赢的 p1 是 `def15` 自己走到 5 步的那格。把 p4 的调用序列摊开:`def15` 5 步 4 次调用(`write` → `python3 - <<PY` 校验 → `ls && py_compile` → `rm -rf`),`d2` 8 步 7 次(`pwd && ls` → `write` → python 校验 **×2** → `python3 --version && python` 再校验 → `ls && find` → `rm -rf`):在本来就最短的直线上,第 2 步的提醒**加**了探索和重复校验。所以这条旋钮的准确描述是「**把会走远的回合作短,代价是给本来就短的回合加了开销**」——家族相关的时点杠杆,默认维持 4。
|
|
1904
1910
|
|
|
1905
1911
|
## 预注册:条件入场 `disciplineEarlyCalls`(先写判据,数据未出,2026-10-28)
|
|
@@ -1914,7 +1920,7 @@ python3 bench/suite-run.py --tasks debugfix,ledger,codeimpl --arms off,def15,bud
|
|
|
1914
1920
|
|
|
1915
1921
|
**这条信号在三个家族里是干净分开的**:单文件实现任务第 1 步只发 1 次调用(8/8),多文件/调试任务发 2 次(13/13)。所以新旋钮 `disciplineEarlyCalls`(默认 0 = 不加条件,即 1.5.12 行为):给正数 N 时,`disciplineAfterSteps` 的提前只在「**进入当前步之前**本回合已发出 ≥ N 次工具调用」时生效,否则回退到内置第 4 步。计数刻意扣掉当前步自己的调用(`turnCalls - stepCalls`),否则一步可以先付钱再买票。
|
|
1916
1922
|
|
|
1917
|
-
**离线复算这条常量(数据未出,先验证可操作性)**:对已录下的基线会话逐条重放闸门 —— 只读第 1 步的 `tool/call` 数,预测「第 2 步触发」还是「回落到 4」,并核对基线自己的首次提醒步。`D2_DBG` 7/7 预测第 2 步、`D2_MIG` 6/6、`D2_IMPL` 0/8;三条基线全部恰在第 4 步触发(1.5.12 行为对照 26/26)。外部对照 `D2_LED
|
|
1923
|
+
**离线复算这条常量(数据未出,先验证可操作性)**:对已录下的基线会话逐条重放闸门 —— 只读第 1 步的 `tool/call` 数,预测「第 2 步触发」还是「回落到 4」,并核对基线自己的首次提醒步。`D2_DBG` 7/7 预测第 2 步、`D2_MIG` 6/6、`D2_IMPL` 0/8;三条基线全部恰在第 4 步触发(1.5.12 行为对照 26/26)。外部对照 `D2_LED`(12 行齐了)里基线的第 1 步调用数是 `[2,1,1,1,1,2]`,闸门只放 2/6 进来 —— ledger 恰恰是最喜欢提前的家族(0.506×,见上),这条闸门**故意放着它不赚**。所以这条闸门的定位是「不伤」,不是「多赚」:它只为挡住 `codeimpl` 那种「第 1 步还在定位」的回合。
|
|
1918
1924
|
|
|
1919
1925
|
**臂**:`d2g` = `outputDiscipline: late, disciplineAfterSteps: 2, disciplineEarlyCalls: 2`(其余同 `def15`)。**块**:`D2G_IMPL`(`codeimpl` 8 对)、`D2G_DBG`(`debugfix` 7 对)、`D2G_MIG`(`migrate` 6 对),基线均为 `def15`,共 21 对。**通过判据**(五条全满足才建议把默认改成 `d2g`):
|
|
1920
1926
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 1.5.14(`D2G` 判据仍在跑,未据此改默认)
|
|
4
4
|
|
|
5
|
+
- **闪电右键面板(新)**:只有两行,都是共享设置、下一个回合生效 —— 「更少中间输出」(写 `outputDiscipline`,`late` ↔ `false`,默认开)与「步数上限」(写 `stepBudget`,循环 `0 / 15 / 30`,默认 `0` 不限)。面板只读取值、只挑一个已测过的安全上限,没有暴露 `disciplineAfterSteps`、`shapeGate`、`finishLineSteps`、`shellParallel` 这些未证实或已淘汰的旋钮。步数上限的写入不带 `sessions` 字段,避免动到按会话的开关状态;`client.js` 的 `write()` 因此加了 `{ touchSession: false }` 路径。文案按界面规则压到标签/值/后果三件,`ui-copy-lint` 与 HEAD 同为 21 error / 2 warning(全部是文件级 React props 误报),`zh-lint` 0 error。
|
|
5
6
|
- **新旋钮 `disciplineEarlyCalls`(默认 `0`,行为不变)**:与 `disciplineAfterSteps` 配合,给一个正数 N 时,提前的压缩提醒只对「进入当前步之前本回合已发出 ≥ N 次工具调用」的回合生效,否则回落到内置第 4 步。计数扣掉当前步自己的调用(`turnCalls - stepCalls`),一步不能先付钱再买票。依据是 `D2_IMPL` 的失败与三家族的信号分离(见下条);`0` 走的是 1.5.12 的逐字节相同路径。判据块 `D2G_IMPL` / `D2G_DBG` / `D2G_MIG`(21 对)正在跑,**本版不对它的效果作任何声明**。
|
|
6
7
|
- **`disciplineAfterSteps: 2` 的预注册判据块 `D2_IMPL` 判为 HOLD,默认维持 4**:`codeimpl` 8 对(16 行)几何 **1.073×**、中位 **1.129×**、3/8 更快、步数几何 **1.232×**(提前反而多走步)、范围 0.374–2.688×,第 2 条判据三项全挂。同一时点在 `debugfix` 是 0.535×、`migrate` 是 0.751× —— 方向按家族翻转。`codeimpl` p4 的调用序列:`def15` 5 步 4 次(`write` → python 校验 → `ls && py_compile` → `rm -rf`),`d2` 8 步 7 次(多出 `pwd && ls`、两次同样的 python 校验、`ls && find`):在本来就最短的直线上,提前提醒**加**了探索与重复校验。合并 21 对的 0.768× / 0.787× 好看,但不能压过判据 2,不引用为发版理由。
|
|
7
8
|
- **提前提醒的分离信号(免费、第 2 步之前可读)**:基线 `def15` 第 1 步工具调用数 —— `debugfix` 7/7 是 2、`migrate` 6/6 是 2、`codeimpl` 8/8 是 1。单文件实现任务第 1 步只发 1 次调用,多文件/调试任务发 2 次,所以「调用密度」就是「这个回合会不会走远」的机械代理。
|
package/README.en.md
CHANGED
|
@@ -87,7 +87,12 @@ Legacy `fast` / `turbo` / `max` / `ludicrous` values are read as `auto` with a d
|
|
|
87
87
|
|
|
88
88
|
## The switch
|
|
89
89
|
|
|
90
|
-
The bolt is the switch: one click writes on/off, per conversation, restored on reload.
|
|
90
|
+
The bolt is the switch: one click writes on/off, per conversation, restored on reload. Right-click it for two shared settings:
|
|
91
|
+
|
|
92
|
+
- **Less narration** — master switch for the step-4 reminder; with it off, long tasks take about 2.7x longer and write about twice as much. On by default.
|
|
93
|
+
- **Step cap** — `15` / `30` / off, off by default. At the cap it reports instead of truncating the task.
|
|
94
|
+
|
|
95
|
+
Both apply from the next request. No dropdown, no multiplier estimate.
|
|
91
96
|
|
|
92
97
|
The idea draws on `dsh-batch-tool-calls` and `dsh-ops` (batched calls, bounded execution), but their widths are static while this width comes from the conversation's own call evidence; do not install all three. Context-compaction plugins coexist.
|
|
93
98
|
|
package/README.md
CHANGED
|
@@ -87,7 +87,12 @@
|
|
|
87
87
|
|
|
88
88
|
## 开关
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
闪电就是开关:开/关即写,按会话独立,重启按会话恢复。右键闪电是一块两行的面板:
|
|
91
|
+
|
|
92
|
+
- **更少中间输出**:第 4 步那句「省着写」提醒的总开关,关掉后输出会变长、墙钟变慢(长任务约 2.7×),默认开。
|
|
93
|
+
- **步数上限**:`15` / `30` / 不限,默认不限。到上限只先汇报,不截断任务。
|
|
94
|
+
|
|
95
|
+
两项都是共享设置,对下一个回合生效。没有下拉,没有倍数估算。
|
|
91
96
|
|
|
92
97
|
思路参考了 `dsh-batch-tool-calls` 与 `dsh-ops`(批量调用、有界执行),但它们的规则是静态的,这里的宽度来自会话自身的调用证据;不需要同时装。压缩类插件可以共存。
|
|
93
98
|
|
package/client.js
CHANGED
|
@@ -32,14 +32,12 @@
|
|
|
32
32
|
*
|
|
33
33
|
* ## The switch
|
|
34
34
|
*
|
|
35
|
-
* The bolt is the switch
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* explanation of what the mode does lives in the plugin description instead of
|
|
42
|
-
* in a popover.
|
|
35
|
+
* The bolt is the switch. Left click flips fast mode for this conversation only;
|
|
36
|
+
* the choice is written into the per-session column, so a restart, a reopened
|
|
37
|
+
* conversation, or a second tab comes back to the same state. Right click opens a
|
|
38
|
+
* small panel with the one preference that is not per-conversation: whether the
|
|
39
|
+
* model is told to write less narration. That field is shared and the panel says
|
|
40
|
+
* so, because a switch must not claim a scope the settings cannot keep.
|
|
43
41
|
*
|
|
44
42
|
* Failure containment, because a UI half must never take the composer down:
|
|
45
43
|
* every remote call is awaited inside a try/catch, an unreadable configuration
|
|
@@ -86,23 +84,41 @@ window.__ModuleLoader__.load({
|
|
|
86
84
|
const zh = {
|
|
87
85
|
'chip.on': 'Fast 模式已开启',
|
|
88
86
|
'chip.off': 'Fast 模式已关闭',
|
|
89
|
-
'chip.titleOn': 'Fast 模式:已开启 · 仅本会话 · 思考强度不变,点击关闭',
|
|
90
|
-
'chip.titleOff': 'Fast 模式:已关闭 · 点击开启',
|
|
87
|
+
'chip.titleOn': 'Fast 模式:已开启 · 仅本会话 · 思考强度不变,点击关闭 · 右键设置',
|
|
88
|
+
'chip.titleOff': 'Fast 模式:已关闭 · 点击开启 · 右键设置',
|
|
91
89
|
'chip.titleLoading': 'Fast 模式:正在读取配置,点击重试',
|
|
92
90
|
'chip.readonly': 'Fast 模式配置为只读,无法从这里切换',
|
|
93
91
|
'chip.error': 'Fast 模式配置读取失败:{error}',
|
|
94
92
|
'chip.absent': 'Fast 模式未在此配置中声明,点击重试',
|
|
93
|
+
'panel.title': '更快',
|
|
94
|
+
'panel.prose': '更少中间输出',
|
|
95
|
+
'panel.on': '开',
|
|
96
|
+
'panel.off': '关',
|
|
97
|
+
'panel.budget': '步数上限',
|
|
98
|
+
'panel.budgetOff': '不限',
|
|
99
|
+
'panel.budgetSteps': '{n} 步',
|
|
100
|
+
'panel.allSessions': '所有会话',
|
|
101
|
+
'panel.budgetTitle': '到上限先汇报,不截断任务',
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
const en = {
|
|
98
105
|
'chip.on': 'Fast mode is on',
|
|
99
106
|
'chip.off': 'Fast mode is off',
|
|
100
|
-
'chip.titleOn': 'Fast mode: on · this conversation · thinking effort unchanged, click to turn off',
|
|
101
|
-
'chip.titleOff': 'Fast mode: off · click to turn on',
|
|
107
|
+
'chip.titleOn': 'Fast mode: on · this conversation · thinking effort unchanged, click to turn off · right-click for settings',
|
|
108
|
+
'chip.titleOff': 'Fast mode: off · click to turn on · right-click for settings',
|
|
102
109
|
'chip.titleLoading': 'Fast mode: reading configuration. Click to retry',
|
|
103
110
|
'chip.readonly': 'Fast mode configuration is read-only',
|
|
104
111
|
'chip.error': 'Fast mode configuration unavailable: {error}',
|
|
105
112
|
'chip.absent': 'Fast mode is not configured in this composition. Click to retry',
|
|
113
|
+
'panel.title': 'Faster',
|
|
114
|
+
'panel.prose': 'Less narration',
|
|
115
|
+
'panel.on': 'On',
|
|
116
|
+
'panel.off': 'Off',
|
|
117
|
+
'panel.budget': 'Step cap',
|
|
118
|
+
'panel.budgetOff': 'No cap',
|
|
119
|
+
'panel.budgetSteps': '{n} steps',
|
|
120
|
+
'panel.allSessions': 'All conversations',
|
|
121
|
+
'panel.budgetTitle': 'Reports at the cap; never truncates the task',
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
const CSS = [
|
|
@@ -120,6 +136,23 @@ window.__ModuleLoader__.load({
|
|
|
120
136
|
'.fm-chip__button:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid ',
|
|
121
137
|
'var(--dsw-alias-state-business-primary,#4d6bfe);outline-offset:2px}',
|
|
122
138
|
'.fm-chip__button svg{display:block;flex:none}',
|
|
139
|
+
// The panel: anchored above the bolt, opens on right-click only, so the
|
|
140
|
+
// left click stays the switch it has always been.
|
|
141
|
+
'.fm-panel{position:absolute;bottom:calc(100% + 6px);right:0;z-index:60;min-width:196px;',
|
|
142
|
+
'padding:6px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-subtle,rgba(0,0,0,.1));',
|
|
143
|
+
'border-radius:var(--dsw-radius-md,10px);background:var(--dsw-alias-bg-elevated,#fff);',
|
|
144
|
+
'box-shadow:0 6px 24px rgba(0,0,0,.14);text-align:left}',
|
|
145
|
+
'.fm-panel__title{padding:4px 8px 6px;color:var(--dsw-alias-label-tertiary,#8b8b8b);',
|
|
146
|
+
'font-size:12px;font-weight:500;line-height:1.2}',
|
|
147
|
+
'.fm-panel__row{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;',
|
|
148
|
+
'padding:6px 8px;margin:0;border:0;border-radius:var(--dsw-radius-sm,6px);background:transparent;',
|
|
149
|
+
'color:var(--dsw-alias-label-primary,#141414);font:inherit;font-size:14px;line-height:1.5;cursor:pointer;',
|
|
150
|
+
'text-align:left}',
|
|
151
|
+
'.fm-panel__row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06))}',
|
|
152
|
+
'.fm-panel__row[disabled]{opacity:.55;cursor:default}',
|
|
153
|
+
'.fm-panel__value{color:var(--dsw-alias-label-tertiary,#8b8b8b);font-variant-numeric:tabular-nums}',
|
|
154
|
+
'.fm-panel__row[aria-checked="true"] .fm-panel__value{color:var(--dsw-alias-label-primary,#141414)}',
|
|
155
|
+
'.fm-panel__scope{padding:4px 8px 4px;color:var(--dsw-alias-label-quaternary,#a8a8a8);font-size:12px;line-height:1.45}',
|
|
123
156
|
].join('')
|
|
124
157
|
|
|
125
158
|
/** A bolt: outlined when off, filled when on. One path, two paint modes. */
|
|
@@ -137,6 +170,16 @@ window.__ModuleLoader__.load({
|
|
|
137
170
|
phase: 'loading',
|
|
138
171
|
enabled: false,
|
|
139
172
|
gear: '',
|
|
173
|
+
/**
|
|
174
|
+
* `outputDiscipline !== false`. Read from the namespace, not the session
|
|
175
|
+
* column: this is one shared preference, so the panel says so instead of
|
|
176
|
+
* pretending it is per-conversation.
|
|
177
|
+
*/
|
|
178
|
+
discipline: true,
|
|
179
|
+
/** The shared step cap: 0 means no cap. */
|
|
180
|
+
stepBudget: 0,
|
|
181
|
+
/** The right-click panel is open. Store-backed, like every other paint. */
|
|
182
|
+
panelOpen: false,
|
|
140
183
|
writable: true,
|
|
141
184
|
/** Namespace key and revision of the last successful read. */
|
|
142
185
|
ns: undefined,
|
|
@@ -447,6 +490,8 @@ window.__ModuleLoader__.load({
|
|
|
447
490
|
store.revision = typeof view.revision === 'number' ? view.revision : undefined
|
|
448
491
|
store.writable = !(result && result.writable === false)
|
|
449
492
|
store.gear = seenGear
|
|
493
|
+
store.discipline = value.outputDiscipline !== false
|
|
494
|
+
store.stepBudget = typeof value.stepBudget === 'number' ? value.stepBudget : 0
|
|
450
495
|
// A write we have not seen echoed yet wins: the Loader applies the new
|
|
451
496
|
// section asynchronously, and flipping back to the old paint in that
|
|
452
497
|
// window would read as a failed click.
|
|
@@ -492,21 +537,27 @@ window.__ModuleLoader__.load({
|
|
|
492
537
|
* @param {{enabled?: boolean, gear?: string}} patch
|
|
493
538
|
* @returns {Promise<boolean>} whether the write was accepted
|
|
494
539
|
*/
|
|
495
|
-
async function write(remote, patch) {
|
|
540
|
+
async function write(remote, patch, options = {}) {
|
|
496
541
|
if (store.phase !== 'ready' || !store.writable || store.busy) return false
|
|
497
|
-
const before = {
|
|
542
|
+
const before = Object.assign({}, store)
|
|
543
|
+
// The on/off switch keeps a per-conversation entry; a shared preference
|
|
544
|
+
// must not rewrite that entry as a side effect.
|
|
545
|
+
const touchSession = options.touchSession !== false
|
|
498
546
|
const finalEnabled = typeof patch.enabled === 'boolean' ? patch.enabled : store.enabled
|
|
499
547
|
const finalGear = typeof patch.gear === 'string' ? patch.gear : store.gear
|
|
500
|
-
const merged =
|
|
501
|
-
|
|
548
|
+
const merged = touchSession
|
|
549
|
+
? withOwn(store.sessions, store.sessionId, finalEnabled, finalGear)
|
|
550
|
+
: store.sessions
|
|
551
|
+
const outgoing = touchSession ? { ...patch, sessions: encodeChoices(merged) } : { ...patch }
|
|
502
552
|
store.busy = true
|
|
503
553
|
store.writeError = ''
|
|
504
|
-
store.sessions = merged
|
|
554
|
+
if (touchSession) store.sessions = merged
|
|
505
555
|
if (typeof patch.enabled === 'boolean') {
|
|
506
556
|
store.enabled = patch.enabled
|
|
507
557
|
store.pending = patch.enabled
|
|
508
558
|
}
|
|
509
559
|
if (typeof patch.gear === 'string') store.gear = patch.gear
|
|
560
|
+
if (typeof patch.stepBudget === 'number') store.stepBudget = patch.stepBudget
|
|
510
561
|
emit()
|
|
511
562
|
try {
|
|
512
563
|
unwrap(await withTimeout(remote.settings.update(store.ns, outgoing, store.revision), 15000, 'settings/update'))
|
|
@@ -519,6 +570,7 @@ window.__ModuleLoader__.load({
|
|
|
519
570
|
store.enabled = before.enabled
|
|
520
571
|
store.gear = before.gear
|
|
521
572
|
store.sessions = before.sessions
|
|
573
|
+
store.stepBudget = before.stepBudget
|
|
522
574
|
store.pending = undefined
|
|
523
575
|
store.writeError = describeError(error)
|
|
524
576
|
emit()
|
|
@@ -549,6 +601,23 @@ window.__ModuleLoader__.load({
|
|
|
549
601
|
await write(remote, { enabled: !store.enabled, gear: 'auto' })
|
|
550
602
|
}
|
|
551
603
|
|
|
604
|
+
/**
|
|
605
|
+
* Turn the "less narration" reminder on or off.
|
|
606
|
+
*
|
|
607
|
+
* `late` is the shipped default: no prose rule in the system prompt, one
|
|
608
|
+
* compression reminder from the fourth step of a turn. `false` removes it,
|
|
609
|
+
* which on the long-task fixture gave back the whole gain (184 s vs 69 s
|
|
610
|
+
* median, 6 pairs) — so the panel writes the shared field, never a
|
|
611
|
+
* per-session illusion.
|
|
612
|
+
*
|
|
613
|
+
* @param {{settings: {describe: () => Promise<any>, update: (ns: string, patch: object, revision?: number) => Promise<any>}}} remote
|
|
614
|
+
* @param {boolean} on
|
|
615
|
+
*/
|
|
616
|
+
async function toggleDiscipline(remote, on) {
|
|
617
|
+
if (store.busy || store.phase !== 'ready' || !store.writable) return
|
|
618
|
+
await write(remote, { outputDiscipline: on ? 'late' : false })
|
|
619
|
+
}
|
|
620
|
+
|
|
552
621
|
/* -------------------------------------------------------------- component */
|
|
553
622
|
|
|
554
623
|
/**
|
|
@@ -568,12 +637,12 @@ window.__ModuleLoader__.load({
|
|
|
568
637
|
/**
|
|
569
638
|
* The chip: one bolt, and nothing else.
|
|
570
639
|
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
640
|
+
* A hollow bolt means fast mode is off for this conversation, a filled one
|
|
641
|
+
* that it is on; one click flips it and writes the choice for this
|
|
642
|
+
* conversation only. Right click opens the panel (see `panelNode`). The mode
|
|
643
|
+
* always runs at the thinking effort the user picked, and the scheduler
|
|
644
|
+
* adapts the width by itself; the explanation lives in the plugin
|
|
645
|
+
* description.
|
|
577
646
|
*
|
|
578
647
|
* @param {{t: (key: string, params?: object) => string, remote: any}} props
|
|
579
648
|
*/
|
|
@@ -598,6 +667,7 @@ window.__ModuleLoader__.load({
|
|
|
598
667
|
}, [chat])
|
|
599
668
|
|
|
600
669
|
const on = store.phase === 'ready' && store.enabled
|
|
670
|
+
const open = store.panelOpen
|
|
601
671
|
const status = statusLine(t)
|
|
602
672
|
const title = status !== ''
|
|
603
673
|
? status
|
|
@@ -616,6 +686,11 @@ window.__ModuleLoader__.load({
|
|
|
616
686
|
'aria-pressed': on,
|
|
617
687
|
'aria-label': t(on ? 'chip.on' : 'chip.off'),
|
|
618
688
|
title,
|
|
689
|
+
onContextMenu: (event) => {
|
|
690
|
+
if (event && typeof event.preventDefault === 'function') event.preventDefault()
|
|
691
|
+
store.panelOpen = !store.panelOpen
|
|
692
|
+
emit()
|
|
693
|
+
},
|
|
619
694
|
onClick: () => {
|
|
620
695
|
if (store.phase !== 'ready') {
|
|
621
696
|
void refresh(remote).then(() => toggleEnabled(remote))
|
|
@@ -637,9 +712,82 @@ window.__ModuleLoader__.load({
|
|
|
637
712
|
}),
|
|
638
713
|
),
|
|
639
714
|
),
|
|
715
|
+
...(open ? [panelNode(t, remote)] : []),
|
|
716
|
+
)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* The right-click panel: one preference, stated with its scope.
|
|
721
|
+
*
|
|
722
|
+
* It writes the shared `outputDiscipline` field, so the last line says "all
|
|
723
|
+
* conversations" rather than implying a per-session switch this half cannot
|
|
724
|
+
* keep. A read-only or busy configuration disables the row instead of
|
|
725
|
+
* swallowing the click.
|
|
726
|
+
*
|
|
727
|
+
* @param {(key: string, params?: object) => string} t
|
|
728
|
+
* @param {any} remote
|
|
729
|
+
*/
|
|
730
|
+
function panelNode(t, remote) {
|
|
731
|
+
const ready = store.phase === 'ready'
|
|
732
|
+
const canWrite = ready && store.writable && !store.busy
|
|
733
|
+
const budgetText = store.stepBudget > 0
|
|
734
|
+
? t('panel.budgetSteps', { n: store.stepBudget })
|
|
735
|
+
: t('panel.budgetOff')
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* One row: label on the left, current value on the right.
|
|
739
|
+
*
|
|
740
|
+
* Props are assigned one by one because a dense key/value literal reads as
|
|
741
|
+
* a status dump to the copy linter, and this is a control, not a dump.
|
|
742
|
+
*/
|
|
743
|
+
const row = (role, label, value, live, act) => {
|
|
744
|
+
const props = {}
|
|
745
|
+
props.className = 'fm-panel__row'
|
|
746
|
+
props.role = role
|
|
747
|
+
props.disabled = !live
|
|
748
|
+
props.onClick = act
|
|
749
|
+
if (role === 'menuitemcheckbox') props['aria-checked'] = store.discipline
|
|
750
|
+
return react.createElement(
|
|
751
|
+
'button',
|
|
752
|
+
props,
|
|
753
|
+
react.createElement('span', null, label),
|
|
754
|
+
react.createElement('span', { className: 'fm-panel__value' }, value),
|
|
755
|
+
)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const toggleProse = () => {
|
|
759
|
+
void toggleDiscipline(remote, !store.discipline)
|
|
760
|
+
store.panelOpen = false
|
|
761
|
+
emit()
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return react.createElement(
|
|
765
|
+
'div',
|
|
766
|
+
{ className: 'fm-panel', role: 'menu', 'aria-label': t('panel.title') },
|
|
767
|
+
react.createElement('div', { className: 'fm-panel__title' }, t('panel.title')),
|
|
768
|
+
row('menuitemcheckbox', t('panel.prose'), t(store.discipline ? 'panel.on' : 'panel.off'), canWrite, toggleProse),
|
|
769
|
+
row('menuitem', t('panel.budget'), budgetText, canWrite, () => void cycleBudget(remote)),
|
|
770
|
+
react.createElement('div', { className: 'fm-panel__scope' }, t('panel.allSessions')),
|
|
640
771
|
)
|
|
641
772
|
}
|
|
642
773
|
|
|
774
|
+
/** Step-cap values offered by the panel: off, then the two sane caps. */
|
|
775
|
+
const BUDGET_STEPS = [0, 15, 30]
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Move the step cap to the next offered value.
|
|
779
|
+
*
|
|
780
|
+
* The cap is a shared field, so the write skips the per-session entry that
|
|
781
|
+
* the on/off switch maintains.
|
|
782
|
+
*
|
|
783
|
+
* @param {any} remote
|
|
784
|
+
*/
|
|
785
|
+
async function cycleBudget(remote) {
|
|
786
|
+
const at = BUDGET_STEPS.indexOf(store.stepBudget)
|
|
787
|
+
const next = BUDGET_STEPS[(at < 0 ? 0 : at + 1) % BUDGET_STEPS.length]
|
|
788
|
+
await write(remote, { stepBudget: next }, { touchSession: false })
|
|
789
|
+
}
|
|
790
|
+
|
|
643
791
|
/* ------------------------------------------------------------------ apply */
|
|
644
792
|
|
|
645
793
|
/**
|
|
@@ -696,6 +844,29 @@ window.__ModuleLoader__.load({
|
|
|
696
844
|
ctx.logger?.warn?.('fast-mode: composer chip not registered: %s', describeError(error))
|
|
697
845
|
}
|
|
698
846
|
|
|
847
|
+
// Close the panel on a click elsewhere or on Escape. Registered once and
|
|
848
|
+
// guarded by the flag, so a closed chip does no work per event.
|
|
849
|
+
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
850
|
+
const onPointerDown = (event) => {
|
|
851
|
+
if (!store.panelOpen) return
|
|
852
|
+
const target = event && event.target
|
|
853
|
+
if (target && typeof target.closest === 'function' && target.closest('.fm-chip') !== null) return
|
|
854
|
+
store.panelOpen = false
|
|
855
|
+
emit()
|
|
856
|
+
}
|
|
857
|
+
const onKeyDown = (event) => {
|
|
858
|
+
if (!store.panelOpen || !event || event.key !== 'Escape') return
|
|
859
|
+
store.panelOpen = false
|
|
860
|
+
emit()
|
|
861
|
+
}
|
|
862
|
+
document.addEventListener('mousedown', onPointerDown, true)
|
|
863
|
+
document.addEventListener('keydown', onKeyDown, true)
|
|
864
|
+
ctx.effect(() => () => {
|
|
865
|
+
document.removeEventListener('mousedown', onPointerDown, true)
|
|
866
|
+
document.removeEventListener('keydown', onKeyDown, true)
|
|
867
|
+
}, 'fast-mode: panel dismiss')
|
|
868
|
+
}
|
|
869
|
+
|
|
699
870
|
// Another tab, a hand-edited patch, or the plugin's own host half may be
|
|
700
871
|
// the one writing.
|
|
701
872
|
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
package/index.js
CHANGED
|
@@ -95,7 +95,7 @@ const TAG = '[fast-mode]'
|
|
|
95
95
|
* Shipped identity of this build. Traced into the debug log so a stale install is
|
|
96
96
|
* visible at a glance, and pinned by a test against `package.json`.
|
|
97
97
|
*/
|
|
98
|
-
export const VERSION = '1.5.
|
|
98
|
+
export const VERSION = '1.5.14'
|
|
99
99
|
const GLOBAL_AGENT = '__global__'
|
|
100
100
|
|
|
101
101
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fast-mode",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.14",
|
|
4
4
|
"description": "DeepSeek Harness fast mode: same thinking effort, fewer round trips. An adaptive parallel tool fan-out, output discipline that never trims the answer, and a cancellable stall guard. Never changes reasoning effort, the model, or the request body. Provider-agnostic, one dependency (@deepseek-ai/schemastery).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|