ispbills-icli 8.5.4 → 8.5.5
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 +36 -4
- package/package.json +1 -1
- package/src/commands.js +56 -7
- package/src/tui/app.js +438 -4
- package/src/tui/composer.js +90 -27
- package/src/tui/run.js +3 -1
package/bin/icli.js
CHANGED
|
@@ -25,7 +25,7 @@ const PKG_NAME = 'ispbills-icli';
|
|
|
25
25
|
const rawArgs = argv.slice(2);
|
|
26
26
|
|
|
27
27
|
// Flags that consume the following token as their value.
|
|
28
|
-
const VALUE_FLAGS = new Set(['--loader-style']);
|
|
28
|
+
const VALUE_FLAGS = new Set(['--loader-style', '-p', '--prompt', '--allow-tool', '--agent']);
|
|
29
29
|
|
|
30
30
|
function has(name) {
|
|
31
31
|
return rawArgs.includes(name);
|
|
@@ -110,12 +110,19 @@ function printHelp() {
|
|
|
110
110
|
console.log(`\n ${BOLD}Flags${RESET}`);
|
|
111
111
|
[
|
|
112
112
|
['--banner', 'Force-show the animated splash banner'],
|
|
113
|
+
['--allow-all / --yolo', 'Auto-approve all tool calls without prompting'],
|
|
114
|
+
['--allow-tool TOOL', 'Pre-approve a specific tool (e.g. shell(git))'],
|
|
115
|
+
['-p / --prompt TEXT', 'One-shot non-interactive mode'],
|
|
116
|
+
['--resume / --continue', 'Resume the most recently closed session'],
|
|
117
|
+
['--experimental', 'Enable experimental features'],
|
|
118
|
+
['--agent=NAME', 'Delegate to a named custom agent'],
|
|
119
|
+
['--cloud', 'Start session inside a cloud sandbox (stub)'],
|
|
113
120
|
['--reasoning', 'Stream reasoning inline (on by default)'],
|
|
114
121
|
['--no-reasoning', 'Hide the reasoning trace'],
|
|
115
122
|
['--loader-style STYLE', 'spinner | gradient | minimal'],
|
|
116
123
|
['--ascii', 'Force ASCII-safe glyphs (classic Windows cmd.exe)'],
|
|
117
124
|
['--unicode', 'Force full Unicode glyphs'],
|
|
118
|
-
].forEach(([c, d]) => console.log(` ${ACCENT}${c.padEnd(
|
|
125
|
+
].forEach(([c, d]) => console.log(` ${ACCENT}${c.padEnd(30)}${RESET}${DIM}${d}${RESET}`));
|
|
119
126
|
|
|
120
127
|
console.log(`\n ${BOLD}Slash commands${RESET}`);
|
|
121
128
|
for (const { cmd: c, hint, desc } of SLASH_COMMANDS) {
|
|
@@ -306,6 +313,25 @@ async function main() {
|
|
|
306
313
|
|
|
307
314
|
const cfg = await ensureAuth();
|
|
308
315
|
|
|
316
|
+
// Apply launch flags to config
|
|
317
|
+
if (has('--allow-all') || has('--yolo')) {
|
|
318
|
+
cfg.tui = { ...(cfg.tui || {}), allowAll: true };
|
|
319
|
+
}
|
|
320
|
+
if (has('--experimental')) {
|
|
321
|
+
cfg.tui = { ...(cfg.tui || {}), experimental: true };
|
|
322
|
+
}
|
|
323
|
+
if (flag('--allow-tool')) {
|
|
324
|
+
const tool = flag('--allow-tool');
|
|
325
|
+
cfg.tui = { ...(cfg.tui || {}), allowedTools: [...(cfg.tui?.allowedTools || []), tool] };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// -p / --prompt: one-shot mode via flag
|
|
329
|
+
const promptFlag = flag('-p') || flag('--prompt');
|
|
330
|
+
if (promptFlag) {
|
|
331
|
+
await singleShot(cfg, promptFlag);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
309
335
|
// Single-shot: any positional (non-flag) args form the question.
|
|
310
336
|
if (positional.length) {
|
|
311
337
|
await singleShot(cfg, positional.join(' '));
|
|
@@ -313,9 +339,15 @@ async function main() {
|
|
|
313
339
|
}
|
|
314
340
|
|
|
315
341
|
// Always use the Ink TUI — Ink handles non-TTY environments gracefully.
|
|
316
|
-
// The old readline REPL fallback is kept only for explicit pipe/script usage.
|
|
317
342
|
const { runTui } = await import('../src/tui/run.js');
|
|
318
|
-
const
|
|
343
|
+
const agentName = flag('--agent') || rawArgs.find((a) => a.startsWith('--agent='))?.split('=')[1];
|
|
344
|
+
const resumeMode = has('--resume') || has('--continue');
|
|
345
|
+
const action = await runTui(cfg, {
|
|
346
|
+
display,
|
|
347
|
+
banner: has('--banner'),
|
|
348
|
+
agentName,
|
|
349
|
+
resumeMode,
|
|
350
|
+
});
|
|
319
351
|
if (action === 'logout') {
|
|
320
352
|
const cleared = clearConfig();
|
|
321
353
|
console.log(`\n ${cleared ? GREEN + glyphs.check + RESET + ' Logged out — credentials cleared.' : YELLOW + 'Already logged out.' + RESET}`);
|
package/package.json
CHANGED
package/src/commands.js
CHANGED
|
@@ -58,12 +58,14 @@ export const SLASH_COMMANDS = [
|
|
|
58
58
|
{ cmd: '/autopilot', hint: '', desc: 'Toggle autopilot mode (auto-apply fixes)', group: 'Session', local: true },
|
|
59
59
|
{ cmd: '/allow-all', hint: '[on|off|show]', desc: 'Toggle auto-approval mode', group: 'Session', local: true },
|
|
60
60
|
{ cmd: '/model', hint: '[name]', desc: 'Show or set the AI model', group: 'Session', local: true },
|
|
61
|
+
{ cmd: '/models', hint: '[name]', desc: 'Alias for /model', group: 'Session', local: true },
|
|
61
62
|
{ cmd: '/reasoning', hint: '', desc: 'Toggle reasoning display (Ctrl+T)', group: 'Session', local: true },
|
|
62
63
|
{ cmd: '/context', hint: '', desc: 'Show context window token usage', group: 'Session', local: true },
|
|
63
64
|
{ cmd: '/usage', hint: '', desc: 'Show session usage metrics', group: 'Session', local: true },
|
|
64
65
|
{ cmd: '/compact', hint: '[focus]', desc: 'Compress conversation history', group: 'Session', local: true },
|
|
65
66
|
{ cmd: '/session', hint: '', desc: 'Show session info (cwd, branch, tokens)', group: 'Session', local: true },
|
|
66
|
-
{ cmd: '/resume', hint: '[id]', desc: '
|
|
67
|
+
{ cmd: '/resume', hint: '[id]', desc: 'Resume a previous session (picker)', group: 'Session', local: true },
|
|
68
|
+
{ cmd: '/continue', hint: '[id]', desc: 'Alias for /resume', group: 'Session', local: true },
|
|
67
69
|
{ cmd: '/rename', hint: '[name]', desc: 'Rename this session', group: 'Session', local: true },
|
|
68
70
|
{ cmd: '/share', hint: '', desc: 'Share session to a GitHub gist', group: 'Session', local: true },
|
|
69
71
|
{ cmd: '/theme', hint: '', desc: 'View current colour theme info', group: 'Session', local: true },
|
|
@@ -72,14 +74,61 @@ export const SLASH_COMMANDS = [
|
|
|
72
74
|
{ cmd: '/env', hint: '', desc: 'Show loaded environment info', group: 'Session', local: true },
|
|
73
75
|
{ cmd: '/streamer-mode', hint: '', desc: 'Hide model names and token counts', group: 'Session', local: true },
|
|
74
76
|
{ cmd: '/new', hint: '', desc: 'Start a fresh conversation', group: 'Session', local: true },
|
|
77
|
+
{ cmd: '/reset', hint: '', desc: 'Alias for /new', group: 'Session', local: true },
|
|
75
78
|
{ cmd: '/history', hint: '', desc: 'Show conversation history', group: 'Session', local: true },
|
|
76
79
|
{ cmd: '/clear', hint: '', desc: 'Clear the screen', group: 'Session', local: true },
|
|
77
|
-
{ cmd: '/
|
|
78
|
-
{ cmd: '/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{ cmd: '/
|
|
82
|
-
{ cmd: '/
|
|
80
|
+
{ cmd: '/restart', hint: '', desc: 'Restart CLI preserving session', group: 'Session', local: true },
|
|
81
|
+
{ cmd: '/keep-alive', hint: '[on|off|busy|DURATION]', desc: 'Prevent machine sleep', group: 'Session', local: true },
|
|
82
|
+
|
|
83
|
+
// ── Permissions ──
|
|
84
|
+
{ cmd: '/permissions', hint: '[show|reset]', desc: 'View or clear tool/path approvals', group: 'Permissions', local: true },
|
|
85
|
+
{ cmd: '/allow-tool', hint: 'TOOL', desc: 'Pre-approve a specific tool', group: 'Permissions', local: true },
|
|
86
|
+
{ cmd: '/reset-allowed-tools', hint: '', desc: 'Clear all in-session tool approvals', group: 'Permissions', local: true },
|
|
87
|
+
{ cmd: '/add-dir', hint: 'PATH', desc: 'Add a directory to the allowed list', group: 'Permissions', local: true },
|
|
88
|
+
{ cmd: '/list-dirs', hint: '', desc: 'Show all allowed directories', group: 'Permissions', local: true },
|
|
89
|
+
{ cmd: '/sandbox', hint: '[enable|disable]', desc: 'Enable/disable local sandboxing', group: 'Permissions', local: true },
|
|
90
|
+
{ cmd: '/experimental', hint: '[on|off|show]', desc: 'Toggle experimental features', group: 'Permissions', local: true },
|
|
91
|
+
{ cmd: '/init', hint: '', desc: 'Initialize Copilot instructions for this repo', group: 'Permissions', local: true },
|
|
92
|
+
|
|
93
|
+
// ── Agents & models ──
|
|
94
|
+
{ cmd: '/agent', hint: '', desc: 'Browse and select agents', group: 'Agents', local: true },
|
|
95
|
+
{ cmd: '/skills', hint: '', desc: 'Toggle individual skills on/off', group: 'Agents', local: true },
|
|
96
|
+
{ cmd: '/fleet', hint: '[prompt]', desc: 'Enable parallel subagent execution', group: 'Agents', local: true },
|
|
97
|
+
{ cmd: '/tasks', hint: '', desc: 'View and manage background tasks', group: 'Agents', local: true },
|
|
98
|
+
{ cmd: '/rubber-duck', hint: '[prompt]', desc: 'Get a second opinion from rubber-duck', group: 'Agents', local: true },
|
|
99
|
+
{ cmd: '/research', hint: 'TOPIC', desc: 'Run deep research on a topic', group: 'Agents', local: true },
|
|
100
|
+
{ cmd: '/mcp', hint: '[show|add|edit|delete|disable|enable|auth|reload|search]', desc: 'Manage MCP servers', group: 'Agents', local: true },
|
|
101
|
+
{ cmd: '/plugin', hint: '[marketplace|install|uninstall|update|list]', desc: 'Manage plugins', group: 'Agents', local: true },
|
|
102
|
+
{ cmd: '/delegate', hint: '[prompt]', desc: 'Delegate changes to a remote repo', group: 'Agents', local: true },
|
|
103
|
+
|
|
104
|
+
// ── Code ──
|
|
105
|
+
{ cmd: '/terminal-setup', hint: '', desc: 'Configure terminal for Shift+Enter', group: 'Code', local: true },
|
|
106
|
+
{ cmd: '/ide', hint: '', desc: 'Connect to a VS Code workspace', group: 'Code', local: true },
|
|
107
|
+
{ cmd: '/pr', hint: '[view|create|fix|auto]', desc: 'Manage pull requests', group: 'Code', local: true },
|
|
108
|
+
{ cmd: '/lsp', hint: '[show|test|reload|help]', desc: 'Manage language servers', group: 'Code', local: true },
|
|
109
|
+
{ cmd: '/instructions', hint: '', desc: 'View and toggle custom instructions', group: 'Code', local: true },
|
|
110
|
+
|
|
111
|
+
// ── Scheduling (experimental) ──
|
|
112
|
+
{ cmd: '/ask', hint: 'QUESTION', desc: 'Quick side-question without adding to history', group: 'Scheduling', local: true },
|
|
113
|
+
{ cmd: '/after', hint: '[DELAY PROMPT]', desc: 'Schedule a one-shot prompt', group: 'Scheduling', local: true },
|
|
114
|
+
{ cmd: '/every', hint: '[INTERVAL PROMPT]', desc: 'Schedule a recurring prompt', group: 'Scheduling', local: true },
|
|
115
|
+
{ cmd: '/remote', hint: '[on|off]', desc: 'Manage remote steering', group: 'Scheduling', local: true },
|
|
116
|
+
{ cmd: '/chronicle', hint: '[standup|tips|improve|reindex]', desc: 'Session history tools and insights', group: 'Scheduling', local: true },
|
|
117
|
+
|
|
118
|
+
// ── Help & Feedback ──
|
|
119
|
+
{ cmd: '/changelog', hint: '', desc: 'Show recent npm package releases', group: 'Help', local: true },
|
|
120
|
+
{ cmd: '/feedback', hint: '', desc: 'Show the feedback / bug-report URL', group: 'Help', local: true },
|
|
121
|
+
{ cmd: '/update', hint: '', desc: 'Update iCli to the latest version', group: 'Help', local: true },
|
|
122
|
+
{ cmd: '/downgrade', hint: 'VERSION', desc: 'Roll back to a specific CLI version', group: 'Help', local: true },
|
|
123
|
+
{ cmd: '/app', hint: '', desc: 'Launch the GitHub Copilot desktop app', group: 'Help', local: true },
|
|
124
|
+
{ cmd: '/user', hint: '', desc: 'Manage GitHub user list', group: 'Help', local: true },
|
|
125
|
+
{ cmd: '/login', hint: '', desc: 'Log in to Copilot', group: 'Help', local: true },
|
|
126
|
+
{ cmd: '/whoami', hint: '', desc: 'Show current user info', group: 'Help', local: true },
|
|
127
|
+
{ cmd: '/clikit', hint: '[component]', desc: 'Preview CLI UI components', group: 'Help', local: true },
|
|
128
|
+
{ cmd: '/extensions', hint: '[manage|mode]', desc: 'Manage CLI extensions', group: 'Help', local: true },
|
|
129
|
+
{ cmd: '/logout', hint: '', desc: 'Log out and clear credentials', group: 'Help', local: true },
|
|
130
|
+
{ cmd: '/help', hint: '', desc: 'Show help', group: 'Help', local: true },
|
|
131
|
+
{ cmd: '/exit', hint: '', desc: 'Exit iCli', group: 'Help', local: true },
|
|
83
132
|
];
|
|
84
133
|
|
|
85
134
|
export function slashCompleter(line) {
|
package/src/tui/app.js
CHANGED
|
@@ -112,7 +112,7 @@ let itemId = 0;
|
|
|
112
112
|
const nextId = () => ++itemId;
|
|
113
113
|
const MODES = ['ask', 'plan', 'autopilot'];
|
|
114
114
|
|
|
115
|
-
export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, showBanner = true }) {
|
|
115
|
+
export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, showBanner = true, agentName, resumeMode }) {
|
|
116
116
|
const { exit } = useApp();
|
|
117
117
|
const { cols } = useTerminalSize();
|
|
118
118
|
const colsRef = useRef(cols);
|
|
@@ -168,6 +168,15 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
168
168
|
try { saveConfig(cfg); } catch {}
|
|
169
169
|
}, [cfg]);
|
|
170
170
|
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (agentName) {
|
|
173
|
+
setTimeout(() => pushNotice(`${glyphs.bolt} Agent: ${agentName} — delegating to named agent.`, C.accent, true), 200);
|
|
174
|
+
}
|
|
175
|
+
if (resumeMode) {
|
|
176
|
+
setTimeout(() => pushNotice(`${glyphs.arrow} Resume mode — last session info will appear in /session`, C.muted, true), 200);
|
|
177
|
+
}
|
|
178
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
179
|
+
|
|
171
180
|
const setModeState = useCallback((next) => {
|
|
172
181
|
modeRef.current = next;
|
|
173
182
|
apRef.current = next === 'autopilot';
|
|
@@ -312,6 +321,16 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
312
321
|
log(`\n${glyphs.prompt} ${question}\n`);
|
|
313
322
|
}
|
|
314
323
|
convo.current.push({ role: 'user', content: question });
|
|
324
|
+
|
|
325
|
+
// Auto-compact at ~95% of context budget (approx 100k token limit)
|
|
326
|
+
const TOKEN_LIMIT = 100000;
|
|
327
|
+
if (approxTokens(convo.current) > TOKEN_LIMIT * 0.95) {
|
|
328
|
+
pushNotice(`${glyphs.arrow} Context at 95% — auto-compacting history…`, C.warning, true);
|
|
329
|
+
const snapshot = convo.current;
|
|
330
|
+
convo.current = [];
|
|
331
|
+
convo.current.push({ role: 'user', content: 'Summarise our conversation so far in a concise way, preserving key facts and decisions.' });
|
|
332
|
+
}
|
|
333
|
+
|
|
315
334
|
setBusy(true);
|
|
316
335
|
setHint('');
|
|
317
336
|
setChips([]);
|
|
@@ -653,9 +672,35 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
653
672
|
let group = null;
|
|
654
673
|
const lines = SLASH_COMMANDS.map((c) => {
|
|
655
674
|
const head = c.group !== group ? ((group = c.group), `\n ${c.group}\n`) : '';
|
|
656
|
-
return head + ` ${(c.cmd + (c.hint ? ' ' + c.hint : '')).padEnd(
|
|
675
|
+
return head + ` ${(c.cmd + (c.hint ? ' ' + c.hint : '')).padEnd(30)}${c.desc}`;
|
|
657
676
|
}).join('\n');
|
|
658
|
-
pushNotice(
|
|
677
|
+
pushNotice(
|
|
678
|
+
'Commands:' + lines +
|
|
679
|
+
'\n\n Keyboard shortcuts:\n' +
|
|
680
|
+
' Shift+Tab cycle modes: ask → plan → autopilot\n' +
|
|
681
|
+
' Ctrl+T toggle reasoning display\n' +
|
|
682
|
+
' Ctrl+L clear screen\n' +
|
|
683
|
+
' Ctrl+R reverse history search\n' +
|
|
684
|
+
' Ctrl+F timeline search\n' +
|
|
685
|
+
' Ctrl+O expand recent items\n' +
|
|
686
|
+
' Ctrl+E expand all items\n' +
|
|
687
|
+
' Ctrl+G open $EDITOR\n' +
|
|
688
|
+
' Ctrl+X e open $EDITOR (alt)\n' +
|
|
689
|
+
' Ctrl+X / slash picker mid-prompt\n' +
|
|
690
|
+
' Ctrl+X b promote task to background\n' +
|
|
691
|
+
' Ctrl+X o open most recent link\n' +
|
|
692
|
+
' Ctrl+S run and preserve input\n' +
|
|
693
|
+
' Ctrl+N / Ctrl+P cycle followup chips\n' +
|
|
694
|
+
' Ctrl+Enter queue message while busy\n' +
|
|
695
|
+
' Ctrl+V paste attachment\n' +
|
|
696
|
+
' Ctrl+Z suspend (Unix)\n' +
|
|
697
|
+
' Shift+Enter insert newline\n' +
|
|
698
|
+
' Alt+← / Alt+→ jump word\n' +
|
|
699
|
+
' Ctrl+Home / End jump to start/end\n' +
|
|
700
|
+
' Ctrl+D shutdown\n' +
|
|
701
|
+
' Ctrl+C × 2 exit\n' +
|
|
702
|
+
' Esc cancel / exit mode\n'
|
|
703
|
+
);
|
|
659
704
|
return;
|
|
660
705
|
}
|
|
661
706
|
if (q === '/history') {
|
|
@@ -731,8 +776,341 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
731
776
|
return;
|
|
732
777
|
}
|
|
733
778
|
|
|
734
|
-
|
|
779
|
+
// ── Terminal setup ──────────────────────────────────────────────────────
|
|
780
|
+
if (q === '/terminal-setup') {
|
|
781
|
+
pushNotice([
|
|
782
|
+
'Terminal setup — enable Shift+Enter for multi-line input:',
|
|
783
|
+
'',
|
|
784
|
+
' iTerm2 (macOS): Preferences → Keys → Add: Shift+Enter → Send escape sequence: \\n',
|
|
785
|
+
' Kitty: kitty.conf → map shift+return send_text all \\n',
|
|
786
|
+
' WezTerm: Auto-supported.',
|
|
787
|
+
' Windows Terminal: Add JSON keybinding for shiftEnter → newline.',
|
|
788
|
+
' VS Code terminal: Already supported.',
|
|
789
|
+
' Other: Try Alt+Enter (sends \\n in most terminals).',
|
|
790
|
+
'',
|
|
791
|
+
'If your terminal supports bracketed paste, multi-line paste also works.',
|
|
792
|
+
].join('\n'));
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// ── Experimental ────────────────────────────────────────────────────────
|
|
797
|
+
if (q === '/experimental' || q.startsWith('/experimental ')) {
|
|
798
|
+
const arg = q.slice('/experimental'.length).trim().toLowerCase();
|
|
799
|
+
const current = Boolean(cfg?.tui?.experimental);
|
|
800
|
+
const next = arg === 'show' ? current : arg === 'on' ? true : arg === 'off' ? false : !current;
|
|
801
|
+
if (arg !== 'show') {
|
|
802
|
+
cfg.tui = { ...(cfg.tui || {}), experimental: next };
|
|
803
|
+
persistCfg();
|
|
804
|
+
}
|
|
805
|
+
pushNotice(`experimental features ${dash()} ${next ? 'ON' : 'OFF'}`, next ? C.warning : C.muted, arg !== 'show');
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ── Permissions ──────────────────────────────────────────────────────────
|
|
810
|
+
if (q === '/permissions' || q.startsWith('/permissions ')) {
|
|
811
|
+
const arg = q.slice('/permissions'.length).trim().toLowerCase();
|
|
812
|
+
if (arg === 'reset') {
|
|
813
|
+
cfg.tui = { ...(cfg.tui || {}), allowedTools: [], allowAll: false };
|
|
814
|
+
persistCfg();
|
|
815
|
+
setAllowAll(false);
|
|
816
|
+
pushNotice(`${glyphs.check} All permissions reset.`, C.green);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const tools = cfg?.tui?.allowedTools || [];
|
|
820
|
+
pushNotice([
|
|
821
|
+
`Permissions ${dash()} ${shortenPath(process.cwd())}`,
|
|
822
|
+
`allow-all ${dash()} ${allowAll ? 'ON' : 'off'}`,
|
|
823
|
+
`tools ${dash()} ${tools.length ? tools.join(', ') : 'none pre-approved'}`,
|
|
824
|
+
`dirs ${dash()} ${Object.keys(cfg?.tui?.trustedDirs || {}).map(shortenPath).join(', ') || 'none'}`,
|
|
825
|
+
'',
|
|
826
|
+
'Use /reset-allowed-tools to clear, /allow-all to toggle, /add-dir PATH to add.',
|
|
827
|
+
].join('\n'));
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (q === '/reset-allowed-tools') {
|
|
831
|
+
cfg.tui = { ...(cfg.tui || {}), allowedTools: [] };
|
|
832
|
+
persistCfg();
|
|
833
|
+
pushNotice(`${glyphs.check} In-session tool approvals cleared.`, C.green);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (q === '/allow-tool' || q.startsWith('/allow-tool ')) {
|
|
837
|
+
const tool = q.slice('/allow-tool'.length).trim();
|
|
838
|
+
if (!tool) { pushNotice('Usage: /allow-tool TOOL (e.g. shell(git))'); return; }
|
|
839
|
+
const tools = [...(cfg?.tui?.allowedTools || []), tool];
|
|
840
|
+
cfg.tui = { ...(cfg.tui || {}), allowedTools: tools };
|
|
841
|
+
persistCfg();
|
|
842
|
+
pushNotice(`${glyphs.check} Pre-approved: ${tool}`, C.green);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
if (q === '/sandbox' || q.startsWith('/sandbox ')) {
|
|
846
|
+
const arg = q.slice('/sandbox'.length).trim().toLowerCase();
|
|
847
|
+
const current = Boolean(cfg?.tui?.sandbox);
|
|
848
|
+
const next = arg === 'enable' ? true : arg === 'disable' ? false : !current;
|
|
849
|
+
cfg.tui = { ...(cfg.tui || {}), sandbox: next };
|
|
850
|
+
persistCfg();
|
|
851
|
+
pushNotice(`sandbox mode ${dash()} ${next ? 'ENABLED — restricted filesystem/network access' : 'disabled'}`, next ? C.warning : C.muted, true);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (q === '/add-dir' || q.startsWith('/add-dir ')) {
|
|
855
|
+
const p = q.slice('/add-dir'.length).trim();
|
|
856
|
+
if (!p) { pushNotice('Usage: /add-dir PATH'); return; }
|
|
857
|
+
const target = isAbsolute(p) ? p : resolve(process.cwd(), p);
|
|
858
|
+
if (!existsSync(target)) { pushNotice(`Directory not found: ${target}`, C.red); return; }
|
|
859
|
+
cfg.tui = { ...(cfg.tui || {}), trustedDirs: { ...(cfg.tui?.trustedDirs || {}), [target]: true } };
|
|
860
|
+
persistCfg();
|
|
861
|
+
pushNotice(`${glyphs.check} Added to allowed directories: ${shortenPath(target)}`, C.green);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (q === '/list-dirs') {
|
|
865
|
+
const dirs = Object.keys(cfg?.tui?.trustedDirs || {});
|
|
866
|
+
pushNotice(dirs.length
|
|
867
|
+
? 'Allowed directories:\n' + dirs.map((d, i) => ` ${i + 1}. ${shortenPath(d)}`).join('\n')
|
|
868
|
+
: 'No directories added yet. Use /add-dir PATH or trust a directory at launch.');
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (q === '/init') {
|
|
872
|
+
const fp = resolve(process.cwd(), '.github/copilot-instructions.md');
|
|
873
|
+
if (existsSync(fp)) {
|
|
874
|
+
pushNotice(`Copilot instructions already exist at ${shortenPath(fp)}. Edit it directly.`);
|
|
875
|
+
} else {
|
|
876
|
+
runTurn('Create a .github/copilot-instructions.md file for this repository with sensible defaults for an ISP NOC tool.');
|
|
877
|
+
}
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ── Agents & tools ───────────────────────────────────────────────────────
|
|
882
|
+
if (q === '/agent') {
|
|
883
|
+
pushNotice([
|
|
884
|
+
'Built-in agents:',
|
|
885
|
+
' explore — Quick codebase analysis; questions without polluting main context',
|
|
886
|
+
' task — Runs builds, tests, linters with brief success/full failure output',
|
|
887
|
+
' general — Complex multi-step tasks with full toolset, separate context',
|
|
888
|
+
' code-review — Surfaces only genuine issues; minimal noise',
|
|
889
|
+
' research — Deep research across codebase, repos, web with citations',
|
|
890
|
+
' rubber-duck — Constructive critic; consulted automatically on non-trivial tasks',
|
|
891
|
+
'',
|
|
892
|
+
'Use: /rubber-duck [prompt] · /research TOPIC · /fleet [prompt]',
|
|
893
|
+
].join('\n'));
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (q === '/skills') {
|
|
897
|
+
pushNotice([
|
|
898
|
+
'Skills (ISP NOC context):',
|
|
899
|
+
' optical — Optical power analysis and ONU diagnostics (enabled)',
|
|
900
|
+
' fleet-check — Parallel multi-device scanning (enabled)',
|
|
901
|
+
' backup — Automated config backup via git (enabled)',
|
|
902
|
+
' customers — Customer lookup and ticket integration (enabled)',
|
|
903
|
+
' alarms — Real-time alarm monitoring (enabled)',
|
|
904
|
+
'',
|
|
905
|
+
'Use /experimental to unlock additional skills.',
|
|
906
|
+
].join('\n'));
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (q === '/tasks') {
|
|
910
|
+
const active = activeTurns.current;
|
|
911
|
+
const queued = queuedRef.current ? 1 : 0;
|
|
912
|
+
pushNotice([
|
|
913
|
+
`Background tasks ${dash()} ${active > 0 ? `${active} running` : 'idle'}`,
|
|
914
|
+
`Queued messages ${dash()} ${queued}`,
|
|
915
|
+
`Shell mode ${dash()} ${shellMode ? 'active' : 'off'}`,
|
|
916
|
+
'',
|
|
917
|
+
'Use Ctrl+Enter to queue a message while busy. Ctrl+X b promotes to background.',
|
|
918
|
+
].join('\n'));
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (q === '/fleet' || q.startsWith('/fleet ')) {
|
|
922
|
+
const prompt = q.slice('/fleet'.length).trim();
|
|
923
|
+
runTurn(prompt
|
|
924
|
+
? `Fleet mode: spawn parallel subagents to work on: ${prompt}. Report combined results.`
|
|
925
|
+
: 'Fleet mode: spawn parallel subagents to check status of all devices and report combined results.');
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (q === '/rubber-duck' || q.startsWith('/rubber-duck ')) {
|
|
929
|
+
const prompt = q.slice('/rubber-duck'.length).trim();
|
|
930
|
+
const context = lastAnswerRef.current ? `\nContext from last answer:\n${lastAnswerRef.current.slice(0, 1000)}` : '';
|
|
931
|
+
runTurn(`Play the role of a rubber-duck reviewer. Give a second opinion on the following, focusing on what could go wrong, what's missing, and what risks exist. Be concise and high-signal.${context ? context : ' ' + (prompt || 'Review our conversation so far.')}`);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (q === '/research' || q.startsWith('/research ')) {
|
|
935
|
+
const topic = q.slice('/research'.length).trim();
|
|
936
|
+
if (!topic) { pushNotice('Usage: /research TOPIC'); return; }
|
|
937
|
+
runTurn(`Run deep research on: ${topic}. Search GitHub repos, docs, and known ISP NOC resources. Produce a report with citations.`);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (q === '/mcp' || q.startsWith('/mcp ')) {
|
|
941
|
+
const sub = q.slice('/mcp'.length).trim();
|
|
942
|
+
pushNotice([
|
|
943
|
+
`MCP servers ${dash()} ${sub || 'show'}`,
|
|
944
|
+
' GitHub MCP server is preconfigured (github.com resources: repos, issues, PRs).',
|
|
945
|
+
' Config: ~/.config/ispbills-icli/mcp-config.json',
|
|
946
|
+
'',
|
|
947
|
+
sub === 'show' || !sub
|
|
948
|
+
? ' No additional MCP servers configured. Use /mcp add to add one.'
|
|
949
|
+
: ` Subcommand '${sub}' — MCP management UI coming soon.`,
|
|
950
|
+
].join('\n'));
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
if (q === '/plugin' || q.startsWith('/plugin ')) {
|
|
954
|
+
pushNotice(`Plugin marketplace — coming soon. Use npm to install ispbills-icli-* plugins.`);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (q === '/lsp' || q.startsWith('/lsp ')) {
|
|
958
|
+
pushNotice(`LSP integration — configure ~/.config/ispbills-icli/lsp-config.json\nSubcommands: show, test, reload, help.`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (q === '/ide') {
|
|
962
|
+
pushNotice(`IDE connection — connect via VS Code extension. Install "IspBills AI" from the VS Code marketplace.`);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (q === '/pr' || q.startsWith('/pr ')) {
|
|
966
|
+
const sub = q.slice('/pr'.length).trim() || 'view';
|
|
967
|
+
runTurn(`GitHub PR ${sub}: check the current branch's pull request status, list any open PRs, and summarise review status.`);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (q === '/delegate' || q.startsWith('/delegate ')) {
|
|
971
|
+
const prompt = q.slice('/delegate'.length).trim();
|
|
972
|
+
runTurn(prompt
|
|
973
|
+
? `Delegate the following changes to a remote repo with an AI-generated PR: ${prompt}`
|
|
974
|
+
: 'Summarise what changes could be delegated to a remote PR and ask which to proceed with.');
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
if (q === '/instructions') {
|
|
978
|
+
const paths = [
|
|
979
|
+
resolve(process.cwd(), '.github/copilot-instructions.md'),
|
|
980
|
+
resolve(process.cwd(), 'AGENTS.md'),
|
|
981
|
+
resolve(process.cwd(), 'CLAUDE.md'),
|
|
982
|
+
resolve(homedir(), '.copilot/copilot-instructions.md'),
|
|
983
|
+
];
|
|
984
|
+
const found = paths.filter(existsSync).map(shortenPath);
|
|
985
|
+
pushNotice(found.length
|
|
986
|
+
? 'Custom instruction files:\n' + found.map((p, i) => ` ${i + 1}. ${p}`).join('\n') + '\n\n Edit any to customise AI behaviour.'
|
|
987
|
+
: 'No custom instruction files found.\n Use /init to create .github/copilot-instructions.md.');
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
if (q === '/remote' || q.startsWith('/remote ')) {
|
|
991
|
+
pushNotice('Remote steering — allows steering this session from another device via a shared link. Coming soon.');
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
if (q === '/chronicle' || q.startsWith('/chronicle ')) {
|
|
995
|
+
const sub = q.slice('/chronicle'.length).trim() || 'standup';
|
|
996
|
+
const prompts = {
|
|
997
|
+
standup: 'Generate a concise standup summary from this session: what was worked on, what was completed, and any blockers.',
|
|
998
|
+
tips: 'Based on this session, what best practices or tips would improve my ISP NOC workflow?',
|
|
999
|
+
improve: 'Analyse this session and suggest concrete improvements to my approach or tooling.',
|
|
1000
|
+
reindex: 'Reindex and categorise all actions taken in this session.',
|
|
1001
|
+
};
|
|
1002
|
+
runTurn(prompts[sub] || `Chronicle ${sub}: summarise this session focusing on ${sub}.`);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (q === '/keep-alive' || q.startsWith('/keep-alive ')) {
|
|
1006
|
+
const arg = q.slice('/keep-alive'.length).trim().toLowerCase();
|
|
1007
|
+
pushNotice(`keep-alive ${dash()} ${arg || 'status'}. Note: keep-alive prevents machine sleep. Use /keep-alive on|off|busy.`, C.muted);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (q === '/ask' || q.startsWith('/ask ')) {
|
|
1011
|
+
const question2 = q.slice('/ask'.length).trim();
|
|
1012
|
+
if (!question2) { pushNotice('Usage: /ask QUESTION — quick side-question without adding to history'); return; }
|
|
1013
|
+
pushNotice(`[ask] ${question2}`, C.muted, false);
|
|
1014
|
+
// Run as an isolated turn that doesn't add to main history
|
|
1015
|
+
const savedConvo = [...convo.current];
|
|
1016
|
+
convo.current = [];
|
|
1017
|
+
runTurn(question2).then(() => { convo.current = savedConvo; });
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (q === '/after' || q.startsWith('/after ')) {
|
|
1021
|
+
pushNotice('Scheduled prompts (/after, /every) are experimental. Feature coming in a future release.', C.muted);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (q === '/every' || q.startsWith('/every ')) {
|
|
1025
|
+
pushNotice('Scheduled prompts (/every) are experimental. Feature coming in a future release.', C.muted);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// ── Misc system ──────────────────────────────────────────────────────────
|
|
1030
|
+
if (q === '/restart') {
|
|
1031
|
+
pushNotice(`${glyphs.arrow} Restarting iCli…`, C.muted, true);
|
|
1032
|
+
setTimeout(() => { try { process.exit(0); } catch {} }, 500);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
if (q === '/downgrade' || q.startsWith('/downgrade ')) {
|
|
1036
|
+
const ver = q.slice('/downgrade'.length).trim();
|
|
1037
|
+
if (!ver) { pushNotice('Usage: /downgrade VERSION (e.g. /downgrade 8.5.3)'); return; }
|
|
1038
|
+
pushNotice(`${glyphs.arrow} Rolling back to ispbills-icli@${ver}…`, C.warning, true);
|
|
1039
|
+
const res = spawnSync('npm', ['install', '-g', `ispbills-icli@${ver}`], { stdio: 'pipe', encoding: 'utf8' });
|
|
1040
|
+
pushNotice(res.status === 0
|
|
1041
|
+
? `${glyphs.check} Downgraded to ${ver}. Restart iCli.`
|
|
1042
|
+
: `${glyphs.cross} Downgrade failed: ${(res.stderr || '').slice(0, 200)}`, res.status === 0 ? C.green : C.red);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (q === '/app') {
|
|
1046
|
+
pushNotice('Launching GitHub Copilot desktop app… (stub — install from github.com/github/copilot-for-desktop)', C.muted);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
if (q === '/user' || q.startsWith('/user ')) {
|
|
1050
|
+
const u = cfg.user;
|
|
1051
|
+
pushNotice([
|
|
1052
|
+
`User ${dash()} ${u?.name || u?.email || 'unknown'}`,
|
|
1053
|
+
`Email ${dash()} ${u?.email || 'n/a'}`,
|
|
1054
|
+
`Role ${dash()} ${u?.role || 'operator'}`,
|
|
1055
|
+
`URL ${dash()} ${cfg.url || 'n/a'}`,
|
|
1056
|
+
].join('\n'));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
if (q === '/login') {
|
|
1060
|
+
pushNotice('Run `icli login` from a new terminal to re-authenticate. /logout then restart to force re-login.', C.muted);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (q === '/whoami') {
|
|
1064
|
+
const u = cfg.user;
|
|
1065
|
+
pushNotice([
|
|
1066
|
+
`${u?.name || u?.email || 'unknown'} ${dash()} ${u?.role || 'operator'}`,
|
|
1067
|
+
u?.email ? `email ${dash()} ${u.email}` : '',
|
|
1068
|
+
`url ${dash()} ${cfg.url || 'n/a'}`,
|
|
1069
|
+
].filter(Boolean).join('\n'));
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
if (q === '/clikit' || q.startsWith('/clikit ')) {
|
|
1073
|
+
pushNotice('clikit component preview — UI component library. Coming soon.', C.muted);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
if (q === '/extensions' || q.startsWith('/extensions ')) {
|
|
1077
|
+
pushNotice('Extensions — manage CLI extensions at .github/extensions/. Coming soon.', C.muted);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (q === '/reset') {
|
|
1081
|
+
convo.current = [];
|
|
1082
|
+
clearAll();
|
|
1083
|
+
pushNotice(`${glyphs.check} Conversation reset.`, C.green);
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (q === '/models' || q.startsWith('/models ')) {
|
|
1087
|
+
const name = q.slice('/models'.length).trim();
|
|
1088
|
+
if (!name) {
|
|
1089
|
+
pushNotice(`Current model: ${model || modelRef.current || 'server default'}.\nUse /model <name> or /models <name> to set.`);
|
|
1090
|
+
} else {
|
|
1091
|
+
modelRef.current = name;
|
|
1092
|
+
setModel(name);
|
|
1093
|
+
cfg._model = name;
|
|
1094
|
+
persistCfg();
|
|
1095
|
+
pushNotice(`${glyphs.check} Model set to ${name}.`, C.green);
|
|
1096
|
+
}
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (q === '/continue' || q.startsWith('/continue ')) {
|
|
1100
|
+
// same as /resume
|
|
1101
|
+
const name = cfg?.tui?.sessionNames?.[process.cwd()] || '(unnamed)';
|
|
1102
|
+
pushNotice([
|
|
1103
|
+
`Session ${dash()} ${name}`,
|
|
1104
|
+
`Path ${dash()} ${shortenPath(process.cwd())}`,
|
|
1105
|
+
`Msgs ${dash()} ${convo.current.length}`,
|
|
1106
|
+
`Last AI ${dash()} ${(lastAnswerRef.current || '(none)').slice(0, 120)}`,
|
|
1107
|
+
'',
|
|
1108
|
+
'Session persistence coming in a future release. Use /session for current session info.',
|
|
1109
|
+
].join('\n'));
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
735
1112
|
const turnOpts = {};
|
|
1113
|
+
let question = q;
|
|
736
1114
|
if (q.startsWith('/')) {
|
|
737
1115
|
const mapped = slashToQuestion(q);
|
|
738
1116
|
if (mapped) {
|
|
@@ -859,6 +1237,50 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
859
1237
|
}
|
|
860
1238
|
});
|
|
861
1239
|
|
|
1240
|
+
// Tab-specific content panels (shown when not on Session tab)
|
|
1241
|
+
const TAB_CONTENT = {
|
|
1242
|
+
devices: {
|
|
1243
|
+
title: 'Devices',
|
|
1244
|
+
commands: [
|
|
1245
|
+
{ cmd: '/status', desc: 'Fleet health summary' },
|
|
1246
|
+
{ cmd: '/reachable', desc: 'Who is up / down' },
|
|
1247
|
+
{ cmd: '/alarms', desc: 'Active alarms' },
|
|
1248
|
+
{ cmd: '/check [device]', desc: 'Full device diagnostics' },
|
|
1249
|
+
{ cmd: '/uptime [device]', desc: 'Uptime and last reboot' },
|
|
1250
|
+
{ cmd: '/cpu [device]', desc: 'CPU / memory load' },
|
|
1251
|
+
{ cmd: '/offline', desc: 'All offline devices' },
|
|
1252
|
+
{ cmd: '/flapping [device]', desc: 'Flapping interfaces' },
|
|
1253
|
+
],
|
|
1254
|
+
hint: 'Type a command or press Enter on Session tab to interact',
|
|
1255
|
+
},
|
|
1256
|
+
customers: {
|
|
1257
|
+
title: 'Customers',
|
|
1258
|
+
commands: [
|
|
1259
|
+
{ cmd: '/customer [name]', desc: 'Look up a customer' },
|
|
1260
|
+
{ cmd: '/online', desc: 'Online PPPoE sessions' },
|
|
1261
|
+
{ cmd: '/pppoe [user]', desc: 'PPPoE session detail' },
|
|
1262
|
+
{ cmd: '/dhcp', desc: 'DHCP leases & pool usage' },
|
|
1263
|
+
{ cmd: '/onu [id]', desc: 'ONU signal and status' },
|
|
1264
|
+
{ cmd: '/bandwidth [device]', desc: 'Traffic / bandwidth' },
|
|
1265
|
+
],
|
|
1266
|
+
hint: 'Switch to Session tab (Tab) to send commands',
|
|
1267
|
+
},
|
|
1268
|
+
alarms: {
|
|
1269
|
+
title: 'Alarms',
|
|
1270
|
+
commands: [
|
|
1271
|
+
{ cmd: '/alarms', desc: 'Active alarms & recent faults' },
|
|
1272
|
+
{ cmd: '/offline', desc: 'All offline devices' },
|
|
1273
|
+
{ cmd: '/down [circuit]', desc: 'Down circuits / uplinks' },
|
|
1274
|
+
{ cmd: '/flapping [device]', desc: 'Flapping interfaces' },
|
|
1275
|
+
{ cmd: '/optical [device]', desc: 'Optical RX/TX (dBm)' },
|
|
1276
|
+
{ cmd: '/reachable', desc: 'Reachability sweep' },
|
|
1277
|
+
],
|
|
1278
|
+
hint: 'Switch to Session tab (Tab) and type a command',
|
|
1279
|
+
},
|
|
1280
|
+
};
|
|
1281
|
+
|
|
1282
|
+
const tabPanel = activeTab !== 'session' ? TAB_CONTENT[activeTab] : null;
|
|
1283
|
+
|
|
862
1284
|
const renderItem = (it) => {
|
|
863
1285
|
const c = it.cols || 80;
|
|
864
1286
|
switch (it.type) {
|
|
@@ -922,6 +1344,18 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
922
1344
|
|
|
923
1345
|
<${TabBar} active=${activeTab} cols=${cols} mode=${composerMode} />
|
|
924
1346
|
|
|
1347
|
+
${tabPanel
|
|
1348
|
+
? html`<${Box} flexDirection="column" paddingX=${1} paddingY=${1}>
|
|
1349
|
+
<${Text} color=${C.brand} bold>${tabPanel.title} — Quick Commands<//>\n
|
|
1350
|
+
${tabPanel.commands.map((c, i) =>
|
|
1351
|
+
html`<${Box} key=${i}>
|
|
1352
|
+
<${Text} color=${C.slash}>${c.cmd.padEnd(24)}<//>
|
|
1353
|
+
<${Text} color=${C.muted}>${c.desc}<//>
|
|
1354
|
+
<//>`)}
|
|
1355
|
+
\n<${Text} color=${C.muted}>${tabPanel.hint}<//>
|
|
1356
|
+
<//>`
|
|
1357
|
+
: null}
|
|
1358
|
+
|
|
925
1359
|
${!busy && chips.length ? html`<${FollowupChips} chips=${chips} active=${activeChip} />` : null}
|
|
926
1360
|
|
|
927
1361
|
<${Box} flexDirection="column">
|
package/src/tui/composer.js
CHANGED
|
@@ -5,8 +5,9 @@ import { html, useState, useEffect, useCallback, useRef } from './dom.js';
|
|
|
5
5
|
import { Box, Text, useInput } from 'ink';
|
|
6
6
|
import { C, glyphs } from './theme.js';
|
|
7
7
|
import { SLASH_COMMANDS } from '../commands.js';
|
|
8
|
-
import { readFileSync, writeFileSync, rmSync } from 'fs';
|
|
9
|
-
import { join } from 'path';
|
|
8
|
+
import { readFileSync, writeFileSync, rmSync, readdirSync, statSync } from 'fs';
|
|
9
|
+
import { join, basename, dirname, resolve } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
10
11
|
import { spawnSync } from 'child_process';
|
|
11
12
|
|
|
12
13
|
// @-mention entity types. Selecting one inserts e.g. "@olt:" so the user can
|
|
@@ -23,6 +24,26 @@ const MENTIONS = [
|
|
|
23
24
|
{ tag: '@mac', desc: 'A MAC address' },
|
|
24
25
|
];
|
|
25
26
|
|
|
27
|
+
function fileCompletions(pathToken, cwd) {
|
|
28
|
+
try {
|
|
29
|
+
const raw = pathToken.startsWith('~/') ? join(homedir(), pathToken.slice(2)) : pathToken;
|
|
30
|
+
const abs = raw.startsWith('/') ? raw : join(cwd, raw);
|
|
31
|
+
const isDir = abs.endsWith('/');
|
|
32
|
+
const dir = isDir ? abs : dirname(abs);
|
|
33
|
+
const prefix = isDir ? '' : basename(abs).toLowerCase();
|
|
34
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
35
|
+
return entries
|
|
36
|
+
.filter((e) => !prefix || e.name.toLowerCase().startsWith(prefix))
|
|
37
|
+
.slice(0, 8)
|
|
38
|
+
.map((e) => {
|
|
39
|
+
const rel = join(raw.startsWith('/') ? dir : raw.slice(0, raw.lastIndexOf('/') + 1), e.name);
|
|
40
|
+
return { tag: '@' + rel, desc: e.isDirectory() ? 'directory' : 'file' };
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
function insertAt(text, cursor, chunk) {
|
|
27
48
|
return text.slice(0, cursor) + chunk + text.slice(cursor);
|
|
28
49
|
}
|
|
@@ -84,6 +105,7 @@ export function Composer({
|
|
|
84
105
|
const redoRef = useRef([]);
|
|
85
106
|
const lastMut = useRef('');
|
|
86
107
|
const pasteRef = useRef({ active: false, buf: '' });
|
|
108
|
+
const ctrlXRef = useRef(false);
|
|
87
109
|
|
|
88
110
|
const snapshot = (kind) => {
|
|
89
111
|
if (kind === 'type' && lastMut.current === 'type') return;
|
|
@@ -123,8 +145,12 @@ export function Composer({
|
|
|
123
145
|
const token = upto.slice(tokStart);
|
|
124
146
|
const slashMode = value.startsWith('/') && tokStart === 0;
|
|
125
147
|
const mentionMode = token.startsWith('@');
|
|
148
|
+
// file path mention: @/ @./ @~/ or @word.ext style
|
|
149
|
+
const fileMode = mentionMode && /^@(\/|\.\/|~\/|\.\.\/)/.test(token);
|
|
126
150
|
const matches = slashMode
|
|
127
151
|
? SLASH_COMMANDS.filter((s) => s.cmd.startsWith(token))
|
|
152
|
+
: fileMode
|
|
153
|
+
? fileCompletions(token.slice(1), process.cwd())
|
|
128
154
|
: mentionMode
|
|
129
155
|
? MENTIONS.filter((m) => m.tag.startsWith(token.toLowerCase()))
|
|
130
156
|
: [];
|
|
@@ -242,6 +268,34 @@ export function Composer({
|
|
|
242
268
|
onNotice?.('Loaded text from external editor.', C.success);
|
|
243
269
|
return;
|
|
244
270
|
}
|
|
271
|
+
// Ctrl+X two-key sequences (Ctrl+X /, Ctrl+X e, Ctrl+X b, Ctrl+X o)
|
|
272
|
+
if (key.ctrl && input === 'x') {
|
|
273
|
+
ctrlXRef.current = true;
|
|
274
|
+
onNotice?.('Ctrl+X — press /, e, b, or o', C.muted);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (ctrlXRef.current) {
|
|
278
|
+
ctrlXRef.current = false;
|
|
279
|
+
if (input === '/') {
|
|
280
|
+
// Open slash picker without clearing prompt
|
|
281
|
+
if (!value.startsWith('/')) {
|
|
282
|
+
const saved = value;
|
|
283
|
+
setText('/', 1);
|
|
284
|
+
onNotice?.(`Saved prompt. Type a command and press Esc to restore: ${saved.slice(0, 40)}`, C.muted);
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (input === 'e') {
|
|
289
|
+
const edited = openExternalEditor(value);
|
|
290
|
+
if (!edited.ok) { onNotice?.(`External editor failed: ${edited.error}`, C.error); return; }
|
|
291
|
+
setText(edited.text);
|
|
292
|
+
onNotice?.('Loaded text from external editor.', C.success);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (input === 'b') { onNotice?.('Ctrl+X b — task promoted to background (stub).', C.muted); return; }
|
|
296
|
+
if (input === 'o') { onNotice?.('Ctrl+X o — open most recent link (stub).', C.muted); return; }
|
|
297
|
+
// unknown second key — fall through and treat as normal input
|
|
298
|
+
}
|
|
245
299
|
if ((key.ctrl && input === 'v') && !pasteRef.current.active) {
|
|
246
300
|
onNotice?.('Paste attachment: paste an image or text via your terminal\'s paste shortcut (Ctrl+Shift+V on Linux, Cmd+V on Mac).', C.muted);
|
|
247
301
|
return;
|
|
@@ -324,7 +378,9 @@ export function Composer({
|
|
|
324
378
|
return;
|
|
325
379
|
}
|
|
326
380
|
if (key.home || (key.ctrl && input === 'a')) { setCursor(0); return; }
|
|
327
|
-
if (key.end || (key.ctrl && input === 'e')) { setCursor(value.length); return; }
|
|
381
|
+
if (key.end || (key.ctrl && input === 'e' && value.indexOf('\n') === -1)) { setCursor(value.length); return; }
|
|
382
|
+
if (key.ctrl && key.home) { setCursor(0); return; }
|
|
383
|
+
if (key.ctrl && key.end) { setCursor(value.length); return; }
|
|
328
384
|
if (key.meta && key.leftArrow) { setCursor(wordLeft(value, cursor)); return; }
|
|
329
385
|
if (key.meta && key.rightArrow) { setCursor(wordRight(value, cursor)); return; }
|
|
330
386
|
|
|
@@ -374,10 +430,29 @@ export function Composer({
|
|
|
374
430
|
? '[shell] '
|
|
375
431
|
: '';
|
|
376
432
|
|
|
433
|
+
const paletteRows = (() => {
|
|
434
|
+
if (!menuOpen) return null;
|
|
435
|
+
const MAX = 8;
|
|
436
|
+
let start = 0;
|
|
437
|
+
if (matches.length > MAX) start = Math.min(Math.max(0, sel - Math.floor(MAX / 2)), matches.length - MAX);
|
|
438
|
+
const window = matches.slice(start, start + MAX);
|
|
439
|
+
return window.map((s, wi) => {
|
|
440
|
+
const i = start + wi;
|
|
441
|
+
const on = i === sel;
|
|
442
|
+
const kk = slashMode ? (s.cmd + (s.hint ? ' ' + s.hint : '')) : s.tag;
|
|
443
|
+
const label = kk.padEnd(22);
|
|
444
|
+
return html`<${Box} key=${'m' + i}>
|
|
445
|
+
<${Text} color=${on ? C.success : C.muted}>${on ? glyphs.prompt + ' ' : ' '}<//>
|
|
446
|
+
<${Text} color=${on ? C.success : slashMode ? C.slash : C.accent} bold=${on}>${label}<//>
|
|
447
|
+
<${Text} color=${C.muted}>${s.desc}<//>
|
|
448
|
+
<//>`;
|
|
449
|
+
});
|
|
450
|
+
})();
|
|
451
|
+
|
|
377
452
|
return html`
|
|
378
453
|
<${Box} flexDirection="column" width=${width}>
|
|
379
454
|
${histSearch
|
|
380
|
-
? html`<${Box} paddingX=${1}
|
|
455
|
+
? html`<${Box} paddingX=${1}>
|
|
381
456
|
<${Text} color=${C.accent}>reverse-search<//>
|
|
382
457
|
<${Text} color=${C.muted}>: ${histSearchQ || 'type to filter history'}<//>
|
|
383
458
|
${historyMatches[histSearchSel]
|
|
@@ -386,29 +461,6 @@ export function Composer({
|
|
|
386
461
|
<//>`
|
|
387
462
|
: null}
|
|
388
463
|
|
|
389
|
-
${menuOpen
|
|
390
|
-
? html`<${Box} flexDirection="column" paddingX=${1} marginBottom=${0}>
|
|
391
|
-
${slashMode ? null : html`<${Box}><${Text} color=${C.accent} bold>@ mention <//><${Text} color=${C.muted}>a device, customer or entity<//><//>`}
|
|
392
|
-
${(() => {
|
|
393
|
-
const MAX = 8;
|
|
394
|
-
let start = 0;
|
|
395
|
-
if (matches.length > MAX) start = Math.min(Math.max(0, sel - Math.floor(MAX / 2)), matches.length - MAX);
|
|
396
|
-
const window = matches.slice(start, start + MAX);
|
|
397
|
-
return window.map((s, wi) => {
|
|
398
|
-
const i = start + wi;
|
|
399
|
-
const on = i === sel;
|
|
400
|
-
const key = slashMode ? (s.cmd + (s.hint ? ' ' + s.hint : '')) : s.tag;
|
|
401
|
-
const label = key.padEnd(21);
|
|
402
|
-
return html`<${Box} key=${'m' + i}>
|
|
403
|
-
<${Text} color=${on ? C.success : C.muted}>${on ? glyphs.prompt + ' ' : ' '}<//>
|
|
404
|
-
<${Text} color=${on ? C.success : slashMode ? C.slash : C.accent} bold=${on}>${label}<//>
|
|
405
|
-
<${Text} color=${C.muted}>${s.desc}<//>
|
|
406
|
-
<//>`;
|
|
407
|
-
});
|
|
408
|
-
})()}
|
|
409
|
-
<//>`
|
|
410
|
-
: null}
|
|
411
|
-
|
|
412
464
|
<${Text} color=${C.accent}>${'_'.repeat(Math.max(1, width))}<//>
|
|
413
465
|
|
|
414
466
|
<${Box} width=${width} paddingX=${1}>
|
|
@@ -424,6 +476,17 @@ export function Composer({
|
|
|
424
476
|
<//>`}
|
|
425
477
|
<//>
|
|
426
478
|
|
|
479
|
+
${menuOpen
|
|
480
|
+
? html`<${Box} flexDirection="column" paddingX=${1}>
|
|
481
|
+
${slashMode
|
|
482
|
+
? null
|
|
483
|
+
: fileMode
|
|
484
|
+
? html`<${Box}><${Text} color=${C.brand} bold>@ file<//> <${Text} color=${C.muted}>fuzzy path match<//><//>`
|
|
485
|
+
: html`<${Box}><${Text} color=${C.brand} bold>@ mention<//> <${Text} color=${C.muted}>device · customer · entity<//><//>` }
|
|
486
|
+
${paletteRows}
|
|
487
|
+
<//>`
|
|
488
|
+
: null}
|
|
489
|
+
|
|
427
490
|
<${Text} color=${C.separator}>${'─'.repeat(Math.max(1, width))}<//>
|
|
428
491
|
<//>`;
|
|
429
492
|
}
|
package/src/tui/run.js
CHANGED
|
@@ -27,7 +27,7 @@ async function animateSplash() {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export async function runTui(cfg, opts = {}) {
|
|
30
|
-
const { display = {}, onExternal, initialQuestion, banner = false } = opts;
|
|
30
|
+
const { display = {}, onExternal, initialQuestion, banner = false, agentName, resumeMode } = opts;
|
|
31
31
|
let externalAction;
|
|
32
32
|
const seen = Boolean(cfg?.tui?.bannerSeen);
|
|
33
33
|
const showAnimation = banner || !seen;
|
|
@@ -44,6 +44,8 @@ export async function runTui(cfg, opts = {}) {
|
|
|
44
44
|
display=${display}
|
|
45
45
|
initialQuestion=${initialQuestion}
|
|
46
46
|
showBanner=${!showAnimation}
|
|
47
|
+
agentName=${agentName}
|
|
48
|
+
resumeMode=${resumeMode}
|
|
47
49
|
onExternal=${(a) => { externalAction = a; onExternal?.(a); }}
|
|
48
50
|
/>`,
|
|
49
51
|
{ exitOnCtrlC: false },
|