ispbills-icli 8.4.12 → 8.4.13

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/bin/icli.js CHANGED
@@ -315,33 +315,13 @@ async function main() {
315
315
  // plain readline REPL when stdout/stdin is not a TTY (pipes, dumb terminals).
316
316
  if (output.isTTY && input.isTTY) {
317
317
  const { runTui } = await import('../src/tui/run.js');
318
- const { loadResume } = await import('../src/resume.js');
319
-
320
- // Hot-reload resume: if started with --resume <file>, restore the saved session.
321
- const resumeFile = flag('--resume');
322
- const resumeSession = loadResume(resumeFile);
323
-
324
- const action = await runTui(cfg, { display, version: pkgVersion(), resumeSession });
325
-
326
- const actionName = typeof action === 'string' ? action : action?.action;
327
- if (actionName === 'logout') {
318
+ const action = await runTui(cfg, { display });
319
+ if (action === 'logout') {
328
320
  const cleared = clearConfig();
329
321
  console.log(`\n ${cleared ? GREEN + glyphs.check + RESET + ' Logged out — credentials cleared.' : YELLOW + 'Already logged out.' + RESET}`);
330
322
  console.log(` ${DIM}Run ${RESET}${ACCENT}icli login${RESET}${DIM} to sign in again.${RESET}\n`);
331
- } else if (actionName === 'update') {
332
- const ok = selfUpdate();
333
- if (ok && action?.resumeFile) {
334
- // Re-exec with the new binary, passing --resume so the session is restored.
335
- const { execFileSync } = await import('child_process');
336
- const newBin = process.argv[1]; // same script path — npm replaced the file in-place
337
- const baseArgs = process.argv.slice(2).filter((a) => a !== '--resume' && a !== action.resumeFile);
338
- process.stdout.write(`\n ${ACCENT}Reloading iCli with your session restored…${RESET}\n\n`);
339
- try {
340
- execFileSync(process.execPath, [newBin, ...baseArgs, '--resume', action.resumeFile], {
341
- stdio: 'inherit',
342
- });
343
- } catch { /* user Ctrl+C'd the resumed session — that's fine */ }
344
- }
323
+ } else if (action === 'update') {
324
+ selfUpdate();
345
325
  }
346
326
  return;
347
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "8.4.12",
3
+ "version": "8.4.13",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/banner.js CHANGED
@@ -22,7 +22,6 @@ const LOGO_WIDTH = Math.max(...LOGO.map((l) => l.length));
22
22
  export function printBanner(cfg = {}, opts = {}) {
23
23
  const width = cols();
24
24
  const model = opts.model || cfg._model || 'IspBills AI';
25
- const version = opts.version || '';
26
25
 
27
26
  console.log();
28
27
  if (width >= LOGO_WIDTH) {
@@ -37,11 +36,10 @@ export function printBanner(cfg = {}, opts = {}) {
37
36
  parts.push(`${DIM}model${RESET} ${CYAN}${shortModel(model)}${RESET}`);
38
37
  if (cfg.user?.name) parts.push(`${DIM}user${RESET} ${WHITE}${cfg.user.name}${RESET}`);
39
38
  if (cfg.user?.role) parts.push(`${DIM}role${RESET} ${WHITE}${cfg.user.role}${RESET}`);
40
- if (version) parts.push(`${DIM}v${RESET} ${WHITE}${version}${RESET}`);
41
39
  console.log(' ' + parts.join(` ${GRAY}·${RESET} `));
42
40
  if (cfg.url) console.log(` ${DIM}url${RESET} ${ACCENT}${cfg.url}${RESET}`);
43
41
  console.log();
44
- console.log(` ${DIM}Type a message to start. ${RESET}${ACCENT}/help${RESET}${DIM} for commands · ${RESET}${ACCENT}!cmd${RESET}${DIM} to run shell · ${RESET}${ACCENT}Ctrl+C${RESET}${DIM} to exit${RESET}`);
42
+ console.log(` ${DIM}Type a message to start. ${RESET}${ACCENT}/help${RESET}${DIM} for commands · ${RESET}${ACCENT}Ctrl+C${RESET}${DIM} to exit${RESET}`);
45
43
  console.log();
46
44
  hr();
47
45
  console.log();
package/src/client.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { refreshToken } from './auth.js';
2
- import { logStream } from './logger.js';
3
2
 
4
3
  /**
5
4
  * Incrementally extract the answer text from the raw JSON the backend streams
@@ -161,10 +160,5 @@ export async function streamChat(cfg, messages, options = {}) {
161
160
  // committed answer below what the operator already saw stream by.
162
161
  const finalText = reply.text || '';
163
162
  const text = finalText.length >= answerText.length ? finalText : answerText;
164
-
165
- // Log prompt + reply to ~/.config/ispbills-icli/logs/<user>/stream.log
166
- const prompt = [...messages].reverse().find((m) => m.role === 'user')?.content ?? '';
167
- logStream(cfg, prompt, text);
168
-
169
163
  return { reply, text, meta };
170
164
  }
package/src/clipboard.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Cross-platform "copy to system clipboard" helper. Best-effort: resolves to
2
2
  // false (rather than throwing) when no clipboard tool is available so the TUI
3
3
  // can show a friendly notice.
4
- import { spawn, execFile } from 'child_process';
4
+ import { spawn } from 'child_process';
5
5
 
6
6
  function candidates() {
7
7
  if (process.platform === 'darwin') return [['pbcopy', []]];
@@ -40,37 +40,3 @@ export async function copyToClipboard(text) {
40
40
  }
41
41
  return null;
42
42
  }
43
-
44
- // ── Read from clipboard ─────────────────────────────────────────────────────
45
-
46
- function readCandidates() {
47
- if (process.platform === 'darwin') return [['pbpaste', []]];
48
- if (process.platform === 'win32') return [['powershell', ['-noprofile', '-command', 'Get-Clipboard']]];
49
- return [
50
- ['wl-paste', ['--no-newline']],
51
- ['xclip', ['-selection', 'clipboard', '-o']],
52
- ['xsel', ['--clipboard', '--output']],
53
- ];
54
- }
55
-
56
- /**
57
- * Read text from the system clipboard.
58
- * Returns the clipboard string on success, or null when no tool is available
59
- * or the clipboard is empty.
60
- */
61
- export function readFromClipboard() {
62
- return new Promise((resolve) => {
63
- const cands = readCandidates();
64
- let i = 0;
65
- function tryNext() {
66
- if (i >= cands.length) { resolve(null); return; }
67
- const [cmd, args] = cands[i++];
68
- execFile(cmd, args, { timeout: 3000, maxBuffer: 1024 * 512 }, (err, stdout) => {
69
- if (err) { tryNext(); return; }
70
- const text = stdout.trimEnd();
71
- resolve(text || null);
72
- });
73
- }
74
- tryNext();
75
- });
76
- }
package/src/commands.js CHANGED
@@ -52,12 +52,9 @@ export const SLASH_COMMANDS = [
52
52
  { cmd: '/copy', hint: '', desc: 'Copy the last AI answer to the clipboard', group: 'Memory', local: true },
53
53
 
54
54
  // ── Session & app ──
55
- { cmd: '/usage', hint: '', desc: 'Show conversation stats (turns, ~tokens, model)', group: 'Session', local: true },
56
55
  { cmd: '/autopilot', hint: '', desc: 'Toggle autopilot (auto-apply fixes)', group: 'Session', local: true },
57
56
  { cmd: '/model', hint: '[name]', desc: 'Show or set the AI model', group: 'Session', local: true },
58
57
  { cmd: '/reasoning', hint: '', desc: 'Toggle reasoning display (Ctrl+T)', group: 'Session', local: true },
59
- { cmd: '/paste', hint: '', desc: 'Paste multiline text from clipboard into input', group: 'Session', local: true },
60
- { cmd: '/about', hint: '', desc: 'About iCopilot — version, server, user', group: 'Session', local: true },
61
58
  { cmd: '/new', hint: '', desc: 'Start a fresh conversation', group: 'Session', local: true },
62
59
  { cmd: '/history', hint: '', desc: 'Show conversation history', group: 'Session', local: true },
63
60
  { cmd: '/clear', hint: '', desc: 'Clear the screen', group: 'Session', local: true },
package/src/tui/app.js CHANGED
@@ -8,8 +8,7 @@ import { Box, Text, Static, useStdout, useApp, useInput } from 'ink';
8
8
  import { C, glyphs, midDot, dash } from './theme.js';
9
9
  import {
10
10
  Banner, UserMessage, AssistantMessage, StatusLines, Reasoning,
11
- Plan, Connect, Notice, Thinking, Choice, ShellInput, ShellOut,
12
- TabBar, MonitoringView, IssuesView, GistsView,
11
+ Plan, Connect, Notice, Thinking, Choice,
13
12
  } from './components.js';
14
13
  import { Composer } from './composer.js';
15
14
  import { streamChat } from '../client.js';
@@ -17,19 +16,9 @@ import { slashToQuestion, SLASH_COMMANDS } from '../commands.js';
17
16
  import { saveConfig } from '../config.js';
18
17
  import { shortModel } from '../ui.js';
19
18
  import { loadNotes, addNote, forgetNote, memoryPreamble } from '../memory.js';
20
- import { copyToClipboard, readFromClipboard } from '../clipboard.js';
19
+ import { copyToClipboard } from '../clipboard.js';
21
20
  import { createGist } from '../gist.js';
22
21
  import { git as gitCmd, commitBackup } from '../gitrepo.js';
23
- import { runShell } from '../shell.js';
24
- import { checkLatestVersion } from '../version-check.js';
25
- import { saveResume } from '../resume.js';
26
- import { basename } from 'path';
27
-
28
- // CWD basename — computed once at startup (before any cd).
29
- const APP_CWD = basename(process.cwd());
30
-
31
- // Tab identifiers (order = display order).
32
- export const TABS = ['Session', 'Monitoring', 'Issues', 'Gists'];
33
22
 
34
23
  // Split a command argument string into argv, honouring "quoted" segments.
35
24
  function splitArgs(s) {
@@ -58,7 +47,7 @@ function useTerminalSize() {
58
47
  let itemId = 0;
59
48
  const nextId = () => ++itemId;
60
49
 
61
- export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, version = '', resumeSession = null }) {
50
+ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion }) {
62
51
  const { exit } = useApp();
63
52
  const { cols, rows } = useTerminalSize();
64
53
 
@@ -72,9 +61,6 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
72
61
  const [startedAt, setStartedAt] = useState(0);
73
62
  const [choice, setChoice] = useState(null); // { title, note, options, allowText, sel, text, focusText, onPick }
74
63
  const [clearEpoch, setClearEpoch] = useState(0); // bump to remount <Static> on /clear /new
75
- const [prefillText, setPrefillText] = useState(null); // clipboard paste → pre-fills Composer
76
- const [activeTab, setActiveTab] = useState(0); // 0=Session 1=Monitoring 2=Issues 3=Gists
77
- const [latestVersion, setLatestVersion] = useState(null); // { latest, isNewer } from npm
78
64
 
79
65
  const convo = useRef([]); // [{role, content}] for the backend
80
66
  const plain = useRef([]); // plain-text transcript for exit reprint
@@ -84,7 +70,6 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
84
70
  const modelRef = useRef(cfg._model); // preferred model to request
85
71
  const lastAnswerRef = useRef(''); // most recent assistant answer (for /copy, /gist)
86
72
  const choiceActionRef = useRef(null); // action to run when a non-AI Choice is picked
87
- const issuesRef = useRef([]); // AI-flagged issues accumulated across session
88
73
 
89
74
  // Enable terminal bracketed-paste so multi-line pastes (mouse right-click or
90
75
  // Cmd/Ctrl+V) arrive wrapped in markers as one block — the Composer inserts
@@ -94,35 +79,6 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
94
79
  return () => { try { process.stdout.write('\x1b[?2004l'); } catch {} };
95
80
  }, []);
96
81
 
97
- // Background version check — runs once after mount, non-blocking.
98
- useEffect(() => {
99
- if (!version || version === 'unknown') return;
100
- checkLatestVersion(version).then((result) => {
101
- if (result?.isNewer) setLatestVersion(result);
102
- });
103
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
104
-
105
- // Restore session after a hot-reload /update.
106
- useEffect(() => {
107
- if (!resumeSession) return;
108
- convo.current = resumeSession.convo || [];
109
- plain.current = resumeSession.plain || [];
110
- // Re-hydrate items list from plain text so the user sees their history.
111
- if (resumeSession.items?.length) {
112
- setItems(resumeSession.items.map((it) => ({ id: nextId(), ...it })));
113
- }
114
- push({ type: 'notice', text: `${glyphs.check} Session restored after update — conversation history preserved.`, color: C.green });
115
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
116
-
117
- // Tab switching: Ctrl+1/2/3/4.
118
- useInput((input, key) => {
119
- if (!key.ctrl) return;
120
- if (input === '1') { setActiveTab(0); return; }
121
- if (input === '2') { setActiveTab(1); return; }
122
- if (input === '3') { setActiveTab(2); return; }
123
- if (input === '4') { setActiveTab(3); return; }
124
- });
125
-
126
82
  const setAutopilotMode = useCallback((next) => {
127
83
  apRef.current = next;
128
84
  setAutopilot(next);
@@ -285,22 +241,6 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
285
241
  if (!q) return;
286
242
  setHint('');
287
243
 
288
- // ── ! shell command execution (GitHub Copilot CLI parity) ──────────────
289
- if (q.startsWith('!')) {
290
- const cmd = q.slice(1).trim();
291
- if (!cmd) return;
292
- push({ type: 'shell_in', cmd });
293
- log(`$ ${cmd}`);
294
- runShell(cmd).then((result) => {
295
- const out = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
296
- push({ type: 'shell_out', ok: result.ok, output: out || `(exit code ${result.exitCode})` });
297
- if (out) log(out);
298
- }).catch((e) => {
299
- push({ type: 'notice', text: `${glyphs.cross} Shell error: ${e.message}`, color: C.red });
300
- });
301
- return;
302
- }
303
-
304
244
  if (q === '/exit' || q === 'exit' || q === 'quit') { doExit(); return; }
305
245
  if (q === '/clear') { clearAll(); return; }
306
246
  if (q === '/new') {
@@ -350,31 +290,8 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
350
290
  push({ type: 'notice', text: us.length ? us.map((m, i) => ` ${i + 1}. ${m.content.slice(0, 80)}`).join('\n') : 'No history yet.' });
351
291
  return;
352
292
  }
353
- if (q === '/usage') {
354
- const total = convo.current.length;
355
- const userTurns = convo.current.filter((m) => m.role === 'user').length;
356
- const chars = convo.current.reduce((acc, m) => acc + m.content.length, 0);
357
- const approxTokens = Math.round(chars / 4);
358
- const lines = [
359
- `Conversation stats:`,
360
- ` turns ${userTurns}`,
361
- ` messages ${total}`,
362
- ` ~tokens ${approxTokens.toLocaleString()} (estimated)`,
363
- ` ~chars ${chars.toLocaleString()}`,
364
- ` model ${model || cfg._model || 'server default'}`,
365
- ` cwd ${APP_CWD}`,
366
- ];
367
- push({ type: 'notice', text: lines.join('\n'), color: C.cyan });
368
- return;
369
- }
370
293
  if (q === '/logout') { onExternal?.('logout'); doExit(); return; }
371
- if (q === '/update') {
372
- // Save current conversation so the new binary can resume it.
373
- const resumeFile = saveResume(convo.current, plain.current, items);
374
- onExternal?.({ action: 'update', resumeFile });
375
- doExit();
376
- return;
377
- }
294
+ if (q === '/update') { onExternal?.('update'); doExit(); return; }
378
295
 
379
296
  // ── Memory ──
380
297
  if (q === '/remember' || q.startsWith('/remember ')) {
@@ -402,39 +319,8 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
402
319
  }
403
320
 
404
321
  // ── Clipboard ──
405
- // ── /about ─────────────────────────────────────────────────────────────
406
- if (q === '/about') {
407
- const lines = [
408
- `iCopilot CLI v${version || '?'}`,
409
- `Server ${cfg.url || '(unknown)'}`,
410
- `User ${cfg.user?.name || '—'} (${cfg.user?.role || '—'})`,
411
- `Model ${shortModel(model || cfg._model || '—')}`,
412
- '',
413
- 'Type /help for all commands · Ctrl+C to exit',
414
- 'npm: ispbills-icli · github: lupael/IspBills',
415
- ];
416
- push({ type: 'notice', text: lines.join('\n'), color: C.accent });
417
- return;
418
- }
419
-
420
- // ── /paste ─────────────────────────────────────────────────────────────
421
- if (q === '/paste') {
422
- push({ type: 'notice', text: `${glyphs.arrow} Reading clipboard…`, color: C.gray });
423
- readFromClipboard().then((text) => {
424
- if (!text) {
425
- push({ type: 'notice', text: `${glyphs.cross} Clipboard is empty or no clipboard tool found.\nInstall xclip / wl-clipboard (Linux), or use macOS/Windows.`, color: C.red });
426
- return;
427
- }
428
- const lines = text.split('\n').length;
429
- push({ type: 'notice',
430
- text: `${glyphs.check} Pasted ${lines} line${lines !== 1 ? 's' : ''} (${text.length} chars) into the input box — review and press Enter to send.`,
431
- color: C.green });
432
- setPrefillText(text);
433
- });
434
- return;
435
- }
436
-
437
- if (q === '/copy') { const ans = lastAnswerRef.current;
322
+ if (q === '/copy') {
323
+ const ans = lastAnswerRef.current;
438
324
  if (!ans) { push({ type: 'notice', text: 'Nothing to copy yet — ask something first.', color: C.gray }); return; }
439
325
  copyToClipboard(ans).then((tool) => push({ type: 'notice',
440
326
  text: tool ? `${glyphs.check} Copied last answer (${ans.length} chars) to the clipboard.`
@@ -568,21 +454,13 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
568
454
  case 'connect': return html`<${Connect} key=${it.id} reply=${it.reply} />`;
569
455
  case 'activity': return html`<${StatusLines} key=${it.id} lines=${it.status} busy=${false} />`;
570
456
  case 'notice': return html`<${Notice} key=${it.id} text=${it.text} color=${it.color} />`;
571
- case 'shell_in': return html`<${ShellInput} key=${it.id} cmd=${it.cmd} />`;
572
- case 'shell_out': return html`<${ShellOut} key=${it.id} ok=${it.ok} output=${it.output} />`;
573
457
  default: return null;
574
458
  }
575
459
  };
576
460
 
577
- const ctx = [model ? shortModel(model) : null, APP_CWD, cfg.user?.name, cfg.user?.role].filter(Boolean).join(midDot());
578
- // Footer: left = session context (dimmed), right = hint or key-hints.
579
- // Autopilot mode turns the right side yellow.
580
- const footerHint = hint
581
- ? hint
582
- : autopilot
583
- ? `${glyphs.bolt} autopilot ON${midDot()}/help${midDot()}Shift+Tab off${midDot()}Ctrl+C exit`
584
- : `/help${midDot()}Shift+Tab autopilot${midDot()}Ctrl+C exit`;
585
- const footerColor = hint ? C.yellow : autopilot ? C.yellow : void 0;
461
+ const ctx = [model ? shortModel(model) : null, cfg.user?.name, cfg.user?.role].filter(Boolean).join(midDot());
462
+ const modeTag = autopilot ? `${glyphs.bolt} autopilot ${midDot()} ` : '';
463
+ const footerRight = hint || `${modeTag}/help ${midDot()} Shift+Tab autopilot ${midDot()} Ctrl+C exit`;
586
464
  const compact = cols < 60;
587
465
 
588
466
  // The banner is the first <Static> item so it prints once at the very top and
@@ -593,53 +471,31 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
593
471
 
594
472
  return html`
595
473
  <${Box} flexDirection="column" width=${cols}>
596
- ${/* Tab bar — always pinned above content */null}
597
- <${TabBar} tabs=${TABS} active=${activeTab} onSelect=${setActiveTab} width=${cols} />
474
+ <${Static} key=${clearEpoch} items=${staticItems}>
475
+ ${(it) => it.type === 'banner'
476
+ ? html`<${Box} key=${it.id} flexDirection="column" alignItems="center" marginBottom=${1}>
477
+ <${Banner} cfg=${cfg} model=${model} width=${cols} compact=${compact} />
478
+ <//>`
479
+ : renderItem(it)}
480
+ <//>
598
481
 
599
- ${activeTab === 0
482
+ ${live
600
483
  ? html`<${Box} flexDirection="column">
601
- <${Static} key=${clearEpoch} items=${staticItems}>
602
- ${(it) => it.type === 'banner'
603
- ? html`<${Box} key=${it.id} flexDirection="column" alignItems="center" marginBottom=${1}>
604
- <${Banner} cfg=${cfg} model=${model} width=${cols} compact=${compact} version=${version} cwd=${APP_CWD} />
605
- <//>`
606
- : renderItem(it)}
607
- <//>
608
- ${live
609
- ? html`<${Box} flexDirection="column">
610
- <${StatusLines} lines=${live.status} busy=${busy} />
611
- ${showReasoning && live.reasoning ? html`<${Reasoning} text=${live.reasoning} />` : null}
612
- ${live.reply?.steps ? html`<${Plan} reply=${live.reply} live=${true} />` : null}
613
- ${live.text
614
- ? html`<${Box} marginTop=${live.status.length ? 0 : 1}>
615
- <${AssistantMessage} text=${live.text} streaming=${true} />
616
- <//>` : null}
617
- ${!live.text && !live.reply?.steps
618
- ? html`<${Thinking}
619
- label=${live.status.length ? 'Working' : 'Thinking'}
620
- startedAt=${startedAt} />`
621
- : null}
622
- <//>`
623
- : null}
484
+ <${StatusLines} lines=${live.status} busy=${busy} />
485
+ ${showReasoning && live.reasoning ? html`<${Reasoning} text=${live.reasoning} />` : null}
486
+ ${live.reply?.steps ? html`<${Plan} reply=${live.reply} live=${true} />` : null}
487
+ ${live.text ? html`<${Box} marginTop=${1}><${AssistantMessage} text=${live.text} /><//>` : null}
488
+ <${Thinking} label=${live.reply ? 'Planning' : live.text ? 'Responding' : 'Working'} startedAt=${startedAt} />
624
489
  <//>`
625
490
  : null}
626
- ${activeTab === 1 ? html`<${MonitoringView} cfg=${cfg} />` : null}
627
- ${activeTab === 2 ? html`<${IssuesView} issues=${issuesRef.current} />` : null}
628
- ${activeTab === 3 ? html`<${GistsView} cfg=${cfg} />` : null}
629
491
 
630
492
  <${Box} flexDirection="column" marginTop=${1}>
631
- ${activeTab === 0
632
- ? (choice
633
- ? html`<${Choice} ...${choice} />`
634
- : html`<${Composer} onSubmit=${onSubmit} busy=${busy} width=${cols} prefill=${prefillText} onPrefillConsumed=${() => setPrefillText(null)} />`)
635
- : html`<${Box} paddingX=${1}><${Text} dimColor>Ctrl+1 for Session · Ctrl+2/3/4 for other tabs<//> <//>` }
493
+ ${choice
494
+ ? html`<${Choice} ...${choice} />`
495
+ : html`<${Composer} onSubmit=${onSubmit} busy=${busy} width=${cols} />`}
636
496
  <${Box} paddingX=${1} width=${cols}>
637
- <${Box} flexGrow=${1}>
638
- <${Text} dimColor wrap="truncate-end">${ctx}<//>
639
- <//>
640
- ${latestVersion?.isNewer
641
- ? html`<${Text} color=${C.yellow} wrap="truncate-end">🔔 v${latestVersion.latest} available — /update<//>`
642
- : html`<${Text} color=${footerColor} dimColor=${!footerColor} wrap="truncate-end">${footerHint}<//>` }
497
+ <${Box} flexGrow=${1}><${Text} color=${C.gray} wrap="truncate-end">${ctx}<//><//>
498
+ <${Text} color=${hint || autopilot ? C.yellow : C.gray} wrap="truncate-end">${footerRight}<//>
643
499
  <//>
644
500
  <//>
645
501
  <//>`;