codex-to-im 1.0.38 → 1.0.40

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/README.md CHANGED
@@ -11,6 +11,15 @@
11
11
  3. 把桌面里的 Codex 会话接到 IM
12
12
  4. 在 IM 中继续对话、切线程、查看状态
13
13
 
14
+ ## 项目来源
15
+
16
+ 当前代码库是两个早期工程整合后的延续版本:
17
+
18
+ - `Claude-to-IM`
19
+ - `Claude-to-IM-skill`
20
+
21
+ `codex-to-im` 基于这两个项目继续演进,重构为单包本地应用,并统一了本地工作台、bridge、共享线程工作流,以及可选的 skill 集成方式。
22
+
14
23
  ## 核心能力
15
24
 
16
25
  - 共享桌面线程:把 Codex Desktop 中正在使用的 thread 绑定到 IM,在 IM 中继续同一条会话。
package/README_EN.md CHANGED
@@ -11,6 +11,15 @@ Its main path is not to modify Codex itself, but to:
11
11
  3. bind desktop Codex sessions to IM chats
12
12
  4. continue the same conversation, switch threads, and inspect status from IM
13
13
 
14
+ ## Project Origin
15
+
16
+ The current codebase is a consolidated continuation of two earlier projects:
17
+
18
+ - `Claude-to-IM`
19
+ - `Claude-to-IM-skill`
20
+
21
+ `codex-to-im` continues from those two repositories and has been reworked into a single local package with a unified workbench, bridge, shared-thread workflow, and optional skill integration model.
22
+
14
23
  ## Core Capabilities
15
24
 
16
25
  - Shared desktop threads: bind a thread currently used in Codex Desktop to IM and continue the same conversation there.
package/dist/daemon.mjs CHANGED
@@ -4059,6 +4059,7 @@ var init_codex_provider = __esm({
4059
4059
  thread = codex.startThread(threadOptions);
4060
4060
  }
4061
4061
  let sawAnyEvent = false;
4062
+ let sawTerminalEvent = false;
4062
4063
  try {
4063
4064
  const { events } = await thread.runStreamed(input, {
4064
4065
  signal: params.abortController?.signal
@@ -4093,19 +4094,25 @@ var init_codex_provider = __esm({
4093
4094
  } : void 0,
4094
4095
  ...threadId ? { session_id: threadId } : {}
4095
4096
  }));
4097
+ sawTerminalEvent = true;
4096
4098
  break;
4097
4099
  }
4098
4100
  case "turn.failed": {
4099
4101
  const error = event.message;
4100
4102
  controller.enqueue(sseEvent("error", error || "Turn failed"));
4103
+ sawTerminalEvent = true;
4101
4104
  break;
4102
4105
  }
4103
4106
  case "error": {
4104
4107
  const error = event.message;
4105
4108
  controller.enqueue(sseEvent("error", error || "Thread error"));
4109
+ sawTerminalEvent = true;
4106
4110
  break;
4107
4111
  }
4108
4112
  }
4113
+ if (sawTerminalEvent) {
4114
+ break;
4115
+ }
4109
4116
  }
4110
4117
  break;
4111
4118
  } catch (err) {
@@ -16621,6 +16628,8 @@ function createMirrorTurnState(sessionId, timestamp, turnId) {
16621
16628
  streamKey: buildMirrorStreamKey(sessionId, turnId || null, safeTimestamp),
16622
16629
  startedAt: safeTimestamp,
16623
16630
  lastActivityAt: safeTimestamp,
16631
+ lastStatusText: null,
16632
+ lastStatusAt: 0,
16624
16633
  userText: null,
16625
16634
  lastAssistantText: null,
16626
16635
  lastCommentaryText: null,
@@ -19318,6 +19327,14 @@ async function runInteractiveMessage(adapter, msg, text2, attachments, deps) {
19318
19327
  }
19319
19328
 
19320
19329
  // src/lib/bridge/interactive-runtime.ts
19330
+ var TERMINAL_SESSION_HEALTH_STATUSES = /* @__PURE__ */ new Set([
19331
+ "completed",
19332
+ "failed",
19333
+ "aborted"
19334
+ ]);
19335
+ function isTerminalSessionHealthStatus(status) {
19336
+ return Boolean(status && TERMINAL_SESSION_HEALTH_STATUSES.has(status));
19337
+ }
19321
19338
  function buildInteractiveIdleReminderNotice() {
19322
19339
  return [
19323
19340
  "\u63D0\u9192\uFF1A\u8FD9\u8F6E\u4EFB\u52A1\u4ECD\u5728\u8FD0\u884C\uFF0C\u4F46\u5DF2\u7ECF\u8D85\u8FC7 10 \u5206\u949F\u6CA1\u6709\u65B0\u7684\u6267\u884C\u8F93\u51FA\u3002",
@@ -19391,6 +19408,24 @@ function createInteractiveRuntime(getState2, options, deps) {
19391
19408
  await remindIdleInteractiveTask(task);
19392
19409
  }
19393
19410
  }
19411
+ function reconcileTerminalSessionRuntimeState() {
19412
+ const store = deps.getStore();
19413
+ for (const session of store.listSessions()) {
19414
+ const queuedCount = getQueuedCount(session.id);
19415
+ const persistedQueuedCount = session.queued_count && session.queued_count > 0 ? session.queued_count : 0;
19416
+ const hasActiveTask = getState2().activeTasks.has(session.id);
19417
+ if (hasActiveTask || queuedCount > 0) continue;
19418
+ if (!isTerminalSessionHealthStatus(session.health_status)) continue;
19419
+ if (persistedQueuedCount === 0 && session.runtime_status !== "running" && session.runtime_status !== "queued") {
19420
+ continue;
19421
+ }
19422
+ store.updateSession(session.id, {
19423
+ queued_count: 0,
19424
+ runtime_status: "idle",
19425
+ last_runtime_update_at: deps.nowIso()
19426
+ });
19427
+ }
19428
+ }
19394
19429
  function resetPersistedInteractiveRuntimeState() {
19395
19430
  const store = deps.getStore();
19396
19431
  for (const session of store.listSessions()) {
@@ -19452,6 +19487,7 @@ function createInteractiveRuntime(getState2, options, deps) {
19452
19487
  releaseInteractiveTask,
19453
19488
  syncSessionRuntimeState,
19454
19489
  reconcileIdleInteractiveTasks,
19490
+ reconcileTerminalSessionRuntimeState,
19455
19491
  resetPersistedInteractiveRuntimeState,
19456
19492
  processWithSessionLock
19457
19493
  };
@@ -20601,6 +20637,8 @@ var MIRROR_EVENT_BATCH_LIMIT = 8;
20601
20637
  var MIRROR_SUPPRESSION_WINDOW_MS = 4e3;
20602
20638
  var MIRROR_PROMPT_MATCH_GRACE_MS = 12e4;
20603
20639
  var INTERACTIVE_IDLE_REMINDER_MS = 6e5;
20640
+ var MIRROR_STREAM_STATUS_IDLE_START_MS = 18e4;
20641
+ var MIRROR_STREAM_STATUS_HEARTBEAT_MS = 1e4;
20604
20642
  var MIRROR_IDLE_TIMEOUT_MS = 6e5;
20605
20643
  function describeUnknownError(error) {
20606
20644
  if (error instanceof Error) {
@@ -20782,6 +20820,21 @@ function getMirrorStreamingText(subscription, turnState) {
20782
20820
  );
20783
20821
  return rendered || buildMirrorTitle(title, markdown);
20784
20822
  }
20823
+ function getMirrorStructuredStreamStatusConfig() {
20824
+ const { store } = getBridgeContext();
20825
+ const idleStartSeconds = parseInt(store.getSetting("bridge_stream_status_idle_start_seconds") || "", 10);
20826
+ const heartbeatSeconds = parseInt(store.getSetting("bridge_stream_status_check_interval_seconds") || "", 10);
20827
+ return {
20828
+ idleStartMs: Math.max(
20829
+ 0,
20830
+ (Number.isFinite(idleStartSeconds) && idleStartSeconds > 0 ? idleStartSeconds : MIRROR_STREAM_STATUS_IDLE_START_MS / 1e3) * 1e3
20831
+ ),
20832
+ heartbeatMs: Math.max(
20833
+ 1e3,
20834
+ (Number.isFinite(heartbeatSeconds) && heartbeatSeconds > 0 ? heartbeatSeconds : MIRROR_STREAM_STATUS_HEARTBEAT_MS / 1e3) * 1e3
20835
+ )
20836
+ };
20837
+ }
20785
20838
  function startMirrorStreaming(subscription, turnState) {
20786
20839
  const adapter = getMirrorStreamingAdapter(subscription);
20787
20840
  if (!adapter || turnState.streamStarted) return;
@@ -20805,6 +20858,50 @@ function createMirrorStreamFeedbackTarget(subscription, turnState, adapter) {
20805
20858
  }
20806
20859
  };
20807
20860
  }
20861
+ function pushMirrorStreamingStatus(subscription, turnState, options = {}) {
20862
+ const adapter = getMirrorStreamingAdapter(subscription);
20863
+ if (!adapter || typeof adapter.onStreamStatus !== "function") return;
20864
+ if (!(adapter.supportsStructuredStreamingUi?.(subscription.chatId) ?? true)) return;
20865
+ const startedAtMs = Date.parse(turnState.startedAt);
20866
+ if (!Number.isFinite(startedAtMs)) return;
20867
+ const nowMs = options.nowMs ?? Date.now();
20868
+ const minIntervalMs = Math.max(0, options.minIntervalMs ?? 0);
20869
+ if (minIntervalMs > 0 && turnState.lastStatusAt > 0 && nowMs - turnState.lastStatusAt < minIntervalMs) {
20870
+ return;
20871
+ }
20872
+ const statusText = formatInteractiveRuntimeStatus(
20873
+ Math.max(0, nowMs - startedAtMs),
20874
+ options.silentMs
20875
+ );
20876
+ if (turnState.lastStatusText === statusText) return;
20877
+ pushStreamFeedbackStatus(
20878
+ createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
20879
+ statusText
20880
+ );
20881
+ turnState.lastStatusText = statusText;
20882
+ turnState.lastStatusAt = nowMs;
20883
+ }
20884
+ function refreshMirrorStreamingStatus(subscription, nowMs = Date.now(), config2 = getMirrorStructuredStreamStatusConfig()) {
20885
+ const pendingTurn = subscription.pendingTurn;
20886
+ if (!pendingTurn?.streamStarted) return;
20887
+ const startedAtMs = Date.parse(pendingTurn.startedAt);
20888
+ const lastActivityMs = Date.parse(pendingTurn.lastActivityAt);
20889
+ if (!Number.isFinite(startedAtMs) || !Number.isFinite(lastActivityMs)) return;
20890
+ const elapsedMs = nowMs - startedAtMs;
20891
+ if (elapsedMs < config2.idleStartMs) return;
20892
+ const silentMs = nowMs - lastActivityMs;
20893
+ if (silentMs < config2.heartbeatMs) return;
20894
+ pushMirrorStreamingStatus(subscription, pendingTurn, {
20895
+ nowMs,
20896
+ silentMs,
20897
+ minIntervalMs: config2.heartbeatMs
20898
+ });
20899
+ }
20900
+ function refreshActiveMirrorStreamingStatuses(nowMs = Date.now()) {
20901
+ for (const subscription of getState().mirrorSubscriptions.values()) {
20902
+ refreshMirrorStreamingStatus(subscription, nowMs);
20903
+ }
20904
+ }
20808
20905
  function updateMirrorStreaming(subscription, turnState) {
20809
20906
  const adapter = getMirrorStreamingAdapter(subscription);
20810
20907
  if (!adapter) return;
@@ -20812,6 +20909,7 @@ function updateMirrorStreaming(subscription, turnState) {
20812
20909
  createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
20813
20910
  getMirrorStreamingText(subscription, turnState)
20814
20911
  );
20912
+ pushMirrorStreamingStatus(subscription, turnState);
20815
20913
  }
20816
20914
  function updateMirrorToolProgress(subscription, turnState) {
20817
20915
  const adapter = getMirrorStreamingAdapter(subscription);
@@ -20820,6 +20918,7 @@ function updateMirrorToolProgress(subscription, turnState) {
20820
20918
  createMirrorStreamFeedbackTarget(subscription, turnState, adapter),
20821
20919
  Array.from(turnState.toolCalls.values())
20822
20920
  );
20921
+ pushMirrorStreamingStatus(subscription, turnState);
20823
20922
  }
20824
20923
  function stopMirrorStreaming(subscription, status = "interrupted") {
20825
20924
  const adapter = getMirrorStreamingAdapter(subscription);
@@ -20928,6 +21027,7 @@ function resetMirrorSessionForInteractiveRun(sessionId) {
20928
21027
  }
20929
21028
  async function reconcileMirrorSubscriptions() {
20930
21029
  await MIRROR_RUNTIME.reconcileMirrorSubscriptions();
21030
+ refreshActiveMirrorStreamingStatuses();
20931
21031
  }
20932
21032
  function clearMirrorSubscriptions() {
20933
21033
  MIRROR_RUNTIME.clearMirrorSubscriptions();
@@ -20978,6 +21078,7 @@ async function start() {
20978
21078
  });
20979
21079
  try {
20980
21080
  SESSION_HEALTH_RUNTIME.reconcileSessionHealth();
21081
+ INTERACTIVE_RUNTIME.reconcileTerminalSessionRuntimeState();
20981
21082
  } catch (err) {
20982
21083
  console.error("[bridge-manager] Session health reconcile failed:", describeUnknownError(err));
20983
21084
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-to-im",
3
- "version": "1.0.38",
3
+ "version": "1.0.40",
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",