dsh-cursor-dev 0.11.0 → 0.12.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 +7 -7
- package/docs/INSTALL.md +12 -6
- package/lib/journal.js +23 -0
- package/lib/usage.js +253 -7
- package/package.json +3 -3
- package/screenshots/panel-zh.png +0 -0
package/README.md
CHANGED
|
@@ -55,9 +55,9 @@ flowchart LR
|
|
|
55
55
|
3. 按 DSH 提示完成加载;涉及后台代码更新时重启 DSH,然后刷新页面。在 Cursor 面板中选择模型并保存。
|
|
56
56
|
4. 明确告诉父模型使用 `cursor_code`,或者用下面的斜杠命令派发第一个任务。
|
|
57
57
|
|
|
58
|
-
**版本说明(2026-10-
|
|
58
|
+
**版本说明(2026-10-18 核对):** `0.12.0` 已推送到公开仓库的 `main`,沿用 `0.11.1` 的 DSH `0.2.1-rc.2` 契约(外部子智能体契约、顶层 `engines.dsh: >=0.2.1-rc.2 <0.3.0-0`),并重做了面板的历史会话卡:列表收进限高滚动框、点标题即可整段折叠(选择记在浏览器本地),每行新增两段式**删除**,经新增的 `api/forget` 路由一并清掉索引记录、轨迹和镜像子会话。插件市场(dshmarket)已收录本插件(目录条目合并于 2026-10-17),可以直接在市场里找到并安装;npm 公共源上当前可安装的版本仍是 `0.11.1`(`0.12.0` 尚未发布,要这版删除/折叠功能请用 Git 来源)。要跟进后续改动请用 Git 来源;`.tgz` 是某一刻的固定快照。详见[安装来源与版本](docs/INSTALL.md#2-选择安装来源)。
|
|
59
59
|
|
|
60
|
-
运行要求:DSH
|
|
60
|
+
运行要求:DSH `>=0.2.1-rc.2 <0.3.0-0`(已在 `0.2.1-rc.2` 上验证);Node.js `^22.19.0 || >=24.0.0`;已登录且可访问服务的 Cursor CLI。原生子智能体功能需要 DSH 的 `subagents`、`agents` 服务,后台任务还需要对应任务服务,建议使用当前 DSH 桌面版。CLI 安装方法见 [Cursor 官方文档](https://cursor.com/docs/cli)。
|
|
61
61
|
|
|
62
62
|
### 三个入口
|
|
63
63
|
|
|
@@ -152,7 +152,7 @@ flowchart LR
|
|
|
152
152
|
|
|
153
153
|
从 DSH 的 Cursor 插件入口打开面板,也可以访问当前 DSH 地址下的 `/plugins/dsh-cursor-dev/`。例如,DSH 运行在 `http://127.0.0.1:19387` 时,面板地址为 `http://127.0.0.1:19387/plugins/dsh-cursor-dev/`;其他用户应使用自己的地址和端口。
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
面板包含安装/登录状态、账号额度、模型和模式设置、活动与历史会话。点击会话可查看镜像记录。历史会话会一直累积,所以列表默认收在一个限高的滚动框里,点标题即可整段折叠(选择会被记住);每行右侧的 **删除** 会清掉这次运行的全部痕迹——索引记录、轨迹和镜像子会话,和 `resetSession` 走同一条链路。删除要连点两次(第一次只是变成「确认删除」),因为删掉之后无法恢复。新版本页面可能与上面的截图不同。
|
|
156
156
|
|
|
157
157
|
| 插件配置 | 默认值 | 用途 |
|
|
158
158
|
| --- | --- | --- |
|
|
@@ -194,7 +194,7 @@ npm install
|
|
|
194
194
|
node --test --test-force-exit --test-skip-pattern="the child agent really edits a file|a bad model is rejected|the real Cursor account returns live quota data" "test/**/*.test.mjs"
|
|
195
195
|
```
|
|
196
196
|
|
|
197
|
-
完整测试命令为 `npm test`,共
|
|
197
|
+
完整测试命令为 `npm test`,共 216 项;上面那条命令跳过其中三个真实 CLI/账号测试,剩下 213 项离线测试必须全绿。真实测试需要 Cursor 登录和网络,执行任务会使用该账号额度。当前实现已通过 213 项离线测试,并完成两轮真实 Auto 调用、文件修改和上下文续接验证。打包使用 `npm pack` 或 `pnpm pack`,发布前按[分发检查](docs/INSTALL.md#7-维护者分发前检查)核对安装包和版本。
|
|
198
198
|
|
|
199
199
|
## English
|
|
200
200
|
|
|
@@ -217,12 +217,12 @@ See the [installation guide](docs/INSTALL.md) for detailed setup and troubleshoo
|
|
|
217
217
|
dsh plugin add dsh-cursor-dev
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
-
3. Restart DSH when backend changes require it, then refresh the page. Open the Cursor panel under `/plugins/dsh-cursor-dev/`, select a model and save it.
|
|
220
|
+
3. Restart DSH when backend changes require it, then refresh the page. Open the Cursor panel under `/plugins/dsh-cursor-dev/`, select a model and save it. The history list keeps one row per run forever, so it sits in a height-capped scroll box and folds away when you click its heading (the choice is remembered); a row's **Delete** link clears that run's index record, journal and mirrored child — the same chain `resetSession` uses — and arms on the first click so the irreversible second click is deliberate.
|
|
221
221
|
4. Ask the parent to use `cursor_code`, or send `/cursor-subagent` followed by a complete task. The command dispatches directly and hands a completion or failure summary back to the original parent.
|
|
222
222
|
|
|
223
|
-
As checked on **2026-10-
|
|
223
|
+
As checked on **2026-10-18**, version `0.12.0` is pushed to `main` on the public repository and keeps the DSH `0.2.1-rc.2` contract from `0.11.1` (the external-subagent contract and `engines.dsh: >=0.2.1-rc.2 <0.3.0-0` at the package top level), and reworks the panel's history card: the list sits in a height-capped scroll box, folds away from its heading (remembered per browser), and each row gains a two-click **Delete** that clears the index record, journal and mirrored child through a new `api/forget` route. The dsh-market catalog already lists this plugin (entry merged 2026-10-17), so it can be installed from the market; `0.11.1` is the version currently on the public npm registry as `dsh-cursor-dev` (`0.12.0` is not published yet, so use the Git source for the fold/delete work). Use the Git source to follow later changes; a `.tgz` is a fixed snapshot.
|
|
224
224
|
|
|
225
|
-
Requirements: DSH
|
|
225
|
+
Requirements: DSH `>=0.2.1-rc.2 <0.3.0-0` (verified against `0.2.1-rc.2`); Node.js `^22.19.0 || >=24.0.0`; an authenticated Cursor CLI account with service access. Native dispatch additionally needs DSH's `subagents` and `agents` services, and background runs need the host's job services. A current DSH desktop installation is recommended.
|
|
226
226
|
|
|
227
227
|
### Parent-to-child contract
|
|
228
228
|
|
package/docs/INSTALL.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- 可正常对话的 DSH,以及一个支持工具调用的父模型。
|
|
12
12
|
- 插件使用的 Node.js 满足 `^22.19.0 || >=24.0.0`。桌面版应检查其自带运行时,终端里的 `node --version` 不一定是桌面程序使用的版本。
|
|
13
13
|
- 当前版 Cursor CLI,已完成登录,并有执行任务所需的账号权限、额度和网络连接。
|
|
14
|
-
- 原生子智能体入口需要 DSH 的 `subagents`、`agents` 服务;后台运行还需要宿主任务服务。包声明的 DSH 版本为
|
|
14
|
+
- 原生子智能体入口需要 DSH 的 `subagents`、`agents` 服务;后台运行还需要宿主任务服务。包声明的 DSH 版本为 `>=0.2.1-rc.2 <0.3.0-0`(已在 `0.2.1-rc.2` 上验证),建议使用当前桌面版及其标准 profile。
|
|
15
15
|
|
|
16
16
|
父模型的 API 配置与 Cursor 登录分别管理。插件使用本机 CLI 的登录状态,无需再向插件填写一份 Cursor API Key。
|
|
17
17
|
|
|
@@ -38,15 +38,15 @@ agent status --format json
|
|
|
38
38
|
|
|
39
39
|
## 2. 选择安装来源
|
|
40
40
|
|
|
41
|
-
**发布状态(2026-10-
|
|
41
|
+
**发布状态(2026-10-18 核对):** 公共 GitHub 仓库存在,`0.12.0` 已推送到 `main`(沿用 `0.11.1` 的 DSH `0.2.1-rc.2` 外部子智能体契约与顶层 `engines.dsh: >=0.2.1-rc.2 <0.3.0-0`,并重做历史会话卡:限高滚动、可折叠、每行可删除)。插件市场(dshmarket)的目录条目已于 2026-10-17 合并,可以直接在市场里找到本插件;npm 公共源上当前为 `0.11.1`,`0.12.0` 尚未发布——要用折叠与删除功能请装 Git 来源,市场卡片的版本号要等发布后才会更新。
|
|
42
42
|
|
|
43
43
|
| 你要安装的内容 | 安装来源 |
|
|
44
44
|
| --- | --- |
|
|
45
45
|
| 插件市场里的本插件 | 市场条目解析为 `github:Dclef/dsh-cursor-dev`,安装后会跟随仓库默认分支 |
|
|
46
|
-
| npm 公共源上的已发布版本 | `dsh-cursor-dev`(`0.11.0` 起可用,按包内 `files`
|
|
46
|
+
| npm 公共源上的已发布版本 | `dsh-cursor-dev`(`0.11.0` 起可用,按包内 `files` 白名单安装,当前为 `0.11.1`;`0.12.0` 尚未发布) |
|
|
47
47
|
| 公开仓库的当前版本(推荐) | `git+https://github.com/Dclef/dsh-cursor-dev.git` |
|
|
48
48
|
| 你本地的源码目录 | 该目录的绝对路径,例如 `D:/plugins/dsh-cursor-dev` |
|
|
49
|
-
| 维护者提供的源码快照或发布包 | `.tgz` 文件的绝对路径,例如 `D:/Downloads/dsh-cursor-dev-0.
|
|
49
|
+
| 维护者提供的源码快照或发布包 | `.tgz` 文件的绝对路径,例如 `D:/Downloads/dsh-cursor-dev-0.12.0.tgz` |
|
|
50
50
|
|
|
51
51
|
表中本地路径是示例,需要换成实际位置。npm 来源取 registry 上发布的最新版本;Git 来源会取得仓库默认分支的最新提交;`.tgz` 是某一刻的固定快照——要跟进后续改动就用 Git 来源。
|
|
52
52
|
|
|
@@ -88,7 +88,7 @@ dsh plugin add "/home/you/plugins/dsh-cursor-dev"
|
|
|
88
88
|
安装收到的包:
|
|
89
89
|
|
|
90
90
|
```powershell
|
|
91
|
-
dsh plugin add "D:/Downloads/dsh-cursor-dev-0.
|
|
91
|
+
dsh plugin add "D:/Downloads/dsh-cursor-dev-0.12.0.tgz"
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
终端没有 `dsh` 命令时,使用桌面插件管理即可。不要把在任意项目目录执行 `npm install` 当成已经在 DSH 当前 profile 中启用插件。
|
|
@@ -113,6 +113,12 @@ http://127.0.0.1:19387/plugins/dsh-cursor-dev/
|
|
|
113
113
|
|
|
114
114
|
`auto` 与 `default[]` 都表示 Auto。面板保存的选择与当前已启动任务的模型不是同一项状态;新选择在后续运行时应用。锁定只决定选用哪个模型,不增加账号额度或模型访问权限。
|
|
115
115
|
|
|
116
|
+
### 历史会话列表太长或想删掉某一次运行
|
|
117
|
+
|
|
118
|
+
历史会话按「父会话 + 标签」累积,每一次运行留一行,时间久了列表会很长。所以面板把它收在一个限高的滚动框里,点标题即可整段折叠,折叠状态记在浏览器本地,刷新后仍然保持。
|
|
119
|
+
|
|
120
|
+
每行最右侧是 **删除**:第一次点击只会变成 **确认删除**,再点一次才真正执行。它会清掉这次运行的索引记录、轨迹和镜像子会话,与工具参数 `resetSession` 走同一条链路,所以删掉之后无法恢复,也无法再从这个面板续聊那次运行。Cursor 自己保存的原始会话不受影响;需要它时可从 Cursor 侧查看。
|
|
121
|
+
|
|
116
122
|
### 模型列表不全时先看代理
|
|
117
123
|
|
|
118
124
|
同一张卡片上有 **代理** 输入框,留空即为自动探测,正常情况不需要填。它决定的是**插件启动的 CLI 子进程**走哪条网络出口。
|
|
@@ -189,7 +195,7 @@ continueSession 设为 true。让 Cursor 说明上一轮做了什么,再核对
|
|
|
189
195
|
|
|
190
196
|
| 现象 | 检查与处理 |
|
|
191
197
|
| --- | --- |
|
|
192
|
-
| 按包名安装返回 404 | `0.11.0` 起已发布到 npm
|
|
198
|
+
| 按包名安装返回 404 | `0.11.0` 起已发布到 npm 公共源(当前 `0.11.1`);仍 404 时确认 registry 指向公共源且包名拼写为 `dsh-cursor-dev`,或改用插件市场、上面的 Git 来源、已取得的源码目录或安装包 |
|
|
193
199
|
| DSH 报告找不到 Cursor CLI | 检查 `agent status --format json`;确认 CLI 位于 DSH 可见的 PATH 或常见安装目录,然后重启 DSH。仍找不到时,在插件配置里设置 `command` 为可启动二进制、安装根目录、`agent.cmd`,或 `"node.exe" "index.js"`(非空时只走该路径) |
|
|
194
200
|
| 未登录或账号不可用 | 在运行 DSH 的同一操作系统用户下执行 `agent login`,检查账号和网络状态 |
|
|
195
201
|
| 面板选了 Auto,实际仍是其他模型 | 当前任务不会中途切换;新版请保存“锁定面板模型”。查看返回的 `modelSource` 与 `requestedModel`,确认是否有显式调用参数 |
|
package/lib/journal.js
CHANGED
|
@@ -88,6 +88,29 @@ export class RunJournal {
|
|
|
88
88
|
this.#entries.delete(key);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Drop every generation's journal for one conversation.
|
|
93
|
+
*
|
|
94
|
+
* `clear` is exact-keyed, which is right for a live run but useless for a
|
|
95
|
+
* conversation being forgotten: its buckets are keyed `parent::label#2` and
|
|
96
|
+
* the caller only knows `parent::label`. Sweeping the prefix is what makes
|
|
97
|
+
* "delete this history row" actually delete the timeline behind it.
|
|
98
|
+
*
|
|
99
|
+
* @param conversationKey - `parent::label`.
|
|
100
|
+
* @returns how many buckets were dropped.
|
|
101
|
+
*/
|
|
102
|
+
clearConversation(conversationKey) {
|
|
103
|
+
const prefix = `${conversationKey}#`;
|
|
104
|
+
let dropped = 0;
|
|
105
|
+
for (const key of [...this.#entries.keys()]) {
|
|
106
|
+
if (key === conversationKey || key.startsWith(prefix)) {
|
|
107
|
+
this.#entries.delete(key);
|
|
108
|
+
dropped += 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return dropped;
|
|
112
|
+
}
|
|
113
|
+
|
|
91
114
|
/**
|
|
92
115
|
* Read the newest journal belonging to one conversation.
|
|
93
116
|
*
|
package/lib/usage.js
CHANGED
|
@@ -533,6 +533,90 @@ export function registerUsageRoutes(ctx, deps) {
|
|
|
533
533
|
},
|
|
534
534
|
};
|
|
535
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Forget one conversation: every trace of it, not just the row.
|
|
538
|
+
*
|
|
539
|
+
* The dashboard's delete button, and it deliberately does exactly what
|
|
540
|
+
* `cursor_code`'s `resetSession` does — kill the live generations, drop the
|
|
541
|
+
* journals, remove the durable index row and close the mirrored child —
|
|
542
|
+
* because a row that vanished from the table while any of those survived
|
|
543
|
+
* would come back on the next run and confuse the operator twice.
|
|
544
|
+
*
|
|
545
|
+
* Forgetting something already gone answers `ok` with zero counts: the
|
|
546
|
+
* button's promise is "make it invisible", and a second click on a stale row
|
|
547
|
+
* must not produce a scary 404.
|
|
548
|
+
*/
|
|
549
|
+
const forgetRoute = {
|
|
550
|
+
kind: 'exact',
|
|
551
|
+
path: '/plugins/dsh-cursor-dev/api/forget',
|
|
552
|
+
handler: async (req, res) => {
|
|
553
|
+
const method = String(req?.method ?? 'GET').toUpperCase();
|
|
554
|
+
if (method !== 'POST') {
|
|
555
|
+
sendJson(res, 405, { ok: false, error: `method ${method} is not supported` });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const url = new URL(req?.url ?? '/', 'http://localhost');
|
|
559
|
+
const key = url.searchParams.get('key');
|
|
560
|
+
if (key === null || key === '') {
|
|
561
|
+
sendJson(res, 400, { ok: false, error: 'a conversation key is required' });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
// Same two key spaces as the restore route: a live row arrives
|
|
565
|
+
// generation-scoped (`a::default#1`), the rest of the host knows the
|
|
566
|
+
// bare conversation (`a::default`).
|
|
567
|
+
const durableKey = conversationKeyOf(key);
|
|
568
|
+
const entry = index?.get?.(key) ?? index?.get?.(durableKey);
|
|
569
|
+
const live = pool.snapshot().filter(row => conversationKeyOf(row.key) === durableKey);
|
|
570
|
+
// The pool goes first: a disposed session cannot append to the
|
|
571
|
+
// journal or rewrite the index behind the deletions that follow.
|
|
572
|
+
try {
|
|
573
|
+
pool.dropConversation?.(durableKey);
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
ctx.logger?.warn?.(`dsh-cursor-dev: conversation ${durableKey} was not evicted: ${errorText(error)}`);
|
|
577
|
+
}
|
|
578
|
+
let journals = 0;
|
|
579
|
+
try {
|
|
580
|
+
if (typeof journal?.clearConversation === 'function')
|
|
581
|
+
journals = journal.clearConversation(durableKey);
|
|
582
|
+
else
|
|
583
|
+
journal?.clear?.(durableKey);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
ctx.logger?.warn?.(`dsh-cursor-dev: journal for ${durableKey} was not cleared: ${errorText(error)}`);
|
|
587
|
+
}
|
|
588
|
+
let indexes = 0;
|
|
589
|
+
for (const each of new Set([durableKey, key])) {
|
|
590
|
+
if (index?.get?.(each) === undefined)
|
|
591
|
+
continue;
|
|
592
|
+
try {
|
|
593
|
+
await index.delete(each);
|
|
594
|
+
indexes += 1;
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
ctx.logger?.warn?.(`dsh-cursor-dev: index row ${each} was not removed: ${errorText(error)}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
let mirrorsDropped = 0;
|
|
601
|
+
if (typeof mirrors?.dropConversation === 'function') {
|
|
602
|
+
try {
|
|
603
|
+
await mirrors.dropConversation(durableKey);
|
|
604
|
+
mirrorsDropped = 1;
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
ctx.logger?.warn?.(`dsh-cursor-dev: mirror for ${durableKey} was not closed: ${errorText(error)}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
sendJson(res, 200, {
|
|
611
|
+
ok: true,
|
|
612
|
+
key,
|
|
613
|
+
conversationKey: durableKey,
|
|
614
|
+
existed: entry !== undefined || live.length > 0,
|
|
615
|
+
dropped: { live: live.length, journals, index: indexes, mirrors: mirrorsDropped },
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
|
|
536
620
|
const dashboardRoute = {
|
|
537
621
|
kind: 'exact',
|
|
538
622
|
path: '/plugins/dsh-cursor-dev/',
|
|
@@ -587,7 +671,7 @@ export function registerUsageRoutes(ctx, deps) {
|
|
|
587
671
|
}
|
|
588
672
|
};
|
|
589
673
|
|
|
590
|
-
for (const route of [jsonRoute, journalRoute, restoreRoute, dashboardRoute]) {
|
|
674
|
+
for (const route of [jsonRoute, journalRoute, restoreRoute, forgetRoute, dashboardRoute]) {
|
|
591
675
|
try {
|
|
592
676
|
registrations.push(server.register({
|
|
593
677
|
...route,
|
|
@@ -665,6 +749,7 @@ function dashboardHtml(state) {
|
|
|
665
749
|
endpoint: '/plugins/dsh-cursor-dev/api/usage',
|
|
666
750
|
journalEndpoint: '/plugins/dsh-cursor-dev/api/journal',
|
|
667
751
|
restoreEndpoint: '/plugins/dsh-cursor-dev/api/restore',
|
|
752
|
+
forgetEndpoint: '/plugins/dsh-cursor-dev/api/forget',
|
|
668
753
|
settingsEndpoint: '/plugins/dsh-cursor-dev/api/settings',
|
|
669
754
|
statusEndpoint: '/plugins/dsh-cursor-dev/status',
|
|
670
755
|
settings: {
|
|
@@ -738,6 +823,25 @@ function dashboardHtml(state) {
|
|
|
738
823
|
grows a page-wide scrollbar and the cards drift off-screen. The scroll
|
|
739
824
|
belongs to this wrapper, never to the page. */
|
|
740
825
|
.tablewrap { overflow-x: auto; padding-bottom: 2px; }
|
|
826
|
+
/* The history table is unbounded (one row per run ever made), so it scrolls
|
|
827
|
+
inside a box of its own instead of stretching the card. The inner .tablewrap
|
|
828
|
+
still owns the horizontal scroll; this box owns the vertical one. */
|
|
829
|
+
#convos-body { max-height: 300px; overflow-y: auto; }
|
|
830
|
+
/* A 300px window over a table with one row per run is useless if the column
|
|
831
|
+
labels scroll away with the first row, so the header stays put. */
|
|
832
|
+
#convos-body thead th { position: sticky; top: 0; background: var(--card); z-index: 1; }
|
|
833
|
+
/* A section heading that doubles as the collapse control. */
|
|
834
|
+
h2 > .disclose { color: inherit; text-decoration: none; }
|
|
835
|
+
h2 > .disclose::before { content: '▾ '; color: var(--muted); }
|
|
836
|
+
h2.collapsed > .disclose::before { content: '▸ '; }
|
|
837
|
+
h2 > .disclose:hover { color: var(--accent); }
|
|
838
|
+
h2 > .pill { margin-left: 8px; }
|
|
839
|
+
td.act { white-space: nowrap; }
|
|
840
|
+
/* One row is one run: model names and timestamps must not wrap into a second
|
|
841
|
+
line, or a 300px window shows four rows instead of eight. The long key and
|
|
842
|
+
path columns already clip themselves. */
|
|
843
|
+
#convos td { white-space: nowrap; }
|
|
844
|
+
a.link.danger { color: var(--bad); }
|
|
741
845
|
table { width: 100%; border-collapse: collapse; }
|
|
742
846
|
th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--line); }
|
|
743
847
|
th { color: var(--muted); font-weight: 500; white-space: nowrap; }
|
|
@@ -758,6 +862,9 @@ function dashboardHtml(state) {
|
|
|
758
862
|
.err { border-color: var(--bad); }
|
|
759
863
|
.journal { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
|
|
760
864
|
.journal h3 { font-size: 12px; margin: 0 0 8px; font-weight: 600; }
|
|
865
|
+
.journal.warn { border-top-color: var(--warn); color: var(--warn); }
|
|
866
|
+
.journal.bad { border-top-color: var(--bad); color: var(--bad); }
|
|
867
|
+
.journal.ok { border-top-color: var(--good); color: var(--good); }
|
|
761
868
|
.journal pre { white-space: pre-wrap; word-break: break-word; margin: 6px 0 0;
|
|
762
869
|
background: var(--bg); border: 1px solid var(--line); border-radius: 6px; padding: 8px;
|
|
763
870
|
max-height: 260px; overflow: auto; font-size: 12px; }
|
|
@@ -936,11 +1043,17 @@ function dashboardHtml(state) {
|
|
|
936
1043
|
</div>
|
|
937
1044
|
|
|
938
1045
|
<div class="row card">
|
|
939
|
-
<h2 data-t="conversations">历史会话(可恢复,重装后仍在)</h2>
|
|
1046
|
+
<h2><a href="#" class="disclose" id="convos-toggle" data-t="conversations">历史会话(可恢复,重装后仍在)</a><span class="pill" id="convos-count">0</span></h2>
|
|
940
1047
|
<div class="muted" data-t="conversationsHint"></div>
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1048
|
+
<!-- The history table grows one row per run the operator has ever made, so it
|
|
1049
|
+
gets its own bounded scroll box and can be dropped entirely: on a busy
|
|
1050
|
+
machine this card alone used to push everything else off the page. -->
|
|
1051
|
+
<div id="convos-body">
|
|
1052
|
+
<div class="tablewrap"><table><thead><tr>
|
|
1053
|
+
<th data-t="colKey">会话键</th><th data-t="colSession">会话号</th><th data-t="colWorkspace">工作目录</th><th data-t="colModel">模型</th><th class="num" data-t="colUpdated">更新时间</th><th class="num" data-t="colActions">操作</th>
|
|
1054
|
+
</tr></thead><tbody id="convos"></tbody></table></div>
|
|
1055
|
+
</div>
|
|
1056
|
+
<div class="journal" id="convos-notice" style="display:none"></div>
|
|
944
1057
|
</div>
|
|
945
1058
|
|
|
946
1059
|
<div class="note" id="source"></div>
|
|
@@ -950,6 +1063,8 @@ function dashboardHtml(state) {
|
|
|
950
1063
|
(function () {
|
|
951
1064
|
var boot = JSON.parse(document.getElementById('bootstrap').textContent);
|
|
952
1065
|
var STORE_KEY = 'dsh-cursor-dev:lang';
|
|
1066
|
+
/** Where the "history table folded" choice is remembered. */
|
|
1067
|
+
var FOLD_KEY = 'dsh-cursor-dev:convos-folded';
|
|
953
1068
|
|
|
954
1069
|
var T = {
|
|
955
1070
|
zh: {
|
|
@@ -999,6 +1114,16 @@ function dashboardHtml(state) {
|
|
|
999
1114
|
live: '运行中的 Cursor 会话',
|
|
1000
1115
|
conversations: '历史会话(可恢复,重装后仍在)',
|
|
1001
1116
|
conversationsHint: '点击会话键打开这次运行:有子智能体对话时直接跳过去;旧对话(这个功能上线前跑的)会当场从 Cursor 里读回来建成子会话;读不到才退回去展开轨迹。',
|
|
1117
|
+
colActions: '操作',
|
|
1118
|
+
forget: '删除',
|
|
1119
|
+
forgetArmed: '确认删除',
|
|
1120
|
+
forgetHint: '删除这条历史会话(会话记录、轨迹、镜像一起清掉)',
|
|
1121
|
+
forgetting: '正在删除这条历史会话…',
|
|
1122
|
+
forgetConfirm: '删除这条历史会话?它的会话记录、轨迹和镜像都会一起清掉,无法恢复。再点一次「确认删除」就执行。',
|
|
1123
|
+
forgetFailed: '删除失败:',
|
|
1124
|
+
forgotten: '已删除:',
|
|
1125
|
+
expandHint: '展开或收起历史会话列表',
|
|
1126
|
+
countRows: '条',
|
|
1002
1127
|
openChildHint: '打开这次运行的子智能体对话',
|
|
1003
1128
|
openJournalHint: '展开这次运行的轨迹',
|
|
1004
1129
|
restoreHint: '把这次运行的旧对话读回来,建成一个能点开的子智能体会话',
|
|
@@ -1105,6 +1230,16 @@ function dashboardHtml(state) {
|
|
|
1105
1230
|
live: 'Live Cursor sessions',
|
|
1106
1231
|
conversations: 'Known conversations (durable)',
|
|
1107
1232
|
conversationsHint: 'Click a key to open the run: its child conversation when there is one; an older run (made before this feature) is read back out of Cursor and published as one; only if that fails does the step timeline open instead.',
|
|
1233
|
+
colActions: 'actions',
|
|
1234
|
+
forget: 'Delete',
|
|
1235
|
+
forgetArmed: 'Confirm delete',
|
|
1236
|
+
forgetHint: 'Delete this history row (its index record, journal and mirror go too)',
|
|
1237
|
+
forgetting: 'Deleting this history row…',
|
|
1238
|
+
forgetConfirm: 'Delete this history row? Its index record, journal and mirror go with it, and it cannot be undone. Click Confirm delete to go ahead.',
|
|
1239
|
+
forgetFailed: 'Delete failed: ',
|
|
1240
|
+
forgotten: 'Deleted: ',
|
|
1241
|
+
expandHint: 'Fold or unfold the history list',
|
|
1242
|
+
countRows: 'rows',
|
|
1108
1243
|
openChildHint: 'Open the child conversation for this run',
|
|
1109
1244
|
openJournalHint: 'Unfold this run’s step timeline',
|
|
1110
1245
|
restoreHint: 'Read this run’s old conversation back and publish it as an openable child conversation',
|
|
@@ -1639,7 +1774,8 @@ function dashboardHtml(state) {
|
|
|
1639
1774
|
if (spec.cls) td.className = spec.cls;
|
|
1640
1775
|
if (spec.link) {
|
|
1641
1776
|
var a = document.createElement('a');
|
|
1642
|
-
a.className = 'link' + (spec.on && spec.on(entry) ? ' on' : '')
|
|
1777
|
+
a.className = 'link' + (spec.on && spec.on(entry) ? ' on' : '')
|
|
1778
|
+
+ (spec.danger ? ' danger' : '');
|
|
1643
1779
|
a.href = '#';
|
|
1644
1780
|
a.textContent = spec.value(entry);
|
|
1645
1781
|
var hint = spec.title ? spec.title(entry) : '';
|
|
@@ -1782,6 +1918,97 @@ function dashboardHtml(state) {
|
|
|
1782
1918
|
renderLive(lastPayload || {});
|
|
1783
1919
|
}
|
|
1784
1920
|
|
|
1921
|
+
/**
|
|
1922
|
+
* One history row's delete control.
|
|
1923
|
+
*
|
|
1924
|
+
* Destroying a row is irreversible here (the index, the journal and the
|
|
1925
|
+
* mirrored child all go), so the first click only arms the link and the second
|
|
1926
|
+
* one within the same row does the work. Armed state lives in a module-level
|
|
1927
|
+
* variable rather than in the DOM because the table is re-rendered by the
|
|
1928
|
+
* poll: a pending confirmation must survive a re-render of its own row.
|
|
1929
|
+
*/
|
|
1930
|
+
function forgetRow(entry) {
|
|
1931
|
+
var key = entry.key;
|
|
1932
|
+
if (armedFor !== key) {
|
|
1933
|
+
armedFor = key;
|
|
1934
|
+
forgetNotice(t('forgetConfirm'), 'warn');
|
|
1935
|
+
renderLive(lastPayload || {});
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
armedFor = '';
|
|
1939
|
+
forgetNotice(t('forgetting'), '');
|
|
1940
|
+
fetch(boot.forgetEndpoint + '?key=' + encodeURIComponent(key), {
|
|
1941
|
+
method: 'POST',
|
|
1942
|
+
headers: { accept: 'application/json' },
|
|
1943
|
+
})
|
|
1944
|
+
.then(function (response) {
|
|
1945
|
+
return response.json().then(function (body) { return { ok: response.ok, body: body }; });
|
|
1946
|
+
})
|
|
1947
|
+
.then(function (answer) {
|
|
1948
|
+
var body = answer.body || {};
|
|
1949
|
+
if (answer.ok === true && body.ok === true) {
|
|
1950
|
+
if (journalKey === key) {
|
|
1951
|
+
journalKey = '';
|
|
1952
|
+
var panel = $('journal');
|
|
1953
|
+
panel.style.display = 'none';
|
|
1954
|
+
panel.textContent = '';
|
|
1955
|
+
}
|
|
1956
|
+
// The row is gone on disk, so drop it from the payload that is about
|
|
1957
|
+
// to be re-painted instead of waiting for the next poll to notice.
|
|
1958
|
+
var extended = lastPayload && lastPayload.extended;
|
|
1959
|
+
if (extended && Array.isArray(extended.conversations)) {
|
|
1960
|
+
extended.conversations = extended.conversations.filter(function (row) { return row.key !== key; });
|
|
1961
|
+
}
|
|
1962
|
+
forgetNotice(t('forgotten') + shortKey(key), 'ok');
|
|
1963
|
+
renderLive(lastPayload || {});
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
forgetNotice(t('forgetFailed') + (body.error || 'unknown error'), 'bad');
|
|
1967
|
+
renderLive(lastPayload || {});
|
|
1968
|
+
})
|
|
1969
|
+
.catch(function (error) {
|
|
1970
|
+
forgetNotice(t('forgetFailed') + (error && error.message ? error.message : String(error)), 'bad');
|
|
1971
|
+
renderLive(lastPayload || {});
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
/** One line of feedback under the history table (never the journal panel). */
|
|
1976
|
+
function forgetNotice(message, tone) {
|
|
1977
|
+
var host = $('convos-notice');
|
|
1978
|
+
if (!host) return;
|
|
1979
|
+
host.style.display = message === '' ? 'none' : '';
|
|
1980
|
+
host.className = 'journal' + (tone ? ' ' + tone : '');
|
|
1981
|
+
host.textContent = message;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Fold or unfold the history table.
|
|
1986
|
+
*
|
|
1987
|
+
* The card is the longest thing on the page on a busy machine, and the fold
|
|
1988
|
+
* state is remembered per browser so a panel that fits the operator's screen
|
|
1989
|
+
* stays that way across reloads and language switches.
|
|
1990
|
+
*/
|
|
1991
|
+
function setFolded(folded) {
|
|
1992
|
+
convosFolded = folded === true;
|
|
1993
|
+
try { window.localStorage.setItem(FOLD_KEY, convosFolded ? '1' : '0'); } catch (e) { /* private mode */ }
|
|
1994
|
+
applyFolded();
|
|
1995
|
+
}
|
|
1996
|
+
function applyFolded() {
|
|
1997
|
+
var body = $('convos-body');
|
|
1998
|
+
if (body) body.style.display = convosFolded ? 'none' : '';
|
|
1999
|
+
var title = $('convos-toggle');
|
|
2000
|
+
if (title && title.parentNode && title.parentNode.classList) {
|
|
2001
|
+
title.parentNode.classList[convosFolded ? 'add' : 'remove']('collapsed');
|
|
2002
|
+
}
|
|
2003
|
+
if (title) title.title = t('expandHint');
|
|
2004
|
+
}
|
|
2005
|
+
/** The row count pill next to the heading. */
|
|
2006
|
+
function renderCount(count) {
|
|
2007
|
+
var pill = $('convos-count');
|
|
2008
|
+
if (!pill) return;
|
|
2009
|
+
pill.textContent = String(count) + ' ' + t('countRows');
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1785
2012
|
/**
|
|
1786
2013
|
* The key cell: the conversation when there is one, else the step timeline.
|
|
1787
2014
|
*
|
|
@@ -1899,6 +2126,11 @@ function dashboardHtml(state) {
|
|
|
1899
2126
|
|
|
1900
2127
|
var lastPayload = {};
|
|
1901
2128
|
var journalKey = '';
|
|
2129
|
+
/** The row whose delete is armed; empty means nothing is pending. */
|
|
2130
|
+
var armedFor = '';
|
|
2131
|
+
/** Whether the history table is folded; remembered per browser. */
|
|
2132
|
+
var convosFolded = false;
|
|
2133
|
+
try { convosFolded = window.localStorage.getItem(FOLD_KEY) === '1'; } catch (e) { convosFolded = false; }
|
|
1902
2134
|
/** Live text in the model search box; survives a re-render and a language switch. */
|
|
1903
2135
|
var modelFilter = '';
|
|
1904
2136
|
/** Which chip is active in the model browser; also survives a re-render. */
|
|
@@ -1944,8 +2176,13 @@ function dashboardHtml(state) {
|
|
|
1944
2176
|
{ value: function (e) { return e.workspaceRoot; }, cls: 'path',
|
|
1945
2177
|
title: function (e) { return e.workspaceRoot; } },
|
|
1946
2178
|
{ value: function (e) { return e.model; } },
|
|
1947
|
-
{ value: function (e) { return when(e.updatedAt); }, num: true }
|
|
2179
|
+
{ value: function (e) { return when(e.updatedAt); }, num: true },
|
|
2180
|
+
{ value: function (e) { return armedFor === e.key ? t('forgetArmed') : t('forget'); },
|
|
2181
|
+
cls: 'act', link: true, danger: true, onClick: forgetRow,
|
|
2182
|
+
title: function () { return t('forgetHint'); } }
|
|
1948
2183
|
]);
|
|
2184
|
+
renderCount(convos.length);
|
|
2185
|
+
applyFolded();
|
|
1949
2186
|
}
|
|
1950
2187
|
function render(payload) {
|
|
1951
2188
|
lastPayload = payload;
|
|
@@ -2133,6 +2370,15 @@ function dashboardHtml(state) {
|
|
|
2133
2370
|
loadSettings(false);
|
|
2134
2371
|
refreshAuth(true);
|
|
2135
2372
|
});
|
|
2373
|
+
// The heading is the fold control: the table is the one unbounded list on the
|
|
2374
|
+
// page, and this is how a busy machine keeps it out of the way.
|
|
2375
|
+
try {
|
|
2376
|
+
$('convos-toggle').addEventListener('click', function (event) {
|
|
2377
|
+
event.preventDefault();
|
|
2378
|
+
setFolded(!convosFolded);
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
catch (error) { /* a stubbed DOM in the tests has no heading */ }
|
|
2136
2382
|
$('model-save').addEventListener('click', function () {
|
|
2137
2383
|
var value = $('model').value;
|
|
2138
2384
|
var proxyBox = $('proxy');
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cursor-dev",
|
|
3
3
|
"displayName": "Cursor 子智能体",
|
|
4
|
-
"description": "把 Cursor CLI
|
|
5
|
-
"version": "0.
|
|
4
|
+
"description": "把 Cursor CLI 当作子编码智能体来写业务代码:cursor_code 每次运行都在 DSH 里开一个真实的、可点开的子会话,cursor_subagent 则显示为宿主写入的 External 记录;可按任务选模型、会话在 DSH 重启后仍能续聊、返回逐步工具时间线与真实文件 diff,并自带额度/模型/会话面板。",
|
|
5
|
+
"version": "0.12.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "lib/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"engines": {
|
|
24
24
|
"node": "^22.19.0 || >=24.0.0",
|
|
25
|
-
"dsh": "
|
|
25
|
+
"dsh": ">=0.2.1-rc.2 <0.3.0-0"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"test": "node --test --test-force-exit \"test/**/*.test.mjs\""
|
package/screenshots/panel-zh.png
CHANGED
|
Binary file
|