handsoff 0.1.2-beta.2 → 0.1.3

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.
@@ -167,7 +167,7 @@ function buildPermissionCard(input) {
167
167
  behaviors: [
168
168
  {
169
169
  type: "callback",
170
- value: { action: btn.value }
170
+ value: { action: btn.value, requestId: input.requestId }
171
171
  }
172
172
  ]
173
173
  };
@@ -253,13 +253,14 @@ function buildQuestionCard(input) {
253
253
  {
254
254
  tag: "button",
255
255
  text: { tag: "plain_text", content: item.label },
256
+ hover_tips: { tag: "plain_text", content: item.label },
256
257
  type: "primary",
257
258
  width: "default",
258
259
  size: "small",
259
260
  behaviors: [
260
261
  {
261
262
  type: "callback",
262
- value: { action: "select", optionId: item.id }
263
+ value: { action: "select", optionId: item.id, requestId: input.requestId }
263
264
  }
264
265
  ]
265
266
  }
@@ -289,7 +290,7 @@ function buildQuestionCard(input) {
289
290
  behaviors: [
290
291
  {
291
292
  type: "callback",
292
- value: { action: "check", optionId: item.id }
293
+ value: { action: "check", optionId: item.id, requestId: input.requestId }
293
294
  }
294
295
  ]
295
296
  }));
@@ -325,7 +326,7 @@ function buildQuestionCard(input) {
325
326
  behaviors: [
326
327
  {
327
328
  type: "callback",
328
- value: { action: "submit" }
329
+ value: { action: "submit", requestId: input.requestId }
329
330
  }
330
331
  ]
331
332
  }
@@ -698,7 +699,7 @@ var AgentAdapterRegistry = class _AgentAdapterRegistry {
698
699
  // src/core/session/Session.ts
699
700
  init_esm_shims();
700
701
  var Session = class {
701
- id;
702
+ _id;
702
703
  agent;
703
704
  token;
704
705
  metadata;
@@ -711,7 +712,7 @@ var Session = class {
711
712
  permissionMode;
712
713
  slashCommands;
713
714
  constructor(options) {
714
- this.id = options.id;
715
+ this._id = options.id;
715
716
  this.agent = options.adapter;
716
717
  this.token = options.token;
717
718
  this.metadata = options.metadata ?? {};
@@ -720,6 +721,12 @@ var Session = class {
720
721
  this._updatedAt = this.createdAt;
721
722
  this.cwd = options.cwd;
722
723
  }
724
+ get id() {
725
+ return this._id;
726
+ }
727
+ setId(newId) {
728
+ this._id = newId;
729
+ }
723
730
  get agentType() {
724
731
  return this.agent.agentType;
725
732
  }
@@ -841,6 +848,22 @@ var SessionManager = class {
841
848
  this.logger.debug({ sessionId, ...info }, "[SessionManager] Session info updated");
842
849
  return true;
843
850
  }
851
+ renameSessionId(oldId, newId) {
852
+ const session = this.sessions.get(oldId);
853
+ if (!session) {
854
+ this.logger.warn({ oldId, newId }, "[SessionManager] renameSessionId: session not found");
855
+ return false;
856
+ }
857
+ if (this.sessions.has(newId)) {
858
+ this.logger.warn({ oldId, newId }, "[SessionManager] renameSessionId: newId already exists");
859
+ return false;
860
+ }
861
+ session.setId(newId);
862
+ this.sessions.delete(oldId);
863
+ this.sessions.set(newId, session);
864
+ this.logger.info({ oldId, newId }, "[SessionManager] Session renamed");
865
+ return true;
866
+ }
844
867
  };
845
868
 
846
869
  // src/gateway/events.ts
@@ -3073,6 +3096,9 @@ var Gateway = class {
3073
3096
  updateSessionInfo(sessionId, info) {
3074
3097
  this.sessionManager.updateSessionInfo(sessionId, info);
3075
3098
  }
3099
+ renameSessionId(oldId, newId) {
3100
+ return this.sessionManager.renameSessionId(oldId, newId);
3101
+ }
3076
3102
  };
3077
3103
 
3078
3104
  // src/gateway/publisher.ts
@@ -3336,13 +3362,21 @@ var BaseAgentAdapter = class {
3336
3362
  this.bus.onInbound((msg) => this.handleInboundMessage(msg))
3337
3363
  );
3338
3364
  }
3365
+ /**
3366
+ * Resolve session ID from inbound message. Subclasses may override to
3367
+ * return a stable session ID based on transport-level identity (e.g.
3368
+ * Claude SDK session_id).
3369
+ */
3370
+ resolveSessionId(_message) {
3371
+ return void 0;
3372
+ }
3339
3373
  async handleInboundMessage(message) {
3340
3374
  const text = message.text.trim();
3341
3375
  this.logger.debug({ channel: message.channel, chatId: message.chatId, targetAgent: message.targetAgent }, "Inbound message received");
3342
3376
  if (message.systemCommand) return;
3343
3377
  if (message.targetAgent !== this.agentType) return;
3344
3378
  const parsed = parseInboundCommand(text, this.prefix);
3345
- const sessionId = `${this.agentType}-${message.chatId}`;
3379
+ const sessionId = this.resolveSessionId(message) ?? `${this.agentType}-${message.sender.id}`;
3346
3380
  if (!parsed) {
3347
3381
  if (!this.supportsInboundPrompt) {
3348
3382
  this.logger.debug({ sessionId, textPreview: text.slice(0, 60) }, "Inbound prompt ignored: adapter does not support user input");
@@ -3794,6 +3828,19 @@ var ClaudeHookAgentAdapter = class extends BaseAgentAdapter {
3794
3828
  payload: enrichedPayload
3795
3829
  };
3796
3830
  }
3831
+ /**
3832
+ * Handle permission/question requests from Claude hook events.
3833
+ *
3834
+ * Called for both PreToolUse (regular tools) and PermissionRequest events.
3835
+ * When toolName === 'AskUserQuestion', the request type is 'question' instead
3836
+ * of 'permission', which causes channels to render question cards (single/multi
3837
+ * select) instead of permission cards (Allow/Deny).
3838
+ *
3839
+ * evaluateInteraction blocks until the user responds via a channel. The response
3840
+ * is returned to Claude as part of the HTTP hook response body.
3841
+ *
3842
+ * See file header for full flow diagrams.
3843
+ */
3797
3844
  async processPermissionRequest(eventType, payload, requestId) {
3798
3845
  const sessionId = typeof payload.session_id === "string" ? payload.session_id : void 0;
3799
3846
  const toolName = typeof payload.tool_name === "string" ? payload.tool_name : "unknown";
@@ -4580,6 +4627,8 @@ var ClaudeRemoteAgentAdapter = class _ClaudeRemoteAgentAdapter extends BaseAgent
4580
4627
  defaultModel;
4581
4628
  defaultPermissionMode;
4582
4629
  static VALID_PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan"];
4630
+ /** Maps sender.id → real Claude session_id for stable multi-turn sessions */
4631
+ senderSessionMap = /* @__PURE__ */ new Map();
4583
4632
  constructor(options) {
4584
4633
  super({ bus: options.bus, bridge: options.bridge, logger: options.logger });
4585
4634
  this.token = options.token;
@@ -4630,6 +4679,13 @@ var ClaudeRemoteAgentAdapter = class _ClaudeRemoteAgentAdapter extends BaseAgent
4630
4679
  this.logger.info("Claude session scanner registered");
4631
4680
  }
4632
4681
  }
4682
+ /**
4683
+ * Return the real Claude session_id if this sender already has an active session.
4684
+ * This makes multi-turn conversations stable across turns.
4685
+ */
4686
+ resolveSessionId(message) {
4687
+ return this.senderSessionMap.get(message.sender.id);
4688
+ }
4633
4689
  async createTransportState(opts) {
4634
4690
  const sessionId = opts.sessionId ?? `claude-${randomUUID2().slice(0, 8)}`;
4635
4691
  const chatId = opts.chatId ?? sessionId;
@@ -4664,7 +4720,8 @@ var ClaudeRemoteAgentAdapter = class _ClaudeRemoteAgentAdapter extends BaseAgent
4664
4720
  chatId,
4665
4721
  channel,
4666
4722
  cwd,
4667
- permissionMode: this.defaultPermissionMode
4723
+ permissionMode: this.defaultPermissionMode,
4724
+ senderId: opts.metadata?.senderId
4668
4725
  };
4669
4726
  this.transports.set(sessionId, state);
4670
4727
  this.emitEvent(EventNormalizer.sessionStart(sessionId, this.agentType, this.transportType, {
@@ -4673,7 +4730,7 @@ var ClaudeRemoteAgentAdapter = class _ClaudeRemoteAgentAdapter extends BaseAgent
4673
4730
  cwd,
4674
4731
  ...opts.metadata
4675
4732
  }));
4676
- this.consumeQueryMessages(q, sessionId, state).catch((err) => {
4733
+ this.consumeQueryMessages(q, state).catch((err) => {
4677
4734
  this.logger.error({ sessionId, error: err }, "Query consumer error");
4678
4735
  });
4679
4736
  this.logger.info({ sessionId }, "Claude remote transport state created");
@@ -4687,6 +4744,9 @@ var ClaudeRemoteAgentAdapter = class _ClaudeRemoteAgentAdapter extends BaseAgent
4687
4744
  remoteState.query.kill("SIGTERM");
4688
4745
  remoteState.aggregator.reset();
4689
4746
  this.transports.delete(sessionId);
4747
+ if (remoteState.senderId) {
4748
+ this.senderSessionMap.delete(remoteState.senderId);
4749
+ }
4690
4750
  this.emitEvent(EventNormalizer.sessionEnd(sessionId, this.agentType, this.transportType, "close"));
4691
4751
  }
4692
4752
  async sendCommand(state, command) {
@@ -4728,13 +4788,13 @@ var ClaudeRemoteAgentAdapter = class _ClaudeRemoteAgentAdapter extends BaseAgent
4728
4788
  }
4729
4789
  }
4730
4790
  // === Internal ===
4731
- async consumeQueryMessages(q, sessionId, state) {
4791
+ async consumeQueryMessages(q, state) {
4732
4792
  try {
4733
4793
  for await (const msg of q) {
4734
- await this.handleSdkMessage(msg, sessionId, state);
4794
+ await this.handleSdkMessage(msg, state);
4735
4795
  }
4736
4796
  } catch (err) {
4737
- this.logger.error({ sessionId, error: err }, "Claude remote stream ended with error");
4797
+ this.logger.error({ sessionId: state.sessionId, error: err }, "Claude remote stream ended with error");
4738
4798
  this.bus.publishOutbound({
4739
4799
  channel: state.channel,
4740
4800
  chatId: state.chatId,
@@ -4743,21 +4803,35 @@ var ClaudeRemoteAgentAdapter = class _ClaudeRemoteAgentAdapter extends BaseAgent
4743
4803
  } finally {
4744
4804
  state.aggregator.reset();
4745
4805
  state.turnInProgress = false;
4746
- this.transports.delete(sessionId);
4747
- this.emitEvent(EventNormalizer.sessionEnd(sessionId, this.agentType, this.transportType, "stream_ended"));
4806
+ this.transports.delete(state.sessionId);
4807
+ this.emitEvent(EventNormalizer.sessionEnd(state.sessionId, this.agentType, this.transportType, "stream_ended"));
4748
4808
  }
4749
4809
  }
4750
- async handleSdkMessage(msg, sessionId, state) {
4810
+ async handleSdkMessage(msg, state) {
4811
+ const sessionId = state.sessionId;
4751
4812
  switch (msg.type) {
4752
4813
  case "system": {
4753
4814
  const system = msg;
4754
4815
  if (system.subtype === "init" && system.session_id) {
4755
- this.logger.info({ sessionId, claudeSessionId: system.session_id }, "Claude session initialized");
4756
- this.bridge.updateSessionInfo(sessionId, {
4816
+ const claudeSessionId = system.session_id;
4817
+ this.logger.info({ sessionId, claudeSessionId }, "Claude session initialized");
4818
+ if (claudeSessionId !== sessionId) {
4819
+ const renamed = this.bridge.renameSessionId(sessionId, claudeSessionId);
4820
+ if (renamed) {
4821
+ this.transports.delete(sessionId);
4822
+ state.sessionId = claudeSessionId;
4823
+ this.transports.set(claudeSessionId, state);
4824
+ this.logger.info({ oldId: sessionId, newId: claudeSessionId }, "Session renamed to Claude session_id");
4825
+ }
4826
+ }
4827
+ this.bridge.updateSessionInfo(claudeSessionId, {
4757
4828
  model: system.model,
4758
4829
  slashCommands: system.slash_commands,
4759
4830
  permissionMode: state.permissionMode
4760
4831
  });
4832
+ if (state.senderId) {
4833
+ this.senderSessionMap.set(state.senderId, claudeSessionId);
4834
+ }
4761
4835
  }
4762
4836
  break;
4763
4837
  }
@@ -4810,7 +4884,9 @@ var ClaudeRemoteAgentAdapter = class _ClaudeRemoteAgentAdapter extends BaseAgent
4810
4884
  durationMs: result.duration_ms,
4811
4885
  usage: result.usage ? {
4812
4886
  inputTokens: result.usage.input_tokens,
4813
- outputTokens: result.usage.output_tokens
4887
+ outputTokens: result.usage.output_tokens,
4888
+ cacheReadInputTokens: result.usage.cache_read_input_tokens,
4889
+ cacheCreationInputTokens: result.usage.cache_creation_input_tokens
4814
4890
  } : void 0
4815
4891
  })
4816
4892
  );
@@ -4829,6 +4905,18 @@ var ClaudeRemoteAgentAdapter = class _ClaudeRemoteAgentAdapter extends BaseAgent
4829
4905
  }
4830
4906
  }
4831
4907
  }
4908
+ /**
4909
+ * Handle tool permission requests from the Claude SDK.
4910
+ *
4911
+ * The SDK calls this callback when Claude wants to run a tool. We block
4912
+ * the Promise until the user responds via a channel (Feishu/Telegram).
4913
+ *
4914
+ * Resolution values:
4915
+ * - allow → { behavior: 'allow', updatedInput? }
4916
+ * - deny → { behavior: 'deny', message }
4917
+ *
4918
+ * See file header for full flow diagram.
4919
+ */
4832
4920
  async handleCanCallTool(sessionId, toolName, toolInput, signal) {
4833
4921
  const state = this.transports.get(sessionId);
4834
4922
  if (!state) {
@@ -4873,6 +4961,21 @@ var ClaudeRemoteAgentAdapter = class _ClaudeRemoteAgentAdapter extends BaseAgent
4873
4961
  });
4874
4962
  });
4875
4963
  }
4964
+ /**
4965
+ * Handle question requests from the Claude SDK (AskUserQuestion tool).
4966
+ *
4967
+ * The SDK calls this callback when Claude runs AskUserQuestion. We block
4968
+ * the Promise until the user selects an answer via a channel.
4969
+ *
4970
+ * For multi-select questions, the interaction-utils wraps the response
4971
+ * with updatedInput containing { answers: { [questionText]: answerValue } }.
4972
+ *
4973
+ * Resolution values:
4974
+ * - answer → { action: 'answer', answer: string }
4975
+ * - deny → { action: 'deny', message }
4976
+ *
4977
+ * See file header for full flow diagram.
4978
+ */
4876
4979
  async handleCanAnswerQuestion(sessionId, question, signal) {
4877
4980
  const state = this.transports.get(sessionId);
4878
4981
  if (!state) {
@@ -6259,25 +6362,16 @@ function sendJson(res, statusCode, data) {
6259
6362
  }
6260
6363
  function formatHookResponse(eventType, toolName, result) {
6261
6364
  const isAskUserQuestion = toolName === "AskUserQuestion";
6262
- if (isAskUserQuestion) {
6365
+ const buildAskUserQuestionUpdatedInput = () => {
6263
6366
  const questions = result._toolInput?.questions;
6264
6367
  const question = questions?.[0];
6265
6368
  const selectedValueStr = result.selectedValues && result.selectedValues.length > 0 ? result.selectedValues.map(String).join(", ") : void 0;
6266
6369
  const answerValue = result.customInput ?? selectedValueStr ?? "";
6267
- const updatedInput = {
6370
+ return {
6268
6371
  ...result._toolInput,
6269
6372
  answers: question?.question ? { [question.question]: answerValue } : {}
6270
6373
  };
6271
- return JSON.stringify({
6272
- hookSpecificOutput: {
6273
- hookEventName: "PermissionRequest",
6274
- decision: {
6275
- behavior: "allow",
6276
- updatedInput
6277
- }
6278
- }
6279
- });
6280
- }
6374
+ };
6281
6375
  if (eventType === "PermissionRequest") {
6282
6376
  let updatedPermissions;
6283
6377
  if (result.permissionUpdates && result.permissionUpdates.length > 0) {
@@ -6290,27 +6384,36 @@ function formatHookResponse(eventType, toolName, result) {
6290
6384
  ...rule.directories && { directories: rule.directories }
6291
6385
  }));
6292
6386
  }
6387
+ const decision = {
6388
+ behavior: result.action === "select" ? "allow" : result.action,
6389
+ ...result.message !== void 0 && { message: result.message },
6390
+ ...result.selectedValues !== void 0 && {
6391
+ data: result.selectedValues.map(String).join(", ")
6392
+ },
6393
+ ...updatedPermissions !== void 0 && { updatedPermissions }
6394
+ };
6395
+ if (isAskUserQuestion) {
6396
+ decision.updatedInput = buildAskUserQuestionUpdatedInput();
6397
+ }
6293
6398
  return JSON.stringify({
6294
6399
  hookSpecificOutput: {
6295
6400
  hookEventName: "PermissionRequest",
6296
- decision: {
6297
- behavior: result.action === "select" ? "allow" : result.action,
6298
- ...result.message !== void 0 && { message: result.message },
6299
- ...result.selectedValues !== void 0 && {
6300
- data: result.selectedValues.map(String).join(", ")
6301
- },
6302
- ...updatedPermissions !== void 0 && { updatedPermissions }
6303
- }
6401
+ decision
6304
6402
  }
6305
6403
  });
6306
6404
  }
6307
- return JSON.stringify({
6308
- accepted: ["allow", "select", "confirm"].includes(result.action),
6309
- message: result.message,
6310
- ...result.selectedValues !== void 0 && {
6311
- data: result.selectedValues.map(String).join(", ")
6312
- }
6313
- });
6405
+ const permissionDecision = result.action === "allow" ? "allow" : "deny";
6406
+ const hookSpecificOutput = {
6407
+ hookEventName: "PreToolUse",
6408
+ permissionDecision
6409
+ };
6410
+ if (result.message !== void 0) {
6411
+ hookSpecificOutput.permissionDecisionReason = result.message;
6412
+ }
6413
+ if (isAskUserQuestion) {
6414
+ hookSpecificOutput.updatedInput = buildAskUserQuestionUpdatedInput();
6415
+ }
6416
+ return JSON.stringify({ hookSpecificOutput });
6314
6417
  }
6315
6418
  function setupRoutes(httpServer, gateway, getClientCount) {
6316
6419
  httpServer.registerRoute("GET", "/health", (_req, res) => {
@@ -9799,6 +9902,33 @@ function getEventTitle(eventType) {
9799
9902
  }
9800
9903
  }
9801
9904
 
9905
+ // src/shared/format.ts
9906
+ init_esm_shims();
9907
+ function formatDuration3(ms) {
9908
+ if (ms === void 0 || ms < 0) return "-";
9909
+ if (ms < 1e3) return `${ms}ms`;
9910
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
9911
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
9912
+ const hours = Math.floor(ms / 36e5);
9913
+ const minutes = Math.round(ms % 36e5 / 6e4);
9914
+ if (minutes === 0) return `${hours}h`;
9915
+ return `${hours}h${minutes}m`;
9916
+ }
9917
+ function formatTokenCount(n) {
9918
+ if (n === void 0 || n < 0) return "-";
9919
+ if (n < 1e3) return `${n}`;
9920
+ if (n < 1e6) {
9921
+ const val2 = n / 1e3;
9922
+ return `${stripZeroDecimal(val2)}k`;
9923
+ }
9924
+ const val = n / 1e6;
9925
+ return `${stripZeroDecimal(val)}M`;
9926
+ }
9927
+ function stripZeroDecimal(val) {
9928
+ const fixed = val.toFixed(1);
9929
+ return fixed.endsWith(".0") ? fixed.slice(0, -2) : fixed;
9930
+ }
9931
+
9802
9932
  // src/adapters/channels/telegram/formatter.ts
9803
9933
  var TelegramFormatter = class {
9804
9934
  /**
@@ -9914,6 +10044,7 @@ var TelegramFormatter = class {
9914
10044
  case "turn:finished":
9915
10045
  return this.formatFinished(
9916
10046
  title,
10047
+ context.model || context.agentType,
9917
10048
  context.durationMs,
9918
10049
  context.usage?.inputTokens,
9919
10050
  context.usage?.outputTokens,
@@ -9949,17 +10080,19 @@ var TelegramFormatter = class {
9949
10080
  /**
9950
10081
  * 格式化结果通知消息
9951
10082
  */
9952
- formatFinished(title, durationMs, inputTokens, outputTokens, result) {
10083
+ formatFinished(title, model, durationMs, inputTokens, outputTokens, result) {
9953
10084
  const lines = [
9954
10085
  `${this.escape(title)}`,
9955
10086
  ""
9956
10087
  ];
10088
+ if (model) {
10089
+ lines.push(`<b>Model:</b> ${this.escape(model)}`);
10090
+ }
9957
10091
  if (durationMs !== void 0) {
9958
- const seconds = (durationMs / 1e3).toFixed(1);
9959
- lines.push(`<b>Duration:</b> ${this.escape(seconds)}s`);
10092
+ lines.push(`<b>Duration:</b> ${this.escape(formatDuration3(durationMs))}`);
9960
10093
  }
9961
10094
  if (inputTokens !== void 0 && outputTokens !== void 0) {
9962
- lines.push(`<b>Tokens:</b> in ${inputTokens} / out ${outputTokens}`);
10095
+ lines.push(`<b>Tokens:</b> ${formatTokenCount(inputTokens)}\u2191 / ${formatTokenCount(outputTokens)}\u2193`);
9963
10096
  }
9964
10097
  if (result) {
9965
10098
  lines.push("", "<b>Result:</b>");
@@ -10889,7 +11022,8 @@ var FeishuClient = class {
10889
11022
  userId: operator?.open_id,
10890
11023
  messageId: context?.open_message_id,
10891
11024
  chatId: context?.open_chat_id,
10892
- optionId: value?.optionId
11025
+ optionId: value?.optionId,
11026
+ checked: actionObj?.checked
10893
11027
  };
10894
11028
  await this.cardActionHandler(cardAction);
10895
11029
  } catch (error2) {
@@ -11335,7 +11469,7 @@ var FeishuChannel = class extends (_a4 = BaseChannel) {
11335
11469
  { text: "\u274C Deny", type: "danger_filled", value: "reject", confirm: { title: "\u786E\u8BA4\u62D2\u7EDD", text: "\u786E\u8BA4\u62D2\u7EDD\u8BE5\u5DE5\u5177\u8C03\u7528\uFF1F" } },
11336
11470
  { text: "\u{1F513} Always Allow", type: "default", value: "always_allow", confirm: { title: "\u786E\u8BA4\u603B\u662F\u5141\u8BB8", text: "\u603B\u662F\u5141\u8BB8\u5C06\u653E\u884C\u8BE5\u5DE5\u5177\u7684\u6240\u6709\u8C03\u7528\uFF0C\u8BF7\u786E\u8BA4\u3002" } }
11337
11471
  ];
11338
- const cardContent = this.formatter.formatPermissionCard({ title: this.getTitleForContext(message.context), content, buttons });
11472
+ const cardContent = this.formatter.formatPermissionCard({ title: this.getTitleForContext(message.context), content, buttons, requestId });
11339
11473
  await this.sendCardRaw(cardContent, message);
11340
11474
  } else if (eventType === "question:request") {
11341
11475
  const question = interaction?.question;
@@ -11348,7 +11482,7 @@ var FeishuChannel = class extends (_a4 = BaseChannel) {
11348
11482
  question.options.map((opt) => ({ id: opt.id, value: opt.value }))
11349
11483
  );
11350
11484
  }
11351
- const cardContent = this.formatter.formatQuestionCard({ title: this.getTitleForContext(message.context), content, mode, items });
11485
+ const cardContent = this.formatter.formatQuestionCard({ title: this.getTitleForContext(message.context), content, mode, items, requestId });
11352
11486
  await this.sendCardRaw(cardContent, message);
11353
11487
  } else {
11354
11488
  const content = message.context ? this.formatContextToMarkdown(message.context) : message.text || "";
@@ -11356,10 +11490,10 @@ var FeishuChannel = class extends (_a4 = BaseChannel) {
11356
11490
  if (message.context?.eventType === "turn:finished") {
11357
11491
  const finishedCtx = message.context;
11358
11492
  const model = finishedCtx.model || finishedCtx.agentType || "-";
11359
- const duration = finishedCtx.durationMs ? `${(finishedCtx.durationMs / 1e3).toFixed(1)}s` : "-";
11493
+ const duration = formatDuration3(finishedCtx.durationMs);
11360
11494
  const outTokens = finishedCtx.usage?.outputTokens;
11361
11495
  const inTokens = finishedCtx.usage?.inputTokens;
11362
- const tokens = outTokens !== void 0 && inTokens !== void 0 ? `${outTokens}\u2191 ${inTokens}\u2193` : "-";
11496
+ const tokens = outTokens !== void 0 && inTokens !== void 0 ? `${formatTokenCount(inTokens)}\u2191 ${formatTokenCount(outTokens)}\u2193` : "-";
11363
11497
  meta = `${model} \xB7 ${duration} \xB7 ${tokens}`;
11364
11498
  }
11365
11499
  const cardContent = this.formatter.formatMessageCard({ title: this.getTitleForContext(message.context), content, meta });
@@ -11517,12 +11651,12 @@ var FeishuChannel = class extends (_a4 = BaseChannel) {
11517
11651
  this.pendingSelections.set(requestId, /* @__PURE__ */ new Set());
11518
11652
  }
11519
11653
  const selected = this.pendingSelections.get(requestId);
11520
- if (selected.has(optionId)) {
11521
- selected.delete(optionId);
11522
- } else {
11654
+ if (data.checked) {
11523
11655
  selected.add(optionId);
11656
+ } else {
11657
+ selected.delete(optionId);
11524
11658
  }
11525
- this.logger.info({ requestId, userId, optionId, selectedCount: selected.size }, "[Channel] Checker toggled");
11659
+ this.logger.info({ requestId, userId, optionId, checked: data.checked, selectedCount: selected.size }, "[Channel] Checker changed");
11526
11660
  return;
11527
11661
  }
11528
11662
  if (action === "submit") {
@@ -11669,20 +11803,10 @@ ${context.thinking}`;
11669
11803
  `;
11670
11804
  if (context.cwd) {
11671
11805
  md += `**WorkDir:** ${context.cwd}
11672
- `;
11673
- }
11674
- md += `**Status:** Turn finished
11675
- `;
11676
- if (context.durationMs) {
11677
- md += `**Duration:** ${(context.durationMs / 1e3).toFixed(1)}s
11678
- `;
11679
- }
11680
- if (context.usage?.inputTokens !== void 0 && context.usage?.outputTokens !== void 0) {
11681
- md += `**Tokens:** in ${context.usage.inputTokens} / out ${context.usage.outputTokens}
11682
11806
  `;
11683
11807
  }
11684
11808
  if (context.lastAssistantMessage) {
11685
- md += `**Message:** ${context.lastAssistantMessage}`;
11809
+ md += `${context.lastAssistantMessage}`;
11686
11810
  }
11687
11811
  break;
11688
11812
  case "tool:post":