eve-lark 0.4.2 → 0.4.4

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/dist/index.js CHANGED
@@ -531,6 +531,15 @@ function parseInbound(event, botOpenId) {
531
531
  __name(parseInbound, "parseInbound");
532
532
 
533
533
  // src/card.ts
534
+ function renderToolCalls(calls) {
535
+ if (calls.length === 0) return void 0;
536
+ return calls.map((c) => {
537
+ if (c.state === "running") return `<font color='blue'>\u23F3 ${c.name}</font>`;
538
+ if (c.state === "failed") return `<font color='red'>\u2717 ${c.name}</font>`;
539
+ return `<font color='green'>\u2713 ${c.name}</font>`;
540
+ }).join("\n");
541
+ }
542
+ __name(renderToolCalls, "renderToolCalls");
534
543
  var BASE_CONFIG = {
535
544
  wide_screen_mode: true,
536
545
  update_multi: true
@@ -595,14 +604,35 @@ function buildTextCard(text) {
595
604
  __name(buildTextCard, "buildTextCard");
596
605
  function buildStreamingCard(opts) {
597
606
  const lines = [];
607
+ const toolLine = renderToolCalls(opts.toolCalls ?? []);
608
+ if (toolLine) lines.push(toolLine);
598
609
  if (opts.status) {
599
610
  lines.push(`<font color='grey'>${opts.status}</font>`);
600
611
  }
601
612
  lines.push(opts.buffer.length > 0 ? opts.buffer : "_\u2026_");
602
- return {
603
- config: { ...BASE_CONFIG },
604
- elements: [{ tag: "div", text: { tag: "lark_md", content: lines.join("\n\n") } }]
605
- };
613
+ if (opts.askRequest) {
614
+ lines.push(`**${opts.askRequest.prompt}**`);
615
+ if (opts.askRequest.allowFreeform && (opts.askRequest.options?.length ?? 0) === 0) {
616
+ lines.push(`<font color='grey'>_Reply to this chat with your answer_</font>`);
617
+ }
618
+ }
619
+ const elements = [
620
+ { tag: "div", text: { tag: "lark_md", content: lines.join("\n\n") } }
621
+ ];
622
+ if (opts.askRequest?.options && opts.askRequest.options.length > 0) {
623
+ const buttons = opts.askRequest.options.map((opt) => ({
624
+ tag: "button",
625
+ text: { tag: "plain_text", content: opt.label },
626
+ type: opt.style ?? "default",
627
+ value: {
628
+ [ASK_BUTTON_VALUE_MARKER]: true,
629
+ requestId: opts.askRequest.requestId,
630
+ optionId: opt.id
631
+ }
632
+ }));
633
+ elements.push({ tag: "action", actions: buttons });
634
+ }
635
+ return { config: { ...BASE_CONFIG }, elements };
606
636
  }
607
637
  __name(buildStreamingCard, "buildStreamingCard");
608
638
  function buildErrorCard(message) {
@@ -670,10 +700,12 @@ function buildAskCard(request) {
670
700
  return { config: { ...BASE_CONFIG }, elements };
671
701
  }
672
702
  __name(buildAskCard, "buildAskCard");
673
- function buildAskAnsweredCard(request, selected) {
674
- const elements = [
675
- { tag: "div", text: { tag: "lark_md", content: request.prompt } }
676
- ];
703
+ function buildAskAnsweredCard(request, selected, priorBuffer) {
704
+ const elements = [];
705
+ if (priorBuffer && priorBuffer.length > 0) {
706
+ elements.push({ tag: "div", text: { tag: "lark_md", content: priorBuffer } });
707
+ }
708
+ elements.push({ tag: "div", text: { tag: "lark_md", content: request.prompt } });
677
709
  const summary = selected.kind === "option" ? `<font color='green'>\u2713 ${escapeMarkdown(selected.label)}</font>` : `<font color='green'>\u2713 ${escapeMarkdown(selected.text)}</font>`;
678
710
  elements.push({ tag: "div", text: { tag: "lark_md", content: summary } });
679
711
  return { config: { ...BASE_CONFIG }, elements };
@@ -685,10 +717,34 @@ function escapeMarkdown(s) {
685
717
  __name(escapeMarkdown, "escapeMarkdown");
686
718
  function buildCardKitStreamingCard(opts) {
687
719
  const lines = [];
720
+ const toolLine = renderToolCalls(opts.toolCalls ?? []);
721
+ if (toolLine) lines.push(toolLine);
688
722
  if (opts.status) {
689
723
  lines.push(`<font color='grey'>${opts.status}</font>`);
690
724
  }
691
725
  lines.push(opts.buffer.length > 0 ? opts.buffer : "_\u2026_");
726
+ if (opts.askRequest) {
727
+ lines.push(`**${opts.askRequest.prompt}**`);
728
+ if (opts.askRequest.allowFreeform && (opts.askRequest.options?.length ?? 0) === 0) {
729
+ lines.push(`<font color='grey'>_Reply to this chat with your answer_</font>`);
730
+ }
731
+ }
732
+ const elements = [
733
+ { tag: "markdown", content: lines.join("\n\n") }
734
+ ];
735
+ if (opts.askRequest?.options && opts.askRequest.options.length > 0) {
736
+ const buttons = opts.askRequest.options.map((opt) => ({
737
+ tag: "button",
738
+ text: { tag: "plain_text", content: opt.label },
739
+ type: opt.style ?? "default",
740
+ value: {
741
+ [ASK_BUTTON_VALUE_MARKER]: true,
742
+ requestId: opts.askRequest.requestId,
743
+ optionId: opt.id
744
+ }
745
+ }));
746
+ elements.push({ tag: "action", actions: buttons });
747
+ }
692
748
  return {
693
749
  schema: "2.0",
694
750
  config: {
@@ -696,23 +752,38 @@ function buildCardKitStreamingCard(opts) {
696
752
  wide_screen_mode: true,
697
753
  update_multi: true
698
754
  },
699
- body: {
700
- elements: [
701
- { tag: "div", text: { tag: "lark_md", content: lines.join("\n\n") } }
702
- ]
703
- }
755
+ body: { elements }
704
756
  };
705
757
  }
706
758
  __name(buildCardKitStreamingCard, "buildCardKitStreamingCard");
707
- function buildCardKitFinalCard(text) {
759
+ function buildCardKitFinalCard(text, toolCalls, askRequest) {
760
+ const lines = [];
761
+ const toolLine = renderToolCalls(toolCalls ?? []);
762
+ if (toolLine) lines.push(toolLine);
763
+ lines.push(text);
764
+ if (askRequest) {
765
+ lines.push(`**${askRequest.prompt}**`);
766
+ }
767
+ const elements = [
768
+ { tag: "markdown", content: lines.join("\n\n") }
769
+ ];
770
+ if (askRequest?.options && askRequest.options.length > 0) {
771
+ const buttons = askRequest.options.map((opt) => ({
772
+ tag: "button",
773
+ text: { tag: "plain_text", content: opt.label },
774
+ type: opt.style ?? "default",
775
+ value: {
776
+ [ASK_BUTTON_VALUE_MARKER]: true,
777
+ requestId: askRequest.requestId,
778
+ optionId: opt.id
779
+ }
780
+ }));
781
+ elements.push({ tag: "action", actions: buttons });
782
+ }
708
783
  return {
709
784
  schema: "2.0",
710
785
  config: { streaming_mode: false, wide_screen_mode: true, update_multi: true },
711
- body: {
712
- elements: [
713
- { tag: "div", text: { tag: "lark_md", content: text } }
714
- ]
715
- }
786
+ body: { elements }
716
787
  };
717
788
  }
718
789
  __name(buildCardKitFinalCard, "buildCardKitFinalCard");
@@ -729,6 +800,13 @@ var StreamingCardController = class {
729
800
  status;
730
801
  messageId;
731
802
  fallbackToText = false;
803
+ /** Active HITL input request, when set via `setAskRequest`. The card
804
+ * builder appends prompt + buttons to the same streaming card so the
805
+ * user can answer inline instead of getting a separate ask-card. */
806
+ askRequest = null;
807
+ /** Tool calls made during this turn, in order. Rendered above the buffer
808
+ * so users can see what the agent is doing / has done, Claude-Code-style. */
809
+ toolCalls = [];
732
810
  createTimer = null;
733
811
  patchTimer = null;
734
812
  patchInFlight = null;
@@ -754,6 +832,112 @@ var StreamingCardController = class {
754
832
  this.schedulePatch();
755
833
  }
756
834
  }
835
+ /**
836
+ * Record the start of a tool call. Renders as `🔧 name` (or `⏳ name` while
837
+ * running). Persistent across patches — won't be lost if the tool completes
838
+ * between throttled patches.
839
+ *
840
+ * Schedules a patch regardless of state (idle/creating/streaming) so the
841
+ * user sees the call even when no text has streamed yet, which is the
842
+ * common case (model often calls a tool before any visible output).
843
+ */
844
+ addToolCall(name) {
845
+ if (this.state === "completed" || this.state === "aborted") return;
846
+ if (this.toolCalls.some((t) => t.name === name && t.state === "running")) {
847
+ return;
848
+ }
849
+ this.toolCalls.push({ name, state: "running" });
850
+ if (this.state === "idle") {
851
+ this.scheduleCreate();
852
+ } else if (this.state === "streaming") {
853
+ this.schedulePatch();
854
+ }
855
+ }
856
+ /**
857
+ * Mark the most-recently-started running entry for `name` as done/failed.
858
+ * Rendered as `✓ name` (green) or `✗ name` (red). Stays visible — the
859
+ * entry is not removed, so the user sees the full tool history at the
860
+ * end of the turn.
861
+ */
862
+ completeToolCall(name, failed = false) {
863
+ for (let i = this.toolCalls.length - 1; i >= 0; i--) {
864
+ const entry = this.toolCalls[i];
865
+ if (entry && entry.name === name && entry.state === "running") {
866
+ entry.state = failed ? "failed" : "done";
867
+ break;
868
+ }
869
+ }
870
+ if (this.state === "streaming") {
871
+ this.schedulePatch();
872
+ }
873
+ }
874
+ /** Read-only view of tool calls (for card builders). */
875
+ getToolCalls() {
876
+ return this.toolCalls;
877
+ }
878
+ /** Current streaming buffer (for handleCardAction to preserve when patching
879
+ * the "answered" state of an inline ask card). */
880
+ getBuffer() {
881
+ return this.buffer;
882
+ }
883
+ /** Card message id (so input.requested can reuse the existing card instead
884
+ * of creating a separate ask-card). */
885
+ getMessageId() {
886
+ return this.messageId;
887
+ }
888
+ /** Active HITL request being rendered inline on this card, or null. */
889
+ getAskRequest() {
890
+ return this.askRequest;
891
+ }
892
+ /**
893
+ * Render an `ask_question` request inline on this card by appending prompt
894
+ * + option buttons below the streaming text. Patches immediately so the
895
+ * user sees the buttons as soon as the agent asks.
896
+ *
897
+ * If the prior turn already finalized (state="completed"), transition back
898
+ * to "streaming" so the next patch can update the same card. The card
899
+ * keeps its messageId — no new card is sent.
900
+ */
901
+ setAskRequest(req) {
902
+ if (this.state === "aborted") return;
903
+ this.askRequest = req;
904
+ if (this.state === "completed" && this.messageId) {
905
+ this.state = "streaming";
906
+ }
907
+ if (this.state === "streaming") {
908
+ this.schedulePatch();
909
+ }
910
+ }
911
+ /** Clear the inline ask request (e.g. after the user clicked an option). */
912
+ clearAskRequest() {
913
+ this.askRequest = null;
914
+ }
915
+ /**
916
+ * Reset per-turn state for the next turn within the same session. Clears
917
+ * the streaming buffer, tool-call history, status, and any inline ask —
918
+ * but keeps the card messageId and transitions back to "streaming" so the
919
+ * next message.appended patches the SAME card. Without this, the second
920
+ * turn's message.completed would create a brand-new card and the user
921
+ * would see N cards for N turns within one logical conversation.
922
+ *
923
+ * Called from the channel's `turn.started` event handler.
924
+ */
925
+ resetForNewTurn() {
926
+ this.buffer = "";
927
+ this.status = void 0;
928
+ this.toolCalls = [];
929
+ this.askRequest = null;
930
+ this.fallbackToText = false;
931
+ this.cancelCreateTimer();
932
+ this.cancelPatchTimer();
933
+ this.patchInFlight = null;
934
+ this.patchScheduled = false;
935
+ if (this.messageId) {
936
+ this.state = "streaming";
937
+ } else {
938
+ this.state = "idle";
939
+ }
940
+ }
757
941
  async finalize(fullText) {
758
942
  if (this.state === "completed" || this.state === "aborted") return;
759
943
  this.cancelCreateTimer();
@@ -773,7 +957,7 @@ var StreamingCardController = class {
773
957
  try {
774
958
  const res = await this.client.sendCard({
775
959
  chatId: this.deps.chatId,
776
- card: this.deps.useCardKitV2 ? buildCardKitFinalCard(fullText) : buildTextCard(fullText),
960
+ card: this.deps.useCardKitV2 ? buildCardKitFinalCard(fullText, this.toolCalls, this.askRequest) : buildTextCard(fullText),
777
961
  rootId: this.deps.rootId,
778
962
  parentId: this.deps.parentId
779
963
  });
@@ -799,7 +983,7 @@ var StreamingCardController = class {
799
983
  }
800
984
  await this.client.patchCard({
801
985
  messageId: this.messageId,
802
- card: this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: fullText, streamingMode: false }) : buildStreamingCard({ buffer: fullText, status: void 0 })
986
+ card: this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: fullText, streamingMode: false, toolCalls: this.toolCalls, askRequest: this.askRequest }) : buildStreamingCard({ buffer: fullText, status: void 0, toolCalls: this.toolCalls, askRequest: this.askRequest })
803
987
  });
804
988
  this.state = "completed";
805
989
  }
@@ -851,7 +1035,7 @@ var StreamingCardController = class {
851
1035
  try {
852
1036
  const res = await this.client.sendCard({
853
1037
  chatId: this.deps.chatId,
854
- card: this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: this.buffer, status: this.status, streamingMode: true }) : buildStreamingCard({ buffer: this.buffer, status: this.status }),
1038
+ card: this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: this.buffer, status: this.status, streamingMode: true, toolCalls: this.toolCalls, askRequest: this.askRequest }) : buildStreamingCard({ buffer: this.buffer, status: this.status, toolCalls: this.toolCalls, askRequest: this.askRequest }),
855
1039
  rootId: this.deps.rootId,
856
1040
  parentId: this.deps.parentId
857
1041
  });
@@ -889,7 +1073,7 @@ var StreamingCardController = class {
889
1073
  if (this.state !== "streaming") return;
890
1074
  if (this.patchInFlight) return;
891
1075
  if (this.messageId === void 0) return;
892
- const card = this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: this.buffer, status: this.status, streamingMode: true }) : buildStreamingCard({ buffer: this.buffer, status: this.status });
1076
+ const card = this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: this.buffer, status: this.status, streamingMode: true, toolCalls: this.toolCalls, askRequest: this.askRequest }) : buildStreamingCard({ buffer: this.buffer, status: this.status, toolCalls: this.toolCalls, askRequest: this.askRequest });
893
1077
  this.patchInFlight = this.client.patchCard({ messageId: this.messageId, card }).catch((e) => {
894
1078
  console.warn(
895
1079
  "[eve-lark] streaming card patch failed:",
@@ -1546,11 +1730,6 @@ function createLarkChannel(optionsInput) {
1546
1730
  return ctrl;
1547
1731
  }
1548
1732
  __name(getController, "getController");
1549
- function dropController(sessionId) {
1550
- controllers.delete(sessionId);
1551
- sessionMeta.delete(sessionId);
1552
- }
1553
- __name(dropController, "dropController");
1554
1733
  async function cleanupAckReaction(sessionId) {
1555
1734
  const meta = sessionMeta.get(sessionId);
1556
1735
  if (!meta?.ackReactionId || !meta.messageId) return;
@@ -1920,14 +2099,18 @@ function createLarkChannel(optionsInput) {
1920
2099
  helpers.waitUntil(
1921
2100
  (async () => {
1922
2101
  if (pending.cardMessageId && selectedOpt) {
2102
+ const ctrlForBuffer = controllers.get(pending.sessionId);
2103
+ const priorBuffer = ctrlForBuffer?.getBuffer() ?? void 0;
1923
2104
  try {
1924
2105
  await client.patchCard({
1925
2106
  messageId: pending.cardMessageId,
1926
- card: buildAskAnsweredCard(pending.request, {
1927
- kind: "option",
1928
- label: selectedOpt.label
1929
- })
2107
+ card: buildAskAnsweredCard(
2108
+ pending.request,
2109
+ { kind: "option", label: selectedOpt.label },
2110
+ priorBuffer
2111
+ )
1930
2112
  });
2113
+ ctrlForBuffer?.clearAskRequest();
1931
2114
  } catch (e) {
1932
2115
  console.warn(
1933
2116
  "[eve-lark] patchCard after ask-answer failed:",
@@ -1987,30 +2170,39 @@ function createLarkChannel(optionsInput) {
1987
2170
  const ctrl = getController(sessionId, info);
1988
2171
  ctrl.appendDelta(d.messageDelta);
1989
2172
  },
1990
- // Model is about to call tools. Update the streaming card status so the
1991
- // user sees what's happening mid-turn instead of a static typing dot.
1992
- // Only fires when replyMode is "streaming" (cards exist). Post/static
1993
- // modes have no live surface to update.
2173
+ // Model is about to call tools. Record each call on the streaming
2174
+ // controller so it shows up in the card as name. The controller
2175
+ // creates the card immediately if it doesn't exist yet, so the user
2176
+ // sees the tool call even before any text has streamed (which is the
2177
+ // common case — model often calls tools before producing visible
2178
+ // output). Only fires for streaming modes; post/static have no live
2179
+ // surface to update.
1994
2180
  async "actions.requested"(data, _channel, ctx) {
1995
2181
  if (options.replyMode !== "streaming" && options.replyMode !== "streaming-v2") return;
1996
2182
  const sessionId = ctx.session.id;
1997
- const ctrl = controllers.get(sessionId);
1998
- if (!ctrl) return;
2183
+ const info = sessionInfoFromCtx(ctx);
2184
+ if (!info) return;
1999
2185
  const d = data;
2000
2186
  const names = (d.actions ?? []).map((a) => a.toolName).filter((n) => typeof n === "string");
2001
2187
  if (names.length === 0) return;
2002
- const label = names.length === 1 ? `\u{1F527} ${names[0]}` : `\u{1F527} ${names.join(", ")}`;
2003
- ctrl.setStatus(label);
2188
+ const ctrl = getController(sessionId, info);
2189
+ for (const name of names) {
2190
+ ctrl.addToolCall(name);
2191
+ }
2004
2192
  },
2005
- // A tool finished. Clear the status (the next message.appended or
2006
- // message.completed will overwrite anyway, but clearing here gives
2007
- // snappier feedback for long tool chains). Best-effort.
2008
- async "action.result"(_data, _channel, ctx) {
2193
+ // A tool finished. Mark its entry (or on failure). Stays visible —
2194
+ // the user keeps the tool history at the top of the card through end
2195
+ // of turn. Best-effort: if we can't find the controller (e.g. post
2196
+ // mode, or session already cleaned up), no-op.
2197
+ async "action.result"(data, _channel, ctx) {
2009
2198
  if (options.replyMode !== "streaming" && options.replyMode !== "streaming-v2") return;
2010
2199
  const sessionId = ctx.session.id;
2011
2200
  const ctrl = controllers.get(sessionId);
2012
2201
  if (!ctrl) return;
2013
- ctrl.setStatus("");
2202
+ const d = data;
2203
+ const name = d.result?.toolName;
2204
+ if (!name) return;
2205
+ ctrl.completeToolCall(name, d.result?.status === "failed");
2014
2206
  },
2015
2207
  // eve's ask_question (and similar HITL tools) fire this event with a
2016
2208
  // list of input requests. Each request becomes a Feishu card with
@@ -2029,22 +2221,29 @@ function createLarkChannel(optionsInput) {
2029
2221
  `[eve-lark] input.requested sessionId=${sessionId} chatId=${info.chatId} count=${requests.length}`
2030
2222
  );
2031
2223
  for (const req of requests) {
2032
- const card = buildAskCard(req);
2224
+ const existingCtrl = controllers.get(sessionId);
2225
+ const canPatchExisting = existingCtrl && existingCtrl.getMessageId() && (options.replyMode === "streaming" || options.replyMode === "streaming-v2");
2033
2226
  let cardMessageId;
2034
- try {
2035
- const res = await client.sendCard({
2036
- chatId: info.chatId,
2037
- card,
2038
- rootId: info.rootId,
2039
- parentId: info.parentId
2040
- });
2041
- cardMessageId = res.messageId;
2042
- } catch (e) {
2043
- console.error(
2044
- `[eve-lark] ask card send failed (requestId=${req.requestId}):`,
2045
- e instanceof Error ? e.message : e
2046
- );
2047
- continue;
2227
+ if (canPatchExisting && existingCtrl) {
2228
+ existingCtrl.setAskRequest(req);
2229
+ cardMessageId = existingCtrl.getMessageId();
2230
+ } else {
2231
+ const card = buildAskCard(req);
2232
+ try {
2233
+ const res = await client.sendCard({
2234
+ chatId: info.chatId,
2235
+ card,
2236
+ rootId: info.rootId,
2237
+ parentId: info.parentId
2238
+ });
2239
+ cardMessageId = res.messageId;
2240
+ } catch (e) {
2241
+ console.error(
2242
+ `[eve-lark] ask card send failed (requestId=${req.requestId}):`,
2243
+ e instanceof Error ? e.message : e
2244
+ );
2245
+ continue;
2246
+ }
2048
2247
  }
2049
2248
  const pending = {
2050
2249
  requestId: req.requestId,
@@ -2083,7 +2282,6 @@ function createLarkChannel(optionsInput) {
2083
2282
  await deliverReply(sessionId, info, text);
2084
2283
  } finally {
2085
2284
  await cleanupAckReaction(sessionId);
2086
- dropController(sessionId);
2087
2285
  }
2088
2286
  },
2089
2287
  async "turn.failed"(data, _channel, ctx) {
@@ -2124,7 +2322,6 @@ function createLarkChannel(optionsInput) {
2124
2322
  }
2125
2323
  }
2126
2324
  await cleanupAckReaction(sessionId);
2127
- dropController(sessionId);
2128
2325
  },
2129
2326
  async "session.failed"(data) {
2130
2327
  const userText = formatFailureMessage(data, "session failed", { sentence: "session" });
@@ -2133,11 +2330,23 @@ function createLarkChannel(optionsInput) {
2133
2330
  `[eve-lark] session.failed: ${userText}` + (errorId ? ` (errorId=${errorId})` : "")
2134
2331
  );
2135
2332
  },
2333
+ // A new turn is starting within an existing session (e.g. user clicked
2334
+ // an inline ask button, eve resumed with their InputResponse). Reset
2335
+ // the controller's per-turn state so the new turn's text replaces the
2336
+ // prior turn's text on the SAME card — instead of creating a fresh
2337
+ // card per turn.
2338
+ async "turn.started"(_data, _channel, ctx) {
2339
+ const sessionId = ctx?.session?.id;
2340
+ if (!sessionId) return;
2341
+ const ctrl = controllers.get(sessionId);
2342
+ if (ctrl) {
2343
+ ctrl.resetForNewTurn();
2344
+ }
2345
+ },
2136
2346
  // Turn ended cleanly. eve fires this after the final message.completed
2137
2347
  // (or instead of it when the assistant step ended in tool-calls with no
2138
- // visible text). Either way, free this session's controller + ack
2139
- // reaction so we don't leak waiting for a message.completed that's
2140
- // never coming.
2348
+ // visible text). Just clean up the ack reaction — the controller stays
2349
+ // for the next turn (cleaned by stale-sweep if the session goes quiet).
2141
2350
  async "turn.completed"(data, _channel, ctx) {
2142
2351
  const sessionId = ctx?.session?.id;
2143
2352
  if (!sessionId) return;
@@ -2145,7 +2354,6 @@ function createLarkChannel(optionsInput) {
2145
2354
  await cleanupAckReaction(sessionId);
2146
2355
  } catch {
2147
2356
  }
2148
- dropController(sessionId);
2149
2357
  },
2150
2358
  // The agent needs the user to sign in to an external service (e.g.
2151
2359
  // GitHub, Slack, Linear). Render a card with a "Sign in with <X>"