codex-to-im 1.0.34 → 1.0.36

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 (2) hide show
  1. package/dist/daemon.mjs +292 -48
  2. package/package.json +1 -1
package/dist/daemon.mjs CHANGED
@@ -5496,25 +5496,16 @@ function formatElapsed(ms) {
5496
5496
  const remSec = Math.floor(sec % 60);
5497
5497
  return `${min}m ${remSec}s`;
5498
5498
  }
5499
- function buildStreamingContent(text2, tools) {
5500
- let content = text2 || "";
5501
- const toolMd = buildToolProgressMarkdown(tools);
5502
- if (toolMd) {
5503
- content = content ? `${content}
5504
-
5505
- ${toolMd}` : toolMd;
5506
- }
5507
- return content || "\u{1F4AD} Thinking...";
5499
+ function buildStreamingTextContent(text2) {
5500
+ return text2 || "\u{1F4AD} Thinking...";
5501
+ }
5502
+ function buildStreamingToolsContent(tools) {
5503
+ return buildToolProgressMarkdown(tools);
5508
5504
  }
5509
5505
  function buildFinalCardJson(text2, tools, footer) {
5510
5506
  const elements = [];
5511
- let content = preprocessFeishuMarkdown(text2);
5507
+ const content = preprocessFeishuMarkdown(text2);
5512
5508
  const toolMd = buildToolProgressMarkdown(tools);
5513
- if (toolMd) {
5514
- content = content ? `${content}
5515
-
5516
- ${toolMd}` : toolMd;
5517
- }
5518
5509
  if (content) {
5519
5510
  elements.push({
5520
5511
  tag: "markdown",
@@ -5523,12 +5514,25 @@ ${toolMd}` : toolMd;
5523
5514
  text_size: "normal"
5524
5515
  });
5525
5516
  }
5517
+ if (toolMd) {
5518
+ if (elements.length > 0) {
5519
+ elements.push({ tag: "hr" });
5520
+ }
5521
+ elements.push({
5522
+ tag: "markdown",
5523
+ content: toolMd,
5524
+ text_align: "left",
5525
+ text_size: "normal"
5526
+ });
5527
+ }
5526
5528
  if (footer) {
5527
5529
  const parts = [];
5528
5530
  if (footer.status) parts.push(footer.status);
5529
5531
  if (footer.elapsed) parts.push(footer.elapsed);
5530
5532
  if (parts.length > 0) {
5531
- elements.push({ tag: "hr" });
5533
+ if (elements.length > 0) {
5534
+ elements.push({ tag: "hr" });
5535
+ }
5532
5536
  elements.push({
5533
5537
  tag: "markdown",
5534
5538
  content: parts.join(" \xB7 "),
@@ -5595,6 +5599,8 @@ var DEDUP_MAX = 1e3;
5595
5599
  var MAX_FILE_SIZE = 20 * 1024 * 1024;
5596
5600
  var TYPING_EMOJI = "Typing";
5597
5601
  var CARD_THROTTLE_MS = 200;
5602
+ var INITIAL_STREAMING_STATUS = "\u5DF2\u8FD0\u884C 0s";
5603
+ var EMPTY_STREAMING_TOOLS = "";
5598
5604
  var MIME_BY_TYPE = {
5599
5605
  image: "image/png",
5600
5606
  file: "application/octet-stream",
@@ -5642,6 +5648,9 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5642
5648
  isStreamingEnabled() {
5643
5649
  return this.channelConfig.streamingEnabled !== false;
5644
5650
  }
5651
+ supportsStructuredStreamingUi(_chatId) {
5652
+ return this.isStreamingEnabled();
5653
+ }
5645
5654
  resolveStreamKey(chatId, streamKey) {
5646
5655
  return streamKey?.trim() || chatId;
5647
5656
  }
@@ -5840,13 +5849,29 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5840
5849
  summary: { content: "\u601D\u8003\u4E2D..." }
5841
5850
  },
5842
5851
  body: {
5843
- elements: [{
5844
- tag: "markdown",
5845
- content: "\u{1F4AD} Thinking...",
5846
- text_align: "left",
5847
- text_size: "normal",
5848
- element_id: "streaming_content"
5849
- }]
5852
+ elements: [
5853
+ {
5854
+ tag: "markdown",
5855
+ content: "\u{1F4AD} Thinking...",
5856
+ text_align: "left",
5857
+ text_size: "normal",
5858
+ element_id: "streaming_content"
5859
+ },
5860
+ {
5861
+ tag: "markdown",
5862
+ content: EMPTY_STREAMING_TOOLS,
5863
+ text_align: "left",
5864
+ text_size: "normal",
5865
+ element_id: "streaming_tools"
5866
+ },
5867
+ {
5868
+ tag: "markdown",
5869
+ content: INITIAL_STREAMING_STATUS,
5870
+ text_align: "left",
5871
+ text_size: "notation",
5872
+ element_id: "streaming_status"
5873
+ }
5874
+ ]
5850
5875
  }
5851
5876
  };
5852
5877
  const createResp = await cardkit.card.create({
@@ -5888,8 +5913,14 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5888
5913
  toolCalls: [],
5889
5914
  thinking: true,
5890
5915
  pendingText: null,
5916
+ pendingStatusText: INITIAL_STREAMING_STATUS,
5917
+ renderedText: "\u{1F4AD} Thinking...",
5918
+ renderedToolsText: EMPTY_STREAMING_TOOLS,
5919
+ renderedStatusText: INITIAL_STREAMING_STATUS,
5891
5920
  lastUpdateAt: 0,
5892
- throttleTimer: null
5921
+ throttleTimer: null,
5922
+ flushInFlight: null,
5923
+ flushQueued: false
5893
5924
  });
5894
5925
  console.log(`[feishu-adapter] Streaming card created: streamKey=${cardKey}, cardId=${cardId}, msgId=${messageId}`);
5895
5926
  return true;
@@ -5909,12 +5940,43 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5909
5940
  state.thinking = false;
5910
5941
  }
5911
5942
  state.pendingText = text2;
5943
+ this.scheduleCardFlush(cardKey);
5944
+ }
5945
+ updateCardStatus(chatId, statusText, streamKey) {
5946
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
5947
+ const state = this.activeCards.get(cardKey);
5948
+ if (!state || !this.restClient) return;
5949
+ state.pendingStatusText = statusText || INITIAL_STREAMING_STATUS;
5950
+ this.scheduleCardFlush(cardKey);
5951
+ }
5952
+ enqueueCardFlush(streamKey) {
5953
+ const state = this.activeCards.get(streamKey);
5954
+ if (!state) return;
5955
+ if (state.flushInFlight) {
5956
+ state.flushQueued = true;
5957
+ return;
5958
+ }
5959
+ state.flushInFlight = this.flushCardUpdate(streamKey).catch((err) => {
5960
+ console.warn("[feishu-adapter] cardElement.content failed:", err instanceof Error ? err.message : err);
5961
+ }).finally(() => {
5962
+ const current = this.activeCards.get(streamKey);
5963
+ if (!current) return;
5964
+ current.flushInFlight = null;
5965
+ if (current.flushQueued) {
5966
+ current.flushQueued = false;
5967
+ this.enqueueCardFlush(streamKey);
5968
+ }
5969
+ });
5970
+ }
5971
+ scheduleCardFlush(streamKey) {
5972
+ const state = this.activeCards.get(streamKey);
5973
+ if (!state) return;
5912
5974
  const elapsed = Date.now() - state.lastUpdateAt;
5913
5975
  if (elapsed < CARD_THROTTLE_MS && state.lastUpdateAt > 0) {
5914
5976
  if (!state.throttleTimer) {
5915
5977
  state.throttleTimer = setTimeout(() => {
5916
5978
  state.throttleTimer = null;
5917
- this.flushCardUpdate(cardKey);
5979
+ this.enqueueCardFlush(streamKey);
5918
5980
  }, CARD_THROTTLE_MS - elapsed);
5919
5981
  }
5920
5982
  return;
@@ -5923,28 +5985,65 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5923
5985
  clearTimeout(state.throttleTimer);
5924
5986
  state.throttleTimer = null;
5925
5987
  }
5926
- this.flushCardUpdate(cardKey);
5988
+ this.enqueueCardFlush(streamKey);
5927
5989
  }
5928
5990
  /**
5929
5991
  * Flush pending card update to Feishu API.
5930
5992
  */
5931
- flushCardUpdate(streamKey) {
5993
+ async flushCardUpdate(streamKey) {
5932
5994
  const state = this.activeCards.get(streamKey);
5933
5995
  if (!state || !this.restClient) return;
5934
5996
  const cardkit = this.restClient.cardkit?.v1;
5935
5997
  if (!cardkit?.cardElement?.content) return;
5936
- const content = buildStreamingContent(state.pendingText || "", state.toolCalls);
5937
- state.sequence++;
5938
- const seq = state.sequence;
5998
+ const content = buildStreamingTextContent(state.pendingText || "");
5999
+ const toolsText = buildStreamingToolsContent(state.toolCalls) || EMPTY_STREAMING_TOOLS;
6000
+ const statusText = state.pendingStatusText || INITIAL_STREAMING_STATUS;
6001
+ const updates = [];
6002
+ if (content !== state.renderedText) {
6003
+ updates.push({
6004
+ elementId: "streaming_content",
6005
+ content,
6006
+ onSuccess: () => {
6007
+ state.renderedText = content;
6008
+ }
6009
+ });
6010
+ }
6011
+ if (toolsText !== state.renderedToolsText) {
6012
+ updates.push({
6013
+ elementId: "streaming_tools",
6014
+ content: toolsText,
6015
+ onSuccess: () => {
6016
+ state.renderedToolsText = toolsText;
6017
+ }
6018
+ });
6019
+ }
6020
+ if (statusText !== state.renderedStatusText) {
6021
+ updates.push({
6022
+ elementId: "streaming_status",
6023
+ content: statusText,
6024
+ onSuccess: () => {
6025
+ state.renderedStatusText = statusText;
6026
+ }
6027
+ });
6028
+ }
6029
+ if (updates.length === 0) return;
5939
6030
  const cardId = state.cardId;
5940
- cardkit.cardElement.content({
5941
- path: { card_id: cardId, element_id: "streaming_content" },
5942
- data: { content, sequence: seq }
5943
- }).then(() => {
5944
- state.lastUpdateAt = Date.now();
5945
- }).catch((err) => {
5946
- console.warn("[feishu-adapter] cardElement.content failed:", err instanceof Error ? err.message : err);
5947
- });
6031
+ for (const update of updates) {
6032
+ state.sequence++;
6033
+ try {
6034
+ await cardkit.cardElement.content({
6035
+ path: { card_id: cardId, element_id: update.elementId },
6036
+ data: { content: update.content, sequence: state.sequence }
6037
+ });
6038
+ update.onSuccess();
6039
+ state.lastUpdateAt = Date.now();
6040
+ } catch (err) {
6041
+ console.warn(
6042
+ `[feishu-adapter] cardElement.content failed for ${update.elementId}:`,
6043
+ err instanceof Error ? err.message : err
6044
+ );
6045
+ }
6046
+ }
5948
6047
  }
5949
6048
  /**
5950
6049
  * Update tool progress in the streaming card.
@@ -5954,7 +6053,27 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5954
6053
  const state = this.activeCards.get(cardKey);
5955
6054
  if (!state) return;
5956
6055
  state.toolCalls = tools;
5957
- this.updateCardContent(chatId, state.pendingText || "", cardKey);
6056
+ this.scheduleCardFlush(cardKey);
6057
+ }
6058
+ async awaitCardFlushCompletion(streamKey) {
6059
+ while (true) {
6060
+ const state = this.activeCards.get(streamKey);
6061
+ if (!state) return;
6062
+ const inFlight = state.flushInFlight;
6063
+ if (inFlight) {
6064
+ try {
6065
+ await inFlight;
6066
+ } catch {
6067
+ }
6068
+ continue;
6069
+ }
6070
+ if (state.flushQueued) {
6071
+ state.flushQueued = false;
6072
+ this.enqueueCardFlush(streamKey);
6073
+ continue;
6074
+ }
6075
+ return;
6076
+ }
5958
6077
  }
5959
6078
  /**
5960
6079
  * Finalize the streaming card: close streaming mode, update with final content + footer.
@@ -5976,6 +6095,7 @@ var FeishuAdapter = class extends BaseChannelAdapter {
5976
6095
  clearTimeout(state.throttleTimer);
5977
6096
  state.throttleTimer = null;
5978
6097
  }
6098
+ await this.awaitCardFlushCompletion(cardKey);
5979
6099
  try {
5980
6100
  state.sequence++;
5981
6101
  await cardkit.card.settings({
@@ -6041,6 +6161,9 @@ ${trimmedResponse}`;
6041
6161
  hasActiveCard(chatId, streamKey) {
6042
6162
  return this.activeCards.has(this.resolveStreamKey(chatId, streamKey));
6043
6163
  }
6164
+ hasActiveStreamingUi(chatId, streamKey) {
6165
+ return this.hasActiveCard(chatId, streamKey);
6166
+ }
6044
6167
  // ── Streaming adapter interface ────────────────────────────────
6045
6168
  /**
6046
6169
  * Called by bridge-manager on each text SSE event.
@@ -6070,6 +6193,19 @@ ${trimmedResponse}`;
6070
6193
  if (!this.isStreamingEnabled()) return;
6071
6194
  this.updateToolProgress(chatId, tools, streamKey);
6072
6195
  }
6196
+ onStreamStatus(chatId, statusText, streamKey) {
6197
+ if (!this.isStreamingEnabled()) return;
6198
+ const cardKey = this.resolveStreamKey(chatId, streamKey);
6199
+ if (!this.activeCards.has(cardKey)) {
6200
+ const messageId = this.lastIncomingMessageId.get(chatId);
6201
+ this.createStreamingCard(chatId, messageId, cardKey).then((ok) => {
6202
+ if (ok) this.updateCardStatus(chatId, statusText, cardKey);
6203
+ }).catch(() => {
6204
+ });
6205
+ return;
6206
+ }
6207
+ this.updateCardStatus(chatId, statusText, cardKey);
6208
+ }
6073
6209
  async onStreamEnd(chatId, status, responseText, streamKey) {
6074
6210
  if (!this.isStreamingEnabled()) return false;
6075
6211
  return this.finalizeCard(chatId, status, responseText, streamKey);
@@ -16201,6 +16337,19 @@ function toUserVisibleCommandError(command, error) {
16201
16337
  }
16202
16338
  return `${command} \u6267\u884C\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002`;
16203
16339
  }
16340
+ function pad2(value) {
16341
+ return String(value).padStart(2, "0");
16342
+ }
16343
+ function formatCommandDateTime(value) {
16344
+ const trimmed = value?.trim();
16345
+ if (!trimmed) return "-";
16346
+ const date = new Date(trimmed);
16347
+ if (Number.isNaN(date.getTime())) return trimmed;
16348
+ return [
16349
+ `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`,
16350
+ `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`
16351
+ ].join(" ");
16352
+ }
16204
16353
  function stripStoredAttachmentMarker(content) {
16205
16354
  return content.replace(/\n?<!--files:[\s\S]*?-->$/u, "").trim();
16206
16355
  }
@@ -16258,7 +16407,7 @@ function formatRuntimeStatus(session) {
16258
16407
  }
16259
16408
  function formatMirrorStatus(session) {
16260
16409
  if (session?.mirror_status === "watching") {
16261
- return session.mirror_last_event_at ? `\u76D1\u542C\u4E2D \xB7 \u6700\u8FD1\u540C\u6B65 ${session.mirror_last_event_at}` : "\u76D1\u542C\u4E2D";
16410
+ return session.mirror_last_event_at ? `\u76D1\u542C\u4E2D \xB7 \u6700\u8FD1\u540C\u6B65 ${formatCommandDateTime(session.mirror_last_event_at)}` : "\u76D1\u542C\u4E2D";
16262
16411
  }
16263
16412
  if (session?.mirror_status === "stale") {
16264
16413
  return "\u5F85\u6062\u590D\uFF08\u6682\u65F6\u6CA1\u5B9A\u4F4D\u5230\u684C\u9762 thread \u6587\u4EF6\uFF09";
@@ -16294,14 +16443,15 @@ function formatCommandTimestamp(value) {
16294
16443
  if (!trimmed) return "-";
16295
16444
  const parsed = Date.parse(trimmed);
16296
16445
  if (!Number.isFinite(parsed)) return trimmed;
16446
+ const localized = formatCommandDateTime(trimmed);
16297
16447
  const diffMs = Math.max(0, Date.now() - parsed);
16298
16448
  const diffMinutes = Math.floor(diffMs / 6e4);
16299
- if (diffMinutes < 1) return `${trimmed}\uFF08\u521A\u521A\uFF09`;
16300
- if (diffMinutes < 60) return `${trimmed}\uFF08${diffMinutes} \u5206\u949F\u524D\uFF09`;
16449
+ if (diffMinutes < 1) return `${localized}\uFF08\u521A\u521A\uFF09`;
16450
+ if (diffMinutes < 60) return `${localized}\uFF08${diffMinutes} \u5206\u949F\u524D\uFF09`;
16301
16451
  const diffHours = Math.floor(diffMinutes / 60);
16302
- if (diffHours < 24) return `${trimmed}\uFF08${diffHours} \u5C0F\u65F6\u524D\uFF09`;
16452
+ if (diffHours < 24) return `${localized}\uFF08${diffHours} \u5C0F\u65F6\u524D\uFF09`;
16303
16453
  const diffDays = Math.floor(diffHours / 24);
16304
- return `${trimmed}\uFF08${diffDays} \u5929\u524D\uFF09`;
16454
+ return `${localized}\uFF08${diffDays} \u5929\u524D\uFF09`;
16305
16455
  }
16306
16456
  function formatHealthProcessProbe(diagnosis) {
16307
16457
  if (!diagnosis.processProbe) {
@@ -17891,7 +18041,7 @@ async function handleBridgeCommand(adapter, msg, text2, deps) {
17891
18041
  [
17892
18042
  ["\u6807\u9898", getSessionDisplayName(draftSession, draftSession.working_directory)],
17893
18043
  ["\u76EE\u5F55", formatCommandPath(draftSession.working_directory)],
17894
- ["\u8FC7\u671F\u65F6\u95F4", draftSession.expires_at || "-"],
18044
+ ["\u8FC7\u671F\u65F6\u95F4", formatCommandDateTime(draftSession.expires_at)],
17895
18045
  ["\u6A21\u5F0F", "ask"]
17896
18046
  ],
17897
18047
  ["\u8FD9\u662F\u9690\u85CF\u7684\u8349\u7A3F\u7EBF\u7A0B\uFF0C\u4E0D\u4F1A\u51FA\u73B0\u5728\u5E38\u89C4\u4F1A\u8BDD\u5217\u8868\u4E2D\u3002"],
@@ -18775,6 +18925,16 @@ function pushStreamFeedbackTools(target, tools) {
18775
18925
  } catch {
18776
18926
  }
18777
18927
  }
18928
+ function pushStreamFeedbackStatus(target, text2) {
18929
+ if (typeof target.adapter.onStreamStatus !== "function") return;
18930
+ target.ensureStarted?.();
18931
+ const rendered = renderFeedbackTextForChannel(target.channelType, text2);
18932
+ if (!rendered) return;
18933
+ try {
18934
+ target.adapter.onStreamStatus(target.chatId, rendered, target.streamKey);
18935
+ } catch {
18936
+ }
18937
+ }
18778
18938
  async function finalizeStreamFeedback(target, status, text2) {
18779
18939
  if (typeof target.adapter.onStreamEnd !== "function") return false;
18780
18940
  const rendered = renderFeedbackTextForChannel(target.channelType, text2);
@@ -18793,6 +18953,7 @@ var STREAM_DEFAULTS = {
18793
18953
  telegram: { intervalMs: 700, minDeltaChars: 20, maxChars: 3900 },
18794
18954
  discord: { intervalMs: 1500, minDeltaChars: 40, maxChars: 1900 }
18795
18955
  };
18956
+ var STREAM_STATUS_HEARTBEAT_MS = 1e4;
18796
18957
  function getStreamConfig(channelType = "telegram") {
18797
18958
  const { store } = getBridgeContext();
18798
18959
  const defaults = STREAM_DEFAULTS[channelType] || STREAM_DEFAULTS.telegram;
@@ -18812,12 +18973,40 @@ function flushPreview(adapter, state, config2) {
18812
18973
  }).catch(() => {
18813
18974
  });
18814
18975
  }
18976
+ function formatRuntimeDuration(ms) {
18977
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
18978
+ if (totalSeconds < 60) return `${totalSeconds}s`;
18979
+ const totalMinutes = Math.floor(totalSeconds / 60);
18980
+ const seconds = totalSeconds % 60;
18981
+ if (totalMinutes < 60) {
18982
+ return seconds > 0 ? `${totalMinutes}m ${seconds}s` : `${totalMinutes}m`;
18983
+ }
18984
+ const hours = Math.floor(totalMinutes / 60);
18985
+ const minutes = totalMinutes % 60;
18986
+ if (minutes === 0 && seconds === 0) return `${hours}h`;
18987
+ if (seconds === 0) return `${hours}h ${minutes}m`;
18988
+ return `${hours}h ${minutes}m ${seconds}s`;
18989
+ }
18990
+ function formatInteractiveRuntimeStatus(elapsedMs, silentMs) {
18991
+ const parts = [`\u5DF2\u8FD0\u884C ${formatRuntimeDuration(elapsedMs)}`];
18992
+ if (typeof silentMs === "number" && silentMs >= 0) {
18993
+ parts.push(`\u6700\u8FD1 ${formatRuntimeDuration(silentMs)} \u65E0\u65B0\u8F93\u51FA`);
18994
+ }
18995
+ return parts.join("\uFF0C");
18996
+ }
18815
18997
  async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
18816
18998
  const binding = resolve(msg.address);
18817
18999
  const streamKey = buildInteractiveStreamKey(binding.codepilotSessionId, msg.messageId);
19000
+ const nowMs = deps.nowMs ?? (() => Date.now());
19001
+ const setIntervalFn = deps.setIntervalFn ?? ((callback, intervalMs) => setInterval(callback, intervalMs));
19002
+ const clearIntervalFn = deps.clearIntervalFn ?? ((handle) => clearInterval(handle));
19003
+ const processMessageImpl = deps.processMessageImpl ?? processMessage;
19004
+ const forwardPermissionRequestImpl = deps.forwardPermissionRequestImpl ?? forwardPermissionRequest;
19005
+ const streamStatusHeartbeatMs = Math.max(1e3, deps.streamStatusHeartbeatMs ?? STREAM_STATUS_HEARTBEAT_MS);
18818
19006
  adapter.onMessageStart?.(msg.address.chatId, streamKey);
18819
19007
  const taskAbort = new AbortController();
18820
19008
  const taskId = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
19009
+ const taskStartedAt = nowMs();
18821
19010
  deps.resetMirrorSessionForInteractiveRun(binding.codepilotSessionId);
18822
19011
  const taskState = {
18823
19012
  id: taskId,
@@ -18828,7 +19017,8 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
18828
19017
  streamKey,
18829
19018
  sessionId: binding.codepilotSessionId,
18830
19019
  hasStreamingCards: false,
18831
- lastActivityAt: Date.now(),
19020
+ structuredStreamUiActive: false,
19021
+ lastActivityAt: taskStartedAt,
18832
19022
  idleReminderSent: false,
18833
19023
  streamFinalized: false,
18834
19024
  uiEnded: false,
@@ -18891,6 +19081,33 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
18891
19081
  chatId: msg.address.chatId,
18892
19082
  streamKey
18893
19083
  };
19084
+ const supportsPersistentStreamStatus = hasStreamingCards && adapter.provider === "feishu" && typeof adapter.onStreamStatus === "function";
19085
+ const supportsStructuredStreamUi = supportsPersistentStreamStatus && (adapter.supportsStructuredStreamingUi?.(msg.address.chatId) ?? true);
19086
+ const syncStructuredStreamUiState = () => {
19087
+ if (!supportsStructuredStreamUi || taskState.structuredStreamUiActive) return;
19088
+ if (adapter.hasActiveStreamingUi?.(msg.address.chatId, streamKey)) {
19089
+ taskState.structuredStreamUiActive = true;
19090
+ }
19091
+ };
19092
+ const pushRunningStatus = (silentMs) => {
19093
+ if (!supportsStructuredStreamUi || streamStatusUpdatesClosed) return;
19094
+ pushStreamFeedbackStatus(
19095
+ streamFeedbackTarget,
19096
+ formatInteractiveRuntimeStatus(nowMs() - taskStartedAt, silentMs)
19097
+ );
19098
+ syncStructuredStreamUiState();
19099
+ };
19100
+ let streamStatusHeartbeat = null;
19101
+ let streamStatusUpdatesClosed = false;
19102
+ const clearStreamStatusHeartbeat = () => {
19103
+ if (streamStatusHeartbeat == null) return;
19104
+ clearIntervalFn(streamStatusHeartbeat);
19105
+ streamStatusHeartbeat = null;
19106
+ };
19107
+ const stopStructuredStreamStatusUpdates = () => {
19108
+ streamStatusUpdatesClosed = true;
19109
+ clearStreamStatusHeartbeat();
19110
+ };
18894
19111
  const onStreamCardText = hasStreamingCards ? (fullText) => {
18895
19112
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
18896
19113
  pushStreamFeedbackText(
@@ -18911,6 +19128,8 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
18911
19128
  if (hasStreamingCards) {
18912
19129
  pushStreamFeedbackTools(streamFeedbackTarget, Array.from(toolCallTracker.values()));
18913
19130
  }
19131
+ pushRunningStatus(null);
19132
+ syncStructuredStreamUiState();
18914
19133
  };
18915
19134
  const onPartialText = (fullText) => {
18916
19135
  if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId)) return;
@@ -18918,17 +19137,36 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
18918
19137
  deps.recordInteractiveHealthProgress(binding.codepilotSessionId, "text");
18919
19138
  previewOnPartialText?.(fullText);
18920
19139
  onStreamCardText?.(fullText);
19140
+ pushRunningStatus(null);
19141
+ syncStructuredStreamUiState();
18921
19142
  };
19143
+ if (supportsStructuredStreamUi) {
19144
+ pushRunningStatus(null);
19145
+ streamStatusHeartbeat = setIntervalFn(() => {
19146
+ if (streamStatusUpdatesClosed) {
19147
+ clearStreamStatusHeartbeat();
19148
+ return;
19149
+ }
19150
+ if (!deps.isCurrentInteractiveTask(binding.codepilotSessionId, taskId) || taskAbort.signal.aborted) {
19151
+ clearStreamStatusHeartbeat();
19152
+ return;
19153
+ }
19154
+ const silentMs = nowMs() - taskState.lastActivityAt;
19155
+ if (silentMs < streamStatusHeartbeatMs) return;
19156
+ pushRunningStatus(silentMs);
19157
+ syncStructuredStreamUiState();
19158
+ }, streamStatusHeartbeatMs);
19159
+ }
18922
19160
  let finalOutcome = "failed";
18923
19161
  let finalOutcomeDetail;
18924
19162
  let shouldRecordHealthEnd = true;
18925
19163
  try {
18926
19164
  const promptText = text2 || (attachments && attachments.length > 0 ? "Describe this image." : "");
18927
- const result = await processMessage(
19165
+ const result = await processMessageImpl(
18928
19166
  binding,
18929
19167
  promptText,
18930
19168
  async (perm) => {
18931
- await forwardPermissionRequest(
19169
+ await forwardPermissionRequestImpl(
18932
19170
  adapter,
18933
19171
  msg.address,
18934
19172
  perm.permissionRequestId,
@@ -18960,6 +19198,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
18960
19198
  }
18961
19199
  let cardFinalized = false;
18962
19200
  if (hasStreamingCards) {
19201
+ stopStructuredStreamStatusUpdates();
18963
19202
  cardFinalized = await finalizeStreamFeedback(
18964
19203
  streamFeedbackTarget,
18965
19204
  result.hasError ? "error" : "completed",
@@ -18994,6 +19233,7 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
18994
19233
  finalOutcome = result.hasError ? "failed" : "completed";
18995
19234
  finalOutcomeDetail = result.hasError ? result.errorMessage?.trim() || void 0 : void 0;
18996
19235
  } finally {
19236
+ stopStructuredStreamStatusUpdates();
18997
19237
  if (previewState) {
18998
19238
  if (previewState.throttleTimer) {
18999
19239
  clearTimeout(previewState.throttleTimer);
@@ -19038,6 +19278,9 @@ function buildInteractiveIdleReminderNotice() {
19038
19278
  "\u7CFB\u7EDF\u4E0D\u4F1A\u81EA\u52A8\u7EC8\u6B62\u5B83\uFF1B\u5982\u679C\u4F60\u4ECD\u5728\u5BF9\u5E94\u7EBF\u7A0B\uFF0C\u53EF\u53D1\u9001 `/stop` \u4E3B\u52A8\u505C\u6B62\uFF1B\u5982\u679C\u5DF2\u7ECF\u5207\u5230\u522B\u7684\u7EBF\u7A0B\uFF0C\u9700\u8981\u5148\u5207\u56DE\u5BF9\u5E94\u7EBF\u7A0B\u3002"
19039
19279
  ].join("\n");
19040
19280
  }
19281
+ function shouldSkipIdleReminder(task) {
19282
+ return task.adapter.provider === "feishu" && task.structuredStreamUiActive;
19283
+ }
19041
19284
  function createInteractiveRuntime(getState2, options, deps) {
19042
19285
  function getQueuedCount(sessionId) {
19043
19286
  return getState2().queuedCounts.get(sessionId) || 0;
@@ -19096,6 +19339,7 @@ function createInteractiveRuntime(getState2, options, deps) {
19096
19339
  const now2 = Date.now();
19097
19340
  const tasks = Array.from(getState2().activeTasks.values());
19098
19341
  for (const task of tasks) {
19342
+ if (shouldSkipIdleReminder(task)) continue;
19099
19343
  if (task.idleReminderSent) continue;
19100
19344
  if (now2 - task.lastActivityAt < options.idleReminderMs) continue;
19101
19345
  await remindIdleInteractiveTask(task);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-to-im",
3
- "version": "1.0.34",
3
+ "version": "1.0.36",
4
4
  "description": "Installable Codex-to-IM bridge with local setup UI and background service",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/zhangle1987/codex-to-im#readme",