lark-remote 0.1.7 → 0.1.8

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/bridge/approval-coordinator.d.ts +15 -1
  3. package/dist/bridge/approval-coordinator.js +35 -2
  4. package/dist/bridge/index.d.ts +19 -6
  5. package/dist/bridge/index.js +25 -8
  6. package/dist/card/approval-render.js +187 -45
  7. package/dist/card/card-shared.d.ts +6 -4
  8. package/dist/card/card-shared.js +6 -4
  9. package/dist/card/run-renderer.d.ts +7 -0
  10. package/dist/card/run-renderer.js +22 -11
  11. package/dist/config/index.d.ts +24 -1
  12. package/dist/config/index.js +34 -3
  13. package/dist/index.js +28 -14
  14. package/dist/router/card-action-payload.d.ts +30 -0
  15. package/dist/router/card-action-payload.js +39 -0
  16. package/dist/router/config/claude.js +8 -3
  17. package/dist/router/config/dsh.d.ts +51 -0
  18. package/dist/router/config/dsh.js +179 -0
  19. package/dist/router/config/index.d.ts +1 -1
  20. package/dist/router/config/index.js +4 -1
  21. package/dist/router/config/types.d.ts +6 -0
  22. package/dist/router/index.d.ts +20 -1
  23. package/dist/router/index.js +113 -7
  24. package/dist/router/utils.js +12 -2
  25. package/dist/runner/agent-choices-common.js +3 -0
  26. package/dist/runner/claude/session.js +24 -5
  27. package/dist/runner/codex/app-server/protocol-types.d.ts +35 -0
  28. package/dist/runner/codex/app-server/protocol-types.js +1 -1
  29. package/dist/runner/codex/app-server/runner.d.ts +10 -0
  30. package/dist/runner/codex/app-server/runner.js +56 -1
  31. package/dist/runner/codex/app-server/translator.d.ts +7 -0
  32. package/dist/runner/codex/app-server/translator.js +45 -0
  33. package/dist/runner/common/acp/protocol-types.d.ts +33 -0
  34. package/dist/runner/common/acp/protocol-types.js +2 -0
  35. package/dist/runner/dsh/client.d.ts +95 -0
  36. package/dist/runner/dsh/client.js +418 -0
  37. package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
  38. package/dist/runner/dsh/fake-dsh-server.js +156 -0
  39. package/dist/runner/dsh/index.d.ts +3 -0
  40. package/dist/runner/dsh/index.js +4 -0
  41. package/dist/runner/dsh/runner.d.ts +57 -0
  42. package/dist/runner/dsh/runner.js +191 -0
  43. package/dist/runner/dsh/translator.d.ts +28 -0
  44. package/dist/runner/dsh/translator.js +98 -0
  45. package/dist/runner/dsh/types.d.ts +155 -0
  46. package/dist/runner/dsh/types.js +14 -0
  47. package/dist/runner/index.d.ts +1 -0
  48. package/dist/runner/index.js +1 -0
  49. package/dist/runner/kimi/acp/runner.js +69 -1
  50. package/dist/runner/kimi/acp/translator.d.ts +14 -0
  51. package/dist/runner/kimi/acp/translator.js +108 -3
  52. package/dist/runner/pi/rpc/runner.d.ts +1 -7
  53. package/dist/runner/pi/rpc/runner.js +2 -11
  54. package/dist/runner/probe.js +6 -1
  55. package/dist/runner/question-common.d.ts +26 -0
  56. package/dist/runner/question-common.js +42 -0
  57. package/dist/runner/types.d.ts +63 -10
  58. package/dist/session/claude/sessions.js +37 -15
  59. package/dist/session/dsh/index.d.ts +1 -0
  60. package/dist/session/dsh/index.js +2 -0
  61. package/dist/session/dsh/sessions.d.ts +39 -0
  62. package/dist/session/dsh/sessions.js +237 -0
  63. package/dist/session/index.d.ts +1 -0
  64. package/dist/session/index.js +1 -0
  65. package/dist/workspace/index.d.ts +4 -0
  66. package/dist/workspace/index.js +8 -1
  67. package/package.json +2 -1
@@ -81,6 +81,14 @@ export declare class KimiAcpTranslator {
81
81
  * Handle a server request (reverse RPC from the ACP server).
82
82
  */
83
83
  handleServerRequest(id: number | string, method: string, params: unknown): AcpTranslatorEvent[];
84
+ /**
85
+ * Kimi elicitation form:requestedSchema.properties 按顺序解析为 UserQuestion[]
86
+ * (type:array → 多选;oneOf/anyOf const → 选项 label;title → header??question)。
87
+ * 题面完整文本只在 form 级 message 里合并出现(逐字段 title 可能只是短 header),
88
+ * 因此 message 作为卡片概要行(view.intro)透传。
89
+ * form 会丢弃非声明选项值 → isOther=false(卡片隐藏自定义答案输入)。
90
+ */
91
+ private handleElicitationCreate;
84
92
  /**
85
93
  * Produce a turn_started event. Called by the runner when session/prompt
86
94
  * is sent (no wire notification for this — self-produced).
@@ -108,6 +116,12 @@ export declare class KimiAcpTranslator {
108
116
  private handleToolCallUpdate;
109
117
  private handleUsageUpdate;
110
118
  private handleRequestPermission;
119
+ /**
120
+ * request_permission 兜底桥(elicitation/create 失败或旧版 kimi):
121
+ * 选项为 q{n}_opt_{i}(allow_once,label 直取 name)+ q{n}_skip(reject_once,
122
+ * 跳过语义)。单题单选、无 Other(kimi 桥丢弃非声明值)。
123
+ */
124
+ private handlePermissionQuestion;
111
125
  /**
112
126
  * Derive the approval decision list from the server's offered options.
113
127
  * accept/decline/cancel are universal; acceptForSession requires an
@@ -28,6 +28,7 @@
28
28
  * turn_started (self-produced) → operationKind 'turn'/'compaction'
29
29
  * session/request_permission → approval_requested
30
30
  */
31
+ import { makeQuestionApprovalEvent } from '../../question-common.js';
31
32
  import { NotificationMethod, ServerRequestMethod, SessionEventType, } from '../../common/acp/protocol-types.js';
32
33
  import { getLogger } from '../../../logger/index.js';
33
34
  /**
@@ -96,8 +97,41 @@ export class KimiAcpTranslator {
96
97
  if (method === ServerRequestMethod.REQUEST_PERMISSION) {
97
98
  return this.handleRequestPermission(id, params);
98
99
  }
100
+ if (method === ServerRequestMethod.ELICITATION_CREATE) {
101
+ return this.handleElicitationCreate(id, params);
102
+ }
99
103
  return [];
100
104
  }
105
+ /**
106
+ * Kimi elicitation form:requestedSchema.properties 按顺序解析为 UserQuestion[]
107
+ * (type:array → 多选;oneOf/anyOf const → 选项 label;title → header??question)。
108
+ * 题面完整文本只在 form 级 message 里合并出现(逐字段 title 可能只是短 header),
109
+ * 因此 message 作为卡片概要行(view.intro)透传。
110
+ * form 会丢弃非声明选项值 → isOther=false(卡片隐藏自定义答案输入)。
111
+ */
112
+ handleElicitationCreate(requestId, params) {
113
+ const properties = params.requestedSchema?.properties ?? {};
114
+ const questions = [];
115
+ for (const [key, prop] of Object.entries(properties)) {
116
+ const q = elicitationPropertyToQuestion(prop);
117
+ if (!q) {
118
+ getLogger().warn(`[kimi-acp-translator] elicitation property skipped: key=${key}`);
119
+ continue;
120
+ }
121
+ questions.push(q);
122
+ }
123
+ if (questions.length === 0) {
124
+ // 无法解析 → 交还 runner 自动响应(不悬挂服务端)。
125
+ return [];
126
+ }
127
+ const message = params.message?.trim();
128
+ const single = questions.length === 1;
129
+ const intro = message && (!single || message !== questions[0].question) ? message : undefined;
130
+ const event = makeQuestionApprovalEvent(requestId, questions, params.sessionId);
131
+ if (intro)
132
+ event.view.intro = intro;
133
+ return [event];
134
+ }
101
135
  /**
102
136
  * Produce a turn_started event. Called by the runner when session/prompt
103
137
  * is sent (no wire notification for this — self-produced).
@@ -274,9 +308,20 @@ export class KimiAcpTranslator {
274
308
  // Private approval handlers
275
309
  // =========================================================================
276
310
  handleRequestPermission(requestId, params) {
277
- // §5.4: Question elicitation auto-respond cancelled
278
- if (params.isQuestion || !params.toolCall) {
279
- getLogger().info(`[kimi-acp-translator] question elicitation detected, auto-responding cancelled (requestId=${requestId})`);
311
+ // §5.4 升级:question elicitation 识别(isQuestion 标记 / AskUserQuestion
312
+ // toolCall 标题 / q{n}_opt_* 选项命名空间)→ 渲染为提问卡;无法解析的
313
+ // 退化形态(无 toolCall 也无选项)仍返回 [] 由 runner 自动响应 cancelled。
314
+ const toolCall = params.toolCall;
315
+ const questionMarker = params.isQuestion === true ||
316
+ toolCall?.title === 'AskUserQuestion' ||
317
+ optionIdsMatchQuestionNamespace(params.options);
318
+ if (questionMarker) {
319
+ return this.handlePermissionQuestion(requestId, params);
320
+ }
321
+ // §5.4:无 toolCall 且无提问标记 → 无法构成审批视图,返回 [] 由 runner
322
+ // 自动响应 cancelled(服务端不会悬挂)。
323
+ if (!toolCall) {
324
+ getLogger().info(`[kimi-acp-translator] permission request without toolCall, auto-responding cancelled (requestId=${requestId})`);
280
325
  return [];
281
326
  }
282
327
  const view = {
@@ -301,6 +346,27 @@ export class KimiAcpTranslator {
301
346
  },
302
347
  ];
303
348
  }
349
+ /**
350
+ * request_permission 兜底桥(elicitation/create 失败或旧版 kimi):
351
+ * 选项为 q{n}_opt_{i}(allow_once,label 直取 name)+ q{n}_skip(reject_once,
352
+ * 跳过语义)。单题单选、无 Other(kimi 桥丢弃非声明值)。
353
+ */
354
+ handlePermissionQuestion(requestId, params) {
355
+ const selectable = (params.options ?? []).filter((opt) => /^q\d+_opt_\d+$/.test(opt.optionId));
356
+ if (selectable.length === 0) {
357
+ getLogger().info(`[kimi-acp-translator] question elicitation unparseable, auto-responding cancelled (requestId=${requestId})`);
358
+ return [];
359
+ }
360
+ const questionText = extractQuestionText(params.toolCall);
361
+ const questions = [
362
+ {
363
+ question: questionText,
364
+ isOther: false,
365
+ options: selectable.map((opt) => ({ label: opt.name })),
366
+ },
367
+ ];
368
+ return [makeQuestionApprovalEvent(requestId, questions, params.sessionId)];
369
+ }
304
370
  /**
305
371
  * Derive the approval decision list from the server's offered options.
306
372
  * accept/decline/cancel are universal; acceptForSession requires an
@@ -333,4 +399,43 @@ export class KimiAcpTranslator {
333
399
  function truncate(text, maxLen) {
334
400
  return text.length <= maxLen ? text : text.slice(0, maxLen) + '…';
335
401
  }
402
+ /** elicitation schema property → UserQuestion(无法解析返回 null)。 */
403
+ function elicitationPropertyToQuestion(prop) {
404
+ const title = prop.title?.trim();
405
+ if (!title)
406
+ return null;
407
+ const multiSelect = prop.type === 'array';
408
+ const rawOptions = multiSelect ? prop.items?.anyOf : prop.oneOf;
409
+ const options = (rawOptions ?? [])
410
+ .filter((opt) => typeof opt?.const === 'string')
411
+ .map((opt) => ({
412
+ label: opt.const,
413
+ ...(opt.description ? { description: opt.description } : {}),
414
+ }));
415
+ if (options.length === 0)
416
+ return null;
417
+ return {
418
+ question: title,
419
+ header: title,
420
+ multiSelect,
421
+ isOther: false,
422
+ options,
423
+ };
424
+ }
425
+ /** 选项命名空间是否匹配 kimi question 桥(q{n}_opt_{i} / q{n}_skip)。 */
426
+ function optionIdsMatchQuestionNamespace(options) {
427
+ return (options ?? []).some((opt) => /^q\d+_(opt_\d+|skip)$/.test(opt.optionId));
428
+ }
429
+ /** 从 AskUserQuestion toolCall 提取题面(content 文本优先,标题兜底)。 */
430
+ function extractQuestionText(toolCall) {
431
+ const content = toolCall?.content;
432
+ if (Array.isArray(content)) {
433
+ for (const block of content) {
434
+ const text = block.content?.text;
435
+ if (typeof text === 'string' && text.trim())
436
+ return text.trim();
437
+ }
438
+ }
439
+ return toolCall?.title?.trim() || '请选择';
440
+ }
336
441
  //# sourceMappingURL=translator.js.map
@@ -14,8 +14,7 @@
14
14
  * turn idle watchdog, connection-lost retry). Differences:
15
15
  * - pi binds the session at spawn via `--session-id` (no RPC resume-by-id);
16
16
  * the PiRpcConnectionManager respawns when the requested session changes.
17
- * - no approval server-requests (pi RPC exposes none in this scope) →
18
- * respondApproval is a no-op.
17
+ * - pi,极简风格,没有 Ask User question。
19
18
  * - `prompt` ack ≠ turn completion; the turn ends on `agent_settled`.
20
19
  */
21
20
  import type { AgentSessionReader, AgentEvent, AgentStatusInfo, SpawnOptions } from '../../types.js';
@@ -71,10 +70,5 @@ export declare class PiRpcRunner extends ConnectionBasedRunner<PiRpcClient, PiRp
71
70
  * completion (the completion signal); `compaction_*` events stream meanwhile.
72
71
  */
73
72
  runCompact(_message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
74
- /**
75
- * Approvals are out of scope for pi RPC (no server requests surface in this
76
- * integration); the bridge's duck-typed respondApproval call is a no-op.
77
- */
78
- respondApproval(_requestId: number | string, _response: unknown): Promise<void>;
79
73
  private handleEvent;
80
74
  }
@@ -14,8 +14,7 @@
14
14
  * turn idle watchdog, connection-lost retry). Differences:
15
15
  * - pi binds the session at spawn via `--session-id` (no RPC resume-by-id);
16
16
  * the PiRpcConnectionManager respawns when the requested session changes.
17
- * - no approval server-requests (pi RPC exposes none in this scope) →
18
- * respondApproval is a no-op.
17
+ * - pi,极简风格,没有 Ask User question。
19
18
  * - `prompt` ack ≠ turn completion; the turn ends on `agent_settled`.
20
19
  */
21
20
  import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
@@ -172,16 +171,8 @@ export class PiRpcRunner extends ConnectionBasedRunner {
172
171
  this.pushEvents([translator.produceCompactResult(opts.sessionId, response)]);
173
172
  });
174
173
  }
175
- /**
176
- * Approvals are out of scope for pi RPC (no server requests surface in this
177
- * integration); the bridge's duck-typed respondApproval call is a no-op.
178
- */
179
- async respondApproval(_requestId, _response) {
180
- // no-op
181
- }
182
174
  handleEvent(evt) {
183
- const events = this.currentTranslator?.handleEvent(evt) ?? [];
184
- this.pushEvents(events);
175
+ this.pushEvents(this.currentTranslator?.handleEvent(evt) ?? []);
185
176
  }
186
177
  }
187
178
  /** Build pi `--mode rpc` spawn args. `sessionId` is bound at spawn (pi has no
@@ -28,6 +28,9 @@ const BINARY_MAP = {
28
28
  opencode: 'opencode',
29
29
  pi: 'pi',
30
30
  kimi: 'kimi',
31
+ // DSH is an HTTP-only agent (no CLI). `which ''` fails → reported unavailable,
32
+ // which is semantically correct for the /config availability display.
33
+ dsh: '',
31
34
  };
32
35
  const CACHE_TTL_MS = 5 * 60_000;
33
36
  /** Module-level cache, survives across calls within the process. */
@@ -65,7 +68,9 @@ export async function probeAgentAvailability(kind) {
65
68
  * on resource-constrained devices.
66
69
  */
67
70
  export async function probeAllAgents() {
68
- const kinds = Object.keys(BINARY_MAP);
71
+ // Skip agents with no CLI binary (DSH is HTTP-only, binary === ''): the
72
+ // probe / availability display only covers spawn-able agents.
73
+ const kinds = Object.keys(BINARY_MAP).filter((k) => BINARY_MAP[k] !== '');
69
74
  const entries = await Promise.all(kinds.map(async (k) => [k, await probeAgentAvailability(k)]));
70
75
  return new Map(entries);
71
76
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * AskUserQuestion 公共工厂与映射(agent 无关)。
3
+ *
4
+ * 接入约定(types.ts UserQuestion 注释的落点):
5
+ * - 各 runner 用 makeQuestionApprovalEvent 构造标准 question 审批事件,
6
+ * 避免四处手拼事件形状(claude/codex/kimi/pi 共用);
7
+ * - 用户答案经 ApprovalCoordinator 以 {问题文本: 值} 回传 runner 后,
8
+ * 用 mapAnswersByIndex 按问题顺序展开,再做协议级编码
9
+ * (Codex 问题 id、Kimi q{i}、Pi 单题取值)。
10
+ */
11
+ import type { ApprovalRequestedEvent, UserQuestion } from './types.js';
12
+ export interface QuestionEventOptions {
13
+ /** Per-request 审批超时覆盖(Codex autoResolutionMs)。 */
14
+ timeoutMs?: number;
15
+ }
16
+ /**
17
+ * 构造标准 question approval_requested 事件。
18
+ * availableDecisions 恒为空数组:提问只能作答或跳过(decline/cancel 是
19
+ * coordinator 的通用安全兜底),不允许 accept/decline 按钮混入提问卡。
20
+ */
21
+ export declare function makeQuestionApprovalEvent(requestId: number | string, questions: UserQuestion[], threadId: string, opts?: QuestionEventOptions): ApprovalRequestedEvent;
22
+ /**
23
+ * 按问题顺序把 coordinator 回传的 {问题文本: 值} 展开成索引对齐数组。
24
+ * 未作答的问题对应 undefined,由各 runner 决定协议编码(跳过/空 answers)。
25
+ */
26
+ export declare function mapAnswersByIndex(questions: UserQuestion[], answers: Record<string, string | string[]>): Array<string | string[] | undefined>;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * AskUserQuestion 公共工厂与映射(agent 无关)。
3
+ *
4
+ * 接入约定(types.ts UserQuestion 注释的落点):
5
+ * - 各 runner 用 makeQuestionApprovalEvent 构造标准 question 审批事件,
6
+ * 避免四处手拼事件形状(claude/codex/kimi/pi 共用);
7
+ * - 用户答案经 ApprovalCoordinator 以 {问题文本: 值} 回传 runner 后,
8
+ * 用 mapAnswersByIndex 按问题顺序展开,再做协议级编码
9
+ * (Codex 问题 id、Kimi q{i}、Pi 单题取值)。
10
+ */
11
+ /**
12
+ * 构造标准 question approval_requested 事件。
13
+ * availableDecisions 恒为空数组:提问只能作答或跳过(decline/cancel 是
14
+ * coordinator 的通用安全兜底),不允许 accept/decline 按钮混入提问卡。
15
+ */
16
+ export function makeQuestionApprovalEvent(requestId, questions, threadId, opts) {
17
+ const view = {
18
+ requestId,
19
+ kind: 'question',
20
+ questions,
21
+ availableDecisions: [],
22
+ };
23
+ return {
24
+ type: 'approval_requested',
25
+ requestId,
26
+ kind: 'question',
27
+ threadId,
28
+ turnId: '',
29
+ itemId: '',
30
+ view,
31
+ ...(opts?.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
32
+ timestamp: new Date().toISOString(),
33
+ };
34
+ }
35
+ /**
36
+ * 按问题顺序把 coordinator 回传的 {问题文本: 值} 展开成索引对齐数组。
37
+ * 未作答的问题对应 undefined,由各 runner 决定协议编码(跳过/空 answers)。
38
+ */
39
+ export function mapAnswersByIndex(questions, answers) {
40
+ return questions.map((q) => answers[q.question]);
41
+ }
42
+ //# sourceMappingURL=question-common.js.map
@@ -25,11 +25,18 @@ export interface ApprovalRequestedEvent {
25
25
  type: 'approval_requested';
26
26
  /** JSON-RPC id of the server request. Wire type is string | integer — keep raw. */
27
27
  requestId: number | string;
28
- kind: 'command' | 'file' | 'permissions' | 'question';
28
+ kind: 'command' | 'file' | 'permissions' | 'question' | 'tool';
29
29
  threadId: string;
30
30
  turnId: string;
31
31
  itemId: string;
32
32
  view: ApprovalView;
33
+ /**
34
+ * Per-request 审批超时覆盖(毫秒)。由 runner 在翻译时填入
35
+ * (Codex autoResolutionMs),coordinator 优先使用;
36
+ * 缺省回落 run 级默认超时。放在事件上而非桥按 agentKind 分支,
37
+ * 保证公共层无 agent 分支。
38
+ */
39
+ timeoutMs?: number;
33
40
  timestamp?: string;
34
41
  }
35
42
  export interface ApprovalResolvedEvent {
@@ -67,22 +74,52 @@ export type ApprovalAction = {
67
74
  } | {
68
75
  action: 'cancel';
69
76
  }
70
- /** Claude AskUserQuestion 答案(question → label 或 label[])。 */
77
+ /**
78
+ * AskUserQuestion 答案(question → label 或 label[])。
79
+ * notes:Codex user_note 补充说明(question → 文本),runner 编码为
80
+ * "user_note: <text>" 条目;其他 agent 忽略该字段。
81
+ */
71
82
  | {
72
83
  action: 'answer';
73
84
  answers: Record<string, string | string[]>;
85
+ notes?: Record<string, string>;
74
86
  };
75
- /** Claude Code AskUserQuestion 的单个选项(control_request input.questions[].options[])。 */
76
- export interface ClaudeUserQuestionOption {
87
+ /** AskUserQuestion 公共契约(Agent 无关)的单个选项。 */
88
+ export interface UserQuestionOption {
77
89
  label: string;
78
90
  description?: string;
79
91
  }
80
- /** Claude Code AskUserQuestion 的单个问题(control_request input.questions[])。 */
81
- export interface ClaudeUserQuestion {
92
+ /**
93
+ * AskUserQuestion 公共契约(Agent 无关)的单个问题。
94
+ *
95
+ * 接入约定(新 agent 接入 AskUserQuestion 时在这里扩展,禁止在 coordinator /
96
+ * renderer / router 里写 agent 分支):
97
+ * 1. 各 agent runner 把协议提问翻译为 `ApprovalView{ kind:'question', questions }`;
98
+ * 2. 用户回答经 ApprovalCoordinator 收集,以 `ApprovalAction{action:'answer',
99
+ * answers:{问题文本: string|string[]}}` 回传 runner;
100
+ * 3. runner 侧把文本 key 答案翻译回自己协议的 shape(Claude 直接用文本 key;
101
+ * Codex 需按问题 id 映射)。
102
+ */
103
+ export interface UserQuestion {
104
+ /** Agent 侧问题唯一标识(Codex request_user_input 的 id 预留;Claude 无此字段)。 */
105
+ id?: string;
82
106
  question: string;
83
107
  header?: string;
84
108
  multiSelect?: boolean;
85
- options: ClaudeUserQuestionOption[];
109
+ options: UserQuestionOption[];
110
+ /** 是否允许自定义答案(Codex isOther 预留;当前单选渲染层恒提供 Other 输入)。 */
111
+ isOther?: boolean;
112
+ /** 敏感输入(Codex isSecret 预留)。 */
113
+ isSecret?: boolean;
114
+ /** 自由文本题的输入框占位提示(渲染层自由文本题 input)。 */
115
+ placeholder?: string;
116
+ /**
117
+ * 是否渲染「补充说明(可选)」输入(Codex user_note;数据驱动无 agent 分支)。
118
+ * note 是选项之外的附加文本,随答案提交,不单独构成答案。
119
+ */
120
+ allowNote?: boolean;
121
+ /** 当前已填写的补充说明(卡片回显;coordinator answerNote 回流)。 */
122
+ note?: string;
86
123
  /**
87
124
  * 当前已选选项 label(卡片渲染与多选切换用;单选在协调器内即时提交)。
88
125
  * 与 permissions.items.selected 同模式:协调器原地更新后以
@@ -92,10 +129,24 @@ export interface ClaudeUserQuestion {
92
129
  }
93
130
  export interface ApprovalView {
94
131
  requestId: number | string;
95
- kind: 'command' | 'file' | 'permissions' | 'question';
132
+ kind: 'command' | 'file' | 'permissions' | 'question' | 'tool';
96
133
  reason?: string;
134
+ /**
135
+ * 提问卡顶部概要行(数据驱动,无 agent 分支):Kimi elicitation form 的
136
+ * 题面只在 form 级 message 里合并出现,逐字段 title 可能只是短 header,
137
+ * 概要行用于展示完整题干。
138
+ */
139
+ intro?: string;
97
140
  command?: string;
98
141
  commandCwd?: string;
142
+ /**
143
+ * 被审批的工具名(kind === 'tool' 时展示,如 ExitPlanMode)。非命令工具的
144
+ * 权限请求(plan 退出/通知型工具)input 无 command 语义,用工具名 + reason
145
+ * 表达审批内容,避免落入 command 槽位显示无意义 `{}`。
146
+ */
147
+ toolName?: string;
148
+ /** 原始 input 的 JSON 字符串(kind === 'tool' 且 input 非空时展示)。 */
149
+ toolInput?: string;
99
150
  fileChanges?: Array<{
100
151
  path: string;
101
152
  kind: 'add' | 'update' | 'delete';
@@ -115,7 +166,7 @@ export interface ApprovalView {
115
166
  }>;
116
167
  };
117
168
  /** Claude AskUserQuestion 问题列表(kind === 'question' 时存在)。 */
118
- questions?: ClaudeUserQuestion[];
169
+ questions?: UserQuestion[];
119
170
  availableDecisions: string[];
120
171
  /** Raw payloads for structured decisions (e.g. acceptWithExecpolicyAmendment). */
121
172
  decisionPayloads?: Record<string, unknown>;
@@ -283,12 +334,14 @@ export interface Runner {
283
334
  */
284
335
  unregisterExitHandlers?(): void;
285
336
  }
286
- export type AgentKind = 'claude' | 'codex' | 'opencode' | 'pi' | 'kimi';
337
+ export type AgentKind = 'claude' | 'codex' | 'opencode' | 'pi' | 'kimi' | 'dsh';
287
338
  /** Unified session descriptor returned by every AgentSessionReader. */
288
339
  export interface AgentSession {
289
340
  sessionId: string;
290
341
  summary: string;
291
342
  mtime: number;
343
+ /** DSH:session 创建时固定的 preset;其他 agent 无此概念(undefined)。 */
344
+ agentPreset?: string;
292
345
  }
293
346
  /** A single renderable event from session history. */
294
347
  export interface AgentSessionContentEvent {
@@ -154,7 +154,7 @@ function isSessionActive(filePath, mtimeMs, now = Date.now()) {
154
154
  // Explicit terminal markers — regardless of mtime. Claude CLI 2.x uses
155
155
  // `last-prompt` / `mode` (no `result` event) to mark turn end, so a
156
156
  // 24-minute-old session with a last-prompt tail is already done
157
- // (regression 2026-06-21: c5e929a7 falsely reported as active).
157
+ // (regression 2026-06-21: previously falsely reported as active).
158
158
  const TERMINAL_TYPES = new Set(['result', 'last-prompt', 'mode', 'permission-mode']);
159
159
  if (typeof obj.type === 'string' && TERMINAL_TYPES.has(obj.type))
160
160
  return false;
@@ -224,7 +224,16 @@ export function isClaudeSessionActive(sessionId, cwd, opts = {}) {
224
224
  */
225
225
  function scalarScan(filePath) {
226
226
  const acc = new UsageAccumulator();
227
- const seenMessageIds = new Set();
227
+ // Per-message-id best usage record (per-field max). A streaming assistant
228
+ // message spans multiple jsonl lines sharing the same message id; some
229
+ // backends (third-party gateways, e.g. DeepSeek) write all-zero usage on
230
+ // the early placeholder lines (empty thinking block) and the real usage
231
+ // only on the final line carrying stop_reason. First-occurrence-wins dedup
232
+ // would pin the zero record and discard the real one (regression
233
+ // 2026-08-19: auto-resume card per-run stats all 0). Per-field max
234
+ // reconstructs the full usage regardless of line order — on the official
235
+ // API every line repeats identical usage, so max is a no-op there.
236
+ const usageByMessageId = new Map();
228
237
  let lastPostTokens = 0;
229
238
  let tailOffset = -1;
230
239
  let lastAiTitle;
@@ -244,13 +253,13 @@ function scalarScan(filePath) {
244
253
  const usage = msg?.usage;
245
254
  if (usage) {
246
255
  const msgId = msg?.id;
247
- if (msgId && !seenMessageIds.has(msgId)) {
248
- seenMessageIds.add(msgId);
249
- acc.add({
250
- input: usage.input_tokens ?? 0,
251
- output: usage.output_tokens ?? 0,
252
- cacheRead: usage.cache_read_input_tokens ?? 0,
253
- cacheCreation: usage.cache_creation_input_tokens ?? 0,
256
+ if (msgId) {
257
+ const prev = usageByMessageId.get(msgId);
258
+ usageByMessageId.set(msgId, {
259
+ input: Math.max(prev?.input ?? 0, usage.input_tokens ?? 0),
260
+ output: Math.max(prev?.output ?? 0, usage.output_tokens ?? 0),
261
+ cacheRead: Math.max(prev?.cacheRead ?? 0, usage.cache_read_input_tokens ?? 0),
262
+ cacheCreation: Math.max(prev?.cacheCreation ?? 0, usage.cache_creation_input_tokens ?? 0),
254
263
  });
255
264
  }
256
265
  }
@@ -305,6 +314,12 @@ function scalarScan(filePath) {
305
314
  }
306
315
  }
307
316
  });
317
+ // Feed one best record per message id, in first-appearance order (Map
318
+ // preserves insertion order), so acc.last is the LAST message's full usage
319
+ // and totals sum each API response exactly once.
320
+ for (const record of usageByMessageId.values()) {
321
+ acc.add(record);
322
+ }
308
323
  // Build usage result (same logic as old aggregateSessionUsage)
309
324
  const t = acc.totals;
310
325
  const l = acc.last;
@@ -319,14 +334,19 @@ function scalarScan(filePath) {
319
334
  if (contextLength === 0 || lastWindow > contextLength) {
320
335
  contextLength = lastWindow;
321
336
  }
337
+ // 非累计字段 = 末轮(本 run)scope,累计字段 = session 总和(所有 run)。
338
+ // 对齐 codex reader 语义(§9.21:`last_token_usage`=单 turn 增量、
339
+ // `total_token_usage`=session 累计)——否则 jsonl 兜底路径会把 session 累计
340
+ // 当成"本 run"显示,与卡片"本 run ≤ 累计"不变量冲突(累计反而更小)。
341
+ const lastTotal = l ? l.input + l.output + l.cacheRead + l.cacheCreation : 0;
322
342
  const usage = {
323
- inputTokens: t.input,
324
- outputTokens: t.output,
343
+ inputTokens: l ? l.input : 0,
344
+ outputTokens: l ? l.output : 0,
325
345
  contextLength,
326
346
  compactCount: acc.compactCount,
327
- cacheReadTokens: t.cacheRead > 0 ? t.cacheRead : undefined,
328
- cacheCreationTokens: t.cacheCreation > 0 ? t.cacheCreation : undefined,
329
- totalTokens: t.input + t.output + t.cacheRead + t.cacheCreation,
347
+ cacheReadTokens: l && l.cacheRead > 0 ? l.cacheRead : undefined,
348
+ cacheCreationTokens: l && l.cacheCreation > 0 ? l.cacheCreation : undefined,
349
+ totalTokens: lastTotal,
330
350
  cumulativeTotalTokens: t.input + t.output + t.cacheRead + t.cacheCreation,
331
351
  cumulativeInputTokens: t.input,
332
352
  cumulativeOutputTokens: t.output,
@@ -435,7 +455,9 @@ export function readSessionContent(sessionId, cwd, opts = {}) {
435
455
  // ~3×). Total parse count adds the index-miss fallback guard's fixed
436
456
  // early-stop overhead (~2 parses, independent of line count).
437
457
  const scan = scalarScan(filePath);
438
- const usage = scan.usage.inputTokens + scan.usage.outputTokens > 0 ? scan.usage : undefined;
458
+ // 有 usage 即返回:非累计字段已是末轮 scope,命中判定用 session 累计兜底
459
+ // (末轮可能 input/output 为 0 而仍有 cacheRead,累计口径不会被末轮遮挡)。
460
+ const usage = (scan.usage.cumulativeTotalTokens ?? 0) > 0 ? scan.usage : undefined;
439
461
  // Extract events AFTER the last user message by re-reading + re-parsing
440
462
  // only the tail. If no user message was found (tailOffset === -1), fall
441
463
  // back to re-reading the whole file so the card still has something to
@@ -0,0 +1 @@
1
+ export { DshSessionReader } from './sessions.js';
@@ -0,0 +1,2 @@
1
+ export { DshSessionReader } from './sessions.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * DshSessionReader — AgentSessionReader backed by the DSH Web Host over HTTP.
3
+ *
4
+ * The AgentSessionReader contract is fully synchronous (file-style readers),
5
+ * but DSH exposes only async HTTP. The reader therefore bridges with
6
+ * `spawnSync('curl', ...)` for its unary calls (session.list / session.history)
7
+ * — a synchronous HTTP round-trip to the local DSH host. This keeps the
8
+ * router/bridge interface contract intact without touching any upstream logic.
9
+ *
10
+ * listSessions: session.list → filter by cwd → sort by updatedAt desc → local
11
+ * pagination (DSH v1 has no cursor pagination).
12
+ * readSessionContent: session.history → map SessionEvents to display events +
13
+ * session-wide usage accumulation (assistant/message.usage).
14
+ */
15
+ import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
16
+ interface DshSessionReaderOptions {
17
+ host?: string;
18
+ /** Injectable synchronous unary transport. Defaults to spawnSync curl. */
19
+ syncRequest?: (baseUrl: string, method: string, payload: unknown) => unknown;
20
+ }
21
+ export declare class DshSessionReader implements AgentSessionReader {
22
+ private readonly baseUrl;
23
+ private readonly syncRequest;
24
+ constructor(opts?: DshSessionReaderOptions);
25
+ private unary;
26
+ listSessions(cwd: string, opts?: {
27
+ limit?: number;
28
+ offset?: number;
29
+ }): {
30
+ sessions: AgentSession[];
31
+ total: number;
32
+ };
33
+ getNewestSession(cwd: string): AgentSession | null;
34
+ readSessionContent(sessionId: string, cwd: string, opts?: {
35
+ maxEvents?: number;
36
+ }): SessionContent;
37
+ isSessionActive(sessionId: string, cwd: string): boolean;
38
+ }
39
+ export {};