icopilot 2.3.2 → 2.3.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/CHANGELOG.md +18 -0
- package/dist/commands/slash.js +7 -4
- package/dist/modes/autopilot.js +31 -6
- package/dist/modes/interactive.js +4 -1
- package/dist/tools/file-ops.js +23 -7
- package/dist/ui/prompt.js +39 -3
- package/dist/ui/select.js +82 -0
- package/dist/ui/spinner.js +50 -0
- package/dist/ui/theme.js +19 -33
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.3.4] — 2026-06-29
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Screen stacking / infinite box loop** — input box borders no longer stack on every submission; `drawBoxTop()` now erases the previous bottom border with ANSI cursor-up + clear-line sequences before drawing a fresh frame
|
|
12
|
+
- **`/m` slash command tokenizer** — commands like `/model`, `/provider set <name>`, and `/plan` were truncated to their first character; replaced `indexOf`-based slicing with `split(/\s+/)` so the full command name is always parsed correctly
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- **Splash banner** — replaced pixel-art block logo with the standard figlet-style ASCII logo rendered in bold cyan (`#58A6FF`); status indicators updated to `● Provider: GitHub Models (default: …)` and `● Session: Active (…)` with a dim divider
|
|
16
|
+
- **Hotkey dock** — updated footer to `Ctrl+C Exit │ Ctrl+R Clear History │ Tab Autocomplete`
|
|
17
|
+
- **Terminal resize** — `process.stdout.on('resize')` added alongside `SIGWINCH` for portability (e.g. Windows ConPTY); resize also resets the pending-erase counter to avoid misaligned cursors
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **`src/ui/spinner.ts`** — `Spinner` class with `start(label)`, `update(label)`, `stop(success?)` using braille frames at 80 ms; degrades gracefully to plain text in non-TTY environments
|
|
21
|
+
- **`src/ui/select.ts`** — `selectMenu(choices, initial?)` arrow-key `❯` selection menu; auto-selects the first item in non-TTY mode
|
|
22
|
+
- **Autopilot progress UX** — each step now shows a live spinner that resolves to `✔` or `✖`; when `requireApproval` is enabled a between-step `selectMenu` asks "Continue / Abort" instead of running blindly
|
|
23
|
+
- **Plan-mode file confirmations** — `confirm()` Y/N prompts replaced with `select()` arrow-key menus (`❯ Apply changes / Skip`) for all file write proposals
|
|
24
|
+
- **`@file` mention hint** — ghost text shows `file/path` after a bare `@` token in the input field
|
|
25
|
+
|
|
8
26
|
## [2.1.0] — 2026-06-28
|
|
9
27
|
|
|
10
28
|
### Added — v2.1 Competitive Parity
|
package/dist/commands/slash.js
CHANGED
|
@@ -284,10 +284,13 @@ export async function handleSlash(line, ctx) {
|
|
|
284
284
|
}
|
|
285
285
|
return done(false, modePrefix.forwardInput, modePrefix.turnMode ?? null);
|
|
286
286
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
287
|
+
// Tokenize: split on whitespace so "/model gpt-4o" → ["model","gpt-4o"]
|
|
288
|
+
// This avoids premature character slicing that truncated commands like
|
|
289
|
+
// "/model" to "/m" when multi-byte or invisible whitespace was present.
|
|
290
|
+
const tokens = trimmed.slice(1).split(/\s+/);
|
|
291
|
+
const cmd = tokens[0] ?? '';
|
|
292
|
+
const rest = tokens.slice(1);
|
|
293
|
+
const arg = rest.join(' ');
|
|
291
294
|
const s = ctx.session;
|
|
292
295
|
const roleManager = getRoleManager(s.state.cwd);
|
|
293
296
|
const normalizedCommand = cmd.toLowerCase();
|
package/dist/modes/autopilot.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Session } from '../session/session.js';
|
|
2
2
|
import { hookManager } from '../hooks/lifecycle.js';
|
|
3
3
|
import { theme } from '../ui/theme.js';
|
|
4
|
+
import { Spinner } from '../ui/spinner.js';
|
|
5
|
+
import { selectMenu } from '../ui/select.js';
|
|
4
6
|
import { runTurn } from './turn.js';
|
|
5
7
|
export const AUTOPILOT_MAX_STEPS = 10;
|
|
6
8
|
const AUTOPILOT_REQUIRE_APPROVAL_DEFAULT = true;
|
|
@@ -86,17 +88,40 @@ export async function runAutopilot(goal, opts = {}) {
|
|
|
86
88
|
session.setAutopilotEnabled(false);
|
|
87
89
|
session.setMode('ask');
|
|
88
90
|
session.setSystemPrompt(buildAutopilotSystemPrompt(normalizedGoal));
|
|
91
|
+
const requireApproval = opts.requireApproval ?? AUTOPILOT_REQUIRE_APPROVAL_DEFAULT;
|
|
89
92
|
try {
|
|
90
93
|
for (let step = 1; step <= maxSteps; step++) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
const spinner = new Spinner();
|
|
95
|
+
spinner.start(`Step ${step} of ${maxSteps} …`);
|
|
96
|
+
let stepError;
|
|
97
|
+
try {
|
|
98
|
+
await runTurn({
|
|
99
|
+
session,
|
|
100
|
+
userInput: buildAutopilotTurnPrompt(normalizedGoal, step, maxSteps),
|
|
101
|
+
signal,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
stepError = err;
|
|
106
|
+
}
|
|
107
|
+
const success = stepError == null;
|
|
108
|
+
spinner.stop(success);
|
|
109
|
+
if (!success) {
|
|
110
|
+
process.stdout.write(theme.err(`\n✖ step ${step} failed: ${stepError?.message ?? stepError}\n`));
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
97
113
|
if (isAutopilotComplete(findLastAssistantMessage(session.state.messages))) {
|
|
98
114
|
return session;
|
|
99
115
|
}
|
|
116
|
+
// When requireApproval is on, pause between steps so the user can review.
|
|
117
|
+
if (requireApproval && step < maxSteps) {
|
|
118
|
+
process.stdout.write('\n');
|
|
119
|
+
const choice = await selectMenu(['Continue to next step', 'Abort autopilot']);
|
|
120
|
+
if (choice !== 0) {
|
|
121
|
+
process.stdout.write(theme.dim('\nautopilot aborted by user.\n'));
|
|
122
|
+
return session;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
100
125
|
}
|
|
101
126
|
process.stdout.write(theme.warn(`\n⚠ autopilot stopped after ${maxSteps} steps.\n`));
|
|
102
127
|
return session;
|
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { Session } from '../session/session.js';
|
|
5
5
|
import { theme, banner } from '../ui/theme.js';
|
|
6
|
-
import { createPrompt, prefix } from '../ui/prompt.js';
|
|
6
|
+
import { createPrompt, prefix, invalidateBoxBottom } from '../ui/prompt.js';
|
|
7
7
|
import { handleSlash } from '../commands/slash.js';
|
|
8
8
|
import { loadAliases, resolveAlias } from '../commands/alias-cmd.js';
|
|
9
9
|
import { MetricsCollector } from '../commands/metrics-cmd.js';
|
|
@@ -83,6 +83,9 @@ export async function runInteractive(initialMode = 'ask', opts = {}) {
|
|
|
83
83
|
const resolvedLine = resolveAlias(next.line, loadAliases()) ?? next.line;
|
|
84
84
|
currentAbort = new AbortController();
|
|
85
85
|
try {
|
|
86
|
+
// Any output printed during processing must not be erased by the
|
|
87
|
+
// next input-box render, so invalidate the pending-erase counter.
|
|
88
|
+
invalidateBoxBottom();
|
|
86
89
|
if (next.scheduled) {
|
|
87
90
|
process.stdout.write(theme.dim(`\n[schedule] ${next.line}\n`));
|
|
88
91
|
}
|
package/dist/tools/file-ops.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { createPatch } from 'diff';
|
|
4
|
-
import {
|
|
4
|
+
import { select } from '@inquirer/prompts';
|
|
5
5
|
import { config } from '../config.js';
|
|
6
6
|
import { theme } from '../ui/theme.js';
|
|
7
7
|
import { formatAutoCheckResult, runAutoLint } from './auto-check.js';
|
|
@@ -35,8 +35,12 @@ export async function proposeWrite(relPath, newContent) {
|
|
|
35
35
|
const remembered = toolMemory.isWriteRemembered(abs);
|
|
36
36
|
const ok = config.autoApprove ||
|
|
37
37
|
remembered ||
|
|
38
|
-
(await
|
|
38
|
+
(await select({
|
|
39
39
|
message: exists ? 'Apply this patch?' : 'Create this new file?',
|
|
40
|
+
choices: [
|
|
41
|
+
{ name: '❯ Apply changes', value: true },
|
|
42
|
+
{ name: ' Skip', value: false },
|
|
43
|
+
],
|
|
40
44
|
default: false,
|
|
41
45
|
}).catch(() => false));
|
|
42
46
|
if (!ok) {
|
|
@@ -45,10 +49,14 @@ export async function proposeWrite(relPath, newContent) {
|
|
|
45
49
|
return { wrote: false, path: abs, bytes: 0 };
|
|
46
50
|
}
|
|
47
51
|
if (!config.autoApprove && !remembered) {
|
|
48
|
-
const remember = await
|
|
52
|
+
const remember = (await select({
|
|
49
53
|
message: 'Remember this write path for the session?',
|
|
54
|
+
choices: [
|
|
55
|
+
{ name: ' Yes — skip confirmation next time', value: true },
|
|
56
|
+
{ name: ' No', value: false },
|
|
57
|
+
],
|
|
50
58
|
default: false,
|
|
51
|
-
}).catch(() => false);
|
|
59
|
+
}).catch(() => false));
|
|
52
60
|
if (remember)
|
|
53
61
|
toolMemory.rememberWrite(abs);
|
|
54
62
|
}
|
|
@@ -100,8 +108,12 @@ export async function proposeWriteBatch(items) {
|
|
|
100
108
|
const remembered = prepared.every((item) => toolMemory.isWriteRemembered(item.abs));
|
|
101
109
|
const ok = config.autoApprove ||
|
|
102
110
|
remembered ||
|
|
103
|
-
(await
|
|
111
|
+
(await select({
|
|
104
112
|
message: 'Apply all patches?',
|
|
113
|
+
choices: [
|
|
114
|
+
{ name: '❯ Apply all changes', value: true },
|
|
115
|
+
{ name: ' Skip all', value: false },
|
|
116
|
+
],
|
|
105
117
|
default: false,
|
|
106
118
|
}).catch(() => false));
|
|
107
119
|
if (!ok) {
|
|
@@ -113,10 +125,14 @@ export async function proposeWriteBatch(items) {
|
|
|
113
125
|
};
|
|
114
126
|
}
|
|
115
127
|
if (!config.autoApprove && !remembered) {
|
|
116
|
-
const remember = await
|
|
128
|
+
const remember = (await select({
|
|
117
129
|
message: 'Remember these write paths for the session?',
|
|
130
|
+
choices: [
|
|
131
|
+
{ name: ' Yes — skip confirmation next time', value: true },
|
|
132
|
+
{ name: ' No', value: false },
|
|
133
|
+
],
|
|
118
134
|
default: false,
|
|
119
|
-
}).catch(() => false);
|
|
135
|
+
}).catch(() => false));
|
|
120
136
|
if (remember)
|
|
121
137
|
prepared.forEach((item) => toolMemory.rememberWrite(item.abs));
|
|
122
138
|
}
|
package/dist/ui/prompt.js
CHANGED
|
@@ -2,6 +2,14 @@ import readline from 'node:readline';
|
|
|
2
2
|
import { theme, safeUnicode } from './theme.js';
|
|
3
3
|
import { defaultContext } from '../util/completion.js';
|
|
4
4
|
import { attachKeybindings, applyKeybindingConfig, } from '../util/keybindings.js';
|
|
5
|
+
/**
|
|
6
|
+
* Call this whenever content is written to stdout between two read() calls
|
|
7
|
+
* (e.g. streamed LLM output) so we don't accidentally erase that content
|
|
8
|
+
* when the next input box is rendered.
|
|
9
|
+
*/
|
|
10
|
+
export function invalidateBoxBottom() {
|
|
11
|
+
pendingEraseLines = 0;
|
|
12
|
+
}
|
|
5
13
|
// ─── Slash command completer ───────────────────────────────────────────────
|
|
6
14
|
function slashCompleter(line) {
|
|
7
15
|
const ctx = defaultContext();
|
|
@@ -26,7 +34,18 @@ const PLACEHOLDER = 'Enter @ to mention files or / for commands...';
|
|
|
26
34
|
function boxWidth() {
|
|
27
35
|
return Math.max(60, (process.stdout.columns || 80) - 6);
|
|
28
36
|
}
|
|
37
|
+
// Track lines printed by drawBoxBottom() so the next read() can erase them
|
|
38
|
+
// before drawing a fresh top border (prevents infinite box stacking).
|
|
39
|
+
let pendingEraseLines = 0;
|
|
29
40
|
function drawBoxTop() {
|
|
41
|
+
if (pendingEraseLines > 0 && process.stdout.isTTY) {
|
|
42
|
+
// Erase the bottom border printed by the previous submission:
|
|
43
|
+
// drawBoxBottom writes "\n<border>\n" = 2 extra lines below the readline line.
|
|
44
|
+
for (let i = 0; i < pendingEraseLines; i++) {
|
|
45
|
+
process.stdout.write('\x1b[1A\x1b[2K'); // cursor up + clear line
|
|
46
|
+
}
|
|
47
|
+
pendingEraseLines = 0;
|
|
48
|
+
}
|
|
30
49
|
const w = boxWidth();
|
|
31
50
|
const colorEnabled = theme.dim('') !== ''; // cheap color-enabled check
|
|
32
51
|
const line = colorEnabled ? theme.dim(` ╭${'─'.repeat(w)}╮`) : ` ╭${'─'.repeat(w)}╮`;
|
|
@@ -36,11 +55,15 @@ function drawBoxBottom() {
|
|
|
36
55
|
const w = boxWidth();
|
|
37
56
|
const line = theme.dim(` ╰${'─'.repeat(w)}╯`);
|
|
38
57
|
process.stdout.write('\n' + line + '\n');
|
|
58
|
+
// Two lines were added below the readline line: the blank line (\n) and the
|
|
59
|
+
// border line itself. The trailing \n moves the cursor one further line down,
|
|
60
|
+
// so we need to go back 2 lines on the next drawBoxTop() call.
|
|
61
|
+
pendingEraseLines = 2;
|
|
39
62
|
}
|
|
40
63
|
// ─── Persistent footer (scroll-region docked) ──────────────────────────────
|
|
41
64
|
const FOOTER_KEYS = safeUnicode
|
|
42
|
-
? ' Ctrl+C Exit │
|
|
43
|
-
: ' Ctrl+C Exit |
|
|
65
|
+
? ' Ctrl+C Exit │ Ctrl+R Clear History │ Tab Autocomplete'
|
|
66
|
+
: ' Ctrl+C Exit | Ctrl+R Clear History | Tab Autocomplete';
|
|
44
67
|
let footerInstalled = false;
|
|
45
68
|
function footerLine(cols) {
|
|
46
69
|
const text = FOOTER_KEYS;
|
|
@@ -83,10 +106,16 @@ export function createPrompt(keybindingMode) {
|
|
|
83
106
|
if (isTTY)
|
|
84
107
|
installFooter();
|
|
85
108
|
const onResize = () => {
|
|
86
|
-
if (isTTY && footerInstalled)
|
|
109
|
+
if (isTTY && footerInstalled) {
|
|
110
|
+
pendingEraseLines = 0; // can't reliably erase across a resize
|
|
87
111
|
installFooter();
|
|
112
|
+
}
|
|
88
113
|
};
|
|
89
114
|
process.on('SIGWINCH', onResize);
|
|
115
|
+
// Also listen on stdout directly for environments that emit 'resize'
|
|
116
|
+
// instead of (or in addition to) SIGWINCH (e.g. Windows ConPTY).
|
|
117
|
+
if (isTTY)
|
|
118
|
+
process.stdout.on('resize', onResize);
|
|
90
119
|
// ── Ghost text helpers ────────────────────────────────────────────────
|
|
91
120
|
const clearGhost = () => {
|
|
92
121
|
if (!activeGhost || !isTTY)
|
|
@@ -138,6 +167,12 @@ export function createPrompt(keybindingMode) {
|
|
|
138
167
|
drawGhost(PLACEHOLDER);
|
|
139
168
|
return;
|
|
140
169
|
}
|
|
170
|
+
// Hint after a partial @mention: show "→ @<token>" in dim text.
|
|
171
|
+
const atMatch = line.match(/@([\w./\\-]*)$/);
|
|
172
|
+
if (atMatch) {
|
|
173
|
+
drawGhost(atMatch[0].length > 1 ? '' : 'file/path');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
141
176
|
if (!/^\/\w/.test(line))
|
|
142
177
|
return;
|
|
143
178
|
const [hits] = slashCompleter(line);
|
|
@@ -178,6 +213,7 @@ export function createPrompt(keybindingMode) {
|
|
|
178
213
|
removeFooter();
|
|
179
214
|
process.off('SIGWINCH', onResize);
|
|
180
215
|
if (isTTY) {
|
|
216
|
+
process.stdout.off('resize', onResize);
|
|
181
217
|
process.stdin.removeListener('keypress', onKeypressClear);
|
|
182
218
|
process.stdin.removeListener('keypress', onKeypressDraw);
|
|
183
219
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { theme, safeUnicode } from './theme.js';
|
|
3
|
+
const CURSOR = safeUnicode ? '❯' : '>';
|
|
4
|
+
const BLANK = ' ';
|
|
5
|
+
/**
|
|
6
|
+
* Render an interactive arrow-key selection menu.
|
|
7
|
+
*
|
|
8
|
+
* @param choices List of option labels to display.
|
|
9
|
+
* @param initial Initially selected index (default 0).
|
|
10
|
+
* @returns Resolves with the index of the chosen option, or -1 if
|
|
11
|
+
* the user pressed Escape / Ctrl-C.
|
|
12
|
+
*
|
|
13
|
+
* Example:
|
|
14
|
+
* ❯ Run this command
|
|
15
|
+
* Revise command instructions
|
|
16
|
+
* Abort operation
|
|
17
|
+
*/
|
|
18
|
+
export async function selectMenu(choices, initial = 0) {
|
|
19
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
20
|
+
// Non-interactive: auto-select the first option.
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
let selected = Math.max(0, Math.min(initial, choices.length - 1));
|
|
25
|
+
readline.emitKeypressEvents(process.stdin);
|
|
26
|
+
const wasRaw = process.stdin.isRaw ?? false;
|
|
27
|
+
process.stdin.setRawMode(true);
|
|
28
|
+
const render = () => {
|
|
29
|
+
// Erase all previously drawn lines.
|
|
30
|
+
for (let i = 0; i < choices.length; i++) {
|
|
31
|
+
process.stdout.write('\x1b[2K\r');
|
|
32
|
+
if (i < choices.length - 1)
|
|
33
|
+
process.stdout.write('\x1b[1A');
|
|
34
|
+
}
|
|
35
|
+
// Re-draw.
|
|
36
|
+
for (let i = 0; i < choices.length; i++) {
|
|
37
|
+
const active = i === selected;
|
|
38
|
+
const cursor = active ? theme.brand(CURSOR) : BLANK;
|
|
39
|
+
const label = active ? choices[i] : theme.dim(choices[i] ?? '');
|
|
40
|
+
process.stdout.write(` ${cursor} ${label}`);
|
|
41
|
+
if (i < choices.length - 1)
|
|
42
|
+
process.stdout.write('\n');
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
// Draw initial menu.
|
|
46
|
+
process.stdout.write('\n');
|
|
47
|
+
for (const choice of choices) {
|
|
48
|
+
process.stdout.write(` ${BLANK} ${theme.dim(choice)}\n`);
|
|
49
|
+
}
|
|
50
|
+
// Move cursor back up to rewrite from the first line.
|
|
51
|
+
for (let i = 0; i < choices.length; i++) {
|
|
52
|
+
process.stdout.write('\x1b[1A');
|
|
53
|
+
}
|
|
54
|
+
render();
|
|
55
|
+
const onKeypress = (_ch, key) => {
|
|
56
|
+
if (!key)
|
|
57
|
+
return;
|
|
58
|
+
if (key.name === 'up') {
|
|
59
|
+
selected = (selected - 1 + choices.length) % choices.length;
|
|
60
|
+
render();
|
|
61
|
+
}
|
|
62
|
+
else if (key.name === 'down') {
|
|
63
|
+
selected = (selected + 1) % choices.length;
|
|
64
|
+
render();
|
|
65
|
+
}
|
|
66
|
+
else if (key.name === 'return' || key.name === 'enter') {
|
|
67
|
+
cleanup(selected);
|
|
68
|
+
}
|
|
69
|
+
else if (key.name === 'escape' || (key.ctrl && key.name === 'c')) {
|
|
70
|
+
cleanup(-1);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const cleanup = (result) => {
|
|
74
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
75
|
+
if (!wasRaw)
|
|
76
|
+
process.stdin.setRawMode(false);
|
|
77
|
+
process.stdout.write('\n');
|
|
78
|
+
resolve(result);
|
|
79
|
+
};
|
|
80
|
+
process.stdin.on('keypress', onKeypress);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { theme } from './theme.js';
|
|
2
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
3
|
+
const INTERVAL_MS = 80;
|
|
4
|
+
/**
|
|
5
|
+
* A simple TTY spinner that renders a label next to a rotating braille frame.
|
|
6
|
+
* Falls back to a plain text prefix when the terminal is not a TTY or when
|
|
7
|
+
* Unicode / colours are disabled.
|
|
8
|
+
*/
|
|
9
|
+
export class Spinner {
|
|
10
|
+
timer = null;
|
|
11
|
+
frame = 0;
|
|
12
|
+
label = '';
|
|
13
|
+
isTTY;
|
|
14
|
+
constructor() {
|
|
15
|
+
this.isTTY = Boolean(process.stdout.isTTY);
|
|
16
|
+
}
|
|
17
|
+
start(label) {
|
|
18
|
+
this.label = label;
|
|
19
|
+
this.frame = 0;
|
|
20
|
+
if (!this.isTTY) {
|
|
21
|
+
process.stdout.write(` … ${label}\n`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.render();
|
|
25
|
+
this.timer = setInterval(() => this.render(), INTERVAL_MS);
|
|
26
|
+
}
|
|
27
|
+
update(label) {
|
|
28
|
+
this.label = label;
|
|
29
|
+
if (!this.isTTY) {
|
|
30
|
+
process.stdout.write(` … ${label}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
stop(success = true) {
|
|
34
|
+
if (this.timer !== null) {
|
|
35
|
+
clearInterval(this.timer);
|
|
36
|
+
this.timer = null;
|
|
37
|
+
}
|
|
38
|
+
if (this.isTTY) {
|
|
39
|
+
// Overwrite the spinner line with a final status icon + label.
|
|
40
|
+
process.stdout.write('\r\x1b[2K');
|
|
41
|
+
const icon = success ? theme.ok('✔') : theme.err('✖');
|
|
42
|
+
process.stdout.write(` ${icon} ${this.label}\n`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
render() {
|
|
46
|
+
const f = FRAMES[this.frame % FRAMES.length] ?? FRAMES[0];
|
|
47
|
+
this.frame++;
|
|
48
|
+
process.stdout.write(`\r\x1b[2K ${theme.dim(f)} ${this.label}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/ui/theme.js
CHANGED
|
@@ -50,29 +50,16 @@ export const theme = {
|
|
|
50
50
|
},
|
|
51
51
|
};
|
|
52
52
|
export const safeUnicode = process.platform !== 'win32' || Boolean(process.env.WT_SESSION);
|
|
53
|
-
// ───
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
60
|
-
'
|
|
61
|
-
'
|
|
53
|
+
// ─── ASCII logo (figlet "iCopilot") ──────────────────────────────────────────
|
|
54
|
+
// Rendered in cyan/light-blue to match GitHub Copilot's design language.
|
|
55
|
+
const ASCII_LOGO_LINES = [
|
|
56
|
+
' ___ _ _ _ ',
|
|
57
|
+
'|_ _|___ ___ _ __(_) | ___ | |_ ',
|
|
58
|
+
" | |/ __/ _ \\| '_ \\ | |/ _ \\| __|",
|
|
59
|
+
' | | (_| (_) | |_) | | | (_) | |_ ',
|
|
60
|
+
'|___\\___\\___/| .__/|_|_|\\___/ \\__|',
|
|
61
|
+
' |_| ',
|
|
62
62
|
];
|
|
63
|
-
// ─── Pilot mascot (5 rows) ─────────────────────────────────────────────────
|
|
64
|
-
// Purple frame (#A371F7), cyan accents (#39D2D2).
|
|
65
|
-
function buildMascot(c) {
|
|
66
|
-
const fr = (s) => c.hex('#A371F7')(s);
|
|
67
|
-
const cy = (s) => c.hex('#39D2D2')(s);
|
|
68
|
-
return [
|
|
69
|
-
fr(' ╭─────╮ '),
|
|
70
|
-
fr(' │') + cy('◉') + fr(' ') + cy('◉') + fr('│ '),
|
|
71
|
-
fr(' │') + c.hex('#A371F7')(' ─── ') + fr('│ '),
|
|
72
|
-
fr(' ╰──') + cy('┬') + fr('──╯ '),
|
|
73
|
-
cy(' ▶') + c.hex('#A371F7')(' pilot '),
|
|
74
|
-
];
|
|
75
|
-
}
|
|
76
63
|
export function banner(version, model, sessionDir) {
|
|
77
64
|
if (!colorEnabled()) {
|
|
78
65
|
return [
|
|
@@ -84,30 +71,29 @@ export function banner(version, model, sessionDir) {
|
|
|
84
71
|
}
|
|
85
72
|
const { c, name } = palette();
|
|
86
73
|
const green = name === 'light' ? '#166534' : '#3FB950';
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
// Side-by-side: mascot (10 visible chars) + logo
|
|
91
|
-
const combined = logoRows.map((lr, i) => ` ${mascotRows[i] ?? ' '} ${lr}`).join('\n');
|
|
74
|
+
const blue = '#58A6FF';
|
|
75
|
+
// Render ASCII logo in cyan/light-blue
|
|
76
|
+
const logo = ASCII_LOGO_LINES.map((row) => ` ${c.hex(blue).bold(row)}`).join('\n');
|
|
92
77
|
const sessDir = sessionDir ?? '~/.icopilot/sessions/';
|
|
78
|
+
const divider = ` ${c.gray('─'.repeat(50))}`;
|
|
93
79
|
const diag1 = ` ${c.hex(green)('●')} ` +
|
|
94
|
-
`${c.gray('
|
|
95
|
-
`${c.gray('
|
|
80
|
+
`${c.gray('Provider:')} ${c.hex(blue).bold('GitHub Models')} ` +
|
|
81
|
+
`${c.gray('(default: ' + model + ')')}`;
|
|
96
82
|
const diag2 = ` ${c.hex(green)('●')} ` +
|
|
97
|
-
`${c.gray('Session:')} ${c.hex(
|
|
83
|
+
`${c.gray('Session: ')} ${c.hex(blue)('Active')} ` +
|
|
98
84
|
`${c.gray('(' + sessDir + ')')}`;
|
|
99
85
|
const hints = safeUnicode
|
|
100
86
|
? `${c.gray('/help')} for commands ${c.gray('@file')} to add context ${c.gray('Tab')} to autocomplete`
|
|
101
87
|
: `/help for commands @file to add context Tab to autocomplete`;
|
|
102
88
|
return [
|
|
103
89
|
'',
|
|
104
|
-
|
|
105
|
-
'',
|
|
106
|
-
` ${c.gray('v' + version)} ${c.gray('·')} ${c.hex('#58A6FF')(model)}`,
|
|
90
|
+
logo,
|
|
107
91
|
'',
|
|
108
92
|
diag1,
|
|
109
93
|
diag2,
|
|
94
|
+
divider,
|
|
110
95
|
'',
|
|
96
|
+
` ${c.gray('v' + version)} ${c.gray('·')} ${c.hex(blue)(model)}`,
|
|
111
97
|
` ${hints}`,
|
|
112
98
|
'',
|
|
113
99
|
].join('\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "icopilot",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.4",
|
|
4
4
|
"description": "iCopilot — terminal-native agentic CLI powered by GitHub Models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"@inquirer/prompts": "^5.3.8",
|
|
39
39
|
"chalk": "^5.3.0",
|
|
40
40
|
"commander": "^12.1.0",
|
|
41
|
+
"typescript": "^5.5.4",
|
|
41
42
|
"diff": "^5.2.0",
|
|
42
43
|
"dotenv": "^16.4.5",
|
|
43
44
|
"fast-glob": "^3.3.2",
|
|
@@ -61,7 +62,6 @@
|
|
|
61
62
|
"eslint-config-prettier": "^9.1.0",
|
|
62
63
|
"eslint-plugin-prettier": "^5.2.1",
|
|
63
64
|
"prettier": "^3.3.3",
|
|
64
|
-
"typescript": "^5.5.4",
|
|
65
65
|
"vitest": "^1.6.0"
|
|
66
66
|
},
|
|
67
67
|
"license": "MIT",
|