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 CHANGED
@@ -70,12 +70,12 @@ To upgrade later: `git pull` → `npm run build` → restart `dsh web`. `add .`
70
70
 
71
71
  ---
72
72
 
73
- ## Getting started
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. The plugin ships presets for common ones, and accepts any stdio-capable ACP agent.
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. 至少一个 ACP agent 的命令行程序。插件内置了几个常见预设,也可以填任何支持 stdio 的 ACP agent。
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`, usage) that outlive any single segment.
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
- if (this.latestUsage !== null)
2249
- seg.queue.push({ type: 'usage', usage: this.latestUsage });
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) and usage
2293
- // keeps the turn's tally fresh, but the cursor does NOT advance — the
2294
- // next segment replays everything since it, which re-records those
2295
- // idempotently and folds the prose the agent streamed while its cards
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 === 'usage_update') {
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)