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 +1 -0
- package/package.json +1 -1
- package/src/adapter.mjs +20 -1
- package/src/terminal-activity.mjs +170 -0
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
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
|
|
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
|
+
}
|