lark-remote 0.5.5 → 0.5.7

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 (58) hide show
  1. package/CHANGELOG.md +48 -7
  2. package/README.en.md +2 -2
  3. package/README.md +2 -2
  4. package/dist/bridge/approval-coordinator.d.ts +13 -0
  5. package/dist/bridge/approval-coordinator.js +34 -1
  6. package/dist/bridge/index.d.ts +48 -1
  7. package/dist/bridge/index.js +86 -14
  8. package/dist/card/approval-render.js +23 -1
  9. package/dist/card/card-budget.d.ts +37 -0
  10. package/dist/card/card-budget.js +128 -5
  11. package/dist/card/run-renderer.d.ts +24 -0
  12. package/dist/card/run-renderer.js +256 -60
  13. package/dist/card/run-state.d.ts +26 -0
  14. package/dist/card/run-state.js +45 -0
  15. package/dist/card/time.d.ts +14 -0
  16. package/dist/card/time.js +30 -0
  17. package/dist/card/tool-render.d.ts +15 -2
  18. package/dist/card/tool-render.js +40 -66
  19. package/dist/common/text-width.d.ts +24 -0
  20. package/dist/common/text-width.js +93 -0
  21. package/dist/common/tool-summary.d.ts +11 -0
  22. package/dist/common/tool-summary.js +153 -0
  23. package/dist/config/index.d.ts +1 -1
  24. package/dist/config/index.js +1 -1
  25. package/dist/connector/index.js +1 -1
  26. package/dist/index.js +63 -8
  27. package/dist/platform/command.js +1 -1
  28. package/dist/platform/shell.d.ts +2 -2
  29. package/dist/platform/spawn.d.ts +1 -1
  30. package/dist/platform/spawn.js +2 -2
  31. package/dist/platform/types.d.ts +1 -1
  32. package/dist/router/card-helpers.d.ts +18 -16
  33. package/dist/router/card-helpers.js +65 -4
  34. package/dist/router/index.d.ts +9 -0
  35. package/dist/router/index.js +57 -7
  36. package/dist/runner/claude/session.d.ts +1 -1
  37. package/dist/runner/claude/session.js +3 -3
  38. package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
  39. package/dist/runner/common/jsonrpc/transport.js +3 -3
  40. package/dist/runner/common/spawning-runner.d.ts +1 -1
  41. package/dist/runner/common/spawning-runner.js +2 -2
  42. package/dist/runner/dsh/auth.js +2 -1
  43. package/dist/runner/dsh/client.d.ts +124 -6
  44. package/dist/runner/dsh/client.js +398 -12
  45. package/dist/runner/dsh/exit-cancel.d.ts +32 -0
  46. package/dist/runner/dsh/exit-cancel.js +72 -0
  47. package/dist/runner/dsh/inflight.d.ts +62 -0
  48. package/dist/runner/dsh/inflight.js +140 -0
  49. package/dist/runner/dsh/runner.d.ts +117 -7
  50. package/dist/runner/dsh/runner.js +607 -21
  51. package/dist/runner/dsh/translator.d.ts +1 -1
  52. package/dist/runner/dsh/translator.js +103 -1
  53. package/dist/runner/dsh/types.d.ts +91 -1
  54. package/dist/runner/types.d.ts +86 -2
  55. package/dist/session/claude/sessions.js +2 -2
  56. package/dist/session/dsh/sessions.js +100 -15
  57. package/dist/update/install.js +1 -1
  58. package/package.json +1 -1
@@ -12,7 +12,7 @@
12
12
  * completed → success; aborted/interrupted → interrupted;
13
13
  * blocked/error/max-tokens → error (errorMessage carries the reason)
14
14
  *
15
- * Token accounting (ccusage 口径, AGENTS.md §9.21): DSH emits ONE usage per model
15
+ * Token accounting (ccusage 口径): DSH emits ONE usage per model
16
16
  * call (per step) — a tool-looping turn has several `assistant/message` events.
17
17
  * The result event therefore carries **the sum of the turn's samplings**, not the
18
18
  * last one (codex `token_count` has the same shape). `lastUsage` is kept only as
@@ -12,7 +12,7 @@
12
12
  * completed → success; aborted/interrupted → interrupted;
13
13
  * blocked/error/max-tokens → error (errorMessage carries the reason)
14
14
  *
15
- * Token accounting (ccusage 口径, AGENTS.md §9.21): DSH emits ONE usage per model
15
+ * Token accounting (ccusage 口径): DSH emits ONE usage per model
16
16
  * call (per step) — a tool-looping turn has several `assistant/message` events.
17
17
  * The result event therefore carries **the sum of the turn's samplings**, not the
18
18
  * last one (codex `token_count` has the same shape). `lastUsage` is kept only as
@@ -118,6 +118,48 @@ export class DshTranslator {
118
118
  }
119
119
  return [];
120
120
  }
121
+ case 'tool/call': {
122
+ // DSH 把「模型发起调用」落成独立事件(name/arguments 是模型原始输出)。
123
+ // 这是卡片工具行的唯一来源:assistant/message 里只有完成后的正文块,
124
+ // 不含调用轨迹。
125
+ const call = ev.data;
126
+ const id = typeof call.callId === 'string' ? call.callId : '';
127
+ const name = typeof call.name === 'string' ? call.name : '';
128
+ if (!id || !name)
129
+ return [];
130
+ return [
131
+ {
132
+ type: 'assistant',
133
+ message: {
134
+ content: [{ type: 'tool_use', id, name, input: parseToolArguments(call.arguments) }],
135
+ },
136
+ timestamp: new Date(ev.time).toISOString(),
137
+ },
138
+ ];
139
+ }
140
+ case 'tool/result': {
141
+ const data = ev.data;
142
+ const msg = data.message;
143
+ const toolCallId = typeof msg?.toolCallId === 'string' ? msg.toolCallId : '';
144
+ if (!toolCallId)
145
+ return [];
146
+ return [
147
+ {
148
+ type: 'user',
149
+ message: {
150
+ content: [
151
+ {
152
+ type: 'tool_result',
153
+ tool_use_id: toolCallId,
154
+ content: toolResultText(msg?.content, data.error),
155
+ is_error: msg?.isError === true,
156
+ },
157
+ ],
158
+ },
159
+ timestamp: new Date(ev.time).toISOString(),
160
+ },
161
+ ];
162
+ }
121
163
  case 'turn/end': {
122
164
  const turnReason = ev.data.reason;
123
165
  const reason = turnReason?.kind;
@@ -153,6 +195,66 @@ export class DshTranslator {
153
195
  }
154
196
  }
155
197
  }
198
+ /**
199
+ * Parse a `tool/call.arguments` payload (a JSON string on the wire) into the
200
+ * object shape the card renderer consumes. Non-JSON arguments (a truncated or
201
+ * provider-mangled payload) are kept verbatim so the header can still show
202
+ * something instead of silently rendering an empty summary.
203
+ */
204
+ function parseToolArguments(raw) {
205
+ if (typeof raw !== 'string')
206
+ return raw ?? {};
207
+ const trimmed = raw.trim();
208
+ if (!trimmed)
209
+ return {};
210
+ try {
211
+ return JSON.parse(trimmed);
212
+ }
213
+ catch {
214
+ return trimmed;
215
+ }
216
+ }
217
+ /**
218
+ * Flatten a `tool/result.message.content` block list into display text.
219
+ *
220
+ * DSH content is a block array (`{type:'text',text}` / `{type:'image',...}`),
221
+ * the same shape the LLM sees. Images collapse to a marker: the card cannot
222
+ * render them and the raw base64 would blow the byte budget.
223
+ */
224
+ function toolResultText(content, error) {
225
+ const err = error;
226
+ const errorHint = typeof err?.message === 'string' ? err.message : '';
227
+ if (content == null)
228
+ return errorHint;
229
+ if (typeof content === 'string')
230
+ return content;
231
+ if (Array.isArray(content)) {
232
+ const parts = [];
233
+ for (const block of content) {
234
+ if (typeof block === 'string') {
235
+ parts.push(block);
236
+ continue;
237
+ }
238
+ if (!block || typeof block !== 'object')
239
+ continue;
240
+ const rec = block;
241
+ if (typeof rec.text === 'string') {
242
+ parts.push(rec.text);
243
+ }
244
+ else if (rec.type === 'image') {
245
+ parts.push('[image]');
246
+ }
247
+ }
248
+ const text = parts.join('\n');
249
+ return text || errorHint;
250
+ }
251
+ try {
252
+ return JSON.stringify(content);
253
+ }
254
+ catch {
255
+ return String(content);
256
+ }
257
+ }
156
258
  /** Map DSH TokenUsage → ResultEvent.usage (cacheWrite = cache creation). */
157
259
  export function mapUsage(u) {
158
260
  return mapAggregatedUsage({
@@ -60,6 +60,27 @@ export interface DshSessionSummary {
60
60
  export interface DshSessionListValue {
61
61
  items: DshSessionSummary[];
62
62
  }
63
+ /**
64
+ * One pending inbox occurrence from the `inbox` projection.
65
+ *
66
+ * DSH 在 prompt 被受理但还没被 turn 取走时,把它记录成 `inbox.next-turn[]`
67
+ * 的一项;`source.rpcId` 回显 `session/prompt` 的 requestId,因此调用方可以
68
+ * 精确找到“我这条还没开始的消息”。`session/updateQueue` 用 `id` 删除它。
69
+ */
70
+ export interface DshInboxItem {
71
+ id: string;
72
+ source?: {
73
+ kind?: string;
74
+ rpcId?: string;
75
+ };
76
+ }
77
+ /** `session/projections` → value(只声明本仓库用到的部分)。 */
78
+ export interface DshProjectionSnapshot {
79
+ asOfSeq?: number;
80
+ values: Record<string, unknown>;
81
+ }
82
+ /** Whether the host currently has a turn open for a session. */
83
+ export type DshOpenTurnState = 'open' | 'closed' | 'unknown';
63
84
  /** session/page → value.records[]. */
64
85
  export interface DshPageRecord {
65
86
  event: DshSessionEvent;
@@ -218,7 +239,32 @@ export type DshRemoteEventOutcome = {
218
239
  details?: unknown;
219
240
  };
220
241
  };
221
- /** Approval-related items yielded while consuming `$events`. */
242
+ /** One option of a DSH `ask_user_question` prompt. */
243
+ export interface DshQuestionOption {
244
+ label: string;
245
+ description?: string;
246
+ }
247
+ /** One question of a DSH `ask_user_question` prompt (`user-questions/request`). */
248
+ export interface DshQuestion {
249
+ id: string;
250
+ question: string;
251
+ /**
252
+ * Supporting detail kept out of the question text. `intent.kind:'plan-review'`
253
+ * questions carry the plan markdown here, so it must reach the card.
254
+ */
255
+ detail?: string;
256
+ header?: string;
257
+ options?: DshQuestionOption[];
258
+ multiSelect?: boolean;
259
+ }
260
+ /** One answer item of the `$events/result` payload for a question waterfall. */
261
+ export interface DshQuestionAnswer {
262
+ id: string;
263
+ selected: string[];
264
+ /** Free-text answer when the user picked none of the offered labels. */
265
+ custom?: string;
266
+ }
267
+ /** Approval/question items yielded while consuming `$events`. */
222
268
  export type DshApprovalStreamItem = {
223
269
  kind: 'ready';
224
270
  clientId: string;
@@ -229,6 +275,11 @@ export type DshApprovalStreamItem = {
229
275
  toolName: string;
230
276
  callId?: string;
231
277
  reason?: string;
278
+ } | {
279
+ kind: 'question';
280
+ eventId: string;
281
+ clientId: string;
282
+ questions: DshQuestion[];
232
283
  } | {
233
284
  kind: 'cancelled';
234
285
  eventId: string;
@@ -279,3 +330,42 @@ export type DshStreamItem = {
279
330
  callId?: string;
280
331
  reason?: string;
281
332
  };
333
+ /**
334
+ * One finished projection value from the Host-wide `session/control` stream.
335
+ *
336
+ * DSH 的 `session/control` 是唯一的"服务端变一次推一次"通道:连接先收到一份
337
+ * baseline(覆盖当前所有活跃 session 的全部投影值),之后每个投影变化推一帧
338
+ * `{type:'projection', sessionId, key, value, seq}`。调用方按 sessionId 过滤。
339
+ */
340
+ export interface DshProjectionItem {
341
+ kind: 'projection';
342
+ sessionId: string;
343
+ key: string;
344
+ value: unknown;
345
+ }
346
+ /** `contextPressure` 投影(只声明本仓库消费的字段)。 */
347
+ export interface DshContextPressure {
348
+ /** 上一次采样实际占用的 token。 */
349
+ pressureTokens?: number;
350
+ /** 下一次请求的预估占用(官方 UI 优先用它算百分比)。 */
351
+ projectedTokens?: number;
352
+ /** 当前模型上下文窗口。 */
353
+ contextWindow?: number;
354
+ }
355
+ /** `sessionStats` 投影(只声明本仓库消费的字段)。 */
356
+ export interface DshSessionStats {
357
+ turns?: number;
358
+ steps?: number;
359
+ toolMs?: number;
360
+ }
361
+ /** One background job row from the `job/list` stream (fields lark-remote consumes). */
362
+ export interface DshJobView {
363
+ id: string;
364
+ kind?: string;
365
+ label?: string;
366
+ status?: 'running' | 'stopping' | 'completed' | 'killed' | 'failed';
367
+ progress?: string;
368
+ detail?: string;
369
+ startedAt?: number;
370
+ finishedAt?: number;
371
+ }
@@ -124,6 +124,11 @@ export interface UserQuestion {
124
124
  header?: string;
125
125
  multiSelect?: boolean;
126
126
  options: UserQuestionOption[];
127
+ /**
128
+ * 题面外的补充正文(DSH `AskUserQuestionItem.detail`):plan-review 提问用它
129
+ * 承载计划全文。渲染层折叠展示并截断,不能与 header / question 混写。
130
+ */
131
+ detail?: string;
127
132
  /** 是否允许自定义答案(Codex isOther 预留;当前单选渲染层恒提供 Other 输入)。 */
128
133
  isOther?: boolean;
129
134
  /** 敏感输入(Codex isSecret 预留)。 */
@@ -159,6 +164,11 @@ export interface ApprovalView {
159
164
  * 概要行用于展示完整题干。
160
165
  */
161
166
  intro?: string;
167
+ /**
168
+ * 提问超时后是否还能在别处回答的提示(数据驱动,避免共享渲染层写死某个
169
+ * agent 的语义:DSH 提问超时后 Web UI 仍可回答,Claude/Codex 不能)。
170
+ */
171
+ expiredHint?: string;
162
172
  command?: string;
163
173
  commandCwd?: string;
164
174
  /**
@@ -315,6 +325,45 @@ export interface SessionInfoEvent {
315
325
  mode?: string;
316
326
  timestamp?: string;
317
327
  }
328
+ /**
329
+ * 运行期状态投影(上下文水位 / 会话累计统计)。
330
+ *
331
+ * 不是内容块:卡片把它渲染在状态行(statusRow),不进内容流,因此不参与
332
+ * blocks 分组与 MAX_BLOCKS 裁剪。字段缺省表示该维度本次未变。
333
+ *
334
+ * 数据源是 agent 服务端自己算好的投影(DSH `session/control` 的
335
+ * contextPressure / sessionStats),bridge 不重算口径——百分比只在两端都
336
+ * 有值时才渲染。
337
+ */
338
+ export interface RunStatusEvent {
339
+ type: 'run_status';
340
+ /** 已投影 token(官方口径 projectedTokens ?? pressureTokens)。 */
341
+ contextUsed?: number;
342
+ /** 上下文窗口上限(contextWindow)。 */
343
+ contextLimit?: number;
344
+ /** 会话累计步骤数(sessionStats.steps)。 */
345
+ steps?: number;
346
+ /** 会话累计工具耗时(毫秒,sessionStats.toolMs)。 */
347
+ toolMs?: number;
348
+ timestamp?: string;
349
+ }
350
+ /**
351
+ * 运行结束后仍在后台运行的任务(终态卡展示 + 逐个停止入口)。
352
+ *
353
+ * 为什么必须出现在 run 卡片上:后台任务的生命周期跨 turn(`bash
354
+ * run_in_background` 起的东西能活过 run 结束),卡片收尾时如果不说,用户就
355
+ * 以为这一轮真的结束了——机器上却还在跑。
356
+ */
357
+ export interface BackgroundJobsEvent {
358
+ type: 'background_jobs';
359
+ jobs: Array<{
360
+ id: string;
361
+ label: string;
362
+ status?: string;
363
+ progress?: string;
364
+ }>;
365
+ timestamp?: string;
366
+ }
318
367
  /** File change event — for agents that report file modifications (e.g. Codex). */
319
368
  export interface FileChangeEvent {
320
369
  type: 'file_change';
@@ -382,7 +431,7 @@ export interface TurnDiffEvent {
382
431
  export type RunnerLifetime = 'turn' | 'workspace';
383
432
  /** 工具渲染 hint:runner 自愿声明,渲染层只消费不推断。 */
384
433
  export type ToolHint = 'shell' | 'file' | 'search' | 'web' | 'patch';
385
- export type AgentEvent = SystemInitEvent | SystemCompactEvent | ApprovalRequestedEvent | ApprovalResolvedEvent | ApprovalViewUpdatedEvent | ApprovalExpiredEvent | AssistantEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent | TurnStartedEvent | TurnDiffEvent | NoticeEvent | SessionInfoEvent;
434
+ export type AgentEvent = SystemInitEvent | SystemCompactEvent | ApprovalRequestedEvent | ApprovalResolvedEvent | ApprovalViewUpdatedEvent | ApprovalExpiredEvent | AssistantEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent | TurnStartedEvent | TurnDiffEvent | NoticeEvent | SessionInfoEvent | RunStatusEvent | BackgroundJobsEvent;
386
435
  export interface SpawnOptions {
387
436
  cwd: string;
388
437
  sessionId?: string;
@@ -392,6 +441,29 @@ export interface SpawnOptions {
392
441
  reasoningEffort?: string;
393
442
  settings?: string;
394
443
  }
444
+ /**
445
+ * What a stop request actually achieved.
446
+ *
447
+ * Local-process runners (`claude`/`codex`/…) can answer this from the kill
448
+ * itself and keep returning `void`. DSH turns live in the shared Web Host, so
449
+ * its stop is a session-scoped request: the outcome is the only way callers can
450
+ * tell "the turn is gone" apart from "we asked, but it is still running".
451
+ *
452
+ * - `withdrawn` — the prompt had not started yet and was removed from the
453
+ * host's inbox; no turn was cancelled.
454
+ * - `cancelled` — the active turn (or maintenance job) was cancelled and the
455
+ * host reported the session idle.
456
+ * - `unconfirmed` — the cancel was rejected/lost, or the session still reports
457
+ * an open turn afterwards. A turn may still be running.
458
+ * - `noop` — there was no session to stop (e.g. stop during setup).
459
+ */
460
+ export type StopStatus = 'withdrawn' | 'cancelled' | 'unconfirmed' | 'noop';
461
+ export interface StopOutcome {
462
+ status: StopStatus;
463
+ sessionId?: string;
464
+ /** Human-readable reason for `unconfirmed` (logged, not shown verbatim). */
465
+ detail?: string;
466
+ }
395
467
  /**
396
468
  * Seam (§3): the contract any agent runner (claude/codex/opencode/pi/kimi/dsh)
397
469
  * must satisfy for the router/bridge.
@@ -407,10 +479,14 @@ export interface Runner {
407
479
  * Stop the running process. SIGTERM → grace → SIGKILL on the per-workspace
408
480
  * proc. `immediate: true` skips the grace period and SIGKILLs directly
409
481
  * (used by user-initiated `/stop`).
482
+ *
483
+ * Runners whose work is remote (DSH) return a {@link StopOutcome} instead of
484
+ * `void` so callers can tell a confirmed stop from a best-effort request;
485
+ * process runners keep returning `void`.
410
486
  */
411
487
  stop(opts?: {
412
488
  immediate?: boolean;
413
- }): Promise<void>;
489
+ }): Promise<void | StopOutcome>;
414
490
  killOrphan(): void;
415
491
  registerExitHandlers(): void;
416
492
  /**
@@ -457,7 +533,15 @@ export interface AgentSession {
457
533
  /** A single renderable event from session history. */
458
534
  export interface AgentSessionContentEvent {
459
535
  type: string;
536
+ /** 正文(展开后的面板内容)。 */
460
537
  content: string;
538
+ /**
539
+ * 面板标题(可选,覆盖 reader 的默认标签)。
540
+ *
541
+ * 折叠面板默认只显示标题:工具调用这种"标题即全部信息"的事件必须把
542
+ * `bash — npm test` 这类摘要放进标题,否则折叠态只剩 `🔧 工具调用`。
543
+ */
544
+ title?: string;
461
545
  timestamp?: string;
462
546
  }
463
547
  export interface AgentSessionUsage {
@@ -15,8 +15,8 @@ import { SessionIndex, extractText, isTaskNotificationText, CLAUDE_MAPPING, } fr
15
15
  * win32 上 `\` 同样是分隔符、`:` 是非法文件名字符,一并归一为 `-`
16
16
  * (`C:\Users\x\proj` -> `C--Users-x-proj`);见 encodeProjectDirName。
17
17
  *
18
- * Note: this encoding is LOSSY (`disk_d/foo` and `disk-d/foo` both encode to
19
- * `disk-d-foo`). It is only safe to use the result to **locate** files; never
18
+ * Note: this encoding is LOSSY (`my_data/foo` and `my-data/foo` both encode to
19
+ * `my-data-foo`). It is only safe to use the result to **locate** files; never
20
20
  * to **decode** a directory name back to a cwd. The cwd must always be read
21
21
  * from the JSONL content (regression 2026-06-21 /resume & /cd 路径错乱).
22
22
  */
@@ -24,6 +24,7 @@ import { dshArgs, extractDshToken } from '../../runner/dsh/client.js';
24
24
  import { DSH_COOKIE_LIFETIME_MS, isDshCredentialsNotFound, mintDshBrowserCookie, readDshBrowserSecretSync, resolveDshCredentialsFile, } from '../../runner/dsh/auth.js';
25
25
  import { capEvents, paginate } from '../common/pagination.js';
26
26
  import { truncateUtf8, TOOL_RESULT_MAX_BYTES } from '../../common/truncate.js';
27
+ import { summarizeToolInput } from '../../common/tool-summary.js';
27
28
  /**
28
29
  * history 拉取上限。DSH session/page 的 maxMessages 同时限制返回条数,
29
30
  * 若用它同时充当展示截断,>50 事件的会话累计 usage 会少算。这里拉取一个足够大的
@@ -218,6 +219,8 @@ export class DshSessionReader {
218
219
  // null = 历史里没有 turn 标记(旧数据/截断)→ 退回末条 usage 语义。
219
220
  let turnUsage = null;
220
221
  let lastTurnUsage = null;
222
+ /** 等结果的工具调用(见 MappedEvent.pendingTool)。 */
223
+ let pendingTool;
221
224
  for (const entry of value.records ?? []) {
222
225
  const ev = entry.event;
223
226
  if (!ev)
@@ -231,7 +234,30 @@ export class DshSessionReader {
231
234
  turnUsage = null;
232
235
  }
233
236
  const contentEvents = mapEvent(ev);
234
- events.push(...contentEvents.events);
237
+ if (contentEvents.pendingTool) {
238
+ // 未完成的工具调用最多同时一个(DSH 按 step 串行执行;并行调用时按
239
+ // 到达顺序配对,最坏情况是两次调用共用一个面板,不会丢信息)。
240
+ if (pendingTool)
241
+ events.push(pendingTool);
242
+ pendingTool = contentEvents.pendingTool;
243
+ }
244
+ else if (ev.type === 'tool/result' && pendingTool && contentEvents.events[0]) {
245
+ const result = contentEvents.events[0];
246
+ // 标题沿用调用的 `🔧 name — summary`,失败改成 ❌(折叠态一眼看出结果)。
247
+ const failed = result.title?.startsWith('🔴') === true;
248
+ events.push({
249
+ type: 'tool_use',
250
+ title: failed
251
+ ? (pendingTool.title ?? '❌ 工具').replace('🔧', '❌')
252
+ : pendingTool.title,
253
+ content: result.content,
254
+ timestamp: pendingTool.timestamp,
255
+ });
256
+ pendingTool = undefined;
257
+ }
258
+ else {
259
+ events.push(...contentEvents.events);
260
+ }
235
261
  if (contentEvents.title)
236
262
  displayTitle = contentEvents.title;
237
263
  const u = contentEvents.usage;
@@ -278,6 +304,9 @@ export class DshSessionReader {
278
304
  }),
279
305
  };
280
306
  }
307
+ // 中断/仍在执行的调用没有结果:把它当独立事件留下,别让这一条消失。
308
+ if (pendingTool)
309
+ events.push(pendingTool);
281
310
  return {
282
311
  // 展示仍按 maxEvents 尾截断(默认 50,保持原行为),usage 已在上面基于
283
312
  // 完整 history 累加。
@@ -327,6 +356,25 @@ function sessionPreset(item) {
327
356
  function emptyUsageTotals() {
328
357
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
329
358
  }
359
+ /**
360
+ * `user/message.source.kind` 里属于**纯上下文注入**的种类:没有回顾价值,且
361
+ * 会挤掉真正的对话(5 条事件的窗口里被它们占满就什么都看不到了)。
362
+ */
363
+ const CONTEXT_INJECTION_KINDS = new Set([
364
+ 'time-context',
365
+ 'agent-instructions',
366
+ 'runtime-context',
367
+ 'skill-catalog',
368
+ 'compact-checkpoint',
369
+ ]);
370
+ /** 系统通知(后台任务完成、目标变更)在回顾卡里的正文上限。 */
371
+ const NOTICE_MAX_BYTES = 400;
372
+ /** 单条思考在 resume 卡里的上限:过程信息,留头几百字即可。 */
373
+ const REASONING_MAX_BYTES = 800;
374
+ /** 单条 assistant 正文上限(最终答复通常最重要,给足空间)。 */
375
+ const TEXT_MAX_BYTES = 2_000;
376
+ /** 工具原始参数上限(标题已有摘要,正文只用于看完整命令)。 */
377
+ const TOOL_INPUT_MAX_BYTES = 600;
330
378
  /** Map one DSH SessionEvent to display content events (+ title/usage side effects). */
331
379
  function mapEvent(ev) {
332
380
  const out = { events: [] };
@@ -337,42 +385,79 @@ function mapEvent(ev) {
337
385
  .filter((c) => c.type === 'text')
338
386
  .map((c) => c.text ?? '')
339
387
  .join('');
340
- out.events.push({ type: 'user', content: text, timestamp: new Date(ev.time).toISOString() });
388
+ if (!text.trim())
389
+ break;
390
+ // DSH 把上下文注入也写成 user/message,靠 `source.kind` 区分。真实会话里
391
+ // 30 条 user/message 中只有 8 条是用户本人说的(其余是 time-context /
392
+ // agent-instructions / skill-catalog / goal / tool-jobs…)——照单全收会让
393
+ // 回顾卡里塞满"Time sampled while preparing turn…",还把系统提示标成 👤 你。
394
+ const source = ev.data.source;
395
+ const kind = typeof source?.kind === 'string' ? source.kind : undefined;
396
+ if (kind && CONTEXT_INJECTION_KINDS.has(kind))
397
+ break;
398
+ const isNotice = kind !== undefined && kind !== 'user';
399
+ out.events.push({
400
+ type: isNotice ? 'notice' : 'user',
401
+ ...(isNotice ? { title: '🔔 通知' } : {}),
402
+ content: isNotice ? truncateUtf8(text.trim(), NOTICE_MAX_BYTES) : text,
403
+ timestamp: new Date(ev.time).toISOString(),
404
+ });
341
405
  break;
342
406
  }
343
407
  case 'assistant/message': {
344
408
  const content = ev.data.message
345
409
  ?.content ?? [];
346
- const text = content
347
- .filter((c) => c.type === 'text')
348
- .map((c) => c.text ?? '')
349
- .join('');
350
- if (text)
351
- out.events.push({
352
- type: 'assistant',
353
- content: text,
354
- timestamp: new Date(ev.time).toISOString(),
355
- });
410
+ const timestamp = new Date(ev.time).toISOString();
411
+ // DSH 的块类型是 text / reasoning / tool-call(不是 Anthropic 的
412
+ // thinking)。只认 text 会让 resume 卡"只有工具调用、没有思考与输出"
413
+ // (2026-10-19 反馈:一个 31 turn 的会话里 reasoning 块 371 个、text 12 个,
414
+ // 全被丢掉)。tool-call 块不在这里取——独立的 `tool/call` 事件带完整参数。
415
+ for (const block of content) {
416
+ const text = typeof block.text === 'string' ? block.text.trim() : '';
417
+ if (!text)
418
+ continue;
419
+ if (block.type === 'text') {
420
+ out.events.push({
421
+ type: 'assistant',
422
+ content: truncateUtf8(text, TEXT_MAX_BYTES),
423
+ timestamp,
424
+ });
425
+ }
426
+ else if (block.type === 'reasoning') {
427
+ out.events.push({
428
+ type: 'thinking',
429
+ // 思考是过程信息:resume 卡里留头几百字足够判断"当时在想什么"。
430
+ content: truncateUtf8(text, REASONING_MAX_BYTES),
431
+ timestamp,
432
+ });
433
+ }
434
+ }
356
435
  const usage = ev.data.usage;
357
436
  if (usage && typeof usage === 'object')
358
437
  out.usage = usage;
359
438
  break;
360
439
  }
361
440
  case 'tool/call': {
441
+ // 调用本身先不产出事件:DSH 的 tool/call 与 tool/result 是一对,resume 卡
442
+ // 里合成一个面板(`🔧 bash — cmd` + 输出),而不是两个各占一屏。
362
443
  const name = String(ev.data.name ?? '');
363
444
  const args = typeof ev.data.arguments === 'string' ? ev.data.arguments : '';
364
- out.events.push({
445
+ const summary = summarizeToolInput(name, args);
446
+ out.pendingTool = {
365
447
  type: 'tool_use',
366
- content: `${name}(${truncateUtf8(args, 200)})`,
448
+ title: summary ? `🔧 ${name} — ${summary}` : `🔧 ${name}`,
449
+ content: truncateUtf8(args, TOOL_INPUT_MAX_BYTES),
367
450
  timestamp: new Date(ev.time).toISOString(),
368
- });
451
+ };
369
452
  break;
370
453
  }
371
454
  case 'tool/result': {
372
455
  const msg = ev.data.message;
373
456
  const raw = stringifyContent(msg?.content);
457
+ const isError = msg?.isError === true;
374
458
  out.events.push({
375
459
  type: 'tool_result',
460
+ title: isError ? '🔴 结果(失败)' : '🟢 结果',
376
461
  content: truncateUtf8(raw, TOOL_RESULT_MAX_BYTES),
377
462
  timestamp: new Date(ev.time).toISOString(),
378
463
  });
@@ -102,7 +102,7 @@ export function detectPackageManager(scriptPath) {
102
102
  return inferred;
103
103
  }
104
104
  // 3. availability fallback: 纯 Node PATH 解析(win32 PATHEXT),不再依赖外部 which
105
- // (Windows 无 which);win32 再用 where.exe 兜底罕见的解析失败场景(v2 §4.3)
105
+ // (Windows 无 which);win32 再用 where.exe 兜底罕见的解析失败场景
106
106
  const candidates = ['npm', 'bun', 'pnpm'];
107
107
  for (const cmd of candidates) {
108
108
  if (resolveExecutable(cmd))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",