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 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(24)}${RESET}${DIM}${d}${RESET}`));
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 action = await runTui(cfg, { display, banner: has('--banner') });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "8.5.4",
3
+ "version": "8.5.5",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
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: 'Show session stats / resume info', group: 'Session', local: true },
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: '/changelog', hint: '', desc: 'Show recent npm package releases', group: 'Session', local: true },
78
- { cmd: '/feedback', hint: '', desc: 'Show the feedback / bug-report URL', group: 'Session', local: true },
79
- { cmd: '/update', hint: '', desc: 'Update iCli to the latest version', group: 'Session', local: true },
80
- { cmd: '/logout', hint: '', desc: 'Log out and clear credentials', group: 'Session', local: true },
81
- { cmd: '/help', hint: '', desc: 'Show help', group: 'Session', local: true },
82
- { cmd: '/exit', hint: '', desc: 'Exit iCli', group: 'Session', local: true },
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(22)}${c.desc}`;
675
+ return head + ` ${(c.cmd + (c.hint ? ' ' + c.hint : '')).padEnd(30)}${c.desc}`;
657
676
  }).join('\n');
658
- pushNotice('Commands:' + lines + '\n\n Keys: Shift+Tab cycle modes · Ctrl+T toggle reasoning · Ctrl+L clear · Ctrl+N/P followup chips · Ctrl+D exit · Ctrl+S preserve input · Ctrl+G external editor');
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
- let question = q;
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">
@@ -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} marginBottom=${0}>
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 },