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 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
@@ -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
- const spaceIndex = trimmed.indexOf(' ');
288
- const cmd = (spaceIndex === -1 ? trimmed.slice(1) : trimmed.slice(1, spaceIndex)).trim();
289
- const arg = spaceIndex === -1 ? '' : trimmed.slice(spaceIndex + 1).trim();
290
- const rest = arg ? arg.split(/\s+/) : [];
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();
@@ -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
- process.stdout.write(theme.dim(`\n[autopilot] step ${step} of ${maxSteps}\n`));
92
- await runTurn({
93
- session,
94
- userInput: buildAutopilotTurnPrompt(normalizedGoal, step, maxSteps),
95
- signal,
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
  }
@@ -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 { confirm } from '@inquirer/prompts';
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 confirm({
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 confirm({
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 confirm({
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 confirm({
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 │ Tab Autocomplete │ @file ContextCtrl+R Clear'
43
- : ' Ctrl+C Exit | Tab Autocomplete | @file Context | Ctrl+R Clear';
65
+ ? ' Ctrl+C Exit │ Ctrl+R Clear HistoryTab 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
- // ─── Pixel-art logo ────────────────────────────────────────────────────────
54
- // 5-row "ICOPILOT" in full-block characters (2-space gaps between letters).
55
- // I=2 C=4 O=4 P=4 I=2 L=4 O=4 T=4
56
- const LOGO_ROWS = [
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
- // Render logo rows in light-blue (#58A6FF)
88
- const logoRows = LOGO_ROWS.map((r) => c.hex('#58A6FF').bold(r));
89
- const mascotRows = buildMascot(c);
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('Connected to')} ${c.hex('#58A6FF').bold('GitHub Models')} ` +
95
- `${c.gray('[' + model + ']')}`;
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('#58A6FF')('Active')} ` +
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
- combined,
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.2",
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",