dsh-cursor-dev 0.12.1 → 0.12.2
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 +4 -4
- package/docs/INSTALL.md +4 -4
- package/docs/skill-drafts/subagent-model-routing/SKILL.md +66 -0
- package/lib/index.js +9 -3
- package/lib/native-provider.js +18 -16
- package/lib/quota-guard.js +113 -0
- package/lib/session-pool.js +4 -0
- package/lib/tool.js +24 -4
- package/lib/usage.js +257 -107
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ flowchart LR
|
|
|
55
55
|
3. 按 DSH 提示完成加载;涉及后台代码更新时重启 DSH,然后刷新页面。在 Cursor 面板中选择模型并保存。
|
|
56
56
|
4. 明确告诉父模型使用 `cursor_code`,或者用下面的斜杠命令派发第一个任务。
|
|
57
57
|
|
|
58
|
-
**版本说明(0.12.
|
|
58
|
+
**版本说明(0.12.2):** 本版加入额度耗尽保护:调用 `cursor_code` 或通过原生子智能体派发之前先检查 Cursor 用量,总、Auto、API 三个额度都到上限时直接拒绝并说明原因,不再把任务派出去后中途失败;用量接口暂时取不到时不拦截调用。`cursor_subagent` 现在也会生成可点击的对话子会话,面板里点运行中或已完成的行都能查看过程,不再报「没有可恢复的会话」。面板重排为额度四张卡片置顶、代理输入框移到模型选择上方、模型目录与来源说明折叠收起。DSH 兼容性仍为 `>=0.2.1-rc.2 <0.3.0-0`。插件市场已收录本插件,卡片版本随目录探测更新;npm 包名为 `dsh-cursor-dev`,实际已发布版本以 npm 的 `latest` 标签为准。要跟进后续改动请用 Git 来源;`.tgz` 是某一刻的固定快照。详见[安装来源与版本](docs/INSTALL.md#2-选择安装来源)。
|
|
59
59
|
|
|
60
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
|
|
|
@@ -65,7 +65,7 @@ flowchart LR
|
|
|
65
65
|
| --- | --- | --- |
|
|
66
66
|
| `cursor_code` | 父模型派发完整任务,控制目录、模式、模型、会话与报告 | 工具结果回到调用它的父模型,包含答复、步骤和检测到的文件变更 |
|
|
67
67
|
| `/cursor-subagent 任务内容` | 用户直接派发一次任务;可选前缀 `-m 模型` | 完成或失败后,插件把摘要交给原父代理继续处理 |
|
|
68
|
-
| `cursor_subagent` | 当前源码新增的 DSH 原生一次性子智能体工具,可后台运行 | 使用 DSH
|
|
68
|
+
| `cursor_subagent` | 当前源码新增的 DSH 原生一次性子智能体工具,可后台运行 | 使用 DSH 标准结果及后台任务机制;父会话保留一行 External 任务,Cursor 面板里的会话行可直接打开实时镜像子对话 |
|
|
69
69
|
|
|
70
70
|
例如,在一个空测试工作区发送:
|
|
71
71
|
|
|
@@ -220,7 +220,7 @@ See the [installation guide](docs/INSTALL.md) for detailed setup and troubleshoo
|
|
|
220
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
|
-
Version `0.12.
|
|
223
|
+
Version `0.12.2` adds quota exhaustion protection: before `cursor_code` or a native subagent call starts, the plugin reads Cursor usage and refuses the call, explaining why, when the total, Auto and API buckets are all at their limit — instead of dispatching a run that later fails mid-way. A usage lookup that cannot be completed never blocks a call. `cursor_subagent` now also creates a clickable transcript child, so rows for running and finished runs open in the panel instead of reporting that no durable conversation exists. The panel puts the four quota cards first, moves the proxy input above the model picker, and folds the model catalogue and the source/egress notes. DSH compatibility remains `>=0.2.1-rc.2 <0.3.0-0`. The dsh-market catalog already lists this plugin and updates its displayed version on subsequent probes. The public npm package is `dsh-cursor-dev`; its `latest` tag identifies the published version. Use the Git source to follow later changes; a `.tgz` is a fixed snapshot.
|
|
224
224
|
|
|
225
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
|
|
|
@@ -239,7 +239,7 @@ The default model order is call argument → saved panel choice → plugin confi
|
|
|
239
239
|
|
|
240
240
|
The current source registers a native DSH provider named `cursor` and a standard one-shot tool named `cursor_subagent`. Rich `cursor_code` calls also use the native lifecycle when those host services exist. The existing generic `subagent` backend remains configured as before.
|
|
241
241
|
|
|
242
|
-
Cursor executes externally. From DSH 0.2.1 on, the host writes a single non-interactive `External` row for a `cursor_subagent` run
|
|
242
|
+
Cursor executes externally. From DSH 0.2.1 on, the host writes a single non-interactive `External` row for a `cursor_subagent` run. Both entrances also create a real transcript child that the Cursor panel opens while the first turn is running. The native service keeps its own lifecycle id; the separate transcript child does not add a duplicate catalog row. Standard background runs use DSH's job result and cancellation tools. Persistent mailbox continuation through `send_message`, and Cursor as a resident Agent Teams teammate, are not implemented. Use `cursor_code` with `continueSession` for explicit follow-ups.
|
|
243
243
|
|
|
244
244
|
The parent remains responsible for reviewing the actual changes and verification evidence. File reports have scan limits and are not a substitute for final workspace checks. Cursor model availability and usage depend on the Cursor account; the parent model uses its separate DSH provider configuration.
|
|
245
245
|
|
package/docs/INSTALL.md
CHANGED
|
@@ -38,7 +38,7 @@ agent status --format json
|
|
|
38
38
|
|
|
39
39
|
## 2. 选择安装来源
|
|
40
40
|
|
|
41
|
-
**当前源码版本:** `0.12.
|
|
41
|
+
**当前源码版本:** `0.12.2`,新增额度耗尽保护(三个额度都到上限时提前拒绝,用量取不到时不拦截),`cursor_subagent` 生成的子会话可在面板点开查看轨迹,面板重排为额度置顶、代理输入框在模型选择上方。DSH 兼容性保持 `>=0.2.1-rc.2 <0.3.0-0`。npm 包名为 `dsh-cursor-dev`,实际已发布版本以 npm 的 `latest` 标签为准。插件市场已收录本插件,卡片版本随目录探测更新。
|
|
42
42
|
|
|
43
43
|
| 你要安装的内容 | 安装来源 |
|
|
44
44
|
| --- | --- |
|
|
@@ -46,7 +46,7 @@ agent status --format json
|
|
|
46
46
|
| npm 公共源上的已发布版本 | `dsh-cursor-dev`(`0.11.0` 起可用,按包内 `files` 白名单安装,版本以 npm 的 `latest` 标签为准) |
|
|
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.12.
|
|
49
|
+
| 维护者提供的源码快照或发布包 | `.tgz` 文件的绝对路径,例如 `D:/Downloads/dsh-cursor-dev-0.12.2.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.12.
|
|
91
|
+
dsh plugin add "D:/Downloads/dsh-cursor-dev-0.12.2.tgz"
|
|
92
92
|
```
|
|
93
93
|
|
|
94
94
|
终端没有 `dsh` 命令时,使用桌面插件管理即可。不要把在任意项目目录执行 `npm install` 当成已经在 DSH 当前 profile 中启用插件。
|
|
@@ -202,7 +202,7 @@ continueSession 设为 true。让 Cursor 说明上一轮做了什么,再核对
|
|
|
202
202
|
| 显示 usage limit | 检查 Cursor 账号额度和模型可用性;更换父模型不会改变 Cursor 账号额度 |
|
|
203
203
|
| 父模型没有调用插件 | 确认插件已启用、工具允许使用且父模型支持工具调用;明确要求使用 `cursor_code` 或直接用斜杠命令 |
|
|
204
204
|
| 有 `cursor_code`,没有 `cursor_subagent` | 确认所装版本包含原生接入、bundle 配置已应用,以及当前 profile 提供子智能体和任务服务;升级后重启 |
|
|
205
|
-
|
|
|
205
|
+
| 点击运行中的 `cursor_subagent` 会话只显示运行轨迹 | 更新插件后台并重启 DSH;新版从首轮运行时即建立镜像,在 Cursor 面板中点击会话键即可打开子对话。宿主仍保留一行 External 任务;旧版本留下的已完成记录可在历史会话中读回 |
|
|
206
206
|
| 更新后页面还是旧的 | 检查更新的是当前 profile 中实际加载的包,再重启 DSH 并刷新浏览器;只修改另一份源码不会自动更新所有安装副本 |
|
|
207
207
|
| 续聊没有原来的内容 | 确认父会话、`sessionLabel` 和工作区;检查是否重置过映射、删除过 Cursor 记录,或发生了加载失败回退 |
|
|
208
208
|
| 想彻底开始新任务 | 使用新 `sessionLabel`,或对原标签设置 `resetSession:true`;不要只依赖 `continueSession:false` 清空历史 |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: subagent-model-routing
|
|
3
|
+
description: 调用 subagent、选择子智能体模型或排查 provider/model/reasoning_effort 路由错误时使用;默认省略路由字段,指定前查询目录,禁止空字符串与猜测路由。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 子智能体模型路由与参数校验
|
|
7
|
+
|
|
8
|
+
## 适用范围
|
|
9
|
+
|
|
10
|
+
调用 `subagent`、选择子智能体的提供方、模型或推理等级,或排查以下错误时使用:
|
|
11
|
+
|
|
12
|
+
- `Error: child LLM \`provider\` must be non-empty`
|
|
13
|
+
- 子智能体模型路由不被当前 Session 允许
|
|
14
|
+
- provider 与 model 不成对,或推理等级无效
|
|
15
|
+
|
|
16
|
+
本技能只约束子智能体模型参数。用户明确指定专家、专家团或其他工具时,遵守对应工具的要求。
|
|
17
|
+
|
|
18
|
+
## 调用规则
|
|
19
|
+
|
|
20
|
+
1. **使用默认模型时,省略 provider、model、reasoning_effort 三个键。** 不用空字符串、空白字符串、null 或猜测值表示默认。
|
|
21
|
+
2. **显式指定模型前,先调用 list_subagent_models。** 先查提供方,再按需查模型及其推理等级。使用返回的 ID,不从父会话模型、自身模型身份或显示名称推断。
|
|
22
|
+
3. **provider 和 model 必须成对且非空。** 只有一个字段时,先补齐可验证的信息;本意为默认时,删除全部路由字段。
|
|
23
|
+
4. **reasoning_effort 只使用目标模型公布的有效值。** 不要求固定等级时省略;不要硬套父模型等级。
|
|
24
|
+
5. **目录与当前 Session 的允许路由分别校验。** 目录存在不保证当前会话允许。如果路由被拒绝,不猜另一个提供方、不反复碰撞。默认模型满足用户要求时,删除全部路由字段重试一次;用户锁定模型时,说明具体限制,不擅自替换。
|
|
25
|
+
6. **不同工具的模型接口不能混用。** subagent 的路由、cursor_code 的 Cursor 模型、cursor_subagent 的默认配置和专家工具的专家名称不是同一接口。subagent_fork 不接受这些模型覆盖字段。
|
|
26
|
+
7. **只发送实际需要的受支持字段。** 不为填满对象而给可选字段补空字符串。若调用层强制要求这些字段,检查正确调用方式;仍无法表达省略时报告接口限制,不伪造 ID。
|
|
27
|
+
|
|
28
|
+
## 发送前检查
|
|
29
|
+
|
|
30
|
+
- 默认调用:三个可选键都不存在。
|
|
31
|
+
- 指定调用:provider/model 是已查证、成对的非空 ID。
|
|
32
|
+
- reasoning_effort 若存在,与目标模型匹配。
|
|
33
|
+
- prompt 是完整、可独立执行的任务说明;cwd 是实际工作区。
|
|
34
|
+
- 没有把父会话或其他工具的模型名称当作已验证路由。
|
|
35
|
+
|
|
36
|
+
## 默认调用示例
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"cwd": "E:/example/project",
|
|
41
|
+
"description": "核对额度显示逻辑",
|
|
42
|
+
"prompt": "只读检查当前工作区的额度规范化和显示逻辑,给出字段语义与最小修复建议,不修改文件。"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
指定模型时,查询目录后把返回的 provider/model 成对加入。仅在需要固定、且已确认有效时加入 reasoning_effort。本技能不保存固定提供方、模型或等级示例值,以免以后照抄失效。
|
|
47
|
+
|
|
48
|
+
禁止发送:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"provider": "",
|
|
53
|
+
"model": "",
|
|
54
|
+
"reasoning_effort": ""
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 失败后的处理
|
|
59
|
+
|
|
60
|
+
- provider must be non-empty:检查是否把可选字段传成空字符串;默认分支删除三个键,再调用一次。
|
|
61
|
+
- provider/model 不成对:查询目录并补齐,或切回省略全部路由字段的默认分支。
|
|
62
|
+
- 路由不允许:按会话权限限制处理,不通过改拼写绕过。
|
|
63
|
+
- 推理等级无效:查询目标模型详情;不需要固定时省略。
|
|
64
|
+
- 参数校验失败时,子智能体没有启动。只有成功结果包含子智能体 ID,才记为已启动;交付前必须收集实际结果。
|
|
65
|
+
|
|
66
|
+
修正参数后继续用户已授权的原任务;不要将工具参数错误归因于用户项目或账户额度。
|
package/lib/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { registerCursorCommand } from './command.js';
|
|
|
26
26
|
import { createMirrorHub } from './mirror.js';
|
|
27
27
|
import { createRouteFence } from './fence.js';
|
|
28
28
|
import { UsageClient, registerUsageRoutes } from './usage.js';
|
|
29
|
+
import { CursorQuotaGuard } from './quota-guard.js';
|
|
29
30
|
import { effectiveModel, readSettings, registerSettingsRoutes, resolveModelSelection } from './settings.js';
|
|
30
31
|
import { createNativeCursorBridge } from './native-provider.js';
|
|
31
32
|
import { describeProxy, proxySettingIsDirect, resolveChildProxy, withProxyEnvironment } from './proxy.js';
|
|
@@ -174,6 +175,8 @@ export function apply(ctx, config) {
|
|
|
174
175
|
// transcript Cursor actually produced. `bind` learns the `agents` service
|
|
175
176
|
// from a context that declared the dependency, because reading it off this
|
|
176
177
|
// fiber answers `undefined` while the agent loop is still activating.
|
|
178
|
+
const usage = new UsageClient({ baseUrl: config.usageBaseUrl });
|
|
179
|
+
const quotaGuard = new CursorQuotaGuard(usage);
|
|
177
180
|
const mirrors = createMirrorHub({ logger, maxChildDepth: config.maxChildDepth });
|
|
178
181
|
const pool = resolved === undefined
|
|
179
182
|
? new UnavailablePool()
|
|
@@ -195,6 +198,7 @@ export function apply(ctx, config) {
|
|
|
195
198
|
index,
|
|
196
199
|
logger,
|
|
197
200
|
mirrors,
|
|
201
|
+
quotaGuard,
|
|
198
202
|
maxScanFiles: config.maxScanFiles,
|
|
199
203
|
maxChildDepth: config.maxChildDepth,
|
|
200
204
|
unavailable,
|
|
@@ -207,7 +211,7 @@ export function apply(ctx, config) {
|
|
|
207
211
|
resolveDefaultMode: () => readSettings(dataDir).mode,
|
|
208
212
|
});
|
|
209
213
|
|
|
210
|
-
const native = createNativeCursorBridge({ execute: rawTool.execute, mirrors, maxChildDepth: config.maxChildDepth });
|
|
214
|
+
const native = createNativeCursorBridge({ execute: rawTool.execute, mirrors, quotaGuard, maxChildDepth: config.maxChildDepth });
|
|
211
215
|
const tool = { ...rawTool, execute: (args, exec) => native.execute(args, exec) };
|
|
212
216
|
const disposeTool = ctx.tools.register({ ...tool, name: config.toolName });
|
|
213
217
|
|
|
@@ -256,6 +260,8 @@ export function apply(ctx, config) {
|
|
|
256
260
|
dataDir,
|
|
257
261
|
logger,
|
|
258
262
|
mirrors,
|
|
263
|
+
usage,
|
|
264
|
+
quotaGuard,
|
|
259
265
|
// The status route asks the CLI whether the account is signed in, which
|
|
260
266
|
// means spawning it — same environment the ACP child gets.
|
|
261
267
|
env: childEnvironment,
|
|
@@ -304,9 +310,9 @@ function mountHttpSurfaces(ctx, config, deps) {
|
|
|
304
310
|
// the dashboard RPC itself and serves both a JSON view and a
|
|
305
311
|
// self-contained HTML dashboard.
|
|
306
312
|
if (config.usageUi === true) {
|
|
307
|
-
const usage = new UsageClient({ baseUrl: config.usageBaseUrl });
|
|
308
313
|
disposers.push(registerUsageRoutes(webCtx, {
|
|
309
|
-
usage,
|
|
314
|
+
usage: deps.usage,
|
|
315
|
+
quotaGuard: deps.quotaGuard,
|
|
310
316
|
pool: deps.pool,
|
|
311
317
|
journal: deps.journal,
|
|
312
318
|
index: deps.index,
|
package/lib/native-provider.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isAbsolute } from 'node:path';
|
|
|
5
5
|
import { stat } from 'node:fs/promises';
|
|
6
6
|
import { renderCursorReport } from './tool.js';
|
|
7
7
|
import { resolveChildDepth } from './mirror.js';
|
|
8
|
+
import { quotaBlockedMessage } from './quota-guard.js';
|
|
8
9
|
|
|
9
10
|
export const CURSOR_PROVIDER = 'cursor';
|
|
10
11
|
|
|
@@ -33,7 +34,7 @@ function failure(error, cancelled = false) {
|
|
|
33
34
|
|
|
34
35
|
/** Generic native calls use the panel settings and get their own conversation.
|
|
35
36
|
* The cursor_code adapter supplies provider-private arguments for rich controls. */
|
|
36
|
-
export function createCursorProvider({ execute, mirrors, maxChildDepth }) {
|
|
37
|
+
export function createCursorProvider({ execute, mirrors, quotaGuard, maxChildDepth }) {
|
|
37
38
|
const active = new Set();
|
|
38
39
|
let closed = false;
|
|
39
40
|
return {
|
|
@@ -52,15 +53,17 @@ export function createCursorProvider({ execute, mirrors, maxChildDepth }) {
|
|
|
52
53
|
throw new Error('cursor: native prompts must contain text blocks only');
|
|
53
54
|
const task = request.prompt.map(block => block.text).join('\n\n').trim();
|
|
54
55
|
if (!task) throw new Error('cursor: native prompt must not be empty');
|
|
56
|
+
const quotaStatus = await quotaGuard?.check?.();
|
|
57
|
+
if (quotaStatus?.blocked === true)
|
|
58
|
+
throw new Error(quotaBlockedMessage(quotaStatus));
|
|
55
59
|
const originalParent = request.parent;
|
|
56
60
|
// The recursion budget is the plugin's own now that the host leaves it
|
|
57
61
|
// to the provider (`maxDepth: provider-managed`); refuse before any
|
|
58
62
|
// process, child row, or lifecycle event exists.
|
|
59
63
|
if (typeof maxChildDepth === 'number') resolveChildDepth(originalParent, maxChildDepth);
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// belongs to the cursor_code bridge, which starts the provider directly.
|
|
64
|
+
// Keep the service's external lifecycle id separate from the local
|
|
65
|
+
// transcript child. The mirror is opened explicitly by the panel
|
|
66
|
+
// and does not publish another catalog row.
|
|
64
67
|
const viaService = request.cursor === undefined;
|
|
65
68
|
const args = request.cursor?.args ?? { task, sessionLabel: `native-${randomUUID()}` };
|
|
66
69
|
const requestedCwd = typeof request.cwd === 'string' && request.cwd !== ''
|
|
@@ -82,15 +85,14 @@ export function createCursorProvider({ execute, mirrors, maxChildDepth }) {
|
|
|
82
85
|
let settled = false;
|
|
83
86
|
let disposal;
|
|
84
87
|
const cursorLifecycle = {
|
|
85
|
-
descriptor
|
|
88
|
+
// The service descriptor belongs to its external row.
|
|
89
|
+
descriptor: viaService ? undefined : request.descriptor,
|
|
86
90
|
label: request.label,
|
|
87
91
|
acquired(session) { leasedSession = session; },
|
|
88
|
-
|
|
89
|
-
opened
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
}),
|
|
92
|
+
opened(entry, key) {
|
|
93
|
+
opened = { entry, key };
|
|
94
|
+
ready.resolve();
|
|
95
|
+
},
|
|
94
96
|
};
|
|
95
97
|
const execution = Promise.resolve().then(() => execute({ ...args, workspaceRoot: cwd }, {
|
|
96
98
|
agent: originalParent,
|
|
@@ -125,8 +127,8 @@ export function createCursorProvider({ execute, mirrors, maxChildDepth }) {
|
|
|
125
127
|
}
|
|
126
128
|
})().finally(() => active.delete(dispose));
|
|
127
129
|
active.add(dispose);
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
+
// Publish the external lifecycle immediately, including while queued.
|
|
131
|
+
// Its id stays distinct from the panel's real transcript child.
|
|
130
132
|
if (viaService)
|
|
131
133
|
return { id: `cursor-${randomUUID()}`, result, cursorReport, dispose };
|
|
132
134
|
try {
|
|
@@ -168,13 +170,13 @@ function announceMirror(parent, child, label) {
|
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
/** Late-bound native service; absence preserves the existing standalone tool. */
|
|
171
|
-
export function createNativeCursorBridge({ execute, mirrors, maxChildDepth }) {
|
|
173
|
+
export function createNativeCursorBridge({ execute, mirrors, quotaGuard, maxChildDepth }) {
|
|
172
174
|
let binding;
|
|
173
175
|
return {
|
|
174
176
|
bind(ctx) {
|
|
175
177
|
const runtime = ctx.get?.('subagents');
|
|
176
178
|
if (typeof runtime?.registerProvider !== 'function') return;
|
|
177
|
-
const provider = createCursorProvider({ execute, mirrors, maxChildDepth });
|
|
179
|
+
const provider = createCursorProvider({ execute, mirrors, quotaGuard, maxChildDepth });
|
|
178
180
|
const unregister = runtime.registerProvider(provider);
|
|
179
181
|
const current = { runtime, provider };
|
|
180
182
|
binding = current;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const RECHECK_MS = 30_000;
|
|
2
|
+
|
|
3
|
+
const QUOTA_ERROR = /\b(?:resource[_ -]?exhausted|insufficient[_ -]?quota|quota[_ -]?exceeded|credit[_ -]?balance[_ -]?exhausted)\b|\b(?:insufficient|out of|no remaining|zero|empty|exhausted|depleted)\b.{0,50}\b(?:cursor\s+)?(?:usage|quota|credits?|balance)\b|\b(?:cursor\s+)?(?:usage|quota|credits?|balance)\b.{0,50}\b(?:exhausted|depleted|used up|is empty|limit (?:has been )?reached|limit exceeded|insufficient)\b|\b(?:you(?:'ve| have) hit your usage limit|usage limit has been reached|no usage credits remaining)\b/i;
|
|
4
|
+
|
|
5
|
+
/** Only the explicit upstream bucket percentages can establish global exhaustion. */
|
|
6
|
+
export function usageQuotaExhausted(usage) {
|
|
7
|
+
const percentages = ['total', 'auto', 'api'].map(name => usage?.[name]?.percentUsed);
|
|
8
|
+
return percentages.every(value => typeof value === 'number' && Number.isFinite(value) && value >= 100);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Inspect an actual Cursor/ACP failure, never the dashboard's advisory prose. */
|
|
12
|
+
export function cursorQuotaFailure(error) {
|
|
13
|
+
const code = error?.code;
|
|
14
|
+
if (code === 8 || /^(?:RESOURCE_EXHAUSTED|INSUFFICIENT_QUOTA|QUOTA_EXCEEDED|CREDIT_BALANCE_EXHAUSTED)$/i.test(String(code ?? '')))
|
|
15
|
+
return true;
|
|
16
|
+
const parts = [error?.name, error?.message, error?.data?.message, error?.cause?.message];
|
|
17
|
+
if (typeof error === 'string') parts.push(error);
|
|
18
|
+
try {
|
|
19
|
+
if (error !== null && typeof error === 'object') parts.push(JSON.stringify(error));
|
|
20
|
+
}
|
|
21
|
+
catch { /* An unstringifiable error can still match its message/code. */ }
|
|
22
|
+
return QUOTA_ERROR.test(parts.filter(value => typeof value === 'string').join(' '));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function quotaBlockedMessage(state) {
|
|
26
|
+
return state?.source === 'cursor-error'
|
|
27
|
+
? 'CURSOR_QUOTA_EXHAUSTED: Cursor reported that its usage is exhausted. New Cursor child-agent runs are paused until a successful quota refresh confirms capacity has returned.'
|
|
28
|
+
: 'CURSOR_QUOTA_EXHAUSTED: Cursor reports that all usage buckets are exhausted. No child agent was started. Check the quota panel and retry after a successful refresh confirms capacity has returned.';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Shared gate for native subagent starts and direct cursor_code calls. */
|
|
32
|
+
export class CursorQuotaGuard {
|
|
33
|
+
#usage;
|
|
34
|
+
#now;
|
|
35
|
+
#recheckMs;
|
|
36
|
+
#state = { blocked: false, source: 'unknown' };
|
|
37
|
+
#lastCheckAt = 0;
|
|
38
|
+
#revision = 0;
|
|
39
|
+
#inflight;
|
|
40
|
+
|
|
41
|
+
constructor(usage, options = {}) {
|
|
42
|
+
this.#usage = usage;
|
|
43
|
+
this.#now = options.now ?? Date.now;
|
|
44
|
+
this.#recheckMs = options.recheckMs ?? RECHECK_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
snapshot() {
|
|
48
|
+
return { ...this.#state };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async check() {
|
|
52
|
+
const now = this.#now();
|
|
53
|
+
if (this.#state.blocked && now - this.#lastCheckAt < this.#recheckMs)
|
|
54
|
+
return this.snapshot();
|
|
55
|
+
if (this.#inflight !== undefined)
|
|
56
|
+
return this.#inflight;
|
|
57
|
+
|
|
58
|
+
const revision = this.#revision;
|
|
59
|
+
this.#inflight = (async () => {
|
|
60
|
+
let snapshot;
|
|
61
|
+
try {
|
|
62
|
+
snapshot = await this.#usage?.get?.({ force: this.#state.blocked });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
snapshot = undefined;
|
|
66
|
+
}
|
|
67
|
+
this.#lastCheckAt = this.#now();
|
|
68
|
+
if (revision !== this.#revision && this.#state.blocked)
|
|
69
|
+
return this.snapshot();
|
|
70
|
+
return this.observeUsage(snapshot);
|
|
71
|
+
})().finally(() => { this.#inflight = undefined; });
|
|
72
|
+
return this.#inflight;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
observeUsage(snapshot) {
|
|
76
|
+
if (snapshot?.ok !== true) {
|
|
77
|
+
if (!this.#state.blocked)
|
|
78
|
+
this.#state = { blocked: false, source: 'unknown' };
|
|
79
|
+
return this.snapshot();
|
|
80
|
+
}
|
|
81
|
+
const fetchedAt = snapshot.fetchedAt;
|
|
82
|
+
if (this.#state.blocked
|
|
83
|
+
&& (!Number.isFinite(fetchedAt) || fetchedAt <= this.#state.since))
|
|
84
|
+
return this.snapshot();
|
|
85
|
+
this.#lastCheckAt = this.#now();
|
|
86
|
+
if (usageQuotaExhausted(snapshot.usage)) {
|
|
87
|
+
this.#state = {
|
|
88
|
+
blocked: true,
|
|
89
|
+
source: 'usage',
|
|
90
|
+
reason: 'all-usage-buckets-exhausted',
|
|
91
|
+
since: Number.isFinite(fetchedAt) ? fetchedAt : this.#now(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.#state = { blocked: false, source: 'usage' };
|
|
96
|
+
}
|
|
97
|
+
return this.snapshot();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
observeFailure(error) {
|
|
101
|
+
if (!cursorQuotaFailure(error))
|
|
102
|
+
return this.snapshot();
|
|
103
|
+
this.#revision += 1;
|
|
104
|
+
this.#lastCheckAt = this.#now();
|
|
105
|
+
this.#state = {
|
|
106
|
+
blocked: true,
|
|
107
|
+
source: 'cursor-error',
|
|
108
|
+
reason: 'cursor-reported-exhaustion',
|
|
109
|
+
since: this.#lastCheckAt,
|
|
110
|
+
};
|
|
111
|
+
return this.snapshot();
|
|
112
|
+
}
|
|
113
|
+
}
|
package/lib/session-pool.js
CHANGED
|
@@ -57,6 +57,7 @@ export class SessionPool {
|
|
|
57
57
|
/** Tail of the FIFO queue that serializes every Cursor turn. */
|
|
58
58
|
#gate = Promise.resolve();
|
|
59
59
|
#lockDepth = 0;
|
|
60
|
+
#activeKey;
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* @param config - plugin config (maxLiveSessions, sessionIdleMinutes, spawn/env).
|
|
@@ -99,6 +100,7 @@ export class SessionPool {
|
|
|
99
100
|
modelChoice: entry.modelChoice,
|
|
100
101
|
mode: entry.session.modes?.currentModeId,
|
|
101
102
|
turns: entry.turns.length,
|
|
103
|
+
running: entry.key === this.#activeKey,
|
|
102
104
|
reloaded: entry.reloaded,
|
|
103
105
|
idleMs: Date.now() - entry.lastUsed,
|
|
104
106
|
alive: entry.session.alive,
|
|
@@ -357,6 +359,7 @@ export class SessionPool {
|
|
|
357
359
|
*/
|
|
358
360
|
async run(options, body) {
|
|
359
361
|
const release = await this.#enter(options.signal);
|
|
362
|
+
this.#activeKey = options.key;
|
|
360
363
|
try {
|
|
361
364
|
options.signal?.throwIfAborted();
|
|
362
365
|
const lease = await this.#acquire(options, options.signal);
|
|
@@ -373,6 +376,7 @@ export class SessionPool {
|
|
|
373
376
|
}
|
|
374
377
|
}
|
|
375
378
|
finally {
|
|
379
|
+
this.#activeKey = undefined;
|
|
376
380
|
release();
|
|
377
381
|
}
|
|
378
382
|
}
|
package/lib/tool.js
CHANGED
|
@@ -22,6 +22,7 @@ import { delegationDepthOf, resolveChildDepth } from './mirror.js';
|
|
|
22
22
|
import { sameModel } from './models.js';
|
|
23
23
|
import { buildPrompt } from './prompt.js';
|
|
24
24
|
import { storeKeyOf } from './session-pool.js';
|
|
25
|
+
import { cursorQuotaFailure, quotaBlockedMessage } from './quota-guard.js';
|
|
25
26
|
|
|
26
27
|
/** Default wall-clock budget for one Cursor turn. */
|
|
27
28
|
const DEFAULT_TIMEOUT_SECONDS = 1800;
|
|
@@ -89,7 +90,7 @@ function clean(value, seen) {
|
|
|
89
90
|
* @param deps - plugin services and shared state.
|
|
90
91
|
*/
|
|
91
92
|
export function cursorCodeTool(deps) {
|
|
92
|
-
const { pool, journal, index, logger, unavailable, mirrors } = deps;
|
|
93
|
+
const { pool, journal, index, logger, unavailable, mirrors, quotaGuard } = deps;
|
|
93
94
|
// The recursion budget this tool enforces: the child it starts is a real
|
|
94
95
|
// DSH child session, so a subagent calling this tool would create a
|
|
95
96
|
// GRANDCHILD. `1` is the platform's own default (only a top-level agent
|
|
@@ -291,6 +292,15 @@ export function cursorCodeTool(deps) {
|
|
|
291
292
|
}
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
const quotaStatus = await quotaGuard?.check?.();
|
|
296
|
+
if (quotaStatus?.blocked === true) {
|
|
297
|
+
return toLosslessJson({
|
|
298
|
+
ok: false, sessionKey: 'quota-exhausted', conversationKey,
|
|
299
|
+
stopReason: 'error', durationMs: 0,
|
|
300
|
+
error: quotaBlockedMessage(quotaStatus), steps: [], changes: [],
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
294
304
|
if (input.resetSession) {
|
|
295
305
|
pool.dropConversation(conversationKey);
|
|
296
306
|
journal.clear(conversationKey);
|
|
@@ -377,8 +387,8 @@ export function cursorCodeTool(deps) {
|
|
|
377
387
|
let reused = false;
|
|
378
388
|
let reloaded = false;
|
|
379
389
|
const lifecycle = exec.cursorLifecycle;
|
|
380
|
-
//
|
|
381
|
-
//
|
|
390
|
+
// The service's external lifecycle id is separate from this child:
|
|
391
|
+
// the panel follows the mirror, whose transcript is written below.
|
|
382
392
|
const mirrorEnabled = lifecycle?.mirror !== false;
|
|
383
393
|
const openChild = async () => {
|
|
384
394
|
const entry = await mirrors?.open({
|
|
@@ -436,6 +446,13 @@ export function cursorCodeTool(deps) {
|
|
|
436
446
|
if (pickedMode !== undefined && session.modes?.currentModeId !== pickedMode)
|
|
437
447
|
await session.setMode(pickedMode);
|
|
438
448
|
turn = await session.prompt(input.prompt, abort.signal);
|
|
449
|
+
if (cursorQuotaFailure(turn.text)) {
|
|
450
|
+
const status = quotaGuard?.observeFailure?.(turn.text);
|
|
451
|
+
if (status?.blocked === true) {
|
|
452
|
+
const partialTurn = { ...turn, stopReason: 'error' };
|
|
453
|
+
throw Object.assign(new Error(quotaBlockedMessage(status)), { partialTurn });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
439
456
|
pool.recordTurn(sessionKey, turn);
|
|
440
457
|
const after = await fingerprint(workspaceRoot, deps.maxScanFiles);
|
|
441
458
|
const changes = mergeChanges(turn.diffs, compareFingerprints(before, after), workspaceRoot);
|
|
@@ -484,7 +501,10 @@ export function cursorCodeTool(deps) {
|
|
|
484
501
|
});
|
|
485
502
|
}
|
|
486
503
|
catch (error) {
|
|
487
|
-
const
|
|
504
|
+
const quotaStatus = cursorQuotaFailure(error) ? quotaGuard?.observeFailure?.(error) : undefined;
|
|
505
|
+
const message = quotaStatus?.blocked === true
|
|
506
|
+
? quotaBlockedMessage(quotaStatus)
|
|
507
|
+
: error instanceof Error ? error.message : String(error);
|
|
488
508
|
logger?.warn?.(`cursor_code failed: ${message}`);
|
|
489
509
|
return toLosslessJson({
|
|
490
510
|
ok: false, sessionKey, conversationKey, generation: picked.generation,
|
package/lib/usage.js
CHANGED
|
@@ -485,7 +485,7 @@ export class UsageClient {
|
|
|
485
485
|
* @param deps - `usage` client, `pool`, `journal`, `index`, `dataDir`, `mirrors`.
|
|
486
486
|
*/
|
|
487
487
|
export function registerUsageRoutes(ctx, deps) {
|
|
488
|
-
const { usage, pool, index, journal, dataDir, mirrors, fence } = deps;
|
|
488
|
+
const { usage, quotaGuard, pool, index, journal, dataDir, mirrors, fence } = deps;
|
|
489
489
|
const registrations = [];
|
|
490
490
|
// `ctx.get` rather than `ctx.webServer`: a missing service must degrade to
|
|
491
491
|
// "no UI", never to a failed plugin load.
|
|
@@ -505,24 +505,42 @@ export function registerUsageRoutes(ctx, deps) {
|
|
|
505
505
|
*/
|
|
506
506
|
const panelState = () => {
|
|
507
507
|
const children = new Map((mirrors?.snapshot?.() ?? []).map(child => [child.key, child]));
|
|
508
|
+
const live = pool.snapshot();
|
|
509
|
+
const conversations = index.list();
|
|
510
|
+
const saved = new Map(conversations.map(entry => [conversationKeyOf(entry.key), entry]));
|
|
511
|
+
const running = new Set(live.filter(entry => entry.running === true).map(entry => conversationKeyOf(entry.key)));
|
|
512
|
+
const withRestore = entry => {
|
|
513
|
+
const key = conversationKeyOf(entry.key);
|
|
514
|
+
const recorded = saved.get(key);
|
|
515
|
+
return {
|
|
516
|
+
...entry,
|
|
517
|
+
restorable: typeof entry.cursorSessionId === 'string' && entry.cursorSessionId !== ''
|
|
518
|
+
&& recorded?.cursorSessionId === entry.cursorSessionId && !running.has(key),
|
|
519
|
+
};
|
|
520
|
+
};
|
|
508
521
|
const withActivity = entry => {
|
|
509
522
|
const activity = journal?.lastActivity?.(entry.key);
|
|
510
523
|
return activity === undefined ? entry : { ...entry, activity };
|
|
511
524
|
};
|
|
512
525
|
const withChild = entry => {
|
|
513
526
|
const child = children.get(entry.key);
|
|
514
|
-
const
|
|
527
|
+
const recorded = saved.get(conversationKeyOf(entry.key));
|
|
528
|
+
// Native run disposal releases the live mirror; its persisted log is
|
|
529
|
+
// still openable. Only use that address for the same Cursor session.
|
|
530
|
+
const remembered = typeof entry.cursorSessionId === 'string'
|
|
531
|
+
&& recorded?.cursorSessionId === entry.cursorSessionId ? recorded : undefined;
|
|
532
|
+
const childSessionId = child?.childSessionId ?? entry.childSessionId ?? remembered?.childSessionId;
|
|
515
533
|
if (typeof childSessionId !== 'string' || childSessionId === '')
|
|
516
534
|
return entry;
|
|
517
535
|
return {
|
|
518
536
|
...entry,
|
|
519
537
|
childSessionId,
|
|
520
|
-
parentSessionId: child?.parentSessionId ?? entry.parentSessionId ?? parentOfKey(entry.key),
|
|
538
|
+
parentSessionId: child?.parentSessionId ?? entry.parentSessionId ?? remembered?.parentSessionId ?? parentOfKey(entry.key),
|
|
521
539
|
};
|
|
522
540
|
};
|
|
523
541
|
return {
|
|
524
|
-
live:
|
|
525
|
-
conversations:
|
|
542
|
+
live: live.map(withRestore).map(withActivity).map(withChild),
|
|
543
|
+
conversations: conversations.map(withRestore).map(withActivity).map(withChild),
|
|
526
544
|
};
|
|
527
545
|
};
|
|
528
546
|
|
|
@@ -532,6 +550,7 @@ export function registerUsageRoutes(ctx, deps) {
|
|
|
532
550
|
handler: async (req, res) => {
|
|
533
551
|
const force = typeof req?.url === 'string' && req.url.includes('force=1');
|
|
534
552
|
const snapshot = await usage.get({ force });
|
|
553
|
+
quotaGuard?.observeUsage?.(snapshot);
|
|
535
554
|
// `raw` is dropped: the normalized view is the contract, and the
|
|
536
555
|
// upstream payload carries 200+ model ids nobody polls for.
|
|
537
556
|
const publicView = { ...snapshot };
|
|
@@ -544,6 +563,7 @@ export function registerUsageRoutes(ctx, deps) {
|
|
|
544
563
|
queueDepth: pool.queued,
|
|
545
564
|
conversations: state.conversations,
|
|
546
565
|
journal: journal?.stats?.(),
|
|
566
|
+
quotaGuard: quotaGuard?.snapshot?.() ?? { blocked: false },
|
|
547
567
|
dataDir,
|
|
548
568
|
},
|
|
549
569
|
});
|
|
@@ -631,8 +651,18 @@ export function registerUsageRoutes(ctx, deps) {
|
|
|
631
651
|
// conversation key (`parent::label`), so try both before giving up.
|
|
632
652
|
const durableKey = conversationKeyOf(key);
|
|
633
653
|
const entry = index?.get?.(key) ?? index?.get?.(durableKey);
|
|
634
|
-
|
|
635
|
-
|
|
654
|
+
const live = pool.snapshot().filter(row => conversationKeyOf(row.key) === durableKey);
|
|
655
|
+
const current = live.find(row => row.key === key);
|
|
656
|
+
if (entry === undefined) {
|
|
657
|
+
if (live.length > 0)
|
|
658
|
+
sendJson(res, 409, { ok: false, key, code: 'conversation_pending', error: 'that conversation has not completed a saved turn; open its live timeline instead' });
|
|
659
|
+
else
|
|
660
|
+
sendJson(res, 404, { ok: false, key, error: 'no durable conversation for that key' });
|
|
661
|
+
}
|
|
662
|
+
else if (live.some(row => row.running === true))
|
|
663
|
+
sendJson(res, 409, { ok: false, key, code: 'conversation_running', error: 'that conversation is still running; open its live timeline instead' });
|
|
664
|
+
else if (current && current.cursorSessionId !== entry.cursorSessionId)
|
|
665
|
+
sendJson(res, 409, { ok: false, key, code: 'conversation_pending', error: 'this generation has not completed a saved turn; open its live timeline instead' });
|
|
636
666
|
else if (typeof entry.cursorSessionId !== 'string' || entry.cursorSessionId === '')
|
|
637
667
|
sendJson(res, 409, { ok: false, key, error: 'that conversation never recorded a Cursor session id' });
|
|
638
668
|
else if (typeof pool?.replay !== 'function')
|
|
@@ -781,6 +811,7 @@ export function registerUsageRoutes(ctx, deps) {
|
|
|
781
811
|
handler: (req, res) => {
|
|
782
812
|
sendHtml(res, dashboardHtml({
|
|
783
813
|
...panelState(),
|
|
814
|
+
quotaGuard: quotaGuard?.snapshot?.() ?? { blocked: false },
|
|
784
815
|
journal,
|
|
785
816
|
dataDir,
|
|
786
817
|
configModel: deps.configModel,
|
|
@@ -904,6 +935,7 @@ function dashboardHtml(state) {
|
|
|
904
935
|
conversations: state.conversations ?? [],
|
|
905
936
|
journal: state.journal?.stats?.() ?? { keys: 0, events: 0 },
|
|
906
937
|
queueDepth: state.queueDepth ?? 0,
|
|
938
|
+
quotaGuard: state.quotaGuard ?? { blocked: false },
|
|
907
939
|
endpoint: '/plugins/dsh-cursor-dev/api/usage',
|
|
908
940
|
journalEndpoint: '/plugins/dsh-cursor-dev/api/journal',
|
|
909
941
|
restoreEndpoint: '/plugins/dsh-cursor-dev/api/restore',
|
|
@@ -949,33 +981,52 @@ function dashboardHtml(state) {
|
|
|
949
981
|
color-scheme: dark;
|
|
950
982
|
--bg: #16161a; --card: #1e1e24; --line: #2c2c34; --fg: #e8e8ea;
|
|
951
983
|
--muted: #9a9aa4; --accent: #6d8cff; --warn: #ffb454; --bad: #ff6b6b; --good: #56d364;
|
|
952
|
-
--hover: rgba(255, 255, 255, .04);
|
|
984
|
+
--hover: rgba(255, 255, 255, .04); --on-accent: #0d1020;
|
|
953
985
|
}
|
|
954
986
|
@media (prefers-color-scheme: light) {
|
|
955
987
|
:root {
|
|
956
988
|
color-scheme: light;
|
|
957
989
|
--bg: #f6f6f8; --card: #ffffff; --line: #e2e2e8; --fg: #1c1c22;
|
|
958
990
|
--muted: #6b6b76; --accent: #3b5bdb; --warn: #b26a00; --bad: #c92a2a; --good: #2b8a3e;
|
|
959
|
-
--hover: rgba(0, 0, 0, .035);
|
|
991
|
+
--hover: rgba(0, 0, 0, .035); --on-accent: #ffffff;
|
|
960
992
|
}
|
|
961
993
|
}
|
|
962
994
|
* { box-sizing: border-box; }
|
|
963
|
-
|
|
995
|
+
html { background: var(--bg); }
|
|
996
|
+
body { margin: 0 auto; padding: 20px; max-width: 1080px; background: var(--bg); color: var(--fg);
|
|
964
997
|
font: 13px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
998
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
965
999
|
h1 { font-size: 15px; margin: 0 0 2px; font-weight: 600; }
|
|
966
1000
|
h2 { font-size: 12px; margin: 0 0 10px; font-weight: 600; text-transform: uppercase;
|
|
967
1001
|
letter-spacing: .06em; color: var(--muted); }
|
|
968
1002
|
.head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
969
1003
|
.muted { color: var(--muted); }
|
|
970
|
-
/* min() keeps the track from overflowing a panel narrower than 220px — plain
|
|
971
|
-
minmax(220px, 1fr) makes the grid wider than its card and clips the last tile. */
|
|
972
|
-
.grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); }
|
|
973
1004
|
.card { background: var(--card); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; }
|
|
974
|
-
.big { font-size: 26px; font-weight: 650; font-variant-numeric: tabular-nums; }
|
|
1005
|
+
.big { font-size: 26px; font-weight: 650; font-variant-numeric: tabular-nums; line-height: 1.2; }
|
|
975
1006
|
.bar { height: 7px; border-radius: 4px; background: var(--line); overflow: hidden; margin: 8px 0 6px; }
|
|
976
|
-
.bar > i { display: block; height: 100%; background: var(--accent); }
|
|
1007
|
+
.bar > i { display: block; width: 0; height: 100%; background: var(--accent); transition: width .3s ease; }
|
|
977
1008
|
.bar.warn > i { background: var(--warn); }
|
|
978
1009
|
.bar.bad > i { background: var(--bad); }
|
|
1010
|
+
/* Keep the four usage figures in the same two-by-two layout as the panel. */
|
|
1011
|
+
.quota-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
|
1012
|
+
.quota-card { min-width: 0; min-height: 174px; padding: 16px 18px; border-radius: 12px; }
|
|
1013
|
+
.quota-card h2 { margin-bottom: 16px; font-size: 13px; letter-spacing: .02em; }
|
|
1014
|
+
.quota-card .big { font-size: 34px; font-weight: 700; letter-spacing: -.025em; overflow-wrap: anywhere; }
|
|
1015
|
+
.quota-card .bar { height: 8px; margin: 14px 0 8px; }
|
|
1016
|
+
.quota-card .muted { font-size: 13px; overflow-wrap: anywhere; }
|
|
1017
|
+
.quota-meta { margin-top: 12px; font-size: 12px; }
|
|
1018
|
+
.quota-period { display: flex; flex-wrap: wrap; gap: 2px 8px; }
|
|
1019
|
+
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
|
|
1020
|
+
overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
1021
|
+
details.fold { margin-top: 10px; border-top: 1px solid var(--line); padding-top: 8px; }
|
|
1022
|
+
details.fold > summary { cursor: pointer; width: fit-content; max-width: 100%; color: var(--muted); font-size: 12px; border-radius: 4px; }
|
|
1023
|
+
details.fold > summary:hover { color: var(--fg); }
|
|
1024
|
+
.quota-meta > details.fold { margin-top: 8px; }
|
|
1025
|
+
.status { margin-top: 6px; font-size: 12px; }
|
|
1026
|
+
.quota-alert { margin: 0 0 12px; padding: 10px 12px; border: 1px solid var(--bad);
|
|
1027
|
+
border-radius: 8px; background: var(--card); color: var(--bad); }
|
|
1028
|
+
.quota-alert[hidden] { display: none; }
|
|
1029
|
+
#model-state, #model-status, #message, #error, .note { overflow-wrap: anywhere; }
|
|
979
1030
|
/* A session key is one ~84-character token with no break opportunity, so an
|
|
980
1031
|
uncontained table stretches <body> instead of scrolling: the panel then
|
|
981
1032
|
grows a page-wide scrollbar and the cards drift off-screen. The scroll
|
|
@@ -1035,12 +1086,23 @@ function dashboardHtml(state) {
|
|
|
1035
1086
|
font: inherit; color: var(--fg); background: var(--bg);
|
|
1036
1087
|
border: 1px solid var(--line); border-radius: 7px; padding: 5px 9px;
|
|
1037
1088
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
input[type="
|
|
1041
|
-
|
|
1042
|
-
|
|
1089
|
+
/* min() lets a control shrink inside a panel narrower than its usual minimum. */
|
|
1090
|
+
select { flex: 1 1 260px; min-width: min(180px, 100%); max-width: 100%; }
|
|
1091
|
+
input[type="search"] { flex: 0 1 200px; min-width: min(130px, 100%); max-width: 100%; }
|
|
1092
|
+
input[type="text"] { flex: 1 1 240px; min-width: min(150px, 100%); max-width: 100%; }
|
|
1093
|
+
select:focus, input[type="search"]:focus, input[type="text"]:focus { border-color: var(--accent); }
|
|
1094
|
+
button { cursor: pointer; transition: border-color .15s ease, background-color .15s ease; }
|
|
1043
1095
|
button:hover { border-color: var(--accent); }
|
|
1096
|
+
button.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); font-weight: 600; padding: 5px 16px; }
|
|
1097
|
+
button.primary:hover { background: var(--fg); border-color: var(--fg); color: var(--bg); }
|
|
1098
|
+
.lock { display: flex; align-items: center; gap: 8px; margin: 8px 0 0; }
|
|
1099
|
+
/* The proxy is machine configuration, not the main choice, so it gets one
|
|
1100
|
+
thin strip above the picker rather than a box of its own. It stays open:
|
|
1101
|
+
a wrong proxy is the usual reason the model list looks short. */
|
|
1102
|
+
.env { margin-top: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
|
|
1103
|
+
.env .controls { margin: 0; }
|
|
1104
|
+
.env input[type="text"] { font-size: 12px; padding: 4px 8px; }
|
|
1105
|
+
.env label { font-size: 12px; }
|
|
1044
1106
|
button.ghost { color: var(--muted); }
|
|
1045
1107
|
button[disabled] { opacity: .5; cursor: default; border-color: var(--line); }
|
|
1046
1108
|
.filter-note { font-size: 12px; }
|
|
@@ -1090,10 +1152,10 @@ function dashboardHtml(state) {
|
|
|
1090
1152
|
/* The settings page hands this dashboard a short slot and clips the
|
|
1091
1153
|
overflow, so a tall layout hides everything that matters. When the
|
|
1092
1154
|
viewport is short everything tightens and the long explanatory note
|
|
1093
|
-
goes away, which keeps the
|
|
1155
|
+
goes away, which keeps the quota and the model picker in view. */
|
|
1094
1156
|
body.compact { padding: 12px; }
|
|
1095
1157
|
body.compact .head { margin-bottom: 10px; }
|
|
1096
|
-
body.compact .grid { gap:
|
|
1158
|
+
body.compact .quota-grid { gap: 12px; }
|
|
1097
1159
|
body.compact .card { padding: 8px 10px; border-radius: 8px; }
|
|
1098
1160
|
body.compact h2 { margin-bottom: 6px; }
|
|
1099
1161
|
body.compact .big { font-size: 20px; }
|
|
@@ -1103,7 +1165,18 @@ function dashboardHtml(state) {
|
|
|
1103
1165
|
body.compact .controls { margin: 8px 0 4px; }
|
|
1104
1166
|
body.compact .controls label { margin: 6px 0; }
|
|
1105
1167
|
body.compact th, body.compact td { padding: 4px 7px; }
|
|
1106
|
-
body.compact [data-t="modelNote"] { display: none; }
|
|
1168
|
+
body.compact [data-t="modelNote"], body.compact [data-t="modelHint"] { display: none; }
|
|
1169
|
+
body.compact .quota-card { min-height: 158px; padding: 14px 16px; border-radius: 12px; }
|
|
1170
|
+
body.compact .quota-card .big { font-size: 32px; }
|
|
1171
|
+
body.compact .quota-card .bar { height: 8px; margin: 12px 0 8px; }
|
|
1172
|
+
@media (max-width: 420px) {
|
|
1173
|
+
body { padding: 12px; }
|
|
1174
|
+
.quota-grid { grid-template-columns: minmax(0, 1fr); }
|
|
1175
|
+
.quota-card { min-height: 158px; }
|
|
1176
|
+
}
|
|
1177
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1178
|
+
*, *::before, *::after { transition: none !important; }
|
|
1179
|
+
}
|
|
1107
1180
|
</style>
|
|
1108
1181
|
</head>
|
|
1109
1182
|
<body>
|
|
@@ -1115,82 +1188,98 @@ function dashboardHtml(state) {
|
|
|
1115
1188
|
<span class="muted" style="margin-left:auto"><a href="#" class="link" id="refresh" data-t="refresh">刷新</a></span>
|
|
1116
1189
|
</div>
|
|
1117
1190
|
|
|
1191
|
+
<div id="quota-alert" class="quota-alert" role="alert" aria-live="assertive" hidden data-t="quotaBlocked">Cursor 额度已耗尽,已暂停新的子智能体任务;额度刷新确认恢复后会自动解除拦截。</div>
|
|
1118
1192
|
<div id="error" class="card err" style="display:none"></div>
|
|
1119
1193
|
<div id="setup" class="card" style="display:none"></div>
|
|
1120
1194
|
|
|
1121
|
-
<!--
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
<
|
|
1126
|
-
|
|
1195
|
+
<!-- Usage stays above the model controls; each figure keeps its own card. -->
|
|
1196
|
+
<section id="usage-overview" aria-labelledby="usage-title">
|
|
1197
|
+
<h2 class="sr-only" id="usage-title" data-t="usage">额度与用量</h2>
|
|
1198
|
+
<div class="quota-grid">
|
|
1199
|
+
<article class="card quota-card">
|
|
1200
|
+
<h2 id="total-label" data-t="included">总包含额度</h2>
|
|
1201
|
+
<div class="big" id="total-pct">—</div>
|
|
1202
|
+
<div class="bar" id="total-bar" role="progressbar" aria-labelledby="total-label" aria-valuemin="0" aria-valuemax="100"><i></i></div>
|
|
1203
|
+
<div class="muted" id="total-sub">—</div>
|
|
1204
|
+
</article>
|
|
1205
|
+
<article class="card quota-card">
|
|
1206
|
+
<h2 id="auto-label" data-t="auto">Auto 额度</h2>
|
|
1207
|
+
<div class="big" id="auto-pct">—</div>
|
|
1208
|
+
<div class="bar" id="auto-bar" role="progressbar" aria-labelledby="auto-label" aria-valuemin="0" aria-valuemax="100"><i></i></div>
|
|
1209
|
+
<div class="muted" id="auto-sub">—</div>
|
|
1210
|
+
</article>
|
|
1211
|
+
<article class="card quota-card">
|
|
1212
|
+
<h2 id="api-label" data-t="api">API / 指定模型额度</h2>
|
|
1213
|
+
<div class="big" id="api-pct">—</div>
|
|
1214
|
+
<div class="bar" id="api-bar" role="progressbar" aria-labelledby="api-label" aria-valuemin="0" aria-valuemax="100"><i></i></div>
|
|
1215
|
+
<div class="muted" id="api-sub">—</div>
|
|
1216
|
+
</article>
|
|
1217
|
+
<article class="card quota-card">
|
|
1218
|
+
<h2 data-t="spend">本期消费</h2>
|
|
1219
|
+
<div class="big" id="spend">—</div>
|
|
1220
|
+
<div class="muted" id="spend-sub" style="margin-top:8px">—</div>
|
|
1221
|
+
</article>
|
|
1222
|
+
</div>
|
|
1223
|
+
<div class="quota-meta muted">
|
|
1224
|
+
<div class="quota-period">
|
|
1225
|
+
<span data-t="period">计费周期</span>
|
|
1226
|
+
<span id="cycle">—</span>
|
|
1227
|
+
</div>
|
|
1228
|
+
<details class="fold" id="message-box" hidden>
|
|
1229
|
+
<summary data-t="officialNote">官方说明</summary>
|
|
1230
|
+
<div id="message"></div>
|
|
1231
|
+
</details>
|
|
1232
|
+
</div>
|
|
1233
|
+
</section>
|
|
1234
|
+
|
|
1235
|
+
<section class="row card" id="modelcard" aria-labelledby="model-title">
|
|
1236
|
+
<h2 id="model-title" data-t="model">子智能体模型</h2>
|
|
1127
1237
|
<div class="muted" data-t="modelHint"></div>
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1238
|
+
<!-- The child CLI does not read the Windows registry, so a machine whose proxy
|
|
1239
|
+
lives only in Internet Settings gets a child that lists a fraction of the
|
|
1240
|
+
models. Empty means "ask the machine", which is what makes the full
|
|
1241
|
+
catalogue appear with nobody configuring anything. -->
|
|
1242
|
+
<div class="env">
|
|
1243
|
+
<div class="controls">
|
|
1244
|
+
<label class="muted" for="proxy" data-t="proxyLabel">代理</label>
|
|
1245
|
+
<input type="text" id="proxy" autocomplete="off" spellcheck="false">
|
|
1246
|
+
</div>
|
|
1132
1247
|
</div>
|
|
1133
|
-
<!-- The child CLI does not read the Windows registry, so a machine whose
|
|
1134
|
-
proxy lives only in Internet Settings gets a child that lists a fraction
|
|
1135
|
-
of the models. Empty means "ask the machine", which is what makes the
|
|
1136
|
-
full catalogue appear with nobody configuring anything. -->
|
|
1137
1248
|
<div class="controls">
|
|
1138
|
-
<
|
|
1139
|
-
<
|
|
1249
|
+
<select id="model" aria-labelledby="model-title"></select>
|
|
1250
|
+
<button type="button" id="model-save" class="primary" data-t="save">保存</button>
|
|
1140
1251
|
</div>
|
|
1141
|
-
<label
|
|
1252
|
+
<label class="lock">
|
|
1142
1253
|
<input type="checkbox" id="model-lock">
|
|
1143
1254
|
<span data-t="modelLock">锁定面板模型(忽略单次调用的 model 参数)</span>
|
|
1144
1255
|
</label>
|
|
1145
|
-
<div class="
|
|
1256
|
+
<div class="status" id="model-status" role="status" aria-live="polite"></div>
|
|
1146
1257
|
<!-- The catalogue the CLI reports is too long for a dropdown, so it gets a
|
|
1147
1258
|
browser: search, filter, and a switch per model. Switching a model off
|
|
1148
1259
|
only hides it from the picker — it is never deleted, so switching it
|
|
1149
1260
|
back on restores the same entry. -->
|
|
1150
|
-
<
|
|
1151
|
-
<
|
|
1152
|
-
|
|
1153
|
-
<
|
|
1261
|
+
<details class="fold" id="model-browser">
|
|
1262
|
+
<summary data-t="modelBrowse">模型目录、搜索与筛选</summary>
|
|
1263
|
+
<div class="controls">
|
|
1264
|
+
<button type="button" id="model-reload" class="ghost" data-t="reloadModels">重新读取模型列表</button>
|
|
1154
1265
|
</div>
|
|
1155
|
-
<div class="picker
|
|
1156
|
-
<div class="picker-
|
|
1157
|
-
|
|
1266
|
+
<div class="picker">
|
|
1267
|
+
<div class="picker-top">
|
|
1268
|
+
<input type="search" id="model-filter" autocomplete="off" spellcheck="false">
|
|
1269
|
+
<div class="tabs" id="model-tabs"></div>
|
|
1270
|
+
</div>
|
|
1271
|
+
<div class="picker-body">
|
|
1272
|
+
<div class="picker-list" id="model-list"></div>
|
|
1273
|
+
<div class="picker-detail" id="model-detail"></div>
|
|
1274
|
+
</div>
|
|
1158
1275
|
</div>
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
<div class="
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
<div class="big" id="total-pct">—</div>
|
|
1167
|
-
<div class="bar" id="total-bar"><i style="width:0%"></i></div>
|
|
1168
|
-
<div class="muted" id="total-sub">—</div>
|
|
1169
|
-
</div>
|
|
1170
|
-
<div class="card">
|
|
1171
|
-
<h2 data-t="auto">Auto 额度</h2>
|
|
1172
|
-
<div class="big" id="auto-pct">—</div>
|
|
1173
|
-
<div class="bar" id="auto-bar"><i style="width:0%"></i></div>
|
|
1174
|
-
<div class="muted" id="auto-sub">—</div>
|
|
1175
|
-
</div>
|
|
1176
|
-
<div class="card">
|
|
1177
|
-
<h2 data-t="api">API / 指定模型额度</h2>
|
|
1178
|
-
<div class="big" id="api-pct">—</div>
|
|
1179
|
-
<div class="bar" id="api-bar"><i style="width:0%"></i></div>
|
|
1180
|
-
<div class="muted" id="api-sub">—</div>
|
|
1181
|
-
</div>
|
|
1182
|
-
<div class="card">
|
|
1183
|
-
<h2 data-t="spend">本期消费</h2>
|
|
1184
|
-
<div class="big" id="spend">—</div>
|
|
1185
|
-
<div class="muted" id="spend-sub">—</div>
|
|
1186
|
-
</div>
|
|
1187
|
-
</div>
|
|
1188
|
-
|
|
1189
|
-
<div class="row card">
|
|
1190
|
-
<h2 data-t="period">计费周期</h2>
|
|
1191
|
-
<div id="cycle" class="muted">—</div>
|
|
1192
|
-
<div id="message" class="muted" style="margin-top:6px"></div>
|
|
1193
|
-
</div>
|
|
1276
|
+
<div class="muted" data-t="modelNote" style="margin-top:6px"></div>
|
|
1277
|
+
</details>
|
|
1278
|
+
<details class="fold" id="model-advanced">
|
|
1279
|
+
<summary data-t="modelAdvanced">来源与出口</summary>
|
|
1280
|
+
<div class="muted" id="model-state"></div>
|
|
1281
|
+
</details>
|
|
1282
|
+
</section>
|
|
1194
1283
|
|
|
1195
1284
|
<div class="row card">
|
|
1196
1285
|
<h2 data-t="live">运行中的 Cursor 会话</h2>
|
|
@@ -1230,11 +1319,23 @@ function dashboardHtml(state) {
|
|
|
1230
1319
|
account: '账号',
|
|
1231
1320
|
loading: '加载中…',
|
|
1232
1321
|
refresh: '刷新',
|
|
1322
|
+
usage: '额度与用量',
|
|
1233
1323
|
included: '总包含额度',
|
|
1324
|
+
used: '已用',
|
|
1234
1325
|
auto: 'Auto 额度',
|
|
1235
1326
|
api: 'API / 指定模型额度',
|
|
1236
1327
|
spend: '本期消费',
|
|
1328
|
+
remaining: '剩余 ',
|
|
1329
|
+
noData: '暂无数据',
|
|
1330
|
+
levelWarn: '偏高',
|
|
1331
|
+
levelBad: '接近上限',
|
|
1332
|
+
officialNote: '官方说明',
|
|
1237
1333
|
model: '子智能体模型',
|
|
1334
|
+
modelBrowse: '模型目录、搜索与筛选',
|
|
1335
|
+
modelAdvanced: '来源与出口',
|
|
1336
|
+
appliesNext: '下次调用生效',
|
|
1337
|
+
unsavedShort: '有改动未保存',
|
|
1338
|
+
seeAdvanced: '模型列表或代理有异常,见「来源与出口」',
|
|
1238
1339
|
modelHint: '默认跟随这里的模型;勾选锁定后,智能体传入的 model 参数也不会覆盖你的选择。',
|
|
1239
1340
|
modelLock: '锁定面板模型(忽略单次调用的 model 参数)',
|
|
1240
1341
|
modelLocked: '已锁定模型',
|
|
@@ -1301,11 +1402,10 @@ function dashboardHtml(state) {
|
|
|
1301
1402
|
none: '暂无',
|
|
1302
1403
|
unreachable: '无法连接',
|
|
1303
1404
|
usageUnavailable: '用量暂时取不到:',
|
|
1405
|
+
quotaBlocked: 'Cursor 额度已耗尽,已暂停新的子智能体任务;额度刷新确认恢复后会自动解除拦截。',
|
|
1304
1406
|
updated: '更新于 ',
|
|
1305
1407
|
failed: '获取失败 ',
|
|
1306
|
-
|
|
1307
|
-
of: ' / 共 ',
|
|
1308
|
-
includedIn: '包含 ',
|
|
1408
|
+
includedIn: '套餐内 ',
|
|
1309
1409
|
bonus: ' · 赠送 ',
|
|
1310
1410
|
starts: '开始 ',
|
|
1311
1411
|
renews: ' → 续订 ',
|
|
@@ -1346,11 +1446,23 @@ function dashboardHtml(state) {
|
|
|
1346
1446
|
account: 'account',
|
|
1347
1447
|
loading: 'loading…',
|
|
1348
1448
|
refresh: 'refresh',
|
|
1449
|
+
usage: 'Quota & usage',
|
|
1349
1450
|
included: 'Included total',
|
|
1451
|
+
used: 'used',
|
|
1350
1452
|
auto: 'Auto bucket',
|
|
1351
1453
|
api: 'API / named bucket',
|
|
1352
|
-
spend: 'Spend',
|
|
1454
|
+
spend: 'Spend this period',
|
|
1455
|
+
remaining: 'remaining ',
|
|
1456
|
+
noData: 'no data yet',
|
|
1457
|
+
levelWarn: 'high',
|
|
1458
|
+
levelBad: 'near the limit',
|
|
1459
|
+
officialNote: 'Official note',
|
|
1353
1460
|
model: 'Child-agent model',
|
|
1461
|
+
modelBrowse: 'Model catalogue, search and filters',
|
|
1462
|
+
modelAdvanced: 'Source and egress',
|
|
1463
|
+
appliesNext: 'applies on the next call',
|
|
1464
|
+
unsavedShort: 'unsaved changes',
|
|
1465
|
+
seeAdvanced: 'model list or proxy problem — see Source and egress',
|
|
1354
1466
|
modelHint: 'Calls follow this default unless they specify a model. Lock it to prevent the agent from overriding your choice.',
|
|
1355
1467
|
modelLock: 'Lock the panel model (ignore per-call model arguments)',
|
|
1356
1468
|
modelLocked: 'model locked',
|
|
@@ -1417,10 +1529,9 @@ function dashboardHtml(state) {
|
|
|
1417
1529
|
none: 'none',
|
|
1418
1530
|
unreachable: 'unreachable',
|
|
1419
1531
|
usageUnavailable: 'Usage unavailable: ',
|
|
1532
|
+
quotaBlocked: 'Cursor quota is exhausted. New child-agent runs are paused until a quota refresh confirms capacity has returned.',
|
|
1420
1533
|
updated: 'updated ',
|
|
1421
1534
|
failed: 'failed ',
|
|
1422
|
-
spentOf: 'spent ',
|
|
1423
|
-
of: ' of ',
|
|
1424
1535
|
includedIn: 'included ',
|
|
1425
1536
|
bonus: ' · bonus ',
|
|
1426
1537
|
starts: 'starts ',
|
|
@@ -1469,6 +1580,7 @@ function dashboardHtml(state) {
|
|
|
1469
1580
|
var models = (boot.models && boot.models.list) || [];
|
|
1470
1581
|
var modelsFetchedAt = (boot.models && boot.models.fetchedAt) || 0;
|
|
1471
1582
|
var modelsSource = (boot.models && boot.models.source) || '';
|
|
1583
|
+
var quotaGate = boot.quotaGuard || { blocked: false };
|
|
1472
1584
|
var configModel = boot.configModel || '';
|
|
1473
1585
|
var setupState = null;
|
|
1474
1586
|
var LANG = pickLang();
|
|
@@ -1513,7 +1625,9 @@ function dashboardHtml(state) {
|
|
|
1513
1625
|
}
|
|
1514
1626
|
renderLangs();
|
|
1515
1627
|
renderModel();
|
|
1516
|
-
|
|
1628
|
+
renderQuotaGuard(quotaGate);
|
|
1629
|
+
if (lastPayload.ok === true) render(lastPayload);
|
|
1630
|
+
else renderLive(lastPayload);
|
|
1517
1631
|
renderSetup();
|
|
1518
1632
|
}
|
|
1519
1633
|
/** The 中文 / English switch, in the header. */
|
|
@@ -1545,8 +1659,15 @@ function dashboardHtml(state) {
|
|
|
1545
1659
|
});
|
|
1546
1660
|
}
|
|
1547
1661
|
|
|
1662
|
+
function known(value) { return typeof value === 'number' && isFinite(value); }
|
|
1548
1663
|
function pct(value) {
|
|
1549
|
-
return
|
|
1664
|
+
return known(value) ? value.toFixed(1) + '%' : '—';
|
|
1665
|
+
}
|
|
1666
|
+
/** The note under a bar, read off the same upstream percentage as the bar. */
|
|
1667
|
+
function remaining(value) {
|
|
1668
|
+
if (!known(value)) return t('noData');
|
|
1669
|
+
var level = value >= 90 ? ' · ' + t('levelBad') : value >= 70 ? ' · ' + t('levelWarn') : '';
|
|
1670
|
+
return t('remaining') + Math.max(0, 100 - value).toFixed(1) + '%' + level;
|
|
1550
1671
|
}
|
|
1551
1672
|
function money(value) {
|
|
1552
1673
|
return typeof value === 'number' ? '$' + value.toFixed(2) : '—';
|
|
@@ -1565,9 +1686,12 @@ function dashboardHtml(state) {
|
|
|
1565
1686
|
function text(node, value) { node.textContent = value == null ? '—' : String(value); }
|
|
1566
1687
|
function setBar(id, percent) {
|
|
1567
1688
|
var bar = $(id);
|
|
1568
|
-
var p =
|
|
1689
|
+
var p = known(percent) ? Math.max(0, Math.min(100, percent)) : 0;
|
|
1569
1690
|
bar.firstElementChild.style.width = p + '%';
|
|
1570
1691
|
bar.className = 'bar' + (p >= 90 ? ' bad' : p >= 70 ? ' warn' : '');
|
|
1692
|
+
if (known(percent)) bar.setAttribute('aria-valuenow', String(p));
|
|
1693
|
+
else bar.removeAttribute('aria-valuenow');
|
|
1694
|
+
bar.setAttribute('aria-valuetext', known(percent) ? pct(percent) : t('noData'));
|
|
1571
1695
|
}
|
|
1572
1696
|
|
|
1573
1697
|
/** Compare the CLI's Auto id with the panel alias without conflating variants. */
|
|
@@ -1759,6 +1883,12 @@ function dashboardHtml(state) {
|
|
|
1759
1883
|
if (proxyError) bits.push(t('proxyFailed') + proxyError);
|
|
1760
1884
|
if (modelsError) bits.push(t('modelsFailed') + modelsError);
|
|
1761
1885
|
text($('model-state'), bits.join(' · '));
|
|
1886
|
+
var dirty = (draftModel !== null && canonicalModel(draftModel) !== canonicalModel(settings.model)) || switchesDirty();
|
|
1887
|
+
var status = [t('inEffect') + modelLabel(inEffectNow),
|
|
1888
|
+
t(settings.modelPolicy === 'panel' && inEffectNow ? 'modelLocked' : 'modelOverridable'),
|
|
1889
|
+
t(dirty ? 'unsavedShort' : 'appliesNext')];
|
|
1890
|
+
if (modelsError || proxyError) status.push(t('seeAdvanced'));
|
|
1891
|
+
text($('model-status'), status.join(' · '));
|
|
1762
1892
|
renderPicker(rows, selected, inEffectNow);
|
|
1763
1893
|
}
|
|
1764
1894
|
|
|
@@ -2016,7 +2146,7 @@ function dashboardHtml(state) {
|
|
|
2016
2146
|
* (an agent loop to hold it, a recorded Cursor session to read).
|
|
2017
2147
|
*/
|
|
2018
2148
|
function canRestore(entry) {
|
|
2019
|
-
return boot.children.available === true
|
|
2149
|
+
return boot.children.available === true && entry.restorable !== false && entry.running !== true
|
|
2020
2150
|
&& typeof entry.cursorSessionId === 'string' && entry.cursorSessionId !== '';
|
|
2021
2151
|
}
|
|
2022
2152
|
|
|
@@ -2056,6 +2186,10 @@ function dashboardHtml(state) {
|
|
|
2056
2186
|
});
|
|
2057
2187
|
return;
|
|
2058
2188
|
}
|
|
2189
|
+
if (body.code === 'conversation_pending' || body.code === 'conversation_running') {
|
|
2190
|
+
openJournal(entry.key);
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2059
2193
|
notice(t('restoreFailed') + (body.error || 'unknown error'));
|
|
2060
2194
|
})
|
|
2061
2195
|
.catch(function (error) {
|
|
@@ -2342,8 +2476,14 @@ function dashboardHtml(state) {
|
|
|
2342
2476
|
renderCount(convos.length);
|
|
2343
2477
|
applyFolded();
|
|
2344
2478
|
}
|
|
2479
|
+
function renderQuotaGuard(state) {
|
|
2480
|
+
if (state && typeof state.blocked === 'boolean') quotaGate = state;
|
|
2481
|
+
$('quota-alert').hidden = quotaGate.blocked !== true;
|
|
2482
|
+
}
|
|
2345
2483
|
function render(payload) {
|
|
2346
2484
|
lastPayload = payload;
|
|
2485
|
+
var gate = payload.extended && payload.extended.quotaGuard;
|
|
2486
|
+
if (gate) renderQuotaGuard(gate);
|
|
2347
2487
|
var err = $('error');
|
|
2348
2488
|
// A failed snapshot carries no extended block; keep whatever the table
|
|
2349
2489
|
// already shows rather than blanking it.
|
|
@@ -2357,20 +2497,26 @@ function dashboardHtml(state) {
|
|
|
2357
2497
|
}
|
|
2358
2498
|
err.style.display = 'none';
|
|
2359
2499
|
var u = payload.usage || {};
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2500
|
+
var d = u.dollars || {};
|
|
2501
|
+
// Every percentage is the upstream percentUsed: the dollar fields count
|
|
2502
|
+
// different things (spend includes bonus, limit does not), so a ratio of
|
|
2503
|
+
// them disagrees with the bar.
|
|
2504
|
+
['total', 'auto', 'api'].forEach(function (name) {
|
|
2505
|
+
var p = u[name] && u[name].percentUsed;
|
|
2506
|
+
setBar(name + '-bar', p);
|
|
2507
|
+
text($(name + '-pct'), pct(p));
|
|
2508
|
+
text($(name + '-sub'), remaining(p));
|
|
2509
|
+
});
|
|
2510
|
+
$('auto-sub').title = u.autoMessage || '';
|
|
2511
|
+
$('api-sub').title = u.namedMessage || '';
|
|
2366
2512
|
text($('tier'), u.plan || payload.tier || t('account'));
|
|
2367
|
-
text($('
|
|
2368
|
-
text($('
|
|
2369
|
-
|
|
2370
|
-
text($('
|
|
2371
|
-
|
|
2372
|
-
text($('cycle'), t('starts') + when(u.billingCycle && u.billingCycle.start) + t('renews') + when(u.billingCycle && u.billingCycle.end));
|
|
2513
|
+
text($('spend'), money(d.total));
|
|
2514
|
+
text($('spend-sub'), t('includedIn') + money(d.included) + t('bonus') + money(d.bonus));
|
|
2515
|
+
var cycle = u.billingCycle || {};
|
|
2516
|
+
text($('cycle'), known(cycle.start) || known(cycle.end)
|
|
2517
|
+
? t('starts') + when(cycle.start) + t('renews') + when(cycle.end) : t('noData'));
|
|
2373
2518
|
text($('message'), u.displayMessage || '');
|
|
2519
|
+
$('message-box').hidden = !u.displayMessage;
|
|
2374
2520
|
text($('stamp'), t('updated') + when(payload.fetchedAt));
|
|
2375
2521
|
text($('source'), t('source') + (payload.source || '') + (payload.authPath ? t('credentials') + payload.authPath : ''));
|
|
2376
2522
|
renderLive(payload);
|
|
@@ -2437,6 +2583,7 @@ function dashboardHtml(state) {
|
|
|
2437
2583
|
})
|
|
2438
2584
|
.catch(function (e) {
|
|
2439
2585
|
text($('model-state'), t('saveFailed') + String(e));
|
|
2586
|
+
text($('model-status'), t('saveFailed') + String(e));
|
|
2440
2587
|
return undefined;
|
|
2441
2588
|
});
|
|
2442
2589
|
}
|
|
@@ -2543,6 +2690,7 @@ function dashboardHtml(state) {
|
|
|
2543
2690
|
var nextProxy = proxyBox ? proxyBox.value.trim() : '';
|
|
2544
2691
|
var proxyChanged = nextProxy !== (settings.proxy == null ? '' : String(settings.proxy));
|
|
2545
2692
|
text($('model-state'), '…');
|
|
2693
|
+
text($('model-status'), '…');
|
|
2546
2694
|
post({
|
|
2547
2695
|
model: value,
|
|
2548
2696
|
modelPolicy: $('model-lock').checked ? 'panel' : 'per-call',
|
|
@@ -2556,8 +2704,9 @@ function dashboardHtml(state) {
|
|
|
2556
2704
|
draftDisabled = null;
|
|
2557
2705
|
renderLive(lastPayload);
|
|
2558
2706
|
var announce = function () {
|
|
2559
|
-
|
|
2560
|
-
|
|
2707
|
+
var lock = t(settings.modelPolicy === 'panel' && data.effectiveModel ? 'modelLocked' : 'modelOverridable');
|
|
2708
|
+
text($('model-state'), t('saved') + ' · ' + t('inEffect') + modelLabel(data.effectiveModel) + ' · ' + lock);
|
|
2709
|
+
text($('model-status'), t('saved') + ' · ' + modelLabel(data.effectiveModel) + ' · ' + lock + ' · ' + t('appliesNext'));
|
|
2561
2710
|
};
|
|
2562
2711
|
// A new route reaches a different catalogue, which makes the list on
|
|
2563
2712
|
// screen a fact about the old one — so re-read it before saying "saved".
|
|
@@ -2567,6 +2716,7 @@ function dashboardHtml(state) {
|
|
|
2567
2716
|
});
|
|
2568
2717
|
$('model-reload').addEventListener('click', function () {
|
|
2569
2718
|
text($('model-state'), t('loading'));
|
|
2719
|
+
text($('model-status'), t('loading'));
|
|
2570
2720
|
loadSettings(true);
|
|
2571
2721
|
});
|
|
2572
2722
|
// Filtering is client-side on purpose: the list is already in the payload, so
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-cursor-dev",
|
|
3
3
|
"displayName": "Cursor 子智能体",
|
|
4
|
-
"description": "把 Cursor CLI 当作子编码智能体来写业务代码:cursor_code
|
|
5
|
-
"version": "0.12.
|
|
4
|
+
"description": "把 Cursor CLI 当作子编码智能体来写业务代码:cursor_code 与 cursor_subagent 都提供可从面板点开的实时子对话,原生任务保留宿主的 External 运行记录;可按任务选模型、会话在 DSH 重启后仍能续聊、返回逐步工具时间线与真实文件 diff,并自带额度/模型/会话面板。",
|
|
5
|
+
"version": "0.12.2",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "lib/index.js",
|