dsh-newbe-response-window 0.2.0 → 0.3.0

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
@@ -16,6 +16,10 @@ DeepSeek Harness (DSH) Web 插件:把一轮里的 think(推理)和工具
16
16
  - 主体:`max-height: N 行`(默认 10)的**内部滚动区**
17
17
  - **think 与工具调用严格按真实输出顺序交错排列**(不做「think 全在上 / bash 全在下」的强行分区)
18
18
  - 每段 think 一行(Think + 单行摘要 + 展开箭头),默认折叠,点击展开完整推理(内部限高滚动);展开文字与工具输出同字号(同一 mono 字体)
19
+ - **think 流式显示三种模式**(`liveStreamThink`,默认 `'out'`):
20
+ - `'out'`(默认):正在流式的 think 在 **slide 外**(原生位置)实时显示,流式结束后**收进 slide 折叠**成单行摘要
21
+ - `'in'`:流式 think 在 **slide 内**自动展开、正文实时滚动,结束后自动收起为单行摘要
22
+ - `'off'`:think 全程折叠为单行摘要,不做流式展开
19
23
  - 每条工具调用一行(状态点 + 工具名 + 单行摘要),点击展开参数/输出(输出再限高一档,内部滚动)
20
24
  - 执行中自动跟随底部
21
25
  - **原生 Think 行从消息流中隐藏**(该段已并入 slide,避免重复显示),与 slide 内展示共用同一份内容
@@ -26,6 +30,14 @@ DeepSeek Harness (DSH) Web 插件:把一轮里的 think(推理)和工具
26
30
 
27
31
  ## 安装
28
32
 
33
+ 从 npm 安装:
34
+
35
+ ```bash
36
+ dsh plugin --profile web add dsh-newbe-response-window
37
+ ```
38
+
39
+ 或从合集仓库 GitHub 子目录安装:
40
+
29
41
  ```bash
30
42
  dsh plugin --profile web add github:qiqiangvae/dsh-newbe-plugins#path:packages/dsh-newbe-response-window
31
43
  ```
@@ -54,13 +66,15 @@ dsh plugin --profile web add "link:$(pwd)/packages/dsh-newbe-response-window"
54
66
  | `collapsed` | `false` | 每轮 slide 是否默认收起成一行 bar。默认 `false`:始终展开、内容可见 |
55
67
  | `showReadOnly` | `true` | 是否在 slide 里列出 read/grep/web_search 等只读调用(默认全列出,不藏) |
56
68
  | `minCollapseRows` | `3` | 仅 `collapsed: true` 时生效:少于该数量的轮次不收起 |
69
+ | `liveStreamThink` | `'out'` | 流式 think 的显示模式:`'out'` 盒外流式结束后进盒折叠 / `'in'` 盒内自动展开 / `'off'` 全程折叠 |
57
70
 
58
71
  ### 设置页(Settings → General)
59
72
 
60
- 插件在 Web UI 的 **Settings → General** 里注册了一项 **「响应窗口大小(行数)」**:
73
+ 插件在 Web UI 的 **Settings → General** 里注册了两项:
61
74
 
62
- - `−` / 数值输入 / `+`:调整 `lines`(0–200,`0` = 不限高,默认 10)
63
- - **即时生效**:改动后已渲染的 slide 高度立刻变化(经宿主 settings namespace `dsh-newbe-response-window` 持久化)
75
+ - **「响应窗口大小(行数)」**:`−` / 数值输入 / `+` 调整 `lines`(0–200,`0` = 不限高,默认 10)
76
+ - **「流式思考显示」**:三选一(关闭 / 盒内 / 出盒),对应 `liveStreamThink`(默认出盒)
77
+ - 均**即时生效**:改动后已渲染的 slide 立刻变化(经宿主 settings namespace `dsh-newbe-response-window` 持久化)
64
78
 
65
79
  ## 实现说明(为什么安全)
66
80
 
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
@@ -61,17 +61,28 @@ window.__ModuleLoader__.load({
61
61
  var createSnapshotStore = storeLib.createSnapshotStore
62
62
 
63
63
  // ---- config -----------------------------------------------------------
64
+ // liveStreamThink mode: 'off' (always folded) | 'in' (auto-expand the row
65
+ // inside the slide while streaming) | 'out' (stream outside the slide in
66
+ // the native position, fold into the slide when settled).
64
67
  var DEFAULTS = {
65
68
  lines: 10,
66
69
  collapsed: false,
67
70
  showReadOnly: true,
68
71
  minCollapseRows: 3,
72
+ liveStreamThink: 'out',
69
73
  }
70
74
  function clampInt(value, min, max, fallback) {
71
75
  var n = Number(value)
72
76
  if (!Number.isFinite(n)) return fallback
73
77
  return Math.min(max, Math.max(min, Math.round(n)))
74
78
  }
79
+ function normalizeLiveStreamThink(value, fallback) {
80
+ if (value === 'off' || value === 'in' || value === 'out') return value
81
+ // Legacy boolean row config: true meant in-box live streaming.
82
+ if (value === true) return 'in'
83
+ if (value === false) return 'off'
84
+ return fallback
85
+ }
75
86
  function readConfig(cfg) {
76
87
  cfg = cfg || {}
77
88
  return {
@@ -79,19 +90,22 @@ window.__ModuleLoader__.load({
79
90
  collapsed: cfg.collapsed === true,
80
91
  showReadOnly: cfg.showReadOnly !== false,
81
92
  minCollapseRows: clampInt(cfg.minCollapseRows, 1, 50, DEFAULTS.minCollapseRows),
93
+ liveStreamThink: normalizeLiveStreamThink(cfg.liveStreamThink, DEFAULTS.liveStreamThink),
82
94
  }
83
95
  }
84
96
 
85
97
  // ---- live settings state ----------------------------------------------
86
- // `lines` is user-adjustable from Settings; the rest come from row config.
98
+ // `lines` and `liveStreamThink` are user-adjustable from Settings; the
99
+ // rest come from row config.
87
100
  var linesStore = null
101
+ var liveStreamThinkStore = null
88
102
  var settingsScope = null
89
103
  var NS = 'dsh-newbe-response-window'
90
104
  function clampLines(value) { return clampInt(value, 0, 200, DEFAULTS.lines) }
91
105
 
92
106
  /** 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() })
107
+ function useStoreValue(store, fallback) {
108
+ var pair = React.useState(function () { return store === null ? fallback : store.getSnapshot() })
95
109
  var value = pair[0]
96
110
  var setValue = pair[1]
97
111
  React.useEffect(function () {
@@ -105,7 +119,7 @@ window.__ModuleLoader__.load({
105
119
 
106
120
  /** Live window size in lines (follows the Settings store). */
107
121
  function useLiveLines() {
108
- return useStoreValue(linesStore)
122
+ return useStoreValue(linesStore, DEFAULTS.lines)
109
123
  }
110
124
 
111
125
  // ---- shared style injection ------------------------------------------
@@ -195,7 +209,7 @@ window.__ModuleLoader__.load({
195
209
  '.drw-think-name { color: var(--dsw-alias-label-secondary, #bbb); font-size: 11px; font-weight: 600; letter-spacing: 0.04em; flex: none; }',
196
210
  '.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
211
  '.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; }',
212
+ '.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
213
  /* Hide the native Think row once it's inside a slide (DOM + CSS only) */
200
214
  '[data-variant="think"][data-drw-hidethink="1"] { display: none !important; }',
201
215
  /* Settings row: response window size */
@@ -213,6 +227,10 @@ window.__ModuleLoader__.load({
213
227
  '}',
214
228
  '.drw-set-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16)); }',
215
229
  '.drw-set-btn:disabled { opacity: 0.4; cursor: default; }',
230
+ '.drw-set-btn.drw-set-on {',
231
+ ' border-color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand, #3dbbf5));',
232
+ ' color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand, #3dbbf5));',
233
+ '}',
216
234
  '.drw-set-input {',
217
235
  ' width: 56px; height: 28px; text-align: center;',
218
236
  ' border: 1px solid var(--dsw-alias-border-l2, var(--dsw-alias-border-strong, rgba(128,128,128,0.35)));',
@@ -343,16 +361,36 @@ window.__ModuleLoader__.load({
343
361
  // ---- one think (reasoning) row inside the slide -------------------------
344
362
  // A compact single-line row, collapsible to the full reasoning text — the
345
363
  // 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).
364
+ // "implementation" and should read the same inside the window). While the
365
+ // block is still streaming the row auto-expands and the body pins to the
366
+ // latest line; when it settles the row folds back to its one-line summary.
367
+ // The head stays a static first-line summary the whole time, so the only
368
+ // moving part during a stream is the expanding body — one visual focus.
347
369
  function ThinkRow(props) {
348
370
  var text = props.text
349
371
  var index = props.index
372
+ var streaming = props.streaming === true
350
373
  var openState = React.useState(false)
351
374
  var open = openState[0]
352
375
  var setOpen = openState[1]
376
+ var bodyRef = React.useRef(null)
353
377
  var firstLine = text.split('\n')[0].trim() || text.trim()
354
378
  var summary = firstLine.length > 120 ? firstLine.slice(0, 117) + '…' : firstLine
355
- return React.createElement('div', { className: 'drw-think', 'data-open': open ? '1' : undefined },
379
+ // Live streaming: auto-expand while streaming, fold back once settled.
380
+ React.useEffect(function () {
381
+ if (streaming) setOpen(true)
382
+ else setOpen(false)
383
+ }, [streaming])
384
+ // While streaming, keep the body pinned to the latest line.
385
+ React.useEffect(function () {
386
+ if (!open || !bodyRef.current) return
387
+ if (streaming) bodyRef.current.scrollTop = bodyRef.current.scrollHeight
388
+ })
389
+ return React.createElement('div', {
390
+ className: 'drw-think',
391
+ 'data-open': open ? '1' : undefined,
392
+ 'data-streaming': streaming ? '1' : undefined,
393
+ },
356
394
  React.createElement('div', {
357
395
  className: 'drw-think-head', role: 'button', tabIndex: 0,
358
396
  'aria-expanded': open || undefined,
@@ -364,7 +402,7 @@ window.__ModuleLoader__.load({
364
402
  React.createElement('span', { className: 'drw-think-toggle', 'aria-hidden': true }, open ? '\u25BE' : '\u25B8'),
365
403
  ),
366
404
  open
367
- ? React.createElement('pre', { className: 'drw-think-body' }, text)
405
+ ? React.createElement('pre', { ref: bodyRef, className: 'drw-think-body' }, text)
368
406
  : null,
369
407
  )
370
408
  }
@@ -382,6 +420,7 @@ window.__ModuleLoader__.load({
382
420
  var openFile = props.openFile
383
421
  var inspectCall = props.inspectCall
384
422
  var cfg = props.config
423
+ var streamingThink = props.streamingThink || null
385
424
  var lines = useLiveLines() // live window size from Settings
386
425
  var toolItems = items.filter(function (it) { return it.kind === 'tool' })
387
426
  var reasoningCount = items.reduce(function (acc, it) { return it.kind === 'think' ? acc + 1 : acc }, 0)
@@ -393,10 +432,27 @@ window.__ModuleLoader__.load({
393
432
  var setOpen = openState[1]
394
433
  var bodyRef = React.useRef(null)
395
434
  var running = stats.running > 0
396
- // Auto-follow to the bottom while calls are still running.
435
+ // Streaming detection depends on the mode:
436
+ // 'in' — a think block inside the box is still streaming (the row
437
+ // auto-expands and the slide is forced open so the live
438
+ // reasoning stays readable);
439
+ // 'out' — the live tail is streaming OUTSIDE the slide (native row)
440
+ // and folds in when settled; the slide is forced open so the
441
+ // fold-in is visible.
442
+ // In both cases streaming overrides `collapsed`, then the user's
443
+ // collapsed setting is restored (Q7).
444
+ var mode = cfg.liveStreamThink
445
+ var streaming = mode === 'in'
446
+ ? items.some(function (it) { return it.kind === 'think' && it.streaming === true })
447
+ : mode === 'out'
448
+ ? streamingThink !== null
449
+ : false
450
+ var effectiveOpen = open || streaming
451
+ // Auto-follow to the bottom while calls are still running or a think is
452
+ // still streaming.
397
453
  React.useEffect(function () {
398
- if (!open || !bodyRef.current) return
399
- if (running) bodyRef.current.scrollTop = bodyRef.current.scrollHeight
454
+ if (!effectiveOpen || !bodyRef.current) return
455
+ if (running || streaming) bodyRef.current.scrollTop = bodyRef.current.scrollHeight
400
456
  })
401
457
  var list = items
402
458
  if (!cfg.showReadOnly) {
@@ -405,10 +461,10 @@ window.__ModuleLoader__.load({
405
461
  })
406
462
  }
407
463
  var thinkSeen = 0
408
- return React.createElement('div', { className: 'drw-slide', 'data-running': running ? '1' : undefined },
464
+ return React.createElement('div', { className: 'drw-slide', 'data-running': running || streaming ? '1' : undefined },
409
465
  React.createElement('div', {
410
466
  className: 'drw-head', role: 'button', tabIndex: 0,
411
- 'aria-expanded': open || undefined,
467
+ 'aria-expanded': effectiveOpen || undefined,
412
468
  onClick: function () { setOpen(!open) },
413
469
  onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(!open) } },
414
470
  },
@@ -416,6 +472,9 @@ window.__ModuleLoader__.load({
416
472
  stats.total + ' 个工具调用'
417
473
  + (reasoningCount > 0 ? ' · ' + reasoningCount + ' Think' : '')
418
474
  ),
475
+ streaming
476
+ ? React.createElement('span', { className: 'drw-head-badge', 'data-state': 'running' }, '思考中')
477
+ : null,
419
478
  running
420
479
  ? React.createElement('span', { className: 'drw-head-badge', 'data-state': 'running' }, '进行中')
421
480
  : null,
@@ -425,18 +484,23 @@ window.__ModuleLoader__.load({
425
484
  !cfg.showReadOnly && stats.readOnly > 0
426
485
  ? React.createElement('span', { className: 'drw-head-badge' }, '隐藏只读 ' + stats.readOnly)
427
486
  : null,
428
- React.createElement('span', { className: 'drw-head-toggle' }, open ? '收起 ▴' : '展开 ▾'),
487
+ React.createElement('span', { className: 'drw-head-toggle' }, effectiveOpen ? '收起 ▴' : '展开 ▾'),
429
488
  ),
430
489
  React.createElement('div', {
431
490
  ref: bodyRef,
432
- className: 'drw-body' + (open ? '' : ' drw-collapsed'),
433
- style: open && lines > 0 ? { maxHeight: (lines * 1.55) + 'em' } : undefined,
491
+ className: 'drw-body' + (effectiveOpen ? '' : ' drw-collapsed'),
492
+ style: effectiveOpen && lines > 0 ? { maxHeight: (lines * 1.55) + 'em' } : undefined,
434
493
  },
435
494
  list.map(function (it) {
436
495
  if (it.kind === 'think') {
437
496
  var tid = thinkSeen
438
497
  thinkSeen += 1
439
- return React.createElement(ThinkRow, { key: 'think-' + tid, text: it.text, index: tid })
498
+ return React.createElement(ThinkRow, {
499
+ key: 'think-' + tid,
500
+ text: it.text,
501
+ index: tid,
502
+ streaming: it.streaming === true,
503
+ })
440
504
  }
441
505
  return React.createElement(SimpleToolRow, {
442
506
  key: it.node.key,
@@ -469,12 +533,13 @@ window.__ModuleLoader__.load({
469
533
  var current = null
470
534
  function ensure() {
471
535
  if (current === null) {
472
- current = { items: [], tools: [], startKey: null }
536
+ current = { items: [], tools: [], startKey: null, streamingThink: null }
473
537
  segments.push(current)
474
538
  }
475
539
  return current
476
540
  }
477
541
  function closeSegment() { current = null }
542
+ var mode = currentConfig.liveStreamThink
478
543
  for (var i = 0; i < order.length; i++) {
479
544
  var n = chat.nodes.get(order[i])
480
545
  if (n === undefined) continue
@@ -482,13 +547,29 @@ window.__ModuleLoader__.load({
482
547
  if (n.kind === 'assistant-step') {
483
548
  var blocks = n.data && Array.isArray(n.data.blocks) ? n.data.blocks : []
484
549
  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.
550
+ // While a step is running, its LAST reasoning block is the live
551
+ // streaming tail the exact rule the native ReasoningRow uses
552
+ // (`running: streaming && i === last`).
553
+ // mode 'out': the tail streams OUTSIDE the slide (native row) and
554
+ // folds in once settled — recorded as streamingThink.
555
+ // mode 'in' : the tail stays INSIDE the box, tagged `streaming` so
556
+ // the row auto-expands and shows live progress.
557
+ // mode 'off': no streaming behavior, always folded.
558
+ var stepRunning = !!n.data && n.data.status === 'running'
559
+ var lastIdx = blocks.length - 1
488
560
  for (var b = 0; b < blocks.length; b++) {
489
561
  var block = blocks[b]
490
562
  if (block && block.kind === 'reasoning' && typeof block.text === 'string' && block.text.trim() !== '') {
491
- ensure().items.push({ kind: 'think', text: block.text })
563
+ if (mode === 'out' && stepRunning && b === lastIdx) {
564
+ ensure().streamingThink = { text: block.text, nodeKey: n.key }
565
+ } else {
566
+ ensure().items.push({
567
+ kind: 'think',
568
+ text: block.text,
569
+ nodeKey: n.key,
570
+ streaming: mode === 'in' && stepRunning && b === lastIdx,
571
+ })
572
+ }
492
573
  }
493
574
  }
494
575
  if (staged) closeSegment()
@@ -554,6 +635,7 @@ window.__ModuleLoader__.load({
554
635
  cwd: cwd,
555
636
  openFile: openFile,
556
637
  inspectCall: inspectCall,
638
+ streamingThink: seg.streamingThink || null,
557
639
  config: currentConfig,
558
640
  })
559
641
  } catch (e) {
@@ -658,7 +740,15 @@ window.__ModuleLoader__.load({
658
740
  if (isSegmentBoundary(next)) break
659
741
  if (next.querySelector && next.querySelector('.drw-slide')) { hasSlide = true; break }
660
742
  }
661
- if (hasSlide) think.dataset.drwHidethink = '1'
743
+ if (hasSlide) {
744
+ // In 'out' mode the streaming think tail lives OUTSIDE the box
745
+ // (native row) until it settles: keep it visible while
746
+ // `data-streaming` is present, then fold it in (hide) the moment the
747
+ // stream is gone. In 'in'/'off' modes the native row is always
748
+ // hidden — the box renders the same content.
749
+ var streamingOut = currentConfig.liveStreamThink === 'out' && !!think.closest('[data-streaming]')
750
+ if (!streamingOut) think.dataset.drwHidethink = '1'
751
+ }
662
752
  else if (think.dataset.drwHidethink) delete think.dataset.drwHidethink
663
753
  }
664
754
  }
@@ -711,34 +801,64 @@ window.__ModuleLoader__.load({
711
801
  }
712
802
 
713
803
  // ---- Settings row: response window size ---------------------------------
804
+ var STREAM_MODES = [
805
+ { value: 'off', label: '关闭' },
806
+ { value: 'in', label: '盒内' },
807
+ { value: 'out', label: '出盒' },
808
+ ]
714
809
  function ResponseWindowSettingsRow(props) {
715
810
  var useLines = props.useLines
716
811
  var setLines = props.setLines
812
+ var useLiveStreamThink = props.useLiveStreamThink
813
+ var setLiveStreamThink = props.setLiveStreamThink
717
814
  var lines = (typeof useLines === 'function' ? useLines(function (s) { return s }) : null) ?? currentConfig.lines
718
815
  var value = clampLines(lines)
719
816
  var step = function (delta) {
720
817
  setLines(value + delta)
721
818
  }
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 = 不限高,改动即时生效。'),
819
+ var liveStreamThink = (typeof useLiveStreamThink === 'function' ? useLiveStreamThink(function (s) { return s }) : null) ?? currentConfig.liveStreamThink
820
+ var pickMode = function (mode) {
821
+ if (typeof setLiveStreamThink === 'function') setLiveStreamThink(mode)
822
+ }
823
+ return React.createElement(React.Fragment, null,
824
+ React.createElement('div', { className: 'drw-set-row' },
825
+ React.createElement('div', { className: 'drw-set-text' },
826
+ React.createElement('div', { className: 'drw-set-title' }, '响应窗口大小(行数)'),
827
+ React.createElement('div', { className: 'drw-set-desc' }, '每个响应 slide 的限高滚动窗口。默认 10 行,0 = 不限高,改动即时生效。'),
828
+ ),
829
+ React.createElement('div', { className: 'drw-set-control' },
830
+ React.createElement('button', {
831
+ type: 'button', className: 'drw-set-btn', 'aria-label': '减小窗口',
832
+ disabled: value <= 0,
833
+ onClick: function () { step(-1) },
834
+ }, '−'),
835
+ React.createElement('input', {
836
+ type: 'number', className: 'drw-set-input', min: 0, max: 200, step: 1, value: String(value),
837
+ onChange: function (e) { setLines(e.target.value) },
838
+ }),
839
+ React.createElement('button', {
840
+ type: 'button', className: 'drw-set-btn', 'aria-label': '增大窗口',
841
+ disabled: value >= 200,
842
+ onClick: function () { step(1) },
843
+ }, '+'),
844
+ ),
726
845
  ),
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
- }, '+'),
846
+ React.createElement('div', { className: 'drw-set-row' },
847
+ React.createElement('div', { className: 'drw-set-text' },
848
+ React.createElement('div', { className: 'drw-set-title' }, '流式思考显示'),
849
+ React.createElement('div', { className: 'drw-set-desc' }, '出盒:思考在 slide 外实时显示,结束后收进 slide 折叠;盒内:在 slide 内自动展开实时滚动;关闭:think 全程保持折叠。'),
850
+ ),
851
+ React.createElement('div', { className: 'drw-set-control' },
852
+ STREAM_MODES.map(function (m) {
853
+ return React.createElement('button', {
854
+ key: m.value,
855
+ type: 'button',
856
+ className: 'drw-set-btn' + (liveStreamThink === m.value ? ' drw-set-on' : ''),
857
+ 'aria-pressed': liveStreamThink === m.value,
858
+ onClick: function () { pickMode(m.value) },
859
+ }, m.label)
860
+ }),
861
+ ),
742
862
  ),
743
863
  )
744
864
  }
@@ -754,6 +874,7 @@ window.__ModuleLoader__.load({
754
874
  // Durable settings binding: the host half registered the `dsh-newbe-response-window`
755
875
  // namespace; the browser scope mirrors it and persists user overrides.
756
876
  linesStore = createSnapshotStore(currentConfig.lines)
877
+ liveStreamThinkStore = createSnapshotStore(currentConfig.liveStreamThink)
757
878
  var scopeService = null
758
879
  try { scopeService = ctx.get('settingsScope') } catch (e) { scopeService = null }
759
880
  if (scopeService && typeof scopeService.bind === 'function') {
@@ -772,6 +893,15 @@ window.__ModuleLoader__.load({
772
893
  var n = clampLines(value.lines)
773
894
  if (linesStore !== null && linesStore.getSnapshot() !== n) linesStore.set(n)
774
895
  }
896
+ // liveStreamThink is stored as 'off' | 'in' | 'out'; tolerate
897
+ // legacy boolean rows (true → 'in', false → 'off').
898
+ if (value && value.liveStreamThink !== undefined) {
899
+ var v = normalizeLiveStreamThink(value.liveStreamThink, currentConfig.liveStreamThink)
900
+ if (liveStreamThinkStore !== null && liveStreamThinkStore.getSnapshot() !== v) {
901
+ liveStreamThinkStore.set(v)
902
+ currentConfig.liveStreamThink = v
903
+ }
904
+ }
775
905
  } catch (e) {}
776
906
  }
777
907
  settingsScope.subscribe(adopt)
@@ -796,7 +926,8 @@ window.__ModuleLoader__.load({
796
926
  }, TextWindowDock)
797
927
  })
798
928
 
799
- // Settings → General row for the window size (live, persisted).
929
+ // Settings → General rows (window size + live-stream think, both live
930
+ // and persisted).
800
931
  ctx.slots.inject('settings.general.item', function () {
801
932
  return ctx.slots.register({
802
933
  name: 'settings.general.item',
@@ -804,7 +935,7 @@ window.__ModuleLoader__.load({
804
935
  order: 60,
805
936
  inject: function () {
806
937
  return {
807
- hooks: { lines: linesStore },
938
+ hooks: { lines: linesStore, liveStreamThink: liveStreamThinkStore },
808
939
  setLines: function (value) {
809
940
  var n = clampLines(value)
810
941
  if (linesStore !== null) linesStore.set(n)
@@ -812,6 +943,14 @@ window.__ModuleLoader__.load({
812
943
  settingsScope.set('lines', n).catch(function () {})
813
944
  }
814
945
  },
946
+ setLiveStreamThink: function (value) {
947
+ var v = normalizeLiveStreamThink(value, currentConfig.liveStreamThink)
948
+ if (liveStreamThinkStore !== null) liveStreamThinkStore.set(v)
949
+ currentConfig.liveStreamThink = v
950
+ if (settingsScope !== null) {
951
+ settingsScope.set('liveStreamThink', v).catch(function () {})
952
+ }
953
+ },
815
954
  }
816
955
  },
817
956
  }, ResponseWindowSettingsRow)
package/lib/index.js CHANGED
@@ -10,7 +10,12 @@
10
10
  // collapsed start slides collapsed instead of always-expanded
11
11
  // showReadOnly include read-only tools in the slide list
12
12
  // minCollapseRows min tool-call count for auto-collapse (collapsed mode)
13
- //
13
+ // liveStreamThink how a streaming think block is shown:
14
+ // 'off' — always folded into a one-line summary
15
+ // 'in' — auto-expand inside the slide, live scroll
16
+ // 'out' — stream outside the slide (native row), then
17
+ // fold into the slide once settled (default 'out')
18
+
14
19
  // `settingsNamespace()` is just a validated string (kebab-case pattern); the
15
20
  // namespace below already matches, so we pass the raw string and avoid an extra
16
21
  // host dependency.
@@ -29,6 +34,7 @@ export const Config = z.object({
29
34
  collapsed: z.boolean().default(false),
30
35
  showReadOnly: z.boolean().default(true),
31
36
  minCollapseRows: z.natural().min(1).max(50).default(3),
37
+ liveStreamThink: z.string().default('out'),
32
38
  })
33
39
 
34
40
  /**
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.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. 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": {