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 +4 -24
- package/package.json +1 -1
- package/src/banner.js +1 -3
- package/src/client.js +0 -6
- package/src/clipboard.js +1 -35
- package/src/commands.js +0 -3
- package/src/tui/app.js +27 -171
- package/src/tui/components.js +89 -587
- package/src/tui/composer.js +4 -39
- package/src/tui/markdown.js +3 -19
- package/src/tui/run.js +1 -3
- package/src/tui/theme.js +11 -29
- package/src/logger.js +0 -23
- package/src/resume.js +0 -30
- package/src/shell.js +0 -18
- package/src/version-check.js +0 -43
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
|
|
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 (
|
|
332
|
-
|
|
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
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}
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
406
|
-
|
|
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,
|
|
578
|
-
|
|
579
|
-
|
|
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
|
-
|
|
597
|
-
|
|
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
|
-
${
|
|
482
|
+
${live
|
|
600
483
|
? html`<${Box} flexDirection="column">
|
|
601
|
-
<${
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
${
|
|
632
|
-
?
|
|
633
|
-
|
|
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
|
-
|
|
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
|
<//>`;
|