codex-copilot-dx 0.5.3 → 0.5.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/README.md CHANGED
@@ -185,6 +185,7 @@ Environment variables:
185
185
  | `CCDX_LOG_PATH` | unset | Mirror terminal logs to a file; set to `1` for `~/.local/share/codex-copilot-dx/debug.log` |
186
186
  | `CCDX_LOG_LEVEL` | `info` | Set to `debug` to include upstream request attempts, status codes, retry causes, and timings |
187
187
  | `CCDX_LOG_MAX_BYTES` | `16777216` | Rotate the debug log at this size, retaining one `.1` backup; set to `0` to disable rotation |
188
+ | `CCDX_TERMINAL_ANIMATION` | auto | Show a transient comet while an interactive terminal has active requests and no new output for 800ms; set to `0` to disable |
188
189
  | `CCDX_IMG_MAX_DIM` | `2048` | Max long edge in pixels for image downscaling |
189
190
  | `CCDX_IMG_QUALITY` | `82` | Initial WebP quality used when re-encoding images |
190
191
  | `CCDX_IMG_MIN_BYTES` | `100000` | In-bounds images smaller than this are left untouched; oversized images are still downscaled |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Use Codex Desktop, Claude Code, and Claude App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
package/src/adapter.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  import { createRequestId, runWithRequestContext } from "./request-context.mjs";
14
14
  import { createStreamPerformanceMetrics } from "./stream-performance.mjs";
15
15
  import { status } from "./status.mjs";
16
+ import { createTerminalActivityIndicator } from "./terminal-activity.mjs";
16
17
  import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs";
17
18
  import { createResponsesCompactHandler, createResponsesHandler } from "./responses-handler.mjs";
18
19
  import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
@@ -232,9 +233,20 @@ export function createAdapterHandler(options = {}) {
232
233
  const routeName = classifyAdapterRoute(req.method, pathname);
233
234
  const complete = trackRequest ? requestMetrics.begin(routeName) : () => {};
234
235
  const streamPerformance = trackRequest ? streamPerformanceMetrics.begin(routeName) : null;
236
+ let finishTerminalActivity = () => {};
237
+ if (trackRequest) {
238
+ try {
239
+ const finish = options.terminalActivity?.beginRequest?.();
240
+ if (typeof finish === "function") finishTerminalActivity = finish;
241
+ } catch {}
242
+ }
243
+ let requestFinished = false;
235
244
  const finishRequest = ({ statusCode = 0, aborted = false } = {}) => {
245
+ if (requestFinished) return;
246
+ requestFinished = true;
236
247
  complete({ statusCode, aborted });
237
248
  streamPerformance?.finish({ failed: aborted || statusCode >= 400 });
249
+ try { finishTerminalActivity(); } catch {}
238
250
  };
239
251
  if (trackRequest && typeof res.once === "function") {
240
252
  res.once("finish", () => finishRequest({ statusCode: res.statusCode }));
@@ -268,7 +280,13 @@ export function createAdapterHandler(options = {}) {
268
280
  // Public server entry point.
269
281
 
270
282
  export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
271
- const server = http.createServer(createAdapterHandler(options));
283
+ const ownsTerminalActivity = !Object.hasOwn(options, "terminalActivity");
284
+ const terminalActivity = ownsTerminalActivity ? createTerminalActivityIndicator() : options.terminalActivity;
285
+ const server = http.createServer(createAdapterHandler({ ...options, terminalActivity }));
286
+ const cleanupTerminalActivity = () => {
287
+ if (ownsTerminalActivity) terminalActivity?.cleanup?.();
288
+ };
289
+ server.once("close", cleanupTerminalActivity);
272
290
 
273
291
  server.on("upgrade", (req, socket) => {
274
292
  // Codex Desktop 0.130+ negotiates a "responses_websockets" server-push
@@ -282,6 +300,7 @@ export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
282
300
 
283
301
  return new Promise((resolve, reject) => {
284
302
  const onListenError = (e) => {
303
+ cleanupTerminalActivity();
285
304
  if (e?.code === "EADDRINUSE") {
286
305
  reject(new Error(`Adapter address http://${host}:${port} is already in use. Stop the existing ccdx process or set ADAPTER_PORT to another port.`));
287
306
  return;
@@ -0,0 +1,170 @@
1
+ const ESC = "\u001b";
2
+ const ERASE_LINE = `\r${ESC}[2K`;
3
+ const HIDE_CURSOR = `${ESC}[?25l`;
4
+ const SHOW_CURSOR = `${ESC}[?25h`;
5
+
6
+ const TRACK_WIDTH = 20;
7
+ const TAIL_LENGTH = 7;
8
+ const IDLE_DELAY_MS = 800;
9
+ const FRAME_DELAY_MS = 45;
10
+ const LOOP_PAUSE_MS = 200;
11
+ const TAIL_COLORS = [159, 123, 87, 51, 45, 39, 33];
12
+
13
+ function envFlagEnabled(value) {
14
+ const normalized = String(value || "").trim().toLowerCase();
15
+ return normalized !== "" && !["0", "false", "no", "off"].includes(normalized);
16
+ }
17
+
18
+ function terminalActivityEnabled({ env, output, errorOutput }) {
19
+ const animationSetting = String(env.CCDX_TERMINAL_ANIMATION || "").trim().toLowerCase();
20
+ if (["0", "false", "no", "off"].includes(animationSetting)) return false;
21
+ if (envFlagEnabled(env.CI) || String(env.TERM || "").toLowerCase() === "dumb") return false;
22
+ if (output?.isTTY !== true || errorOutput?.isTTY !== true) return false;
23
+ const columns = Number(output.columns);
24
+ return !Number.isFinite(columns) || columns <= 0 || columns >= TRACK_WIDTH + 2;
25
+ }
26
+
27
+ export function renderCometFrame(position) {
28
+ let track = "";
29
+ for (let column = 0; column < TRACK_WIDTH; column += 1) {
30
+ const distance = position - column;
31
+ if (distance === 0) {
32
+ track += `${ESC}[97m:${ESC}[0m`;
33
+ } else if (distance > 0 && distance <= TAIL_LENGTH) {
34
+ track += `${ESC}[38;5;${TAIL_COLORS[distance - 1]}m:${ESC}[0m`;
35
+ } else {
36
+ track += " ";
37
+ }
38
+ }
39
+ return `[${track}]`;
40
+ }
41
+
42
+ function disabledIndicator() {
43
+ return {
44
+ enabled: false,
45
+ beginRequest() { return () => {}; },
46
+ cleanup() {},
47
+ };
48
+ }
49
+
50
+ export function createTerminalActivityIndicator({
51
+ env = process.env,
52
+ output = process.stdout,
53
+ errorOutput = process.stderr,
54
+ consoleObj = console,
55
+ timers = { setTimeout, clearTimeout },
56
+ } = {}) {
57
+ if (!terminalActivityEnabled({ env, output, errorOutput })) return disabledIndicator();
58
+
59
+ const methods = ["log", "warn", "error", "debug"];
60
+ const originals = Object.fromEntries(methods.map((method) => [method, consoleObj[method]]));
61
+ const wrappers = {};
62
+ let activeRequests = 0;
63
+ let idleTimer = null;
64
+ let frameTimer = null;
65
+ let framePosition = 0;
66
+ let cursorHidden = false;
67
+ let frameVisible = false;
68
+ let cleaned = false;
69
+
70
+ const schedule = (callback, delay) => {
71
+ const timer = timers.setTimeout(callback, delay);
72
+ timer?.unref?.();
73
+ return timer;
74
+ };
75
+
76
+ const clearTimer = (timer) => {
77
+ if (timer) timers.clearTimeout(timer);
78
+ };
79
+
80
+ const clearAnimation = () => {
81
+ clearTimer(frameTimer);
82
+ frameTimer = null;
83
+ framePosition = 0;
84
+ if (cursorHidden || frameVisible) output.write(`${ERASE_LINE}${SHOW_CURSOR}`);
85
+ cursorHidden = false;
86
+ frameVisible = false;
87
+ };
88
+
89
+ const cancelPendingOutput = () => {
90
+ clearTimer(idleTimer);
91
+ idleTimer = null;
92
+ clearAnimation();
93
+ };
94
+
95
+ const drawNextFrame = () => {
96
+ frameTimer = null;
97
+ if (cleaned || activeRequests === 0) {
98
+ clearAnimation();
99
+ return;
100
+ }
101
+ if (framePosition >= TRACK_WIDTH + TAIL_LENGTH) {
102
+ output.write(ERASE_LINE);
103
+ frameVisible = false;
104
+ framePosition = 0;
105
+ frameTimer = schedule(drawNextFrame, LOOP_PAUSE_MS);
106
+ return;
107
+ }
108
+
109
+ const prefix = cursorHidden ? "" : HIDE_CURSOR;
110
+ cursorHidden = true;
111
+ frameVisible = true;
112
+ output.write(`${prefix}${ERASE_LINE}${renderCometFrame(framePosition)}`);
113
+ framePosition += 1;
114
+ frameTimer = schedule(drawNextFrame, FRAME_DELAY_MS);
115
+ };
116
+
117
+ const scheduleAfterIdle = () => {
118
+ clearTimer(idleTimer);
119
+ idleTimer = null;
120
+ if (cleaned || activeRequests === 0) return;
121
+ idleTimer = schedule(() => {
122
+ idleTimer = null;
123
+ drawNextFrame();
124
+ }, IDLE_DELAY_MS);
125
+ };
126
+
127
+ for (const method of methods) {
128
+ const original = typeof originals[method] === "function" ? originals[method] : () => {};
129
+ wrappers[method] = (...args) => {
130
+ cancelPendingOutput();
131
+ try {
132
+ return original.apply(consoleObj, args);
133
+ } finally {
134
+ scheduleAfterIdle();
135
+ }
136
+ };
137
+ consoleObj[method] = wrappers[method];
138
+ }
139
+
140
+ return {
141
+ enabled: true,
142
+ beginRequest() {
143
+ if (cleaned) return () => {};
144
+ activeRequests += 1;
145
+ if (activeRequests === 1 && !idleTimer && !frameTimer) scheduleAfterIdle();
146
+ let finished = false;
147
+ return () => {
148
+ if (finished) return;
149
+ finished = true;
150
+ activeRequests = Math.max(0, activeRequests - 1);
151
+ if (activeRequests === 0) {
152
+ clearTimer(idleTimer);
153
+ idleTimer = null;
154
+ clearAnimation();
155
+ }
156
+ };
157
+ },
158
+ cleanup() {
159
+ if (cleaned) return;
160
+ cleaned = true;
161
+ activeRequests = 0;
162
+ clearTimer(idleTimer);
163
+ idleTimer = null;
164
+ clearAnimation();
165
+ for (const method of methods) {
166
+ if (consoleObj[method] === wrappers[method]) consoleObj[method] = originals[method];
167
+ }
168
+ },
169
+ };
170
+ }