codex-copilot-dx 0.7.0 → 0.7.1
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 +23 -1
- package/bin/cli.mjs +13 -1
- package/package.json +1 -1
- package/src/adapter.mjs +3 -1
- package/src/cli-animation.mjs +108 -0
- package/src/cli-options.mjs +8 -0
- package/src/terminal-activity.mjs +40 -34
- package/src/terminal-animation.mjs +249 -0
- package/src/user-settings.mjs +50 -0
package/README.md
CHANGED
|
@@ -87,6 +87,28 @@ The selector first queries the running adapter, including its last-known-good mo
|
|
|
87
87
|
|
|
88
88
|
The selection is saved in `~/.config/codex-copilot-dx/config.json`, or under `XDG_CONFIG_HOME` when set. Choosing `gpt-5.5` clears the override and restores the package default. A running adapter reads the setting on the next Auto-review request.
|
|
89
89
|
|
|
90
|
+
### Terminal animation
|
|
91
|
+
|
|
92
|
+
Choose the terminal activity animation interactively:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
ccdx animation
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The selector is ordered numerically:
|
|
99
|
+
|
|
100
|
+
1. Comet (default)
|
|
101
|
+
2. Twin
|
|
102
|
+
3. Shuttle
|
|
103
|
+
4. Chase
|
|
104
|
+
5. Mirror
|
|
105
|
+
6. Pulse
|
|
106
|
+
7. Braille
|
|
107
|
+
|
|
108
|
+
Enter the corresponding number to select an animation. The Comet row includes a static preview, and after a selection CCDX plays one cycle of the chosen animation as confirmation when terminal animation is enabled. The setting is saved in `~/.config/codex-copilot-dx/config.json`, or under `XDG_CONFIG_HOME` when set.
|
|
109
|
+
|
|
110
|
+
The selected theme is loaded once when a new adapter starts, so changing it does not restart or alter an adapter that is already running. Stop and start CCDX to apply the new theme. `CCDX_TERMINAL_ANIMATION=0` (also `false`, `no`, or `off`) remains the boolean gate that disables terminal animation regardless of the selected theme.
|
|
111
|
+
|
|
90
112
|
### Update
|
|
91
113
|
|
|
92
114
|
Choose a source interactively:
|
|
@@ -197,7 +219,7 @@ ccdx
|
|
|
197
219
|
| `CCDX_LOG_PATH` | unset | Mirror terminal logs to a file; set to `1` for `~/.local/share/codex-copilot-dx/debug.log` |
|
|
198
220
|
| `CCDX_LOG_LEVEL` | `info` | Set to `debug` for upstream attempts, status codes, retry causes, and timings |
|
|
199
221
|
| `CCDX_LOG_MAX_BYTES` | `16777216` | Rotate the debug log at this size and retain one `.1` backup; set to `0` to disable rotation |
|
|
200
|
-
| `CCDX_TERMINAL_ANIMATION` | auto | Show
|
|
222
|
+
| `CCDX_TERMINAL_ANIMATION` | auto | Show the selected transient animation while an interactive terminal has active requests and no output for 800 ms; set to `0`, `false`, `no`, or `off` to disable |
|
|
201
223
|
| `CCDX_IMG_MAX_DIM` | `2048` | Maximum long edge in pixels for image downscaling |
|
|
202
224
|
| `CCDX_IMG_QUALITY` | `82` | Initial WebP quality for image re-encoding |
|
|
203
225
|
| `CCDX_IMG_MIN_BYTES` | `100000` | In-bounds images smaller than this remain unchanged; oversized images are still downscaled |
|
package/bin/cli.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { cliCommandName, cliHelp, parseAdapterProbeOptions, parseCliArgs, parseR
|
|
|
16
16
|
import { formatAdapterStatus, readAdapterStatus } from "../src/cli-status.mjs";
|
|
17
17
|
import { closeHttpServer } from "../src/shutdown.mjs";
|
|
18
18
|
import { runAutoReviewModelCommand } from "../src/auto-review-model.mjs";
|
|
19
|
-
import { autoReviewModelPreference } from "../src/user-settings.mjs";
|
|
19
|
+
import { autoReviewModelPreference, terminalAnimationPreference } from "../src/user-settings.mjs";
|
|
20
20
|
import { fetchLiveCopilotModels, formatLiveCopilotModels } from "../src/cli-models.mjs";
|
|
21
21
|
import { runAuthCommand } from "../src/cli-auth.mjs";
|
|
22
22
|
import { createProfileRuntime } from "../src/profile-runtime.mjs";
|
|
@@ -43,6 +43,16 @@ if (CLI.command === "version") {
|
|
|
43
43
|
console.log(CLI_BANNER);
|
|
44
44
|
process.exit(0);
|
|
45
45
|
}
|
|
46
|
+
if (CLI.command === "animation") {
|
|
47
|
+
try {
|
|
48
|
+
const { runAnimationCommand } = await import("../src/cli-animation.mjs");
|
|
49
|
+
await runAnimationCommand({ commandName: CLI_NAME });
|
|
50
|
+
process.exit(0);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error(status("err", e.message));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
46
56
|
if (CLI.command === "usage") {
|
|
47
57
|
await printUsageSummary({
|
|
48
58
|
format: CLI.outputFormat || "auto",
|
|
@@ -231,6 +241,7 @@ try {
|
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
// Start the in-process adapter.
|
|
244
|
+
const terminalAnimationTheme = terminalAnimationPreference().theme;
|
|
234
245
|
activeServer = await startAdapter(ADAPTER_PORT, ADAPTER_HOST, {
|
|
235
246
|
autoReviewModelResolver: () => autoReviewModelPreference().model,
|
|
236
247
|
codexClient: profileRuntime.codexClient,
|
|
@@ -239,6 +250,7 @@ try {
|
|
|
239
250
|
upstreamTimeoutMs: RUNTIME.upstreamTimeoutMs,
|
|
240
251
|
streamHandshakeTimeoutMs: RUNTIME.streamHandshakeTimeoutMs,
|
|
241
252
|
streamIdleTimeoutMs: RUNTIME.streamIdleTimeoutMs,
|
|
253
|
+
terminalAnimationTheme,
|
|
242
254
|
});
|
|
243
255
|
|
|
244
256
|
// Point Codex and compatible ChatGPT App builds at the adapter.
|
package/package.json
CHANGED
package/src/adapter.mjs
CHANGED
|
@@ -311,7 +311,9 @@ export function createAdapterHandler(options = {}) {
|
|
|
311
311
|
|
|
312
312
|
export function startAdapter(port = 2026, host = "127.0.0.1", options = {}) {
|
|
313
313
|
const ownsTerminalActivity = !Object.hasOwn(options, "terminalActivity");
|
|
314
|
-
const terminalActivity = ownsTerminalActivity
|
|
314
|
+
const terminalActivity = ownsTerminalActivity
|
|
315
|
+
? createTerminalActivityIndicator({ theme: options.terminalAnimationTheme })
|
|
316
|
+
: options.terminalActivity;
|
|
315
317
|
const handler = createAdapterHandler({ ...options, terminalActivity });
|
|
316
318
|
const server = http.createServer(handler);
|
|
317
319
|
const cleanupTerminalActivity = () => {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_TERMINAL_ANIMATION_THEME,
|
|
5
|
+
playTerminalAnimationPreview,
|
|
6
|
+
renderTerminalAnimationFrame,
|
|
7
|
+
TERMINAL_ANIMATION_THEMES,
|
|
8
|
+
} from "./terminal-animation.mjs";
|
|
9
|
+
import {
|
|
10
|
+
readUserSettings,
|
|
11
|
+
terminalAnimationPreference,
|
|
12
|
+
writeTerminalAnimationTheme,
|
|
13
|
+
} from "./user-settings.mjs";
|
|
14
|
+
|
|
15
|
+
const DISABLED_ENV_VALUES = new Set(["0", "false", "no", "off"]);
|
|
16
|
+
|
|
17
|
+
function markers(theme, currentTheme) {
|
|
18
|
+
const values = [];
|
|
19
|
+
if (theme.id === DEFAULT_TERMINAL_ANIMATION_THEME) values.push("default");
|
|
20
|
+
if (theme.id === currentTheme) values.push("current");
|
|
21
|
+
return values.length ? ` [${values.join(", ")}]` : "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function disabledEnvironmentValue(env) {
|
|
25
|
+
const value = String(env.CCDX_TERMINAL_ANIMATION || "").trim();
|
|
26
|
+
return DISABLED_ENV_VALUES.has(value.toLowerCase()) ? value : "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runAnimationCommand({
|
|
30
|
+
commandName = "ccdx",
|
|
31
|
+
env = process.env,
|
|
32
|
+
home = os.homedir(),
|
|
33
|
+
input = process.stdin,
|
|
34
|
+
output = process.stdout,
|
|
35
|
+
prompt,
|
|
36
|
+
preview = playTerminalAnimationPreview,
|
|
37
|
+
} = {}) {
|
|
38
|
+
if (!prompt && (!input.isTTY || !output.isTTY)) {
|
|
39
|
+
throw new Error(`${commandName} animation requires an interactive terminal`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
readUserSettings({ env, home, strict: true });
|
|
43
|
+
const current = terminalAnimationPreference({ env, home });
|
|
44
|
+
const currentIndex = TERMINAL_ANIMATION_THEMES.findIndex(({ id }) => id === current.theme);
|
|
45
|
+
|
|
46
|
+
output.write(`${commandName} animation\n`);
|
|
47
|
+
output.write(`Current: ${TERMINAL_ANIMATION_THEMES[currentIndex].label} (${current.source})\n\n`);
|
|
48
|
+
TERMINAL_ANIMATION_THEMES.forEach((theme, index) => {
|
|
49
|
+
const staticPreview = theme.id === DEFAULT_TERMINAL_ANIMATION_THEME
|
|
50
|
+
? ` ${renderTerminalAnimationFrame(theme.id, 10)}`
|
|
51
|
+
: "";
|
|
52
|
+
output.write(` ${index + 1}. ${theme.label}${markers(theme, current.theme)}${staticPreview}\n`);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
let readline;
|
|
56
|
+
const ask = prompt || (async (question) => {
|
|
57
|
+
readline ||= createInterface({ input, output });
|
|
58
|
+
return readline.question(question);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
let selectedTheme = current.theme;
|
|
62
|
+
let keepCurrent = false;
|
|
63
|
+
try {
|
|
64
|
+
while (true) {
|
|
65
|
+
const answer = String(await ask(`Select [${currentIndex + 1}], or q to cancel: `) || "").trim();
|
|
66
|
+
if (answer.toLowerCase() === "q") {
|
|
67
|
+
output.write("No changes made.\n");
|
|
68
|
+
return { changed: false, cancelled: true, theme: current.theme };
|
|
69
|
+
}
|
|
70
|
+
if (answer === "") {
|
|
71
|
+
keepCurrent = true;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
const selectedIndex = Number(answer) - 1;
|
|
75
|
+
if (Number.isInteger(selectedIndex)
|
|
76
|
+
&& selectedIndex >= 0
|
|
77
|
+
&& selectedIndex < TERMINAL_ANIMATION_THEMES.length) {
|
|
78
|
+
selectedTheme = TERMINAL_ANIMATION_THEMES[selectedIndex].id;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
output.write(`Enter a number from 1 to ${TERMINAL_ANIMATION_THEMES.length}, or q to cancel.\n`);
|
|
82
|
+
}
|
|
83
|
+
} finally {
|
|
84
|
+
readline?.close();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = keepCurrent
|
|
88
|
+
? { changed: false, theme: current.theme }
|
|
89
|
+
: writeTerminalAnimationTheme(selectedTheme, { env, home });
|
|
90
|
+
const disabledValue = disabledEnvironmentValue(env);
|
|
91
|
+
if (!disabledValue) {
|
|
92
|
+
try {
|
|
93
|
+
await preview(selectedTheme, { output });
|
|
94
|
+
} catch {
|
|
95
|
+
output.write("[WARN] Preview unavailable; the terminal animation setting was still applied.\n");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const selected = TERMINAL_ANIMATION_THEMES.find(({ id }) => id === selectedTheme);
|
|
100
|
+
output.write(`${result.changed ? "Saved" : "Kept"} terminal animation: ${selected.label}\n`);
|
|
101
|
+
if (disabledValue) {
|
|
102
|
+
output.write(`CCDX_TERMINAL_ANIMATION=${disabledValue} remains effective until it is unset; terminal animation stays disabled.\n`);
|
|
103
|
+
output.write("The saved animation will be used on the next ccdx start after the override is unset.\n");
|
|
104
|
+
} else {
|
|
105
|
+
output.write("The next ccdx start will use this animation.\n");
|
|
106
|
+
}
|
|
107
|
+
return { ...result, cancelled: false, theme: selectedTheme };
|
|
108
|
+
}
|
package/src/cli-options.mjs
CHANGED
|
@@ -19,6 +19,7 @@ const HELP_TOPICS = new Set([
|
|
|
19
19
|
"status",
|
|
20
20
|
"models",
|
|
21
21
|
"usage",
|
|
22
|
+
"animation",
|
|
22
23
|
"auto-review-model",
|
|
23
24
|
"update",
|
|
24
25
|
"version",
|
|
@@ -189,6 +190,10 @@ export function parseCliArgs(args = []) {
|
|
|
189
190
|
}
|
|
190
191
|
throw new Error(`Unknown auth action: ${action}`);
|
|
191
192
|
}
|
|
193
|
+
if (command === "animation") {
|
|
194
|
+
if (rest.length) unexpectedArgs(rest);
|
|
195
|
+
return baseCommand("animation");
|
|
196
|
+
}
|
|
192
197
|
if (command === "auto-review-model") {
|
|
193
198
|
if (rest.length) unexpectedArgs(rest);
|
|
194
199
|
return baseCommand("auto-review-model");
|
|
@@ -275,6 +280,7 @@ function topicHelp(name, topic) {
|
|
|
275
280
|
status: `Usage:\n ${name} status\n\nShows bounded runtime, routing, performance, queue, and cache metrics from a running adapter.`,
|
|
276
281
|
models: `Usage:\n ${name} models [--format table|plain]\n\nPerforms a fresh, read-only Copilot model-directory lookup for the saved account. Interactive terminals use a table by default.`,
|
|
277
282
|
usage: `Usage:\n ${name} usage [--format table|plain]\n\nSummarizes local token usage metadata without reading prompt or completion content. Interactive terminals use a table by default.`,
|
|
283
|
+
animation: `Usage:\n ${name} animation\n\nInteractively selects the terminal activity animation used the next time the adapter starts.`,
|
|
278
284
|
"auto-review-model": `Usage:\n ${name} auto-review-model\n\nInteractively selects an enabled Responses model for Codex Auto-review.`,
|
|
279
285
|
update: `Usage:\n ${name} update [npm|github]\n\nUpdates the global package from the configured npm registry or GitHub main. With no source, an interactive terminal prompts for one.`,
|
|
280
286
|
version: `Usage:\n ${name} --version`,
|
|
@@ -293,6 +299,7 @@ export function cliHelp(commandName = "ccdx", topic = "") {
|
|
|
293
299
|
${name} status
|
|
294
300
|
${name} models [--format table|plain]
|
|
295
301
|
${name} usage [--format table|plain]
|
|
302
|
+
${name} animation
|
|
296
303
|
${name} auto-review-model
|
|
297
304
|
${name} update [npm|github]
|
|
298
305
|
${name} --version
|
|
@@ -305,6 +312,7 @@ Commands:
|
|
|
305
312
|
status Show runtime routing, performance, queue, and cache health
|
|
306
313
|
models Query a saved account's live Copilot model catalog
|
|
307
314
|
usage Summarize locally recorded token usage
|
|
315
|
+
animation Select the terminal activity animation
|
|
308
316
|
auto-review-model Select the Codex Auto-review Responses model
|
|
309
317
|
update Update the global package from npm or GitHub
|
|
310
318
|
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_TERMINAL_ANIMATION_THEME,
|
|
3
|
+
TERMINAL_ANIMATION_THEMES,
|
|
4
|
+
TERMINAL_ANIMATION_TRACK_WIDTH,
|
|
5
|
+
getTerminalAnimationCycleLength,
|
|
6
|
+
getTerminalAnimationFrameDelay,
|
|
7
|
+
isTerminalAnimationTheme,
|
|
8
|
+
renderTerminalAnimationFrame,
|
|
9
|
+
} from "./terminal-animation.mjs";
|
|
10
|
+
|
|
11
|
+
export { renderCometFrame } from "./terminal-animation.mjs";
|
|
12
|
+
|
|
1
13
|
const ESC = "\u001b";
|
|
2
14
|
const ERASE_LINE = `\r${ESC}[2K`;
|
|
3
15
|
const HIDE_CURSOR = `${ESC}[?25l`;
|
|
4
16
|
const SHOW_CURSOR = `${ESC}[?25h`;
|
|
5
17
|
|
|
6
|
-
const TRACK_WIDTH = 20;
|
|
7
|
-
const TAIL_LENGTH = 7;
|
|
8
18
|
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
19
|
|
|
13
20
|
function envFlagEnabled(value) {
|
|
14
21
|
const normalized = String(value || "").trim().toLowerCase();
|
|
@@ -21,22 +28,7 @@ function terminalActivityEnabled({ env, output, errorOutput }) {
|
|
|
21
28
|
if (envFlagEnabled(env.CI) || String(env.TERM || "").toLowerCase() === "dumb") return false;
|
|
22
29
|
if (output?.isTTY !== true || errorOutput?.isTTY !== true) return false;
|
|
23
30
|
const columns = Number(output.columns);
|
|
24
|
-
return !Number.isFinite(columns) || columns <= 0 || columns >=
|
|
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}]`;
|
|
31
|
+
return !Number.isFinite(columns) || columns <= 0 || columns >= TERMINAL_ANIMATION_TRACK_WIDTH + 2;
|
|
40
32
|
}
|
|
41
33
|
|
|
42
34
|
function disabledIndicator() {
|
|
@@ -53,8 +45,13 @@ export function createTerminalActivityIndicator({
|
|
|
53
45
|
errorOutput = process.stderr,
|
|
54
46
|
consoleObj = console,
|
|
55
47
|
timers = { setTimeout, clearTimeout },
|
|
48
|
+
theme = DEFAULT_TERMINAL_ANIMATION_THEME,
|
|
56
49
|
} = {}) {
|
|
57
50
|
if (!terminalActivityEnabled({ env, output, errorOutput })) return disabledIndicator();
|
|
51
|
+
if (!isTerminalAnimationTheme(theme)) throw new TypeError(`Unknown terminal animation theme: ${String(theme)}`);
|
|
52
|
+
|
|
53
|
+
const themeMetadata = TERMINAL_ANIMATION_THEMES.find(({ id }) => id === theme);
|
|
54
|
+
const cycleLength = getTerminalAnimationCycleLength(theme);
|
|
58
55
|
|
|
59
56
|
const methods = ["log", "warn", "error", "debug"];
|
|
60
57
|
const originals = Object.fromEntries(methods.map((method) => [method, consoleObj[method]]));
|
|
@@ -81,9 +78,12 @@ export function createTerminalActivityIndicator({
|
|
|
81
78
|
clearTimer(frameTimer);
|
|
82
79
|
frameTimer = null;
|
|
83
80
|
framePosition = 0;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
try {
|
|
82
|
+
if (cursorHidden || frameVisible) output.write(`${ERASE_LINE}${SHOW_CURSOR}`);
|
|
83
|
+
} finally {
|
|
84
|
+
cursorHidden = false;
|
|
85
|
+
frameVisible = false;
|
|
86
|
+
}
|
|
87
87
|
};
|
|
88
88
|
|
|
89
89
|
const cancelPendingOutput = () => {
|
|
@@ -98,20 +98,23 @@ export function createTerminalActivityIndicator({
|
|
|
98
98
|
clearAnimation();
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
|
-
if (framePosition >=
|
|
102
|
-
output.write(ERASE_LINE);
|
|
103
|
-
frameVisible = false;
|
|
101
|
+
if (framePosition >= cycleLength) {
|
|
104
102
|
framePosition = 0;
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
if (themeMetadata.loopPauseMs > 0) {
|
|
104
|
+
output.write(ERASE_LINE);
|
|
105
|
+
frameVisible = false;
|
|
106
|
+
frameTimer = schedule(drawNextFrame, themeMetadata.loopPauseMs);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
107
109
|
}
|
|
108
110
|
|
|
111
|
+
const frameIndex = themeMetadata.startFrame + framePosition;
|
|
109
112
|
const prefix = cursorHidden ? "" : HIDE_CURSOR;
|
|
110
113
|
cursorHidden = true;
|
|
111
114
|
frameVisible = true;
|
|
112
|
-
output.write(`${prefix}${ERASE_LINE}${
|
|
115
|
+
output.write(`${prefix}${ERASE_LINE}${renderTerminalAnimationFrame(theme, frameIndex)}`);
|
|
113
116
|
framePosition += 1;
|
|
114
|
-
frameTimer = schedule(drawNextFrame,
|
|
117
|
+
frameTimer = schedule(drawNextFrame, getTerminalAnimationFrameDelay(theme, frameIndex));
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
const scheduleAfterIdle = () => {
|
|
@@ -161,9 +164,12 @@ export function createTerminalActivityIndicator({
|
|
|
161
164
|
activeRequests = 0;
|
|
162
165
|
clearTimer(idleTimer);
|
|
163
166
|
idleTimer = null;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
try {
|
|
168
|
+
clearAnimation();
|
|
169
|
+
} finally {
|
|
170
|
+
for (const method of methods) {
|
|
171
|
+
if (consoleObj[method] === wrappers[method]) consoleObj[method] = originals[method];
|
|
172
|
+
}
|
|
167
173
|
}
|
|
168
174
|
},
|
|
169
175
|
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const ESC = "\u001b";
|
|
2
|
+
|
|
3
|
+
export const TERMINAL_ANIMATION_TRACK_WIDTH = 20;
|
|
4
|
+
export const DEFAULT_TERMINAL_ANIMATION_THEME = "comet";
|
|
5
|
+
|
|
6
|
+
const TAIL_LENGTH = 7;
|
|
7
|
+
const TAIL_COLORS = Object.freeze([159, 123, 87, 51, 45, 39, 33]);
|
|
8
|
+
const BRAILLE_GLYPHS = Object.freeze(["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]);
|
|
9
|
+
|
|
10
|
+
function colorCell(glyph, color) {
|
|
11
|
+
return `${ESC}[${color}m${glyph}${ESC}[0m`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function blankTrack() {
|
|
15
|
+
return Array.from({ length: TERMINAL_ANIMATION_TRACK_WIDTH }, () => " ");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function setCell(cells, index, glyph, color) {
|
|
19
|
+
if (index < 0 || index >= TERMINAL_ANIMATION_TRACK_WIDTH) return;
|
|
20
|
+
cells[index] = colorCell(glyph, color);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function drawComet(cells, head, direction, tailLength = TAIL_LENGTH) {
|
|
24
|
+
for (let distance = tailLength; distance >= 1; distance -= 1) {
|
|
25
|
+
setCell(cells, head - (direction * distance), ":", `38;5;${TAIL_COLORS[distance - 1]}`);
|
|
26
|
+
}
|
|
27
|
+
setCell(cells, head, ":", "97");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function wrapTrack(cells) {
|
|
31
|
+
return `[${cells.join("")}]`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pingPongState(index) {
|
|
35
|
+
const period = (TERMINAL_ANIMATION_TRACK_WIDTH - 1) * 2;
|
|
36
|
+
const offset = index % period;
|
|
37
|
+
return offset < TERMINAL_ANIMATION_TRACK_WIDTH
|
|
38
|
+
? { position: offset, direction: 1 }
|
|
39
|
+
: { position: period - offset, direction: -1 };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const THEME_DEFINITIONS = Object.freeze([
|
|
43
|
+
Object.freeze({
|
|
44
|
+
id: "comet",
|
|
45
|
+
label: "Comet",
|
|
46
|
+
frameCount: TERMINAL_ANIMATION_TRACK_WIDTH + TAIL_LENGTH,
|
|
47
|
+
frameDelayMs: 45,
|
|
48
|
+
loopPauseMs: 200,
|
|
49
|
+
startFrame: 0,
|
|
50
|
+
render: renderCometFrame,
|
|
51
|
+
}),
|
|
52
|
+
Object.freeze({
|
|
53
|
+
id: "twin",
|
|
54
|
+
label: "Twin",
|
|
55
|
+
frameCount: 26,
|
|
56
|
+
frameDelayMs: 60,
|
|
57
|
+
loopPauseMs: 0,
|
|
58
|
+
startFrame: 5,
|
|
59
|
+
render(index) {
|
|
60
|
+
const cells = blankTrack();
|
|
61
|
+
drawComet(cells, index - 3, 1, 4);
|
|
62
|
+
drawComet(cells, TERMINAL_ANIMATION_TRACK_WIDTH + 2 - index, -1, 4);
|
|
63
|
+
return wrapTrack(cells);
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
Object.freeze({
|
|
67
|
+
id: "shuttle",
|
|
68
|
+
label: "Shuttle",
|
|
69
|
+
frameCount: (TERMINAL_ANIMATION_TRACK_WIDTH - 1) * 2,
|
|
70
|
+
frameDelayMs: 55,
|
|
71
|
+
loopPauseMs: 0,
|
|
72
|
+
startFrame: 7,
|
|
73
|
+
frameDelay(index) {
|
|
74
|
+
const { position } = pingPongState(index);
|
|
75
|
+
return position === 0 || position === TERMINAL_ANIMATION_TRACK_WIDTH - 1 ? 160 : 55;
|
|
76
|
+
},
|
|
77
|
+
render(index) {
|
|
78
|
+
const cells = blankTrack();
|
|
79
|
+
const { position, direction } = pingPongState(index);
|
|
80
|
+
drawComet(cells, position, direction);
|
|
81
|
+
return wrapTrack(cells);
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
Object.freeze({
|
|
85
|
+
id: "chase",
|
|
86
|
+
label: "Chase",
|
|
87
|
+
frameCount: TERMINAL_ANIMATION_TRACK_WIDTH + 14,
|
|
88
|
+
frameDelayMs: 65,
|
|
89
|
+
loopPauseMs: 0,
|
|
90
|
+
startFrame: 13,
|
|
91
|
+
render(index) {
|
|
92
|
+
const cells = blankTrack();
|
|
93
|
+
const first = index - 4;
|
|
94
|
+
let second = first - 11;
|
|
95
|
+
if (second < -4) second += TERMINAL_ANIMATION_TRACK_WIDTH + 14;
|
|
96
|
+
drawComet(cells, first, 1, 4);
|
|
97
|
+
drawComet(cells, second, 1, 4);
|
|
98
|
+
return wrapTrack(cells);
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
Object.freeze({
|
|
102
|
+
id: "mirror",
|
|
103
|
+
label: "Mirror",
|
|
104
|
+
frameCount: (Math.floor(TERMINAL_ANIMATION_TRACK_WIDTH / 2) - 1) * 2,
|
|
105
|
+
frameDelayMs: 75,
|
|
106
|
+
loopPauseMs: 0,
|
|
107
|
+
startFrame: 3,
|
|
108
|
+
render(index) {
|
|
109
|
+
const cells = blankTrack();
|
|
110
|
+
const half = Math.floor(TERMINAL_ANIMATION_TRACK_WIDTH / 2);
|
|
111
|
+
const period = (half - 1) * 2;
|
|
112
|
+
const distance = index < half ? index : period - index;
|
|
113
|
+
drawComet(cells, half - 1 - distance, -1, 3);
|
|
114
|
+
drawComet(cells, half + distance, 1, 3);
|
|
115
|
+
return wrapTrack(cells);
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
Object.freeze({
|
|
119
|
+
id: "pulse",
|
|
120
|
+
label: "Pulse",
|
|
121
|
+
frameCount: 8,
|
|
122
|
+
frameDelayMs: 115,
|
|
123
|
+
loopPauseMs: 0,
|
|
124
|
+
startFrame: 2,
|
|
125
|
+
render(index) {
|
|
126
|
+
const cells = blankTrack();
|
|
127
|
+
const radii = [0, 1, 2, 3, 4, 3, 2, 1];
|
|
128
|
+
const radius = radii[index];
|
|
129
|
+
const center = Math.floor((TERMINAL_ANIMATION_TRACK_WIDTH - 1) / 2);
|
|
130
|
+
setCell(cells, center, ":", "97");
|
|
131
|
+
for (let distance = 1; distance <= radius; distance += 1) {
|
|
132
|
+
const color = `38;5;${TAIL_COLORS[distance]}`;
|
|
133
|
+
setCell(cells, center - distance, ":", color);
|
|
134
|
+
setCell(cells, center + distance, ":", color);
|
|
135
|
+
}
|
|
136
|
+
return wrapTrack(cells);
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
Object.freeze({
|
|
140
|
+
id: "braille",
|
|
141
|
+
label: "Braille",
|
|
142
|
+
frameCount: BRAILLE_GLYPHS.length,
|
|
143
|
+
frameDelayMs: 80,
|
|
144
|
+
loopPauseMs: 0,
|
|
145
|
+
startFrame: 0,
|
|
146
|
+
render(index) {
|
|
147
|
+
const cells = blankTrack();
|
|
148
|
+
setCell(cells, Math.floor(TERMINAL_ANIMATION_TRACK_WIDTH / 2), BRAILLE_GLYPHS[index], `38;5;${TAIL_COLORS[2]}`);
|
|
149
|
+
return wrapTrack(cells);
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
const DEFINITIONS_BY_ID = new Map(THEME_DEFINITIONS.map((theme) => [theme.id, theme]));
|
|
155
|
+
|
|
156
|
+
export const TERMINAL_ANIMATION_THEMES = Object.freeze(THEME_DEFINITIONS.map(({
|
|
157
|
+
id,
|
|
158
|
+
label,
|
|
159
|
+
frameCount,
|
|
160
|
+
frameDelayMs,
|
|
161
|
+
loopPauseMs,
|
|
162
|
+
startFrame,
|
|
163
|
+
}) => Object.freeze({ id, label, frameCount, frameDelayMs, loopPauseMs, startFrame })));
|
|
164
|
+
|
|
165
|
+
export function isTerminalAnimationTheme(value) {
|
|
166
|
+
return typeof value === "string" && DEFINITIONS_BY_ID.has(value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function themeDefinition(theme) {
|
|
170
|
+
const definition = DEFINITIONS_BY_ID.get(theme);
|
|
171
|
+
if (!definition) throw new TypeError(`Unknown terminal animation theme: ${String(theme)}`);
|
|
172
|
+
return definition;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function normalizedFrameIndex(frameIndex, frameCount) {
|
|
176
|
+
if (!Number.isSafeInteger(frameIndex)) throw new TypeError("Terminal animation frame index must be an integer");
|
|
177
|
+
return ((frameIndex % frameCount) + frameCount) % frameCount;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getTerminalAnimationCycleLength(theme) {
|
|
181
|
+
return themeDefinition(theme).frameCount;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function getTerminalAnimationFrameDelay(theme, frameIndex) {
|
|
185
|
+
const definition = themeDefinition(theme);
|
|
186
|
+
const index = normalizedFrameIndex(frameIndex, definition.frameCount);
|
|
187
|
+
return definition.frameDelay?.(index) ?? definition.frameDelayMs;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function renderTerminalAnimationFrame(theme, frameIndex) {
|
|
191
|
+
const definition = themeDefinition(theme);
|
|
192
|
+
return definition.render(normalizedFrameIndex(frameIndex, definition.frameCount));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function renderCometFrame(position) {
|
|
196
|
+
let track = "";
|
|
197
|
+
for (let column = 0; column < TERMINAL_ANIMATION_TRACK_WIDTH; column += 1) {
|
|
198
|
+
const distance = position - column;
|
|
199
|
+
if (distance === 0) {
|
|
200
|
+
track += `${ESC}[97m:${ESC}[0m`;
|
|
201
|
+
} else if (distance > 0 && distance <= TAIL_LENGTH) {
|
|
202
|
+
track += `${ESC}[38;5;${TAIL_COLORS[distance - 1]}m:${ESC}[0m`;
|
|
203
|
+
} else {
|
|
204
|
+
track += " ";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return `[${track}]`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function outputSupportsPreview(output) {
|
|
211
|
+
if (output?.isTTY !== true) return false;
|
|
212
|
+
const columns = Number(output.columns);
|
|
213
|
+
return !Number.isFinite(columns) || columns <= 0 || columns >= TERMINAL_ANIMATION_TRACK_WIDTH + 2;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function playTerminalAnimationPreview(theme, {
|
|
217
|
+
output = process.stdout,
|
|
218
|
+
timers = { setTimeout, clearTimeout },
|
|
219
|
+
} = {}) {
|
|
220
|
+
const definition = themeDefinition(theme);
|
|
221
|
+
if (!outputSupportsPreview(output)) return false;
|
|
222
|
+
|
|
223
|
+
const eraseLine = `\r${ESC}[2K`;
|
|
224
|
+
let timer = null;
|
|
225
|
+
let previewStarted = false;
|
|
226
|
+
|
|
227
|
+
const wait = (delay) => new Promise((resolve) => {
|
|
228
|
+
let settled = false;
|
|
229
|
+
const nextTimer = timers.setTimeout(() => {
|
|
230
|
+
settled = true;
|
|
231
|
+
timer = null;
|
|
232
|
+
resolve();
|
|
233
|
+
}, delay);
|
|
234
|
+
timer = settled ? null : nextTimer;
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
for (let offset = 0; offset < definition.frameCount; offset += 1) {
|
|
239
|
+
const frameIndex = definition.startFrame + offset;
|
|
240
|
+
previewStarted = true;
|
|
241
|
+
output.write(`${eraseLine}${renderTerminalAnimationFrame(theme, frameIndex)}`);
|
|
242
|
+
await wait(getTerminalAnimationFrameDelay(theme, frameIndex));
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
} finally {
|
|
246
|
+
if (timer) timers.clearTimeout(timer);
|
|
247
|
+
if (previewStarted) output.write(eraseLine);
|
|
248
|
+
}
|
|
249
|
+
}
|
package/src/user-settings.mjs
CHANGED
|
@@ -3,8 +3,14 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { atomicWriteFileIfChangedSync } from "./atomic-file.mjs";
|
|
5
5
|
import { DEFAULT_CODEX_AUTO_REVIEW_MODEL } from "./models.mjs";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_TERMINAL_ANIMATION_THEME,
|
|
8
|
+
isTerminalAnimationTheme,
|
|
9
|
+
TERMINAL_ANIMATION_THEMES,
|
|
10
|
+
} from "./terminal-animation.mjs";
|
|
6
11
|
|
|
7
12
|
const AUTO_REVIEW_MODEL_KEY = "auto_review_model";
|
|
13
|
+
const TERMINAL_ANIMATION_KEY = "terminal_animation";
|
|
8
14
|
|
|
9
15
|
export function userSettingsPath({ env = process.env, home = os.homedir() } = {}) {
|
|
10
16
|
const xdgConfigHome = String(env.XDG_CONFIG_HOME || "").trim();
|
|
@@ -29,6 +35,16 @@ export function readUserSettings({ env = process.env, home = os.homedir(), stric
|
|
|
29
35
|
throw invalidSettings(filePath, `${AUTO_REVIEW_MODEL_KEY} must be a non-empty string`);
|
|
30
36
|
}
|
|
31
37
|
}
|
|
38
|
+
if (Object.hasOwn(parsed, TERMINAL_ANIMATION_KEY)) {
|
|
39
|
+
const value = parsed[TERMINAL_ANIMATION_KEY];
|
|
40
|
+
if (!isTerminalAnimationTheme(value)) {
|
|
41
|
+
const choices = TERMINAL_ANIMATION_THEMES.map(({ id }) => id).join(", ");
|
|
42
|
+
if (strict) {
|
|
43
|
+
throw invalidSettings(filePath, `${TERMINAL_ANIMATION_KEY} must be one of: ${choices}`);
|
|
44
|
+
}
|
|
45
|
+
delete parsed[TERMINAL_ANIMATION_KEY];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
32
48
|
return parsed;
|
|
33
49
|
} catch (error) {
|
|
34
50
|
if (error?.code === "ENOENT") return {};
|
|
@@ -54,6 +70,18 @@ export function autoReviewModelPreference({ env = process.env, home = os.homedir
|
|
|
54
70
|
return { model: DEFAULT_CODEX_AUTO_REVIEW_MODEL, source: "default" };
|
|
55
71
|
}
|
|
56
72
|
|
|
73
|
+
export function savedTerminalAnimationTheme(options = {}) {
|
|
74
|
+
const settings = readUserSettings(options);
|
|
75
|
+
const theme = settings[TERMINAL_ANIMATION_KEY];
|
|
76
|
+
return isTerminalAnimationTheme(theme) ? theme : "";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function terminalAnimationPreference({ env = process.env, home = os.homedir() } = {}) {
|
|
80
|
+
const savedTheme = savedTerminalAnimationTheme({ env, home });
|
|
81
|
+
if (savedTheme) return { theme: savedTheme, source: "settings" };
|
|
82
|
+
return { theme: DEFAULT_TERMINAL_ANIMATION_THEME, source: "default" };
|
|
83
|
+
}
|
|
84
|
+
|
|
57
85
|
export function writeAutoReviewModel(model, { env = process.env, home = os.homedir() } = {}) {
|
|
58
86
|
const filePath = userSettingsPath({ env, home });
|
|
59
87
|
const settings = readUserSettings({ env, home, strict: true });
|
|
@@ -65,3 +93,25 @@ export function writeAutoReviewModel(model, { env = process.env, home = os.homed
|
|
|
65
93
|
const changed = atomicWriteFileIfChangedSync(filePath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
66
94
|
return { changed, filePath, model: value || DEFAULT_CODEX_AUTO_REVIEW_MODEL };
|
|
67
95
|
}
|
|
96
|
+
|
|
97
|
+
export function writeTerminalAnimationTheme(theme, { env = process.env, home = os.homedir() } = {}) {
|
|
98
|
+
if (!isTerminalAnimationTheme(theme)) {
|
|
99
|
+
const choices = TERMINAL_ANIMATION_THEMES.map(({ id }) => id).join(", ");
|
|
100
|
+
throw new Error(`Terminal animation must be one of: ${choices}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const filePath = userSettingsPath({ env, home });
|
|
104
|
+
const settings = readUserSettings({ env, home, strict: true });
|
|
105
|
+
const next = { ...settings };
|
|
106
|
+
if (theme === DEFAULT_TERMINAL_ANIMATION_THEME) {
|
|
107
|
+
if (!Object.hasOwn(next, TERMINAL_ANIMATION_KEY)) {
|
|
108
|
+
return { changed: false, filePath, theme };
|
|
109
|
+
}
|
|
110
|
+
delete next[TERMINAL_ANIMATION_KEY];
|
|
111
|
+
} else {
|
|
112
|
+
next[TERMINAL_ANIMATION_KEY] = theme;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const changed = atomicWriteFileIfChangedSync(filePath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
116
|
+
return { changed, filePath, theme };
|
|
117
|
+
}
|