dsh-newbe-response-window 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  DeepSeek Harness (DSH) Web 插件:把一轮里的 think(推理)和工具调用放进**有限高度的可滚动窗口**(默认 10 行),以**阶段性文字回复为界分段**,每个分段一个 **slide**;文字回复保持原生完整显示、不做限高。中间过程**始终可见**(只是不撑爆页面)——Grok Build 风格。
4
4
 
5
- > 已验证 DSH 版本:**0.1.2-alpha.5**(2026-09)。
5
+ > 已验证 DSH 版本:**0.1.7-alpha.1**(2026-09)。
6
6
  > 与「全部折叠成 summary」类插件(如 `dsh-tool-summary`)不同:本插件**不隐藏**任何内容。think 与工具调用一条条列在 slide 里,每段都能展开翻到底;文字回复完全原生展示。
7
7
  > 只包 bash + think:阶段性/最终文字 response **不**加限高、**不**加「展开全部/收起」按钮。
8
+ > **0.1.7 起的一轮折叠**:DSH 0.1.7 原生把「已完成的一轮」的中间过程折叠成一行「已完成工作 N 个工具调用」(简洁/详细/完全展开三种「工作过程展示」模式都折叠)。进行中的一轮 slide 照常常显;已完成的一轮点开这一行后,看到的就是本插件的 slide。
8
9
 
9
10
  ## 效果
10
11
 
@@ -16,6 +17,10 @@ DeepSeek Harness (DSH) Web 插件:把一轮里的 think(推理)和工具
16
17
  - 主体:`max-height: N 行`(默认 10)的**内部滚动区**
17
18
  - **think 与工具调用严格按真实输出顺序交错排列**(不做「think 全在上 / bash 全在下」的强行分区)
18
19
  - 每段 think 一行(Think + 单行摘要 + 展开箭头),默认折叠,点击展开完整推理(内部限高滚动);展开文字与工具输出同字号(同一 mono 字体)
20
+ - **think 流式显示三种模式**(`liveStreamThink`,默认 `'out'`):
21
+ - `'out'`(默认):正在流式的 think 在 **slide 外**(原生位置)实时显示,流式结束后**收进 slide 折叠**成单行摘要
22
+ - `'in'`:流式 think 在 **slide 内**自动展开、正文实时滚动,结束后自动收起为单行摘要
23
+ - `'off'`:think 全程折叠为单行摘要,不做流式展开
19
24
  - 每条工具调用一行(状态点 + 工具名 + 单行摘要),点击展开参数/输出(输出再限高一档,内部滚动)
20
25
  - 执行中自动跟随底部
21
26
  - **原生 Think 行从消息流中隐藏**(该段已并入 slide,避免重复显示),与 slide 内展示共用同一份内容
@@ -26,8 +31,16 @@ DeepSeek Harness (DSH) Web 插件:把一轮里的 think(推理)和工具
26
31
 
27
32
  ## 安装
28
33
 
34
+ 从 npm 安装(跟 `latest`,即最近一次稳定发布):
35
+
36
+ ```bash
37
+ dsh plugin --profile web add dsh-newbe-response-window
38
+ ```
39
+
40
+ 或从合集仓库 GitHub 子目录安装(`alpha` 通道;换 `#rc` 装 rc 通道,换 `#<commit-sha>` 锁死某次发布):
41
+
29
42
  ```bash
30
- dsh plugin --profile web add github:qiqiangvae/dsh-newbe-plugins#path:packages/dsh-newbe-response-window
43
+ dsh plugin --profile web add 'github:qiqiangvae/dsh-newbe-plugins#alpha&path:packages/dsh-newbe-response-window'
31
44
  ```
32
45
 
33
46
  或本地 link 方式(开发调试):
@@ -51,24 +64,28 @@ dsh plugin --profile web add "link:$(pwd)/packages/dsh-newbe-response-window"
51
64
  | 键 | 默认 | 说明 |
52
65
  | --- | --- | --- |
53
66
  | `lines` | `10` | 窗口高度(行)。`0` = 不限高(等价于关闭窗口) |
54
- | `collapsed` | `false` | 每轮 slide 是否默认收起成一行 bar。默认 `false`:始终展开、内容可见 |
67
+ | `collapsed` | `false` | slide 本身是否默认收起成一行 bar。默认 `false`:展开、内容可见(已完成轮次的外层折叠由 DSH 原生「已完成工作」那行控制) |
55
68
  | `showReadOnly` | `true` | 是否在 slide 里列出 read/grep/web_search 等只读调用(默认全列出,不藏) |
56
69
  | `minCollapseRows` | `3` | 仅 `collapsed: true` 时生效:少于该数量的轮次不收起 |
70
+ | `liveStreamThink` | `'out'` | 流式 think 的显示模式:`'out'` 盒外流式结束后进盒折叠 / `'in'` 盒内自动展开 / `'off'` 全程折叠 |
57
71
 
58
72
  ### 设置页(Settings → General)
59
73
 
60
- 插件在 Web UI 的 **Settings → General** 里注册了一项 **「响应窗口大小(行数)」**:
74
+ 插件在 Web UI 的 **Settings → General** 里注册了两项:
61
75
 
62
- - `−` / 数值输入 / `+`:调整 `lines`(0–200,`0` = 不限高,默认 10)
63
- - **即时生效**:改动后已渲染的 slide 高度立刻变化(经宿主 settings namespace `dsh-newbe-response-window` 持久化)
76
+ - **「响应窗口大小(行数)」**:`−` / 数值输入 / `+` 调整 `lines`(0–200,`0` = 不限高,默认 10)
77
+ - **「流式思考显示」**:三选一(关闭 / 盒内 / 出盒),对应 `liveStreamThink`(默认出盒)
78
+ - 均**即时生效**:改动后已渲染的 slide 立刻变化(经宿主 settings namespace `dsh-newbe-response-window` 持久化)
64
79
 
65
80
  ## 实现说明(为什么安全)
66
81
 
67
82
  - 工具调用 slide 通过 `conversation.chat.node`(`tool-call` key,`priority: -100`)的 **slot shadow** 在 React 层实现:每轮第一个 tool-call 节点渲染整个 slide,同轮其余 tool-call 节点渲染空,任何渲染异常由 slot 运行时的 entry boundary **自动 abdicate** 回内置渲染。
68
83
  - **绝不移走 React 拥有的 `[data-chat-anchor-key]` 行节点**。实测:把行移进自定义容器后,一旦 DSH 后续移除该行(会话切换/编辑/压缩),React 会调用 `parent.removeChild(row)` 抛 `NotFoundError`,整个会话树被卸载——因此本插件只用「slot shadow + 类/CSS」两种方式,对 React 行结构零改动。
69
- - 原生 Think 行隐藏与 slide 内的 think 同步:只对「该段内有 slide」的原生 `data-variant="think"` 行加 `display:none`(DOM 类/CSS,无重挂、无删除),其余(无工具调用的纯 Think 段)保持原生显示。
84
+ - 原生 Think 行隐藏与 slide 内的 think 同步:只对「该段内有 slide」的原生 `data-variant="think"` 行加 `display:none`(DOM 类/CSS,无重挂、无删除),其余(无工具调用的纯 Think 段)保持原生显示。0.1.7 把消息流行套进了 process-group 容器(`[data-chat-flow] > group root > content > 行`),行不再是 flow 的直接子节点,所以这段判定改成按文档顺序读全部 `[data-chat-flow-kind]` 行。(改动前 `flow.children` 只看到组外壳,永远匹配不到 Think,导致 slide 与原生 Think 双份显示。)
70
85
  - 插件只读会话快照,不写快照、不调宿主 API。0.1.2-alpha 起快照从 slot 标准 prop **`useChat`** 读取(`{ locations.getTurn(turn), nodes.get(key) }` 形状),不再走 `useSession` 的 `chat` 字段;「幽灵行」判定通过 DOM 行上的 `data-chat-flow-key` 与该快照对号,不再嗅探 markdown CSS 类名(新版 CSS Module 类名已不含 `_markdown`)。
71
86
  - 浏览器半依赖 `@deepseek-ai/dsh-client-store`(`createSnapshotStore`,shell 内置 seed module),替代旧版的 `@deepseek-ai/dsh-client-runtime`(该包在新 alpha 已移除)。旧版 `dsh.plugin.json` 清单在新 alpha 不再被读取,已删除。
87
+ - **设置持久化走 `configForms`**:0.1.7 移除了 `settingsScope` 服务,改为每个 profile 条目一个 controller(`ctx.configForms.get(条目 id)`,id 即 settings namespace)。宿主半的 `Config` 必须把用户可改字段标 `.volatile()`(settings 文档只投影 volatile 字段),并 `settings.configure({ auto: false }, ctx.fiber)` 退出自动生成的配置页。旧写法(`settingsScope` + `settings.register()`)会让 `inject` 永远等不到服务、整个插件静默不挂载——这正是 0.1.7 升级后 slide 全消失的原因。
88
+ - 宿主半因此要求 `@deepseek-ai/schemastery` **≥ 3.18.3**(`.volatile()` 的引入版本;随 DSH 0.1.7-alpha.1 发布)。
72
89
 
73
90
  ## 开发 / 测试
74
91
 
@@ -78,15 +95,18 @@ dsh plugin --profile web add "link:$(pwd)/packages/dsh-newbe-response-window"
78
95
  npm run check # node --check lib/index.js && node --check lib/client.js
79
96
  ```
80
97
 
81
- E2E(需要已启动的 `dsh web` python playwright):
98
+ E2E(需要已启动的 `dsh web`;用 uv python playwright):
82
99
 
83
100
  ```bash
84
101
  dsh --profile web --no-open --port 3639 &
85
- python3 test/e2e.py --url http://127.0.0.1:3639 --session "架构重构不顺原因分析"
102
+ uv run --with playwright playwright install chromium # 首次
103
+ uv run --with playwright python3 test/e2e.py --url http://127.0.0.1:3639 --session "修复 dsh 升级后的插件不兼容"
86
104
  ```
87
105
 
88
- - 宿主半:`lib/index.js`(注册 settings namespace,依赖 `@deepseek-ai/schemastery`)
89
- - 浏览器半:`lib/client.js`(`window.__ModuleLoader__.load`,require `react` 与 shell seed 的 `@deepseek-ai/dsh-client-store`)
106
+ 断言含:客户端半确实挂载(slide 1,服务缺失时这里会先失败)、窗口限高且内部真能滚动、think 单行且点击展开、有 slide 的段落里原生 Think 不重复显示、Settings → General 两行挂载并读到持久值、会话切换不崩。
107
+
108
+ - 宿主半:`lib/index.js`(声明 volatile 设置字段并退出自动配置页,依赖 `@deepseek-ai/schemastery` ≥ 3.18.3)
109
+ - 浏览器半:`lib/client.js`(`window.__ModuleLoader__.load`,require `react` 与 shell seed 的 `@deepseek-ai/dsh-client-store`,设置走 `configForms`)
90
110
 
91
111
  ## License
92
112
 
package/cordis.patch.yml CHANGED
@@ -19,6 +19,11 @@
19
19
  # minCollapseRows only auto-collapse a finished slide when it has at least
20
20
  # this many tool calls (default 3; only used with
21
21
  # collapsed=true).
22
+ # liveStreamThink how a streaming think block is shown (default 'out'):
23
+ # 'off' — always folded into a one-line summary
24
+ # 'in' — auto-expand inside the slide, live scroll
25
+ # 'out' — stream outside the slide (native position),
26
+ # then fold into the slide once settled
22
27
  - insert:
23
28
  - id: dsh-newbe-response-window
24
29
  name: 'dsh-newbe-response-window'
@@ -27,3 +32,4 @@
27
32
  collapsed: false
28
33
  showReadOnly: true
29
34
  minCollapseRows: 3
35
+ liveStreamThink: 'out'
package/lib/client.js CHANGED
@@ -1,9 +1,15 @@
1
1
  // dsh-newbe-response-window — browser half.
2
2
  //
3
- // Adapted for DSH 0.1.2-alpha.5 (the current client-module / slot-runtime
4
- // generation). Breaking changes vs. the older alpha this plugin shipped for:
3
+ // Adapted for DSH 0.1.7-alpha.1 (the current client-module / slot-runtime
4
+ // generation). Breaking changes vs. the alpha this plugin last shipped for:
5
5
  //
6
- // - `@deepseek-ai/dsh-client-runtime` is gone. The browser shell now seeds
6
+ // - The `settingsScope` client service is gone. Durable preferences now go
7
+ // through the `configForms` service, keyed by profile entry id, and the host
8
+ // half has to mark its user-editable fields `volatile()` and call
9
+ // `settings.configure({ auto: false }, ctx.fiber)`. Declaring the old
10
+ // service in `inject` parks the plugin forever ("waiting for service:
11
+ // settingsScope") — that, and only that, is what upgrading DSH broke.
12
+ // - `@deepseek-ai/dsh-client-runtime` is gone. The browser shell seeds
7
13
  // `@deepseek-ai/dsh-client-store` (createSnapshotStore) as a static module,
8
14
  // so the bundle requires that instead — no dsh.client.inject needed.
9
15
  // - `conversation.chat.node` moved from dsh-client-ui-conversation to
@@ -61,17 +67,28 @@ window.__ModuleLoader__.load({
61
67
  var createSnapshotStore = storeLib.createSnapshotStore
62
68
 
63
69
  // ---- config -----------------------------------------------------------
70
+ // liveStreamThink mode: 'off' (always folded) | 'in' (auto-expand the row
71
+ // inside the slide while streaming) | 'out' (stream outside the slide in
72
+ // the native position, fold into the slide when settled).
64
73
  var DEFAULTS = {
65
74
  lines: 10,
66
75
  collapsed: false,
67
76
  showReadOnly: true,
68
77
  minCollapseRows: 3,
78
+ liveStreamThink: 'out',
69
79
  }
70
80
  function clampInt(value, min, max, fallback) {
71
81
  var n = Number(value)
72
82
  if (!Number.isFinite(n)) return fallback
73
83
  return Math.min(max, Math.max(min, Math.round(n)))
74
84
  }
85
+ function normalizeLiveStreamThink(value, fallback) {
86
+ if (value === 'off' || value === 'in' || value === 'out') return value
87
+ // Legacy boolean row config: true meant in-box live streaming.
88
+ if (value === true) return 'in'
89
+ if (value === false) return 'off'
90
+ return fallback
91
+ }
75
92
  function readConfig(cfg) {
76
93
  cfg = cfg || {}
77
94
  return {
@@ -79,19 +96,22 @@ window.__ModuleLoader__.load({
79
96
  collapsed: cfg.collapsed === true,
80
97
  showReadOnly: cfg.showReadOnly !== false,
81
98
  minCollapseRows: clampInt(cfg.minCollapseRows, 1, 50, DEFAULTS.minCollapseRows),
99
+ liveStreamThink: normalizeLiveStreamThink(cfg.liveStreamThink, DEFAULTS.liveStreamThink),
82
100
  }
83
101
  }
84
102
 
85
103
  // ---- live settings state ----------------------------------------------
86
- // `lines` is user-adjustable from Settings; the rest come from row config.
104
+ // `lines` and `liveStreamThink` are user-adjustable from Settings; the
105
+ // rest come from row config.
87
106
  var linesStore = null
107
+ var liveStreamThinkStore = null
88
108
  var settingsScope = null
89
109
  var NS = 'dsh-newbe-response-window'
90
110
  function clampLines(value) { return clampInt(value, 0, 200, DEFAULTS.lines) }
91
111
 
92
112
  /** Minimal observable-value hook over a SnapshotStore (plain React). */
93
- function useStoreValue(store) {
94
- var pair = React.useState(function () { return store === null ? DEFAULTS.lines : store.getSnapshot() })
113
+ function useStoreValue(store, fallback) {
114
+ var pair = React.useState(function () { return store === null ? fallback : store.getSnapshot() })
95
115
  var value = pair[0]
96
116
  var setValue = pair[1]
97
117
  React.useEffect(function () {
@@ -105,7 +125,7 @@ window.__ModuleLoader__.load({
105
125
 
106
126
  /** Live window size in lines (follows the Settings store). */
107
127
  function useLiveLines() {
108
- return useStoreValue(linesStore)
128
+ return useStoreValue(linesStore, DEFAULTS.lines)
109
129
  }
110
130
 
111
131
  // ---- shared style injection ------------------------------------------
@@ -195,7 +215,7 @@ window.__ModuleLoader__.load({
195
215
  '.drw-think-name { color: var(--dsw-alias-label-secondary, #bbb); font-size: 11px; font-weight: 600; letter-spacing: 0.04em; flex: none; }',
196
216
  '.drw-think-summary { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-tertiary, #999); font-size: 12.5px; flex: 1 1 auto; }',
197
217
  '.drw-think-toggle { color: var(--dsw-alias-label-tertiary, #888); font-size: 10px; flex: none; }',
198
- '.drw-think-body { margin: 0; padding: 6px 8px; color: var(--dsw-alias-label-primary, #eee); font: var(--dsw-font-markdown-code-block-small, 12px/1.5 ui-monospace, monospace); white-space: pre-wrap; word-break: break-word; max-height: 240px; overflow-y: auto; }',
218
+ '.drw-think-body { margin: 0; padding: 6px 8px; color: var(--dsw-alias-label-primary, #eee); font: var(--dsw-font-markdown-code-block-small, 12px/1.5 ui-monospace, monospace); white-space: pre-wrap; word-break: break-word; max-height: 96px; overflow-y: auto; }',
199
219
  /* Hide the native Think row once it's inside a slide (DOM + CSS only) */
200
220
  '[data-variant="think"][data-drw-hidethink="1"] { display: none !important; }',
201
221
  /* Settings row: response window size */
@@ -213,6 +233,10 @@ window.__ModuleLoader__.load({
213
233
  '}',
214
234
  '.drw-set-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16)); }',
215
235
  '.drw-set-btn:disabled { opacity: 0.4; cursor: default; }',
236
+ '.drw-set-btn.drw-set-on {',
237
+ ' border-color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand, #3dbbf5));',
238
+ ' color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand, #3dbbf5));',
239
+ '}',
216
240
  '.drw-set-input {',
217
241
  ' width: 56px; height: 28px; text-align: center;',
218
242
  ' border: 1px solid var(--dsw-alias-border-l2, var(--dsw-alias-border-strong, rgba(128,128,128,0.35)));',
@@ -343,16 +367,36 @@ window.__ModuleLoader__.load({
343
367
  // ---- one think (reasoning) row inside the slide -------------------------
344
368
  // A compact single-line row, collapsible to the full reasoning text — the
345
369
  // same look and feel as a tool-call row (think and tool calls are both
346
- // "implementation" and should read the same inside the window).
370
+ // "implementation" and should read the same inside the window). While the
371
+ // block is still streaming the row auto-expands and the body pins to the
372
+ // latest line; when it settles the row folds back to its one-line summary.
373
+ // The head stays a static first-line summary the whole time, so the only
374
+ // moving part during a stream is the expanding body — one visual focus.
347
375
  function ThinkRow(props) {
348
376
  var text = props.text
349
377
  var index = props.index
378
+ var streaming = props.streaming === true
350
379
  var openState = React.useState(false)
351
380
  var open = openState[0]
352
381
  var setOpen = openState[1]
382
+ var bodyRef = React.useRef(null)
353
383
  var firstLine = text.split('\n')[0].trim() || text.trim()
354
384
  var summary = firstLine.length > 120 ? firstLine.slice(0, 117) + '…' : firstLine
355
- return React.createElement('div', { className: 'drw-think', 'data-open': open ? '1' : undefined },
385
+ // Live streaming: auto-expand while streaming, fold back once settled.
386
+ React.useEffect(function () {
387
+ if (streaming) setOpen(true)
388
+ else setOpen(false)
389
+ }, [streaming])
390
+ // While streaming, keep the body pinned to the latest line.
391
+ React.useEffect(function () {
392
+ if (!open || !bodyRef.current) return
393
+ if (streaming) bodyRef.current.scrollTop = bodyRef.current.scrollHeight
394
+ })
395
+ return React.createElement('div', {
396
+ className: 'drw-think',
397
+ 'data-open': open ? '1' : undefined,
398
+ 'data-streaming': streaming ? '1' : undefined,
399
+ },
356
400
  React.createElement('div', {
357
401
  className: 'drw-think-head', role: 'button', tabIndex: 0,
358
402
  'aria-expanded': open || undefined,
@@ -364,7 +408,7 @@ window.__ModuleLoader__.load({
364
408
  React.createElement('span', { className: 'drw-think-toggle', 'aria-hidden': true }, open ? '\u25BE' : '\u25B8'),
365
409
  ),
366
410
  open
367
- ? React.createElement('pre', { className: 'drw-think-body' }, text)
411
+ ? React.createElement('pre', { ref: bodyRef, className: 'drw-think-body' }, text)
368
412
  : null,
369
413
  )
370
414
  }
@@ -382,6 +426,7 @@ window.__ModuleLoader__.load({
382
426
  var openFile = props.openFile
383
427
  var inspectCall = props.inspectCall
384
428
  var cfg = props.config
429
+ var streamingThink = props.streamingThink || null
385
430
  var lines = useLiveLines() // live window size from Settings
386
431
  var toolItems = items.filter(function (it) { return it.kind === 'tool' })
387
432
  var reasoningCount = items.reduce(function (acc, it) { return it.kind === 'think' ? acc + 1 : acc }, 0)
@@ -393,10 +438,27 @@ window.__ModuleLoader__.load({
393
438
  var setOpen = openState[1]
394
439
  var bodyRef = React.useRef(null)
395
440
  var running = stats.running > 0
396
- // Auto-follow to the bottom while calls are still running.
441
+ // Streaming detection depends on the mode:
442
+ // 'in' — a think block inside the box is still streaming (the row
443
+ // auto-expands and the slide is forced open so the live
444
+ // reasoning stays readable);
445
+ // 'out' — the live tail is streaming OUTSIDE the slide (native row)
446
+ // and folds in when settled; the slide is forced open so the
447
+ // fold-in is visible.
448
+ // In both cases streaming overrides `collapsed`, then the user's
449
+ // collapsed setting is restored (Q7).
450
+ var mode = cfg.liveStreamThink
451
+ var streaming = mode === 'in'
452
+ ? items.some(function (it) { return it.kind === 'think' && it.streaming === true })
453
+ : mode === 'out'
454
+ ? streamingThink !== null
455
+ : false
456
+ var effectiveOpen = open || streaming
457
+ // Auto-follow to the bottom while calls are still running or a think is
458
+ // still streaming.
397
459
  React.useEffect(function () {
398
- if (!open || !bodyRef.current) return
399
- if (running) bodyRef.current.scrollTop = bodyRef.current.scrollHeight
460
+ if (!effectiveOpen || !bodyRef.current) return
461
+ if (running || streaming) bodyRef.current.scrollTop = bodyRef.current.scrollHeight
400
462
  })
401
463
  var list = items
402
464
  if (!cfg.showReadOnly) {
@@ -405,10 +467,10 @@ window.__ModuleLoader__.load({
405
467
  })
406
468
  }
407
469
  var thinkSeen = 0
408
- return React.createElement('div', { className: 'drw-slide', 'data-running': running ? '1' : undefined },
470
+ return React.createElement('div', { className: 'drw-slide', 'data-running': running || streaming ? '1' : undefined },
409
471
  React.createElement('div', {
410
472
  className: 'drw-head', role: 'button', tabIndex: 0,
411
- 'aria-expanded': open || undefined,
473
+ 'aria-expanded': effectiveOpen || undefined,
412
474
  onClick: function () { setOpen(!open) },
413
475
  onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(!open) } },
414
476
  },
@@ -416,6 +478,9 @@ window.__ModuleLoader__.load({
416
478
  stats.total + ' 个工具调用'
417
479
  + (reasoningCount > 0 ? ' · ' + reasoningCount + ' Think' : '')
418
480
  ),
481
+ streaming
482
+ ? React.createElement('span', { className: 'drw-head-badge', 'data-state': 'running' }, '思考中')
483
+ : null,
419
484
  running
420
485
  ? React.createElement('span', { className: 'drw-head-badge', 'data-state': 'running' }, '进行中')
421
486
  : null,
@@ -425,18 +490,23 @@ window.__ModuleLoader__.load({
425
490
  !cfg.showReadOnly && stats.readOnly > 0
426
491
  ? React.createElement('span', { className: 'drw-head-badge' }, '隐藏只读 ' + stats.readOnly)
427
492
  : null,
428
- React.createElement('span', { className: 'drw-head-toggle' }, open ? '收起 ▴' : '展开 ▾'),
493
+ React.createElement('span', { className: 'drw-head-toggle' }, effectiveOpen ? '收起 ▴' : '展开 ▾'),
429
494
  ),
430
495
  React.createElement('div', {
431
496
  ref: bodyRef,
432
- className: 'drw-body' + (open ? '' : ' drw-collapsed'),
433
- style: open && lines > 0 ? { maxHeight: (lines * 1.55) + 'em' } : undefined,
497
+ className: 'drw-body' + (effectiveOpen ? '' : ' drw-collapsed'),
498
+ style: effectiveOpen && lines > 0 ? { maxHeight: (lines * 1.55) + 'em' } : undefined,
434
499
  },
435
500
  list.map(function (it) {
436
501
  if (it.kind === 'think') {
437
502
  var tid = thinkSeen
438
503
  thinkSeen += 1
439
- return React.createElement(ThinkRow, { key: 'think-' + tid, text: it.text, index: tid })
504
+ return React.createElement(ThinkRow, {
505
+ key: 'think-' + tid,
506
+ text: it.text,
507
+ index: tid,
508
+ streaming: it.streaming === true,
509
+ })
440
510
  }
441
511
  return React.createElement(SimpleToolRow, {
442
512
  key: it.node.key,
@@ -469,12 +539,13 @@ window.__ModuleLoader__.load({
469
539
  var current = null
470
540
  function ensure() {
471
541
  if (current === null) {
472
- current = { items: [], tools: [], startKey: null }
542
+ current = { items: [], tools: [], startKey: null, streamingThink: null }
473
543
  segments.push(current)
474
544
  }
475
545
  return current
476
546
  }
477
547
  function closeSegment() { current = null }
548
+ var mode = currentConfig.liveStreamThink
478
549
  for (var i = 0; i < order.length; i++) {
479
550
  var n = chat.nodes.get(order[i])
480
551
  if (n === undefined) continue
@@ -482,13 +553,29 @@ window.__ModuleLoader__.load({
482
553
  if (n.kind === 'assistant-step') {
483
554
  var blocks = n.data && Array.isArray(n.data.blocks) ? n.data.blocks : []
484
555
  var staged = hasVisibleTextBlock(blocks)
485
- // The reasoning of this step belongs to the chunk leading up to the
486
- // staged response (i.e. the segment that ends here), and it keeps its
487
- // exact output position among the tools.
556
+ // While a step is running, its LAST reasoning block is the live
557
+ // streaming tail the exact rule the native ReasoningRow uses
558
+ // (`running: streaming && i === last`).
559
+ // mode 'out': the tail streams OUTSIDE the slide (native row) and
560
+ // folds in once settled — recorded as streamingThink.
561
+ // mode 'in' : the tail stays INSIDE the box, tagged `streaming` so
562
+ // the row auto-expands and shows live progress.
563
+ // mode 'off': no streaming behavior, always folded.
564
+ var stepRunning = !!n.data && n.data.status === 'running'
565
+ var lastIdx = blocks.length - 1
488
566
  for (var b = 0; b < blocks.length; b++) {
489
567
  var block = blocks[b]
490
568
  if (block && block.kind === 'reasoning' && typeof block.text === 'string' && block.text.trim() !== '') {
491
- ensure().items.push({ kind: 'think', text: block.text })
569
+ if (mode === 'out' && stepRunning && b === lastIdx) {
570
+ ensure().streamingThink = { text: block.text, nodeKey: n.key }
571
+ } else {
572
+ ensure().items.push({
573
+ kind: 'think',
574
+ text: block.text,
575
+ nodeKey: n.key,
576
+ streaming: mode === 'in' && stepRunning && b === lastIdx,
577
+ })
578
+ }
492
579
  }
493
580
  }
494
581
  if (staged) closeSegment()
@@ -554,6 +641,7 @@ window.__ModuleLoader__.load({
554
641
  cwd: cwd,
555
642
  openFile: openFile,
556
643
  inspectCall: inspectCall,
644
+ streamingThink: seg.streamingThink || null,
557
645
  config: currentConfig,
558
646
  })
559
647
  } catch (e) {
@@ -624,10 +712,15 @@ window.__ModuleLoader__.load({
624
712
  // reasoning does not appear both inside the slide and inline in the flow.
625
713
  // Only DOM class toggling — never reparents React-owned rows.
626
714
  function hideNativeThink(root, textIndex) {
627
- var flow = root.querySelector('[data-chat-flow]')
628
- if (!flow || !flow.children) return
629
- var rows = []
630
- for (var i = 0; i < flow.children.length; i++) rows.push(flow.children[i])
715
+ // 0.1.7-alpha.1 nests flow rows inside process-group containers
716
+ // (`[data-chat-flow] > group root > content > rows`) instead of keeping
717
+ // every row a direct child of one flow element, so walking
718
+ // `flow.children` only ever saw the group wrappers and never matched a
719
+ // Think against the slide that sits beside it in the same group. Read the
720
+ // flow rows in document order instead — that is still reading order —
721
+ // while keeping the segment-boundary walk unchanged.
722
+ var rows = root.querySelectorAll('[data-chat-flow-kind]')
723
+ if (rows.length === 0) return
631
724
  // Segment boundary in the DOM: a user/steering row, a turn-level row
632
725
  // (turn-process / turn-tail), or an assistant-step row that carries
633
726
  // visible staged text (a response shown to the user mid-turn).
@@ -658,7 +751,15 @@ window.__ModuleLoader__.load({
658
751
  if (isSegmentBoundary(next)) break
659
752
  if (next.querySelector && next.querySelector('.drw-slide')) { hasSlide = true; break }
660
753
  }
661
- if (hasSlide) think.dataset.drwHidethink = '1'
754
+ if (hasSlide) {
755
+ // In 'out' mode the streaming think tail lives OUTSIDE the box
756
+ // (native row) until it settles: keep it visible while
757
+ // `data-streaming` is present, then fold it in (hide) the moment the
758
+ // stream is gone. In 'in'/'off' modes the native row is always
759
+ // hidden — the box renders the same content.
760
+ var streamingOut = currentConfig.liveStreamThink === 'out' && !!think.closest('[data-streaming]')
761
+ if (!streamingOut) think.dataset.drwHidethink = '1'
762
+ }
662
763
  else if (think.dataset.drwHidethink) delete think.dataset.drwHidethink
663
764
  }
664
765
  }
@@ -711,34 +812,64 @@ window.__ModuleLoader__.load({
711
812
  }
712
813
 
713
814
  // ---- Settings row: response window size ---------------------------------
815
+ var STREAM_MODES = [
816
+ { value: 'off', label: '关闭' },
817
+ { value: 'in', label: '盒内' },
818
+ { value: 'out', label: '出盒' },
819
+ ]
714
820
  function ResponseWindowSettingsRow(props) {
715
821
  var useLines = props.useLines
716
822
  var setLines = props.setLines
823
+ var useLiveStreamThink = props.useLiveStreamThink
824
+ var setLiveStreamThink = props.setLiveStreamThink
717
825
  var lines = (typeof useLines === 'function' ? useLines(function (s) { return s }) : null) ?? currentConfig.lines
718
826
  var value = clampLines(lines)
719
827
  var step = function (delta) {
720
828
  setLines(value + delta)
721
829
  }
722
- return React.createElement('div', { className: 'drw-set-row' },
723
- React.createElement('div', { className: 'drw-set-text' },
724
- React.createElement('div', { className: 'drw-set-title' }, '响应窗口大小(行数)'),
725
- React.createElement('div', { className: 'drw-set-desc' }, '每个响应 slide 的限高滚动窗口。默认 10 行,0 = 不限高,改动即时生效。'),
830
+ var liveStreamThink = (typeof useLiveStreamThink === 'function' ? useLiveStreamThink(function (s) { return s }) : null) ?? currentConfig.liveStreamThink
831
+ var pickMode = function (mode) {
832
+ if (typeof setLiveStreamThink === 'function') setLiveStreamThink(mode)
833
+ }
834
+ return React.createElement(React.Fragment, null,
835
+ React.createElement('div', { className: 'drw-set-row' },
836
+ React.createElement('div', { className: 'drw-set-text' },
837
+ React.createElement('div', { className: 'drw-set-title' }, '响应窗口大小(行数)'),
838
+ React.createElement('div', { className: 'drw-set-desc' }, '每个响应 slide 的限高滚动窗口。默认 10 行,0 = 不限高,改动即时生效。'),
839
+ ),
840
+ React.createElement('div', { className: 'drw-set-control' },
841
+ React.createElement('button', {
842
+ type: 'button', className: 'drw-set-btn', 'aria-label': '减小窗口',
843
+ disabled: value <= 0,
844
+ onClick: function () { step(-1) },
845
+ }, '−'),
846
+ React.createElement('input', {
847
+ type: 'number', className: 'drw-set-input', min: 0, max: 200, step: 1, value: String(value),
848
+ onChange: function (e) { setLines(e.target.value) },
849
+ }),
850
+ React.createElement('button', {
851
+ type: 'button', className: 'drw-set-btn', 'aria-label': '增大窗口',
852
+ disabled: value >= 200,
853
+ onClick: function () { step(1) },
854
+ }, '+'),
855
+ ),
726
856
  ),
727
- React.createElement('div', { className: 'drw-set-control' },
728
- React.createElement('button', {
729
- type: 'button', className: 'drw-set-btn', 'aria-label': '减小窗口',
730
- disabled: value <= 0,
731
- onClick: function () { step(-1) },
732
- }, ''),
733
- React.createElement('input', {
734
- type: 'number', className: 'drw-set-input', min: 0, max: 200, step: 1, value: String(value),
735
- onChange: function (e) { setLines(e.target.value) },
736
- }),
737
- React.createElement('button', {
738
- type: 'button', className: 'drw-set-btn', 'aria-label': '增大窗口',
739
- disabled: value >= 200,
740
- onClick: function () { step(1) },
741
- }, '+'),
857
+ React.createElement('div', { className: 'drw-set-row' },
858
+ React.createElement('div', { className: 'drw-set-text' },
859
+ React.createElement('div', { className: 'drw-set-title' }, '流式思考显示'),
860
+ React.createElement('div', { className: 'drw-set-desc' }, '出盒:思考在 slide 外实时显示,结束后收进 slide 折叠;盒内:在 slide 内自动展开实时滚动;关闭:think 全程保持折叠。'),
861
+ ),
862
+ React.createElement('div', { className: 'drw-set-control' },
863
+ STREAM_MODES.map(function (m) {
864
+ return React.createElement('button', {
865
+ key: m.value,
866
+ type: 'button',
867
+ className: 'drw-set-btn' + (liveStreamThink === m.value ? ' drw-set-on' : ''),
868
+ 'aria-pressed': liveStreamThink === m.value,
869
+ onClick: function () { pickMode(m.value) },
870
+ }, m.label)
871
+ }),
872
+ ),
742
873
  ),
743
874
  )
744
875
  }
@@ -746,19 +877,26 @@ window.__ModuleLoader__.load({
746
877
  // ---- plugin entry -------------------------------------------------------
747
878
  var currentConfig = readConfig(null)
748
879
 
749
- var INJECT = ['slots', 'settingsScope']
880
+ // 0.1.7-alpha.1 replaced the `settingsScope` service with `configForms`:
881
+ // one controller per profile entry id, exposing getSnapshot/subscribe/set
882
+ // over the host settings document. Same shape we already used, so the bind
883
+ // is a rename — but the service is a hard inject dependency, and a missing
884
+ // one silently parks the whole plugin (no slides at all).
885
+ var INJECT = ['slots', 'configForms']
750
886
  function apply(ctx, config) {
751
887
  currentConfig = readConfig(config)
752
888
  injectStyles()
753
889
 
754
- // Durable settings binding: the host half registered the `dsh-newbe-response-window`
755
- // namespace; the browser scope mirrors it and persists user overrides.
890
+ // Durable settings binding: the host half declares the volatile fields of
891
+ // the `dsh-newbe-response-window` entry; this controller mirrors them and
892
+ // persists user overrides.
756
893
  linesStore = createSnapshotStore(currentConfig.lines)
894
+ liveStreamThinkStore = createSnapshotStore(currentConfig.liveStreamThink)
757
895
  var scopeService = null
758
- try { scopeService = ctx.get('settingsScope') } catch (e) { scopeService = null }
759
- if (scopeService && typeof scopeService.bind === 'function') {
896
+ try { scopeService = ctx.get('configForms') } catch (e) { scopeService = null }
897
+ if (scopeService && typeof scopeService.get === 'function') {
760
898
  try {
761
- settingsScope = scopeService.bind({ namespace: NS })
899
+ settingsScope = scopeService.get(NS)
762
900
  } catch (e) {
763
901
  settingsScope = null
764
902
  }
@@ -772,6 +910,15 @@ window.__ModuleLoader__.load({
772
910
  var n = clampLines(value.lines)
773
911
  if (linesStore !== null && linesStore.getSnapshot() !== n) linesStore.set(n)
774
912
  }
913
+ // liveStreamThink is stored as 'off' | 'in' | 'out'; tolerate
914
+ // legacy boolean rows (true → 'in', false → 'off').
915
+ if (value && value.liveStreamThink !== undefined) {
916
+ var v = normalizeLiveStreamThink(value.liveStreamThink, currentConfig.liveStreamThink)
917
+ if (liveStreamThinkStore !== null && liveStreamThinkStore.getSnapshot() !== v) {
918
+ liveStreamThinkStore.set(v)
919
+ currentConfig.liveStreamThink = v
920
+ }
921
+ }
775
922
  } catch (e) {}
776
923
  }
777
924
  settingsScope.subscribe(adopt)
@@ -796,7 +943,8 @@ window.__ModuleLoader__.load({
796
943
  }, TextWindowDock)
797
944
  })
798
945
 
799
- // Settings → General row for the window size (live, persisted).
946
+ // Settings → General rows (window size + live-stream think, both live
947
+ // and persisted).
800
948
  ctx.slots.inject('settings.general.item', function () {
801
949
  return ctx.slots.register({
802
950
  name: 'settings.general.item',
@@ -804,7 +952,7 @@ window.__ModuleLoader__.load({
804
952
  order: 60,
805
953
  inject: function () {
806
954
  return {
807
- hooks: { lines: linesStore },
955
+ hooks: { lines: linesStore, liveStreamThink: liveStreamThinkStore },
808
956
  setLines: function (value) {
809
957
  var n = clampLines(value)
810
958
  if (linesStore !== null) linesStore.set(n)
@@ -812,6 +960,14 @@ window.__ModuleLoader__.load({
812
960
  settingsScope.set('lines', n).catch(function () {})
813
961
  }
814
962
  },
963
+ setLiveStreamThink: function (value) {
964
+ var v = normalizeLiveStreamThink(value, currentConfig.liveStreamThink)
965
+ if (liveStreamThinkStore !== null) liveStreamThinkStore.set(v)
966
+ currentConfig.liveStreamThink = v
967
+ if (settingsScope !== null) {
968
+ settingsScope.set('liveStreamThink', v).catch(function () {})
969
+ }
970
+ },
815
971
  }
816
972
  },
817
973
  }, ResponseWindowSettingsRow)
package/lib/index.js CHANGED
@@ -1,19 +1,28 @@
1
1
  // dsh-newbe-response-window — host half.
2
2
  //
3
- // Verified against DSH 0.1.2-alpha.5. Registers the durable settings namespace
4
- // so the browser Settings UI can persist the window size etc. in the user
5
- // settings document. The heavy lifting lives in the browser half (exports
6
- // "./client").
3
+ // Verified against DSH 0.1.7-alpha.1. Owns the durable settings namespace so the
4
+ // browser Settings UI can persist the window size etc. in the user settings
5
+ // document. The heavy lifting lives in the browser half (exports "./client").
7
6
  //
8
- // Namespace + schema (browser `settingsScope.bind` mirrors it):
7
+ // The host settings contract is the 0.1.7 one: the settings document is keyed
8
+ // by the profile entry id, a namespace only shows up in it when its Config
9
+ // schema projects at least one `volatile()` field, and a plugin opts out of the
10
+ // auto-generated page with `settings.configure({ auto: false }, ctx.fiber)`.
11
+ //
12
+ // Namespace + schema (browser `configForms.get(SETTINGS_NS)` mirrors it):
9
13
  // lines window height in lines (default 10; 0 = uncapped)
10
14
  // collapsed start slides collapsed instead of always-expanded
11
15
  // showReadOnly include read-only tools in the slide list
12
16
  // minCollapseRows min tool-call count for auto-collapse (collapsed mode)
13
- //
14
- // `settingsNamespace()` is just a validated string (kebab-case pattern); the
15
- // namespace below already matches, so we pass the raw string and avoid an extra
16
- // host dependency.
17
+ // liveStreamThink how a streaming think block is shown:
18
+ // 'off' always folded into a one-line summary
19
+ // 'in' — auto-expand inside the slide, live scroll
20
+ // 'out' stream outside the slide (native row), then
21
+ // fold into the slide once settled (default 'out')
22
+
23
+ // The namespace must equal the profile entry id (the `id:` of the row this
24
+ // bundle patch inserts), because that is what the host settings document and
25
+ // `configForms.get()` are keyed by.
17
26
 
18
27
  import z from '@deepseek-ai/schemastery'
19
28
 
@@ -23,20 +32,28 @@ export const name = 'dsh-newbe-response-window'
23
32
  /** Durable settings namespace for this plugin (lowercase kebab-case). */
24
33
  export const SETTINGS_NS = 'dsh-newbe-response-window'
25
34
 
26
- /** Settings schema, shared with the browser scope and any Settings UI. */
35
+ /**
36
+ * Settings schema. Only the two user-editable preferences are `volatile()`:
37
+ * the browser settings document is projected from volatile fields alone, and
38
+ * the rest stay patch-time configuration that the client reads from row config.
39
+ */
27
40
  export const Config = z.object({
28
- lines: z.natural().min(0).max(200).default(10),
41
+ lines: z.natural().min(0).max(200).default(10).volatile(),
29
42
  collapsed: z.boolean().default(false),
30
43
  showReadOnly: z.boolean().default(true),
31
44
  minCollapseRows: z.natural().min(1).max(50).default(3),
45
+ liveStreamThink: z.string().default('out').volatile(),
32
46
  })
33
47
 
34
48
  /**
35
- * Register the durable settings namespace when a settings provider exists.
49
+ * Opt this plugin's entry out of the auto-generated settings page. Its
50
+ * namespace is the profile entry id (`dsh-newbe-response-window`), which is
51
+ * exactly {@link SETTINGS_NS}, so the browser half can bind the durable scope
52
+ * through `configForms.get(SETTINGS_NS)`.
36
53
  * @param ctx - host plugin context.
37
54
  */
38
55
  export function apply(ctx) {
39
56
  ctx.inject(['settings'], (settingsCtx) => {
40
- settingsCtx.settings.register(SETTINGS_NS, Config)
57
+ settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber))
41
58
  })
42
59
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-newbe-response-window",
3
- "version": "0.2.0",
4
- "description": "DeepSeek Harness (DSH) web plugin: per-turn response window. Each turn's tool calls collapse into ONE always-expanded bounded slide (configurable N-line window, inner scroll, nothing hidden), and long assistant markdown gets a bounded scroll window too — Grok-build style, keeps every intermediate response visible inside a finite window.",
3
+ "version": "0.3.1",
4
+ "description": "DeepSeek Harness (DSH) web plugin: per-turn response window. Each turn's tool calls collapse into ONE always-expanded bounded slide (configurable N-line window, inner scroll, nothing hidden), and long assistant markdown gets a bounded scroll window too — Grok-build style, keeps every intermediate response visible inside a finite window. Streaming think blocks can live-stream outside the slide and fold in when settled (or auto-expand inside), switchable via Settings.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -28,7 +28,7 @@
28
28
  "check": "node --check lib/index.js && node --check lib/client.js"
29
29
  },
30
30
  "dependencies": {
31
- "@deepseek-ai/schemastery": "^3.0.0"
31
+ "@deepseek-ai/schemastery": "^3.18.3"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "react": "^18.0.0 || ^19.0.0"