dsh-ntr 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +5 -2
- package/README.md +5 -2
- package/lib/bridge.js +14 -23
- package/lib/client/panel.js +96 -78
- package/lib/client.js +403 -301
- package/lib/client.js.map +1 -1
- package/lib/types/bridge.d.ts +3 -3
- package/lib/types/client/panel.d.ts +0 -1
- package/package.json +5 -1
- package/src/bridge.ts +16 -22
- package/src/client/panel.tsx +193 -204
package/README.en.md
CHANGED
|
@@ -70,12 +70,12 @@ To upgrade later: `git pull` → `npm run build` → restart `dsh web`. `add .`
|
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
73
|
-
##
|
|
73
|
+
## Quick start
|
|
74
74
|
|
|
75
75
|
### What you need
|
|
76
76
|
|
|
77
77
|
1. A working DSH install (web or desktop).
|
|
78
|
-
2. At least one ACP agent CLI.
|
|
78
|
+
2. At least one ACP-capable agent, using its terminal build (CLI / TUI). For example Qoder CLI (`qodercli`), CodeBuddy Code (`codebuddy`), Claude Code, or OpenCode — presets for these ship with the plugin; any other agent that starts from the command line in the same `xxx --acp` style can be filled in directly.
|
|
79
79
|
|
|
80
80
|
### Three steps
|
|
81
81
|
|
|
@@ -267,11 +267,14 @@ These servers are handed to the agent when a session is created; the agent launc
|
|
|
267
267
|
|
|
268
268
|
```bash
|
|
269
269
|
npm install # dependencies
|
|
270
|
+
git config core.hooksPath .githooks # enable the commit-msg check (rejects AI attribution)
|
|
270
271
|
npm run build # host TS declarations + browser bundle
|
|
271
272
|
npm test # test suite
|
|
272
273
|
dsh plugin --profile web add ./ # mount into the Web profile
|
|
273
274
|
```
|
|
274
275
|
|
|
276
|
+
`.githooks/commit-msg` only inspects the commit message: it rejects AI-tool attribution in `Co-Authored-By` / `Generated with` lines, while ordinary prose and human attribution pass. `core.hooksPath` is local config — run this once after cloning.
|
|
277
|
+
|
|
275
278
|
### Implementation notes
|
|
276
279
|
|
|
277
280
|
Each item below corresponds to a real past failure; read the linked doc before changing code. The browser half talks to the host half via `/acp` RPC and `/api/acp/stream` SSE; the host half runs one `AgentBridge` per conversation, driving the agent child process over stdio JSON-RPC.
|
package/README.md
CHANGED
|
@@ -70,12 +70,12 @@ dsh plugin --profile web add ./
|
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
73
|
-
##
|
|
73
|
+
## 快速开始
|
|
74
74
|
|
|
75
75
|
### 你需要什么
|
|
76
76
|
|
|
77
77
|
1. 一个已经装好的 DSH(Web 或桌面版)。
|
|
78
|
-
2.
|
|
78
|
+
2. 至少一个支持 ACP 协议的 agent,用它的终端版本(CLI / TUI)即可。例如 Qoder CLI(`qodercli`)、CodeBuddy Code(`codebuddy`)、Claude Code、OpenCode——插件内置了这些常见预设;其他 agent 只要能以类似 `xxx --acp` 的方式从命令行启动,也可以直接填。
|
|
79
79
|
|
|
80
80
|
### 三步接入
|
|
81
81
|
|
|
@@ -267,11 +267,14 @@ agent 要动敏感的东西时会问你。询问卡出现在对话里、聊天
|
|
|
267
267
|
|
|
268
268
|
```bash
|
|
269
269
|
npm install # 依赖
|
|
270
|
+
git config core.hooksPath .githooks # 启用 commit-msg 校验(拒绝 AI 署名)
|
|
270
271
|
npm run build # 宿主 TS 声明 + 浏览器 Bundle
|
|
271
272
|
npm test # 测试套件
|
|
272
273
|
dsh plugin --profile web add ./ # 挂载到 Web Profile
|
|
273
274
|
```
|
|
274
275
|
|
|
276
|
+
`.githooks/commit-msg` 只校验提交信息:拒绝把 AI 工具写进 `Co-Authored-By` / `Generated with` 之类署名,普通正文与人类署名不受影响。`core.hooksPath` 是本地配置,clone 后执行一次即可。
|
|
277
|
+
|
|
275
278
|
### 实现要点
|
|
276
279
|
|
|
277
280
|
以下每条都对应一处真实故障,改代码前先读对应文档。浏览器半区经 `/acp` RPC 与 `/api/acp/stream` SSE 和宿主半区通信;宿主半区每会话一个 `AgentBridge`,经 stdio JSON-RPC 驱动 agent 子进程。
|
package/lib/bridge.js
CHANGED
|
@@ -2051,6 +2051,8 @@ export function releaseAllToolOutputWaiters() {
|
|
|
2051
2051
|
for (const waiter of [...set])
|
|
2052
2052
|
waiter(undefined);
|
|
2053
2053
|
}
|
|
2054
|
+
/** ACP `usage_update` is context occupancy, not request tokens; host usage is a 4-chars/token OUTPUT estimate (tool args excluded). */
|
|
2055
|
+
const OUTPUT_CHARS_PER_TOKEN = 4;
|
|
2054
2056
|
/**
|
|
2055
2057
|
* The state of ONE ACP prompt, shared across every driver step of the turn.
|
|
2056
2058
|
*
|
|
@@ -2059,7 +2061,7 @@ export function releaseAllToolOutputWaiters() {
|
|
|
2059
2061
|
* `transcript.since(cursor)` — events that arrive while no segment is open are
|
|
2060
2062
|
* recorded, never lost), the seen-call registry (a call id gets exactly one
|
|
2061
2063
|
* card per turn, wherever its restatements land), and the turn-level facts
|
|
2062
|
-
* (`sawVisibleBlock
|
|
2064
|
+
* (`sawVisibleBlock`) that outlive any single segment.
|
|
2063
2065
|
*/
|
|
2064
2066
|
class TurnStreamController {
|
|
2065
2067
|
manager;
|
|
@@ -2073,7 +2075,6 @@ class TurnStreamController {
|
|
|
2073
2075
|
disposed = false;
|
|
2074
2076
|
seenCalls = new Set();
|
|
2075
2077
|
announcedKinds = new Map();
|
|
2076
|
-
latestUsage = null;
|
|
2077
2078
|
/** Narrative lines (permission asks) that arrived while no segment was open. */
|
|
2078
2079
|
pendingProse = [];
|
|
2079
2080
|
sawVisibleBlock = false;
|
|
@@ -2119,6 +2120,7 @@ class TurnStreamController {
|
|
|
2119
2120
|
index: -1,
|
|
2120
2121
|
activeType: null,
|
|
2121
2122
|
activeText: '',
|
|
2123
|
+
outputChars: 0,
|
|
2122
2124
|
activeToolCall: undefined,
|
|
2123
2125
|
hasCard: false,
|
|
2124
2126
|
finished: false,
|
|
@@ -2245,8 +2247,10 @@ class TurnStreamController {
|
|
|
2245
2247
|
seg.activeType = null;
|
|
2246
2248
|
seg.activeToolCall = undefined;
|
|
2247
2249
|
}
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
+
const outputTokens = Math.round(seg.outputChars / OUTPUT_CHARS_PER_TOKEN);
|
|
2251
|
+
// Zero input keeps the host's cache-hit pill hidden instead of faking 0%.
|
|
2252
|
+
if (outputTokens > 0)
|
|
2253
|
+
seg.queue.push({ type: 'usage', usage: { inputTokens: 0, outputTokens } });
|
|
2250
2254
|
seg.queue.push({ type: 'finish', reason });
|
|
2251
2255
|
seg.queue.close();
|
|
2252
2256
|
if (this.segment === seg)
|
|
@@ -2289,11 +2293,10 @@ class TurnStreamController {
|
|
|
2289
2293
|
const seg = this.segment;
|
|
2290
2294
|
if (seg === undefined || seg.finished) {
|
|
2291
2295
|
// Between segments: tool outputs still reach the shared registry (the
|
|
2292
|
-
// guard's virtual tool may be parked on this very record)
|
|
2293
|
-
//
|
|
2294
|
-
//
|
|
2295
|
-
//
|
|
2296
|
-
// executed on the host.
|
|
2296
|
+
// guard's virtual tool may be parked on this very record), but the
|
|
2297
|
+
// cursor does NOT advance — the next segment replays everything since
|
|
2298
|
+
// it, which re-records those idempotently and folds the prose the agent
|
|
2299
|
+
// streamed while its cards executed on the host.
|
|
2297
2300
|
this.recordWithoutSegment(update);
|
|
2298
2301
|
return;
|
|
2299
2302
|
}
|
|
@@ -2337,17 +2340,8 @@ class TurnStreamController {
|
|
|
2337
2340
|
/** Events that arrive between segments: recorded, replayed into the next one. */
|
|
2338
2341
|
recordWithoutSegment(update) {
|
|
2339
2342
|
const kind = String(update.sessionUpdate ?? '');
|
|
2340
|
-
if (kind === '
|
|
2341
|
-
this.noteUsage(update);
|
|
2342
|
-
}
|
|
2343
|
-
else if (kind === 'tool_call' || kind === 'tool_call_update') {
|
|
2343
|
+
if (kind === 'tool_call' || kind === 'tool_call_update')
|
|
2344
2344
|
this.noteToolRecord(update);
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
noteUsage(update) {
|
|
2348
|
-
const used = typeof update.used === 'number' ? update.used : 0;
|
|
2349
|
-
const size = typeof update.size === 'number' ? update.size : 0;
|
|
2350
|
-
this.latestUsage = { inputTokens: size, outputTokens: used };
|
|
2351
2345
|
}
|
|
2352
2346
|
/** Record a tool call's output (idempotent) and wake any waiter. */
|
|
2353
2347
|
noteToolRecord(update) {
|
|
@@ -2373,6 +2367,7 @@ class TurnStreamController {
|
|
|
2373
2367
|
this.finishSegment(seg, { kind: 'stop' });
|
|
2374
2368
|
return false;
|
|
2375
2369
|
}
|
|
2370
|
+
seg.outputChars += text.length;
|
|
2376
2371
|
this.emitProse(seg, kind === 'agent_thought_chunk' ? 'reasoning' : 'text', text);
|
|
2377
2372
|
return true;
|
|
2378
2373
|
}
|
|
@@ -2395,10 +2390,6 @@ class TurnStreamController {
|
|
|
2395
2390
|
this.openToolCallBlock(seg, snapshot);
|
|
2396
2391
|
return true;
|
|
2397
2392
|
}
|
|
2398
|
-
else if (kind === 'usage_update') {
|
|
2399
|
-
this.noteUsage(update);
|
|
2400
|
-
return true;
|
|
2401
|
-
}
|
|
2402
2393
|
else if (!BOOKKEEPING_UPDATES.has(kind)) {
|
|
2403
2394
|
const text = describeUnhandledUpdate(kind, update);
|
|
2404
2395
|
if (this.announcedKinds.get(kind) === text)
|