dsh-newbe-response-window 0.2.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/LICENSE +21 -0
- package/README.md +93 -0
- package/cordis.patch.yml +29 -0
- package/lib/client.js +826 -0
- package/lib/index.js +42 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 heiheiha798
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# dsh-newbe-response-window
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness (DSH) Web 插件:把一轮里的 think(推理)和工具调用放进**有限高度的可滚动窗口**(默认 10 行),以**阶段性文字回复为界分段**,每个分段一个 **slide**;文字回复保持原生完整显示、不做限高。中间过程**始终可见**(只是不撑爆页面)——Grok Build 风格。
|
|
4
|
+
|
|
5
|
+
> 已验证 DSH 版本:**0.1.2-alpha.5**(2026-09)。
|
|
6
|
+
> 与「全部折叠成 summary」类插件(如 `dsh-tool-summary`)不同:本插件**不隐藏**任何内容。think 与工具调用一条条列在 slide 里,每段都能展开翻到底;文字回复完全原生展示。
|
|
7
|
+
> 只包 bash + think:阶段性/最终文字 response **不**加限高、**不**加「展开全部/收起」按钮。
|
|
8
|
+
|
|
9
|
+
## 效果
|
|
10
|
+
|
|
11
|
+
一个 prompt 之后有 100 次 tool call + 长回复时:
|
|
12
|
+
|
|
13
|
+
- **think(推理)和工具调用** → 合并为 slide(think 与 tool call 都是 implementation,不做区分):
|
|
14
|
+
- **以「阶段性文字回复」为界分段**:模型中途直接返回给用户的每条文字 response 是天然分隔点;两个分隔点之间的一段 think+bash 是一个 slide。不会两个 input prompt 之间挤成一整个 slide
|
|
15
|
+
- 头部:`10 个工具调用 · 5 Think`(无 emoji、无冗余序号)+ 进行中 / 失败徽标,可点击收起/展开
|
|
16
|
+
- 主体:`max-height: N 行`(默认 10)的**内部滚动区**
|
|
17
|
+
- **think 与工具调用严格按真实输出顺序交错排列**(不做「think 全在上 / bash 全在下」的强行分区)
|
|
18
|
+
- 每段 think 一行(Think + 单行摘要 + 展开箭头),默认折叠,点击展开完整推理(内部限高滚动);展开文字与工具输出同字号(同一 mono 字体)
|
|
19
|
+
- 每条工具调用一行(状态点 + 工具名 + 单行摘要),点击展开参数/输出(输出再限高一档,内部滚动)
|
|
20
|
+
- 执行中自动跟随底部
|
|
21
|
+
- **原生 Think 行从消息流中隐藏**(该段已并入 slide,避免重复显示),与 slide 内展示共用同一份内容
|
|
22
|
+
- **阶段性文字回复与最终回复** → 完全原生展示,保持全文可见,**不套 slide 窗口**、不加任何按钮
|
|
23
|
+
- 用户消息始终是「整轮的分隔点」,保持原位
|
|
24
|
+
|
|
25
|
+
> 效果预览见 GitHub 仓库页面或自行安装体验(仓库不再内置运行截图,避免暴露会话内容)。
|
|
26
|
+
|
|
27
|
+
## 安装
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
dsh plugin --profile web add github:qiqiangvae/dsh-newbe-plugins#path:packages/dsh-newbe-response-window
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
或本地 link 方式(开发调试):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone https://github.com/qiqiangvae/dsh-newbe-plugins.git
|
|
37
|
+
cd dsh-newbe-plugins
|
|
38
|
+
dsh plugin --profile web add "link:$(pwd)/packages/dsh-newbe-response-window"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
装完重启 `dsh web`(或等 profile HMR)生效。
|
|
42
|
+
|
|
43
|
+
卸载:`dsh plugin --profile web remove dsh-newbe-response-window`
|
|
44
|
+
|
|
45
|
+
> 迁移提示:本插件已从独立仓库迁入 newbe monorepo,老安装地址(仓库根 `github:qiqiangvae/dsh-newbe-plugins`)已失效。插件名仍是 `dsh-newbe-response-window`——先 `dsh plugin --profile web remove dsh-newbe-response-window`,再用上方新地址重装即可。
|
|
46
|
+
|
|
47
|
+
## 配置
|
|
48
|
+
|
|
49
|
+
`cordis.patch.yml` 里插入了默认配置,可改:
|
|
50
|
+
|
|
51
|
+
| 键 | 默认 | 说明 |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `lines` | `10` | 窗口高度(行)。`0` = 不限高(等价于关闭窗口) |
|
|
54
|
+
| `collapsed` | `false` | 每轮 slide 是否默认收起成一行 bar。默认 `false`:始终展开、内容可见 |
|
|
55
|
+
| `showReadOnly` | `true` | 是否在 slide 里列出 read/grep/web_search 等只读调用(默认全列出,不藏) |
|
|
56
|
+
| `minCollapseRows` | `3` | 仅 `collapsed: true` 时生效:少于该数量的轮次不收起 |
|
|
57
|
+
|
|
58
|
+
### 设置页(Settings → General)
|
|
59
|
+
|
|
60
|
+
插件在 Web UI 的 **Settings → General** 里注册了一项 **「响应窗口大小(行数)」**:
|
|
61
|
+
|
|
62
|
+
- `−` / 数值输入 / `+`:调整 `lines`(0–200,`0` = 不限高,默认 10)
|
|
63
|
+
- **即时生效**:改动后已渲染的 slide 高度立刻变化(经宿主 settings namespace `dsh-newbe-response-window` 持久化)
|
|
64
|
+
|
|
65
|
+
## 实现说明(为什么安全)
|
|
66
|
+
|
|
67
|
+
- 工具调用 slide 通过 `conversation.chat.node`(`tool-call` key,`priority: -100`)的 **slot shadow** 在 React 层实现:每轮第一个 tool-call 节点渲染整个 slide,同轮其余 tool-call 节点渲染空,任何渲染异常由 slot 运行时的 entry boundary **自动 abdicate** 回内置渲染。
|
|
68
|
+
- **绝不移走 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 段)保持原生显示。
|
|
70
|
+
- 插件只读会话快照,不写快照、不调宿主 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
|
+
- 浏览器半依赖 `@deepseek-ai/dsh-client-store`(`createSnapshotStore`,shell 内置 seed module),替代旧版的 `@deepseek-ai/dsh-client-runtime`(该包在新 alpha 已移除)。旧版 `dsh.plugin.json` 清单在新 alpha 不再被读取,已删除。
|
|
72
|
+
|
|
73
|
+
## 开发 / 测试
|
|
74
|
+
|
|
75
|
+
纯 JS,无构建步骤:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm run check # node --check lib/index.js && node --check lib/client.js
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
E2E(需要已启动的 `dsh web` 与 python playwright):
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
dsh --profile web --no-open --port 3639 &
|
|
85
|
+
python3 test/e2e.py --url http://127.0.0.1:3639 --session "架构重构不顺原因分析"
|
|
86
|
+
```
|
|
87
|
+
|
|
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`)
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# dsh-newbe-response-window bundle patch
|
|
2
|
+
#
|
|
3
|
+
# Verified against DSH 0.1.2-alpha.5. Installing this package into a profile
|
|
4
|
+
# (`dsh plugin --profile <profile> add …`) appends it to dsh.profile.bundles
|
|
5
|
+
# because package.json declares `dsh.bundle.patch`. The profile boot applies
|
|
6
|
+
# this insert list, which mounts the package as a Cordis plugin: the host half
|
|
7
|
+
# (exports ".") runs in the host process and — via the `dsh.client` declaration
|
|
8
|
+
# in package.json — the browser half (exports "./client", served at
|
|
9
|
+
# /plugins/dsh-newbe-response-window/client.js) runs in the web GUI.
|
|
10
|
+
#
|
|
11
|
+
# Row config (all optional):
|
|
12
|
+
# lines window height in lines (default 10). 0 disables the
|
|
13
|
+
# height cap (blocks render at natural height).
|
|
14
|
+
# collapsed start every turn slide collapsed to a one-line bar
|
|
15
|
+
# instead of always-expanded (default false: content stays
|
|
16
|
+
# visible, just bounded).
|
|
17
|
+
# showReadOnly list read / grep / glob / web_search etc. in the slide
|
|
18
|
+
# (default true: nothing is hidden by default).
|
|
19
|
+
# minCollapseRows only auto-collapse a finished slide when it has at least
|
|
20
|
+
# this many tool calls (default 3; only used with
|
|
21
|
+
# collapsed=true).
|
|
22
|
+
- insert:
|
|
23
|
+
- id: dsh-newbe-response-window
|
|
24
|
+
name: 'dsh-newbe-response-window'
|
|
25
|
+
config:
|
|
26
|
+
lines: 10
|
|
27
|
+
collapsed: false
|
|
28
|
+
showReadOnly: true
|
|
29
|
+
minCollapseRows: 3
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
// dsh-newbe-response-window — browser half.
|
|
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:
|
|
5
|
+
//
|
|
6
|
+
// - `@deepseek-ai/dsh-client-runtime` is gone. The browser shell now seeds
|
|
7
|
+
// `@deepseek-ai/dsh-client-store` (createSnapshotStore) as a static module,
|
|
8
|
+
// so the bundle requires that instead — no dsh.client.inject needed.
|
|
9
|
+
// - `conversation.chat.node` moved from dsh-client-ui-conversation to
|
|
10
|
+
// dsh-client-ui-chat; the keyed slot contract is the same (`key`,
|
|
11
|
+
// `priority`, lowest priority renders), but its owner props no longer carry
|
|
12
|
+
// `useSession`. Session facts arrive through the slot system's standard
|
|
13
|
+
// props — the per-conversation chat snapshot is the standard `useChat`
|
|
14
|
+
// selector hook (shape: `{ locations.getTurn(turn), nodes.get(key) }`).
|
|
15
|
+
// - Assistant markdown CSS-module classes changed shape (hash_root, no
|
|
16
|
+
// `_markdown` substring), so DOM class sniffing is gone: ghost-row and
|
|
17
|
+
// segment-boundary decisions now read the chat snapshot through the
|
|
18
|
+
// `data-chat-flow-key` attribute instead of markdown classes.
|
|
19
|
+
// - Theme tokens: border-weak/strong → border-l1/l2, brand →
|
|
20
|
+
// state-business-primary; `--dsw-font-mono` → `--ds-font-family-code`.
|
|
21
|
+
// Every token keeps a fallback, so missing tokens degrade gracefully.
|
|
22
|
+
// - `dsh.plugin.json` is no longer read by the harness (removed).
|
|
23
|
+
//
|
|
24
|
+
// What it does (Grok-build style response window):
|
|
25
|
+
// 1. PER-TURN TOOL SLIDE: every tool call of one turn (the response between
|
|
26
|
+
// two user prompts) is grouped into ONE slide. The slide is ALWAYS
|
|
27
|
+
// expanded by default and shows every call — state dot, tool name, one-line
|
|
28
|
+
// summary, click to expand parameters/output — inside a bounded-height
|
|
29
|
+
// scroll body (config `lines`, default 10). Nothing is hidden: it is a
|
|
30
|
+
// window, not a summary. While calls are still running the body
|
|
31
|
+
// auto-follows to the bottom.
|
|
32
|
+
// 2. ASSISTANT TEXT WINDOW: a settled assistant markdown block longer than
|
|
33
|
+
// the window is capped with internal scroll + a subtle gradient and an
|
|
34
|
+
// expand/collapse affordance. The native MarkdownText rendering is kept
|
|
35
|
+
// untouched (class + CSS only).
|
|
36
|
+
//
|
|
37
|
+
// GUI-context safety contract (informed by code audit + a live React crash
|
|
38
|
+
// experiment):
|
|
39
|
+
// - Moving React-owned chat rows into a wrapper container is NOT safe: when
|
|
40
|
+
// DSH later removes a moved row (session switch / edit / compaction), React
|
|
41
|
+
// calls parent.removeChild(row) which throws because the row is no longer a
|
|
42
|
+
// direct child, and the whole conversation tree unmounts. Therefore this
|
|
43
|
+
// plugin NEVER moves [data-chat-anchor-key] rows.
|
|
44
|
+
// - The tool slide is implemented at the React layer via a low-priority slot
|
|
45
|
+
// shadow of `conversation.chat.node` (key `tool-call`, priority -100): the
|
|
46
|
+
// first tool-call node of a turn renders the slide, every sibling tool-call
|
|
47
|
+
// node of the same turn renders null, and the slot runtime's entry boundary
|
|
48
|
+
// abdicates our renderer back to the built-in one on any crash.
|
|
49
|
+
// - The assistant text window is class/CSS only — no DOM reparenting, no
|
|
50
|
+
// removal of React-owned nodes.
|
|
51
|
+
// - The plugin never writes the session snapshot and never calls host APIs;
|
|
52
|
+
// it only reads the chat snapshot (useChat selectors) and styles DOM.
|
|
53
|
+
|
|
54
|
+
window.__ModuleLoader__.load({
|
|
55
|
+
id: 'dsh-newbe-response-window',
|
|
56
|
+
factory: (require) => {
|
|
57
|
+
var module = { exports: {} }
|
|
58
|
+
var exports = module.exports
|
|
59
|
+
var React = require('react')
|
|
60
|
+
var storeLib = require('@deepseek-ai/dsh-client-store')
|
|
61
|
+
var createSnapshotStore = storeLib.createSnapshotStore
|
|
62
|
+
|
|
63
|
+
// ---- config -----------------------------------------------------------
|
|
64
|
+
var DEFAULTS = {
|
|
65
|
+
lines: 10,
|
|
66
|
+
collapsed: false,
|
|
67
|
+
showReadOnly: true,
|
|
68
|
+
minCollapseRows: 3,
|
|
69
|
+
}
|
|
70
|
+
function clampInt(value, min, max, fallback) {
|
|
71
|
+
var n = Number(value)
|
|
72
|
+
if (!Number.isFinite(n)) return fallback
|
|
73
|
+
return Math.min(max, Math.max(min, Math.round(n)))
|
|
74
|
+
}
|
|
75
|
+
function readConfig(cfg) {
|
|
76
|
+
cfg = cfg || {}
|
|
77
|
+
return {
|
|
78
|
+
lines: clampInt(cfg.lines, 0, 200, DEFAULTS.lines),
|
|
79
|
+
collapsed: cfg.collapsed === true,
|
|
80
|
+
showReadOnly: cfg.showReadOnly !== false,
|
|
81
|
+
minCollapseRows: clampInt(cfg.minCollapseRows, 1, 50, DEFAULTS.minCollapseRows),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- live settings state ----------------------------------------------
|
|
86
|
+
// `lines` is user-adjustable from Settings; the rest come from row config.
|
|
87
|
+
var linesStore = null
|
|
88
|
+
var settingsScope = null
|
|
89
|
+
var NS = 'dsh-newbe-response-window'
|
|
90
|
+
function clampLines(value) { return clampInt(value, 0, 200, DEFAULTS.lines) }
|
|
91
|
+
|
|
92
|
+
/** 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() })
|
|
95
|
+
var value = pair[0]
|
|
96
|
+
var setValue = pair[1]
|
|
97
|
+
React.useEffect(function () {
|
|
98
|
+
if (store === null) return undefined
|
|
99
|
+
var unsub = store.subscribe(function () { setValue(store.getSnapshot()) })
|
|
100
|
+
setValue(store.getSnapshot())
|
|
101
|
+
return unsub
|
|
102
|
+
}, [store])
|
|
103
|
+
return value
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Live window size in lines (follows the Settings store). */
|
|
107
|
+
function useLiveLines() {
|
|
108
|
+
return useStoreValue(linesStore)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- shared style injection ------------------------------------------
|
|
112
|
+
var STYLE_ID = 'dsh-newbe-response-window-css'
|
|
113
|
+
function injectStyles() {
|
|
114
|
+
if (typeof document === 'undefined') return
|
|
115
|
+
if (document.getElementById(STYLE_ID)) return
|
|
116
|
+
var el = document.createElement('style')
|
|
117
|
+
el.id = STYLE_ID
|
|
118
|
+
el.textContent = [
|
|
119
|
+
/* per-turn tool slide */
|
|
120
|
+
'.drw-slide {',
|
|
121
|
+
' position: relative;',
|
|
122
|
+
' display: flex; flex-direction: column;',
|
|
123
|
+
' margin: 6px 0;',
|
|
124
|
+
' border: 1px solid var(--dsw-alias-border-l2, var(--dsw-alias-border-strong, rgba(128,128,128,0.35)));',
|
|
125
|
+
' border-radius: 10px;',
|
|
126
|
+
' background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.06));',
|
|
127
|
+
' overflow: hidden;',
|
|
128
|
+
'}',
|
|
129
|
+
'.drw-slide[data-running="1"] { border-color: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand, #3dbbf5)); }',
|
|
130
|
+
/* Tool-call rows that our per-turn slide renders null for (sibling
|
|
131
|
+
nodes of a grouped turn) are empty in the DOM: hide the leftover
|
|
132
|
+
~0-height stubs. Only truly empty rows match, so a native fallback
|
|
133
|
+
(renderer abdication) staying non-empty is never hidden. The shell
|
|
134
|
+
also hides empty flow items natively — this rule is belt and braces. */
|
|
135
|
+
'[data-chat-flow-kind="tool-call"]:empty { display: none; }',
|
|
136
|
+
'[data-chat-flow-kind][data-drw-stub="1"] { display: none !important; }',
|
|
137
|
+
'.drw-head {',
|
|
138
|
+
' display: flex; align-items: center; gap: 8px;',
|
|
139
|
+
' padding: 6px 10px;',
|
|
140
|
+
' cursor: pointer; user-select: none;',
|
|
141
|
+
' color: var(--dsw-alias-label-primary, #eee);',
|
|
142
|
+
' font-size: 12.5px; line-height: 1.4;',
|
|
143
|
+
' background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.10));',
|
|
144
|
+
'}',
|
|
145
|
+
'.drw-head:hover { background: var(--dsw-alias-interactive-bg-hover-solid, rgba(128,128,128,0.16)); }',
|
|
146
|
+
'.drw-head-title { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }',
|
|
147
|
+
'.drw-head-badge {',
|
|
148
|
+
' font-size: 11px; line-height: 1; padding: 2px 7px; border-radius: 999px;',
|
|
149
|
+
' background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.2));',
|
|
150
|
+
' color: var(--dsw-alias-label-secondary, #bbb);',
|
|
151
|
+
' white-space: nowrap;',
|
|
152
|
+
'}',
|
|
153
|
+
'.drw-head-badge[data-state="running"] { color: var(--dsw-alias-state-business-primary, #3dbbf5); }',
|
|
154
|
+
'.drw-head-badge[data-state="error"] { color: var(--dsw-alias-state-error-primary, #ee5858); }',
|
|
155
|
+
'.drw-head-toggle { margin-left: auto; color: var(--dsw-alias-label-secondary, #bbb); font-size: 11.5px; }',
|
|
156
|
+
'.drw-body { overflow-y: auto; overscroll-behavior: contain; }',
|
|
157
|
+
'.drw-body.drw-collapsed { display: none; }',
|
|
158
|
+
'.drw-call { border-top: 1px solid var(--dsw-alias-border-l1, var(--dsw-alias-border-weak, rgba(128,128,128,0.16))); }',
|
|
159
|
+
'.drw-call:first-child { border-top: none; }',
|
|
160
|
+
'.drw-row {',
|
|
161
|
+
' display: flex; align-items: center; gap: 7px;',
|
|
162
|
+
' padding: 5px 10px;',
|
|
163
|
+
' cursor: pointer; user-select: none;',
|
|
164
|
+
' color: var(--dsw-alias-label-primary, #eee);',
|
|
165
|
+
' font-size: 12.5px; line-height: 1.35;',
|
|
166
|
+
'}',
|
|
167
|
+
'.drw-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.10)); }',
|
|
168
|
+
'.drw-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }',
|
|
169
|
+
'.drw-dot[data-state="ok"] { background: var(--dsw-alias-state-success-primary, #34d59a); }',
|
|
170
|
+
'.drw-dot[data-state="running"] { background: var(--dsw-alias-state-business-primary, #3dbbf5); animation: drwPulse 1s ease-in-out infinite; }',
|
|
171
|
+
'.drw-dot[data-state="error"] { background: var(--dsw-alias-state-error-primary, #ee5858); }',
|
|
172
|
+
'.drw-dot[data-state="stopped"] { background: var(--dsw-alias-state-warn-primary, #c9a227); }',
|
|
173
|
+
'@keyframes drwPulse { 50% { opacity: 0.35; } }',
|
|
174
|
+
'.drw-row-name { font-family: var(--ds-font-family-code, var(--dsw-font-mono, ui-monospace, monospace)); font-size: 12px; white-space: nowrap; }',
|
|
175
|
+
'.drw-row-summary { color: var(--dsw-alias-label-secondary, #bbb); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }',
|
|
176
|
+
'.drw-inspect { border: none; background: transparent; color: var(--dsw-alias-label-secondary, #bbb); cursor: pointer; padding: 0 2px; font-size: 12px; }',
|
|
177
|
+
'.drw-inspect:hover { color: var(--dsw-alias-label-primary, #eee); }',
|
|
178
|
+
'.drw-chevron { color: var(--dsw-alias-label-secondary, #bbb); font-size: 10px; transition: transform 0.12s ease; }',
|
|
179
|
+
'.drw-chevron[data-open="1"] { transform: rotate(90deg); }',
|
|
180
|
+
'.drw-row-body { padding: 2px 10px 8px 28px; }',
|
|
181
|
+
'.drw-row-label { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--dsw-alias-label-tertiary, #888); margin: 6px 0 3px; }',
|
|
182
|
+
'.drw-row-pre {',
|
|
183
|
+
' margin: 0; padding: 6px 8px;',
|
|
184
|
+
' border-radius: 6px;',
|
|
185
|
+
' background: var(--dsw-alias-markdown-code-block, rgba(0,0,0,0.25));',
|
|
186
|
+
' color: var(--dsw-alias-label-primary, #eee);',
|
|
187
|
+
' font: var(--dsw-font-markdown-code-block-small, 12px/1.5 ui-monospace, monospace);',
|
|
188
|
+
' white-space: pre-wrap; word-break: break-word;',
|
|
189
|
+
' max-height: 10em; overflow-y: auto;',
|
|
190
|
+
'}',
|
|
191
|
+
/* Think (reasoning) rows inside the slide — one line each, like tool rows */
|
|
192
|
+
'.drw-think { border-top: 1px solid var(--dsw-alias-border-l2, var(--dsw-alias-border-strong, rgba(128,128,128,0.16))); flex: none; }',
|
|
193
|
+
'.drw-think-head { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 0 10px; cursor: pointer; user-select: none; border-radius: 8px; }',
|
|
194
|
+
'.drw-think-head:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.08)); }',
|
|
195
|
+
'.drw-think-name { color: var(--dsw-alias-label-secondary, #bbb); font-size: 11px; font-weight: 600; letter-spacing: 0.04em; flex: none; }',
|
|
196
|
+
'.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
|
+
'.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; }',
|
|
199
|
+
/* Hide the native Think row once it's inside a slide (DOM + CSS only) */
|
|
200
|
+
'[data-variant="think"][data-drw-hidethink="1"] { display: none !important; }',
|
|
201
|
+
/* Settings row: response window size */
|
|
202
|
+
'.drw-set-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; width: 100%; padding: 16px 0 18px; }',
|
|
203
|
+
'.drw-set-text { min-width: 0; }',
|
|
204
|
+
'.drw-set-title { color: var(--dsw-alias-label-primary, #eee); font-size: 14px; font-weight: 400; line-height: 22px; }',
|
|
205
|
+
'.drw-set-desc { color: var(--dsw-alias-label-tertiary, #9296a0); font-size: 12px; line-height: 18px; margin-top: 3px; }',
|
|
206
|
+
'.drw-set-control { display: flex; align-items: center; gap: 6px; flex: none; }',
|
|
207
|
+
'.drw-set-btn {',
|
|
208
|
+
' border: 1px solid var(--dsw-alias-border-l2, var(--dsw-alias-border-strong, rgba(128,128,128,0.35)));',
|
|
209
|
+
' background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,0.08));',
|
|
210
|
+
' color: var(--dsw-alias-label-primary, #eee);',
|
|
211
|
+
' border-radius: 8px; height: 28px; min-width: 28px; padding: 0 8px; cursor: pointer;',
|
|
212
|
+
' font-size: 14px; line-height: 1;',
|
|
213
|
+
'}',
|
|
214
|
+
'.drw-set-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16)); }',
|
|
215
|
+
'.drw-set-btn:disabled { opacity: 0.4; cursor: default; }',
|
|
216
|
+
'.drw-set-input {',
|
|
217
|
+
' width: 56px; height: 28px; text-align: center;',
|
|
218
|
+
' border: 1px solid var(--dsw-alias-border-l2, var(--dsw-alias-border-strong, rgba(128,128,128,0.35)));',
|
|
219
|
+
' border-radius: 8px; background: var(--dsw-alias-bg-layer-1, #1c1c20);',
|
|
220
|
+
' color: var(--dsw-alias-label-primary, #eee); font-size: 13px;',
|
|
221
|
+
'}',
|
|
222
|
+
].join('\n')
|
|
223
|
+
;(document.head || document.documentElement).appendChild(el)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---- tool data helpers -------------------------------------------------
|
|
227
|
+
var READONLY_TOOLS = new Set([
|
|
228
|
+
'read', 'grep', 'glob', 'web_search', 'web_fetch', 'search', 'ls', 'find', 'list',
|
|
229
|
+
])
|
|
230
|
+
function turnNumber(node) {
|
|
231
|
+
var location = node && node.location
|
|
232
|
+
if (!location) return undefined
|
|
233
|
+
if (location.kind === 'turn' || location.kind === 'step') {
|
|
234
|
+
var t = location.turn
|
|
235
|
+
return t && typeof t.turn === 'number' ? t.turn : undefined
|
|
236
|
+
}
|
|
237
|
+
return undefined
|
|
238
|
+
}
|
|
239
|
+
function callName(block) {
|
|
240
|
+
return block && ('kind' in block ? (block.call && block.call.name) || '' : block.name) || ''
|
|
241
|
+
}
|
|
242
|
+
function isRunning(block) {
|
|
243
|
+
return !(block && 'kind' in block)
|
|
244
|
+
}
|
|
245
|
+
function resultText(block) {
|
|
246
|
+
if (!block || !('kind' in block)) return ''
|
|
247
|
+
var parts = []
|
|
248
|
+
var content = block.content || []
|
|
249
|
+
for (var i = 0; i < content.length; i++) {
|
|
250
|
+
var c = content[i]
|
|
251
|
+
if (c && c.type === 'text' && typeof c.text === 'string') parts.push(c.text)
|
|
252
|
+
}
|
|
253
|
+
return parts.join('\n')
|
|
254
|
+
}
|
|
255
|
+
function callSummary(block) {
|
|
256
|
+
var name = callName(block)
|
|
257
|
+
var raw = (block && ('kind' in block ? (block.call && block.call.argsRaw) || '' : block.argsRaw)) || ''
|
|
258
|
+
if (raw === '') return name
|
|
259
|
+
try {
|
|
260
|
+
var parsed = JSON.parse(raw)
|
|
261
|
+
if (typeof parsed !== 'object' || parsed === null) return name + ' · ' + raw
|
|
262
|
+
for (var i = 0; i < KEYS.length; i++) {
|
|
263
|
+
var value = parsed[KEYS[i]]
|
|
264
|
+
if (typeof value === 'string' && value !== '') return value
|
|
265
|
+
}
|
|
266
|
+
return name + ' · ' + raw.slice(0, 80)
|
|
267
|
+
} catch (e) {
|
|
268
|
+
return name + ' · ' + raw.slice(0, 80)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
var KEYS = ['file_path', 'path', 'command', 'url', 'pattern', 'query']
|
|
272
|
+
function computeStats(blocks) {
|
|
273
|
+
var counts = Object.create(null)
|
|
274
|
+
var total = 0, running = 0, errors = 0, readOnly = 0
|
|
275
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
276
|
+
var block = blocks[i]
|
|
277
|
+
if (!block) continue
|
|
278
|
+
var name = callName(block)
|
|
279
|
+
total += 1
|
|
280
|
+
counts[name] = (counts[name] || 0) + 1
|
|
281
|
+
if (isRunning(block)) running += 1
|
|
282
|
+
else if (block.isError) errors += 1
|
|
283
|
+
if (READONLY_TOOLS.has(name)) readOnly += 1
|
|
284
|
+
}
|
|
285
|
+
return { total: total, running: running, errors: errors, readOnly: readOnly, counts: counts }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---- one compact tool row ----------------------------------------------
|
|
289
|
+
function SimpleToolRow(props) {
|
|
290
|
+
var block = props.block
|
|
291
|
+
var openFile = props.openFile
|
|
292
|
+
var inspectCall = props.inspectCall
|
|
293
|
+
var state = React.useState(false)
|
|
294
|
+
var open = state[0]
|
|
295
|
+
var setOpen = state[1]
|
|
296
|
+
var running = isRunning(block)
|
|
297
|
+
var name = callName(block)
|
|
298
|
+
var raw = (block && ('kind' in block ? (block.call && block.call.argsRaw) || '' : block.argsRaw)) || ''
|
|
299
|
+
var output = resultText(block)
|
|
300
|
+
var failed = !running && !!(block && block.isError)
|
|
301
|
+
var stopped = !running && !!(block && block.error) && !failed
|
|
302
|
+
var stateName = running ? 'running' : failed ? 'error' : stopped ? 'stopped' : 'ok'
|
|
303
|
+
return React.createElement('div', { className: 'drw-call', 'data-state': stateName },
|
|
304
|
+
React.createElement('div', {
|
|
305
|
+
className: 'drw-row', role: 'button', tabIndex: 0,
|
|
306
|
+
'aria-expanded': open || undefined,
|
|
307
|
+
onClick: function () { setOpen(!open) },
|
|
308
|
+
onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(!open) } },
|
|
309
|
+
},
|
|
310
|
+
React.createElement('span', { className: 'drw-dot', 'data-state': stateName, 'aria-hidden': true }),
|
|
311
|
+
React.createElement('span', { className: 'drw-row-name', title: name }, name || (block && block.callId) || 'tool'),
|
|
312
|
+
React.createElement('span', { className: 'drw-row-summary', title: output || raw }, callSummary(block)),
|
|
313
|
+
typeof inspectCall === 'function'
|
|
314
|
+
? React.createElement('button', {
|
|
315
|
+
type: 'button', className: 'drw-inspect', title: '在轨迹中查看', 'aria-label': '在轨迹中查看 ' + name,
|
|
316
|
+
onClick: function (e) { e.stopPropagation(); inspectCall(block.callId) },
|
|
317
|
+
}, '\u2934')
|
|
318
|
+
: null,
|
|
319
|
+
React.createElement('span', { className: 'drw-chevron', 'data-open': open ? '1' : undefined, 'aria-hidden': true }, '\u25B6'),
|
|
320
|
+
),
|
|
321
|
+
open
|
|
322
|
+
? React.createElement('div', { className: 'drw-row-body' },
|
|
323
|
+
raw !== ''
|
|
324
|
+
? React.createElement(React.Fragment, null,
|
|
325
|
+
React.createElement('div', { className: 'drw-row-label' }, '参数'),
|
|
326
|
+
React.createElement('pre', { className: 'drw-row-pre' }, raw),
|
|
327
|
+
)
|
|
328
|
+
: null,
|
|
329
|
+
output !== ''
|
|
330
|
+
? React.createElement(React.Fragment, null,
|
|
331
|
+
React.createElement('div', { className: 'drw-row-label' }, '输出'),
|
|
332
|
+
React.createElement('pre', { className: 'drw-row-pre' }, output),
|
|
333
|
+
)
|
|
334
|
+
: null,
|
|
335
|
+
raw === '' && output === ''
|
|
336
|
+
? React.createElement('div', { className: 'drw-row-pre' }, running ? '执行中…' : '无输出')
|
|
337
|
+
: null,
|
|
338
|
+
)
|
|
339
|
+
: null,
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---- one think (reasoning) row inside the slide -------------------------
|
|
344
|
+
// A compact single-line row, collapsible to the full reasoning text — the
|
|
345
|
+
// 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).
|
|
347
|
+
function ThinkRow(props) {
|
|
348
|
+
var text = props.text
|
|
349
|
+
var index = props.index
|
|
350
|
+
var openState = React.useState(false)
|
|
351
|
+
var open = openState[0]
|
|
352
|
+
var setOpen = openState[1]
|
|
353
|
+
var firstLine = text.split('\n')[0].trim() || text.trim()
|
|
354
|
+
var summary = firstLine.length > 120 ? firstLine.slice(0, 117) + '…' : firstLine
|
|
355
|
+
return React.createElement('div', { className: 'drw-think', 'data-open': open ? '1' : undefined },
|
|
356
|
+
React.createElement('div', {
|
|
357
|
+
className: 'drw-think-head', role: 'button', tabIndex: 0,
|
|
358
|
+
'aria-expanded': open || undefined,
|
|
359
|
+
onClick: function () { setOpen(!open) },
|
|
360
|
+
onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(!open) } },
|
|
361
|
+
},
|
|
362
|
+
React.createElement('span', { className: 'drw-think-name' }, 'Think ' + (index + 1)),
|
|
363
|
+
React.createElement('span', { className: 'drw-think-summary', title: firstLine }, summary),
|
|
364
|
+
React.createElement('span', { className: 'drw-think-toggle', 'aria-hidden': true }, open ? '\u25BE' : '\u25B8'),
|
|
365
|
+
),
|
|
366
|
+
open
|
|
367
|
+
? React.createElement('pre', { className: 'drw-think-body' }, text)
|
|
368
|
+
: null,
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---- the per-turn slide -------------------------------------------------
|
|
373
|
+
// One slide = one implementation segment: think + tool calls kept in their
|
|
374
|
+
// exact output order (not think-on-top / bash-on-bottom), all inside the
|
|
375
|
+
// same bounded scroll window.
|
|
376
|
+
function TurnSlide(props) {
|
|
377
|
+
var items = props.items || []
|
|
378
|
+
var turn = props.turn
|
|
379
|
+
var segmentIndex = props.segmentIndex
|
|
380
|
+
var segmentCount = props.segmentCount
|
|
381
|
+
var cwd = props.cwd
|
|
382
|
+
var openFile = props.openFile
|
|
383
|
+
var inspectCall = props.inspectCall
|
|
384
|
+
var cfg = props.config
|
|
385
|
+
var lines = useLiveLines() // live window size from Settings
|
|
386
|
+
var toolItems = items.filter(function (it) { return it.kind === 'tool' })
|
|
387
|
+
var reasoningCount = items.reduce(function (acc, it) { return it.kind === 'think' ? acc + 1 : acc }, 0)
|
|
388
|
+
var stats = React.useMemo(function () {
|
|
389
|
+
return computeStats(toolItems.map(function (it) { return it.node.data.root }))
|
|
390
|
+
}, [items])
|
|
391
|
+
var openState = React.useState(!cfg.collapsed)
|
|
392
|
+
var open = openState[0]
|
|
393
|
+
var setOpen = openState[1]
|
|
394
|
+
var bodyRef = React.useRef(null)
|
|
395
|
+
var running = stats.running > 0
|
|
396
|
+
// Auto-follow to the bottom while calls are still running.
|
|
397
|
+
React.useEffect(function () {
|
|
398
|
+
if (!open || !bodyRef.current) return
|
|
399
|
+
if (running) bodyRef.current.scrollTop = bodyRef.current.scrollHeight
|
|
400
|
+
})
|
|
401
|
+
var list = items
|
|
402
|
+
if (!cfg.showReadOnly) {
|
|
403
|
+
list = items.filter(function (it) {
|
|
404
|
+
return it.kind === 'think' || !READONLY_TOOLS.has(callName(it.node.data.root))
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
var thinkSeen = 0
|
|
408
|
+
return React.createElement('div', { className: 'drw-slide', 'data-running': running ? '1' : undefined },
|
|
409
|
+
React.createElement('div', {
|
|
410
|
+
className: 'drw-head', role: 'button', tabIndex: 0,
|
|
411
|
+
'aria-expanded': open || undefined,
|
|
412
|
+
onClick: function () { setOpen(!open) },
|
|
413
|
+
onKeyDown: function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(!open) } },
|
|
414
|
+
},
|
|
415
|
+
React.createElement('span', { className: 'drw-head-title' },
|
|
416
|
+
stats.total + ' 个工具调用'
|
|
417
|
+
+ (reasoningCount > 0 ? ' · ' + reasoningCount + ' Think' : '')
|
|
418
|
+
),
|
|
419
|
+
running
|
|
420
|
+
? React.createElement('span', { className: 'drw-head-badge', 'data-state': 'running' }, '进行中')
|
|
421
|
+
: null,
|
|
422
|
+
!running && stats.errors > 0
|
|
423
|
+
? React.createElement('span', { className: 'drw-head-badge', 'data-state': 'error' }, '⚠ ' + stats.errors)
|
|
424
|
+
: null,
|
|
425
|
+
!cfg.showReadOnly && stats.readOnly > 0
|
|
426
|
+
? React.createElement('span', { className: 'drw-head-badge' }, '隐藏只读 ' + stats.readOnly)
|
|
427
|
+
: null,
|
|
428
|
+
React.createElement('span', { className: 'drw-head-toggle' }, open ? '收起 ▴' : '展开 ▾'),
|
|
429
|
+
),
|
|
430
|
+
React.createElement('div', {
|
|
431
|
+
ref: bodyRef,
|
|
432
|
+
className: 'drw-body' + (open ? '' : ' drw-collapsed'),
|
|
433
|
+
style: open && lines > 0 ? { maxHeight: (lines * 1.55) + 'em' } : undefined,
|
|
434
|
+
},
|
|
435
|
+
list.map(function (it) {
|
|
436
|
+
if (it.kind === 'think') {
|
|
437
|
+
var tid = thinkSeen
|
|
438
|
+
thinkSeen += 1
|
|
439
|
+
return React.createElement(ThinkRow, { key: 'think-' + tid, text: it.text, index: tid })
|
|
440
|
+
}
|
|
441
|
+
return React.createElement(SimpleToolRow, {
|
|
442
|
+
key: it.node.key,
|
|
443
|
+
block: it.node.data.root,
|
|
444
|
+
cwd: cwd,
|
|
445
|
+
openFile: openFile,
|
|
446
|
+
inspectCall: inspectCall,
|
|
447
|
+
})
|
|
448
|
+
}),
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ---- response-segment grouping -------------------------------------------
|
|
454
|
+
// A "staged response" is an assistant step that returns visible text to the
|
|
455
|
+
// user mid-turn. Each chunk of implementation (think + tool calls) between
|
|
456
|
+
// two staged responses becomes its own slide — not one giant slide per user
|
|
457
|
+
// prompt pair.
|
|
458
|
+
function hasVisibleTextBlock(blocks) {
|
|
459
|
+
if (!blocks) return false
|
|
460
|
+
for (var i = 0; i < blocks.length; i++) {
|
|
461
|
+
var b = blocks[i]
|
|
462
|
+
if (b && b.kind === 'text' && typeof b.text === 'string' && b.text.trim() !== '') return true
|
|
463
|
+
}
|
|
464
|
+
return false
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function buildTurnSegments(order, chat) {
|
|
468
|
+
var segments = []
|
|
469
|
+
var current = null
|
|
470
|
+
function ensure() {
|
|
471
|
+
if (current === null) {
|
|
472
|
+
current = { items: [], tools: [], startKey: null }
|
|
473
|
+
segments.push(current)
|
|
474
|
+
}
|
|
475
|
+
return current
|
|
476
|
+
}
|
|
477
|
+
function closeSegment() { current = null }
|
|
478
|
+
for (var i = 0; i < order.length; i++) {
|
|
479
|
+
var n = chat.nodes.get(order[i])
|
|
480
|
+
if (n === undefined) continue
|
|
481
|
+
if (n.kind === 'user') { closeSegment(); continue }
|
|
482
|
+
if (n.kind === 'assistant-step') {
|
|
483
|
+
var blocks = n.data && Array.isArray(n.data.blocks) ? n.data.blocks : []
|
|
484
|
+
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.
|
|
488
|
+
for (var b = 0; b < blocks.length; b++) {
|
|
489
|
+
var block = blocks[b]
|
|
490
|
+
if (block && block.kind === 'reasoning' && typeof block.text === 'string' && block.text.trim() !== '') {
|
|
491
|
+
ensure().items.push({ kind: 'think', text: block.text })
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (staged) closeSegment()
|
|
495
|
+
continue
|
|
496
|
+
}
|
|
497
|
+
if (n.kind === 'tool-call') {
|
|
498
|
+
var seg = ensure()
|
|
499
|
+
if (seg.startKey === null) seg.startKey = n.key
|
|
500
|
+
seg.items.push({ kind: 'tool', node: n })
|
|
501
|
+
seg.tools.push(n)
|
|
502
|
+
continue
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// Only segments that actually have tool calls get a slide (that is where
|
|
506
|
+
// the slot shadow mounts); think-only chunks stay native.
|
|
507
|
+
return segments.filter(function (s) { return s.tools.length > 0 })
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ---- slot component: one slide per response segment, siblings null ------
|
|
511
|
+
function ToolTurnSlide(props) {
|
|
512
|
+
var node = props.node
|
|
513
|
+
// Standard session-scope prop of the slot runtime (the per-conversation
|
|
514
|
+
// chat snapshot hook). Guarded to a no-op so the component can never
|
|
515
|
+
// crash when an old/new mix of the shell omits it.
|
|
516
|
+
var useChat = typeof props.useChat === 'function'
|
|
517
|
+
? props.useChat
|
|
518
|
+
: function (selector) { return { turn: turnNumber(node), segments: [] } }
|
|
519
|
+
var cwd = props.cwd
|
|
520
|
+
var openFile = props.openFile
|
|
521
|
+
var inspectCall = props.inspectCall
|
|
522
|
+
var turn = turnNumber(node)
|
|
523
|
+
var EMPTY = { turn: turn, segments: [] }
|
|
524
|
+
var snapData = useChat(function (chat) {
|
|
525
|
+
if (turn === undefined || !chat || !chat.locations || !chat.nodes) return EMPTY
|
|
526
|
+
var order = chat.locations.getTurn(turn)
|
|
527
|
+
return { turn: turn, segments: buildTurnSegments(order, chat) }
|
|
528
|
+
})
|
|
529
|
+
var segments = snapData.segments
|
|
530
|
+
try {
|
|
531
|
+
if (!segments || segments.length === 0) return null
|
|
532
|
+
var segIndex = -1
|
|
533
|
+
for (var i = 0; i < segments.length; i++) {
|
|
534
|
+
var seg = segments[i]
|
|
535
|
+
// Render on the segment's first tool-call node that is actually
|
|
536
|
+
// visible; a turn-process fold can mark leading member nodes hidden,
|
|
537
|
+
// and the slide must ride a row the user can see. Fall back to the
|
|
538
|
+
// plain segment start.
|
|
539
|
+
var anchor = null
|
|
540
|
+
for (var t = 0; t < seg.tools.length; t++) {
|
|
541
|
+
var toolNode = seg.tools[t]
|
|
542
|
+
if (toolNode.visibility !== 'hidden') { anchor = toolNode.key; break }
|
|
543
|
+
}
|
|
544
|
+
if (anchor === null) anchor = seg.startKey
|
|
545
|
+
if (anchor === node.key) { segIndex = i; break }
|
|
546
|
+
}
|
|
547
|
+
if (segIndex < 0) return null
|
|
548
|
+
var seg = segments[segIndex]
|
|
549
|
+
return React.createElement(TurnSlide, {
|
|
550
|
+
items: seg.items,
|
|
551
|
+
turn: turn,
|
|
552
|
+
segmentIndex: segments.length > 1 ? segIndex : undefined,
|
|
553
|
+
segmentCount: segments.length,
|
|
554
|
+
cwd: cwd,
|
|
555
|
+
openFile: openFile,
|
|
556
|
+
inspectCall: inspectCall,
|
|
557
|
+
config: currentConfig,
|
|
558
|
+
})
|
|
559
|
+
} catch (e) {
|
|
560
|
+
return null
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ---- DOM housekeeping ----------------------------------------------------
|
|
565
|
+
// A per-chat-snapshot index: assistant-step node key → whether that step
|
|
566
|
+
// carries visible staged text. Cached per snapshot identity so the
|
|
567
|
+
// `useChat` selector result stays referentially stable between updates.
|
|
568
|
+
var NO_TEXT = Object.freeze(new Map())
|
|
569
|
+
var textIndexCache = new WeakMap()
|
|
570
|
+
function hasTextIndexOf(chat) {
|
|
571
|
+
if (!chat || !chat.nodes) return NO_TEXT
|
|
572
|
+
var cached = textIndexCache.get(chat)
|
|
573
|
+
if (cached !== undefined) return cached
|
|
574
|
+
var map = new Map()
|
|
575
|
+
var nodes = chat.nodes.values()
|
|
576
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
577
|
+
var n = nodes[i]
|
|
578
|
+
if (n && n.kind === 'assistant-step' && hasVisibleTextBlock(n.data && n.data.blocks)) {
|
|
579
|
+
map.set(n.key, true)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
textIndexCache.set(chat, map)
|
|
583
|
+
return map
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Leftover ~0-height placeholders: for every tool-call node that is not a
|
|
587
|
+
// segment anchor our slot renders null, but DSH still keeps an (almost)
|
|
588
|
+
// empty flow item per stubbed tool call. The shell hides truly empty flow
|
|
589
|
+
// items by its own CSS; this scan also hides assistant-step rows that only
|
|
590
|
+
// held a slide-hidden Think (zero visible height, but still occupying the
|
|
591
|
+
// flow gap slot between the slide and the next response). Never stub while
|
|
592
|
+
// the row is streaming (content may come), and un-stub as soon as the chat
|
|
593
|
+
// snapshot reports staged text for that row's node (keyed via
|
|
594
|
+
// `data-chat-flow-key` — no CSS-class sniffing, which broke in the alpha
|
|
595
|
+
// that hashed module classes).
|
|
596
|
+
function markStubs(root, textIndex) {
|
|
597
|
+
// Tool-call placeholders: every non-anchor tool-call renders an (almost)
|
|
598
|
+
// empty flow item from our null slot.
|
|
599
|
+
var rows = root.querySelectorAll('[data-chat-flow-kind="tool-call"]')
|
|
600
|
+
for (var i = 0; i < rows.length; i++) {
|
|
601
|
+
var e = rows[i]
|
|
602
|
+
var isStub = !e.querySelector('.drw-slide') && (e.textContent || '').trim() === ''
|
|
603
|
+
if (isStub) e.dataset.drwStub = '1'
|
|
604
|
+
else if (e.dataset.drwStub) delete e.dataset.drwStub
|
|
605
|
+
}
|
|
606
|
+
// Assistant-step rows that only held a reasoning block: once the native
|
|
607
|
+
// think is folded into the slide (hidden), the whole row is 0-height but
|
|
608
|
+
// still occupies a flex gap slot between the slide and the next response.
|
|
609
|
+
// Hide such ghost rows too — but never while streaming (content may come).
|
|
610
|
+
var steps = root.querySelectorAll('[data-chat-flow-kind="assistant-step"]')
|
|
611
|
+
for (var j = 0; j < steps.length; j++) {
|
|
612
|
+
var s = steps[j]
|
|
613
|
+
if (s.querySelector('.drw-slide')) { if (s.dataset.drwStub) delete s.dataset.drwStub; continue }
|
|
614
|
+
if (s.querySelector('[data-streaming]')) { if (s.dataset.drwStub) delete s.dataset.drwStub; continue }
|
|
615
|
+
var hasHiddenThink = !!s.querySelector('[data-variant="think"][data-drw-hidethink="1"]')
|
|
616
|
+
var key = s.getAttribute('data-chat-flow-key')
|
|
617
|
+
var hasText = key === null || textIndex.get(key) === true
|
|
618
|
+
if (hasHiddenThink && !hasText) s.dataset.drwStub = '1'
|
|
619
|
+
else if (s.dataset.drwStub) delete s.dataset.drwStub
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Hide native Think rows that belong to a turn with a slide, so the same
|
|
624
|
+
// reasoning does not appear both inside the slide and inline in the flow.
|
|
625
|
+
// Only DOM class toggling — never reparents React-owned rows.
|
|
626
|
+
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])
|
|
631
|
+
// Segment boundary in the DOM: a user/steering row, a turn-level row
|
|
632
|
+
// (turn-process / turn-tail), or an assistant-step row that carries
|
|
633
|
+
// visible staged text (a response shown to the user mid-turn).
|
|
634
|
+
function isSegmentBoundary(row) {
|
|
635
|
+
if (!row.matches) return false
|
|
636
|
+
if (row.matches('[data-chat-flow-kind="user"]')) return true
|
|
637
|
+
var kind = row.getAttribute('data-chat-flow-kind')
|
|
638
|
+
if (kind === 'steering' || kind === 'turn-tail' || kind === 'turn-process') return true
|
|
639
|
+
if (kind !== 'assistant-step') return false
|
|
640
|
+
var key = row.getAttribute('data-chat-flow-key')
|
|
641
|
+
return key === null || textIndex.get(key) === true
|
|
642
|
+
}
|
|
643
|
+
for (var r = 0; r < rows.length; r++) {
|
|
644
|
+
var row = rows[r]
|
|
645
|
+
if (!row || !row.querySelector) continue
|
|
646
|
+
var think = row.matches && row.matches('[data-variant="think"]') ? row : row.querySelector('[data-variant="think"]')
|
|
647
|
+
if (!think) continue
|
|
648
|
+
var hasSlide = false
|
|
649
|
+
for (var j = r - 1; j >= 0; j--) {
|
|
650
|
+
var prev = rows[j]
|
|
651
|
+
if (!prev || !prev.matches) continue
|
|
652
|
+
if (isSegmentBoundary(prev)) break
|
|
653
|
+
if (prev.querySelector && prev.querySelector('.drw-slide')) { hasSlide = true; break }
|
|
654
|
+
}
|
|
655
|
+
for (var k = r + 1; k < rows.length && !hasSlide; k++) {
|
|
656
|
+
var next = rows[k]
|
|
657
|
+
if (!next || !next.matches) continue
|
|
658
|
+
if (isSegmentBoundary(next)) break
|
|
659
|
+
if (next.querySelector && next.querySelector('.drw-slide')) { hasSlide = true; break }
|
|
660
|
+
}
|
|
661
|
+
if (hasSlide) think.dataset.drwHidethink = '1'
|
|
662
|
+
else if (think.dataset.drwHidethink) delete think.dataset.drwHidethink
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Invisible per-session dock that watches the conversation scroller and
|
|
667
|
+
// keeps native Think rows in sync with the slides (session-scoped).
|
|
668
|
+
function TextWindowDock(props) {
|
|
669
|
+
var useChat = typeof props.useChat === 'function' ? props.useChat : null
|
|
670
|
+
var textIndex = useChat === null ? NO_TEXT : useChat(hasTextIndexOf)
|
|
671
|
+
var textIndexRef = React.useRef(NO_TEXT)
|
|
672
|
+
React.useEffect(function () { textIndexRef.current = textIndex }, [textIndex])
|
|
673
|
+
var mountRef = React.useRef(null)
|
|
674
|
+
React.useEffect(function () {
|
|
675
|
+
var mount = mountRef.current
|
|
676
|
+
if (!mount) return
|
|
677
|
+
var scroller = mount.closest('[data-conversation-scroll]')
|
|
678
|
+
var root = scroller || document.body
|
|
679
|
+
var timer = null
|
|
680
|
+
var bootTimer = null
|
|
681
|
+
var scan = function () {
|
|
682
|
+
try { markStubs(root, textIndexRef.current) } catch (e) {}
|
|
683
|
+
try { hideNativeThink(root, textIndexRef.current) } catch (e) {}
|
|
684
|
+
}
|
|
685
|
+
// Session switch renders the new rows in bursts; a debounce makes the
|
|
686
|
+
// stub hiding lag behind by hundreds of ms, during which the 0-height
|
|
687
|
+
// placeholder rows inflate the gap below a slide. Scan on the same
|
|
688
|
+
// animation frame the mutation lands (throttled to one per frame), so
|
|
689
|
+
// the fix is in place before the next paint — no visible flicker.
|
|
690
|
+
var schedule = function () {
|
|
691
|
+
if (timer != null) return
|
|
692
|
+
timer = requestAnimationFrame(function () {
|
|
693
|
+
timer = null
|
|
694
|
+
scan()
|
|
695
|
+
// trailing pass: rows that landed between the mutation and paint
|
|
696
|
+
window.setTimeout(scan, 80)
|
|
697
|
+
})
|
|
698
|
+
}
|
|
699
|
+
scan()
|
|
700
|
+
var observer = new MutationObserver(function () { schedule() })
|
|
701
|
+
observer.observe(root, { childList: true, subtree: true })
|
|
702
|
+
// Re-scan shortly after mount for already-settled history.
|
|
703
|
+
bootTimer = setTimeout(scan, 200)
|
|
704
|
+
return function () {
|
|
705
|
+
observer.disconnect()
|
|
706
|
+
if (timer != null) cancelAnimationFrame(timer)
|
|
707
|
+
clearTimeout(bootTimer)
|
|
708
|
+
}
|
|
709
|
+
}, [props.sessionId])
|
|
710
|
+
return React.createElement('div', { ref: mountRef, 'data-drw-dock': '', style: { display: 'none' } })
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ---- Settings row: response window size ---------------------------------
|
|
714
|
+
function ResponseWindowSettingsRow(props) {
|
|
715
|
+
var useLines = props.useLines
|
|
716
|
+
var setLines = props.setLines
|
|
717
|
+
var lines = (typeof useLines === 'function' ? useLines(function (s) { return s }) : null) ?? currentConfig.lines
|
|
718
|
+
var value = clampLines(lines)
|
|
719
|
+
var step = function (delta) {
|
|
720
|
+
setLines(value + delta)
|
|
721
|
+
}
|
|
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 = 不限高,改动即时生效。'),
|
|
726
|
+
),
|
|
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
|
+
}, '+'),
|
|
742
|
+
),
|
|
743
|
+
)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// ---- plugin entry -------------------------------------------------------
|
|
747
|
+
var currentConfig = readConfig(null)
|
|
748
|
+
|
|
749
|
+
var INJECT = ['slots', 'settingsScope']
|
|
750
|
+
function apply(ctx, config) {
|
|
751
|
+
currentConfig = readConfig(config)
|
|
752
|
+
injectStyles()
|
|
753
|
+
|
|
754
|
+
// Durable settings binding: the host half registered the `dsh-newbe-response-window`
|
|
755
|
+
// namespace; the browser scope mirrors it and persists user overrides.
|
|
756
|
+
linesStore = createSnapshotStore(currentConfig.lines)
|
|
757
|
+
var scopeService = null
|
|
758
|
+
try { scopeService = ctx.get('settingsScope') } catch (e) { scopeService = null }
|
|
759
|
+
if (scopeService && typeof scopeService.bind === 'function') {
|
|
760
|
+
try {
|
|
761
|
+
settingsScope = scopeService.bind({ namespace: NS })
|
|
762
|
+
} catch (e) {
|
|
763
|
+
settingsScope = null
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (settingsScope !== null) {
|
|
767
|
+
var adopt = function () {
|
|
768
|
+
try {
|
|
769
|
+
var snap = settingsScope.getSnapshot()
|
|
770
|
+
var value = snap && snap.value
|
|
771
|
+
if (value && typeof value.lines === 'number') {
|
|
772
|
+
var n = clampLines(value.lines)
|
|
773
|
+
if (linesStore !== null && linesStore.getSnapshot() !== n) linesStore.set(n)
|
|
774
|
+
}
|
|
775
|
+
} catch (e) {}
|
|
776
|
+
}
|
|
777
|
+
settingsScope.subscribe(adopt)
|
|
778
|
+
adopt()
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
ctx.slots.inject('conversation.chat.node', function () {
|
|
782
|
+
return ctx.slots.register({
|
|
783
|
+
name: 'conversation.chat.node',
|
|
784
|
+
key: 'tool-call',
|
|
785
|
+
priority: -100,
|
|
786
|
+
}, ToolTurnSlide)
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
// Always mounted: hides native Think rows (core feature) and optionally
|
|
790
|
+
// applies the bounded window to long assistant markdown.
|
|
791
|
+
ctx.slots.inject('conversation.input.dock', function () {
|
|
792
|
+
return ctx.slots.register({
|
|
793
|
+
name: 'conversation.input.dock',
|
|
794
|
+
id: 'dsh-newbe-response-window',
|
|
795
|
+
order: 90,
|
|
796
|
+
}, TextWindowDock)
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
// Settings → General row for the window size (live, persisted).
|
|
800
|
+
ctx.slots.inject('settings.general.item', function () {
|
|
801
|
+
return ctx.slots.register({
|
|
802
|
+
name: 'settings.general.item',
|
|
803
|
+
id: 'dsh-newbe-response-window',
|
|
804
|
+
order: 60,
|
|
805
|
+
inject: function () {
|
|
806
|
+
return {
|
|
807
|
+
hooks: { lines: linesStore },
|
|
808
|
+
setLines: function (value) {
|
|
809
|
+
var n = clampLines(value)
|
|
810
|
+
if (linesStore !== null) linesStore.set(n)
|
|
811
|
+
if (settingsScope !== null) {
|
|
812
|
+
settingsScope.set('lines', n).catch(function () {})
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
}, ResponseWindowSettingsRow)
|
|
818
|
+
})
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
exports.apply = apply
|
|
822
|
+
exports.inject = INJECT
|
|
823
|
+
Object.defineProperty(exports, '__esModule', { value: true })
|
|
824
|
+
return module.exports
|
|
825
|
+
},
|
|
826
|
+
})
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// dsh-newbe-response-window — host half.
|
|
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").
|
|
7
|
+
//
|
|
8
|
+
// Namespace + schema (browser `settingsScope.bind` mirrors it):
|
|
9
|
+
// lines window height in lines (default 10; 0 = uncapped)
|
|
10
|
+
// collapsed start slides collapsed instead of always-expanded
|
|
11
|
+
// showReadOnly include read-only tools in the slide list
|
|
12
|
+
// 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
|
+
|
|
18
|
+
import z from '@deepseek-ai/schemastery'
|
|
19
|
+
|
|
20
|
+
/** Stable Cordis plugin name. */
|
|
21
|
+
export const name = 'dsh-newbe-response-window'
|
|
22
|
+
|
|
23
|
+
/** Durable settings namespace for this plugin (lowercase kebab-case). */
|
|
24
|
+
export const SETTINGS_NS = 'dsh-newbe-response-window'
|
|
25
|
+
|
|
26
|
+
/** Settings schema, shared with the browser scope and any Settings UI. */
|
|
27
|
+
export const Config = z.object({
|
|
28
|
+
lines: z.natural().min(0).max(200).default(10),
|
|
29
|
+
collapsed: z.boolean().default(false),
|
|
30
|
+
showReadOnly: z.boolean().default(true),
|
|
31
|
+
minCollapseRows: z.natural().min(1).max(50).default(3),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Register the durable settings namespace when a settings provider exists.
|
|
36
|
+
* @param ctx - host plugin context.
|
|
37
|
+
*/
|
|
38
|
+
export function apply(ctx) {
|
|
39
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
40
|
+
settingsCtx.settings.register(SETTINGS_NS, Config)
|
|
41
|
+
})
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
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.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"dsh": {
|
|
13
|
+
"bundle": {
|
|
14
|
+
"patch": "./cordis.patch.yml"
|
|
15
|
+
},
|
|
16
|
+
"client": {
|
|
17
|
+
"platform": "web"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"lib/index.js",
|
|
22
|
+
"lib/client.js",
|
|
23
|
+
"cordis.patch.yml",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"check": "node --check lib/index.js && node --check lib/client.js"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@deepseek-ai/schemastery": "^3.0.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/qiqiangvae/dsh-newbe-plugins.git",
|
|
40
|
+
"directory": "packages/dsh-newbe-response-window"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/qiqiangvae/dsh-newbe-plugins/tree/main/packages/dsh-newbe-response-window#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/qiqiangvae/dsh-newbe-plugins/issues"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"dsh-plugin",
|
|
48
|
+
"deepseek-harness",
|
|
49
|
+
"dsh",
|
|
50
|
+
"response-window",
|
|
51
|
+
"tool-call",
|
|
52
|
+
"slide",
|
|
53
|
+
"web-ui"
|
|
54
|
+
]
|
|
55
|
+
}
|