ispbills-icli 8.5.6 → 8.5.7
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/package.json +1 -1
- package/src/tui/app.js +107 -16
- package/src/tui/components.js +78 -5
- package/src/tui/composer.js +30 -6
- package/src/tui/theme.js +1 -1
package/package.json
CHANGED
package/src/tui/app.js
CHANGED
|
@@ -30,13 +30,25 @@ function splitArgs(s) {
|
|
|
30
30
|
return out;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function shortenPath(p) {
|
|
33
|
+
function shortenPath(p, maxLen = Infinity) {
|
|
34
|
+
let result;
|
|
34
35
|
try {
|
|
35
36
|
const home = homedir();
|
|
36
37
|
const rel = relative(home, p);
|
|
37
|
-
|
|
38
|
-
} catch {
|
|
39
|
-
|
|
38
|
+
result = rel.startsWith('..') ? p : '~/' + rel;
|
|
39
|
+
} catch {
|
|
40
|
+
result = p;
|
|
41
|
+
}
|
|
42
|
+
if (maxLen === Infinity || result.length <= maxLen) return result;
|
|
43
|
+
// §8: progressive shortening ~/…/sub/path
|
|
44
|
+
const withoutHome = result.replace(/^~\//, '');
|
|
45
|
+
const segs = withoutHome.split('/');
|
|
46
|
+
while (segs.length > 1) {
|
|
47
|
+
segs.shift();
|
|
48
|
+
const candidate = '~/\u2026/' + segs.join('/');
|
|
49
|
+
if (candidate.length <= maxLen) return candidate;
|
|
50
|
+
}
|
|
51
|
+
return '~/\u2026/' + segs[segs.length - 1];
|
|
40
52
|
}
|
|
41
53
|
|
|
42
54
|
function gitBranch(cwd = process.cwd()) {
|
|
@@ -419,9 +431,9 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
419
431
|
focusText: false,
|
|
420
432
|
allowText: false,
|
|
421
433
|
options: [
|
|
422
|
-
{ label: '1. Yes', value: 'session' },
|
|
423
|
-
{ label: '2. Yes, remember', value: 'remember' },
|
|
424
|
-
{ label: '3. No, exit', value: 'exit', danger: true },
|
|
434
|
+
{ label: '1. Yes, proceed (this session)', value: 'session' },
|
|
435
|
+
{ label: '2. Yes, and remember this folder', value: 'remember' },
|
|
436
|
+
{ label: '3. No, exit (Esc)', value: 'exit', danger: true },
|
|
425
437
|
],
|
|
426
438
|
});
|
|
427
439
|
}, [cfg, persistCfg]);
|
|
@@ -646,14 +658,49 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
646
658
|
setLive(null);
|
|
647
659
|
if (type === 'plan') {
|
|
648
660
|
const destructive = (reply.steps || []).some((st) => /destruct|high/.test(st.risk || ''));
|
|
661
|
+
// §9: choiceActionRef handles autopilot hand-off
|
|
662
|
+
choiceActionRef.current = (value) => {
|
|
663
|
+
if (value === '__autopilot__') {
|
|
664
|
+
apRef.current = true;
|
|
665
|
+
setModeState('autopilot');
|
|
666
|
+
runTurn('apply fix');
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (value != null) runTurn(String(value));
|
|
670
|
+
};
|
|
649
671
|
setChoice({
|
|
650
672
|
title: 'Apply this fix?',
|
|
651
673
|
note: reply.summary || '',
|
|
652
674
|
sel: 0, text: '', focusText: false, allowText: false,
|
|
653
675
|
options: [
|
|
654
|
-
{ label: destructive ? 'Apply fix (destructive)' : 'Apply fix', value: 'apply fix', danger: destructive },
|
|
655
|
-
{ label: '
|
|
656
|
-
{ label: '
|
|
676
|
+
{ label: destructive ? '1. Apply fix (destructive ⚠)' : '1. Apply fix', value: 'apply fix', danger: destructive },
|
|
677
|
+
{ label: '2. Accept plan and build on autopilot', value: '__autopilot__' },
|
|
678
|
+
{ label: '3. Explain the plan first', value: 'explain this plan in more detail before applying' },
|
|
679
|
+
{ label: '4. Cancel', value: null },
|
|
680
|
+
],
|
|
681
|
+
});
|
|
682
|
+
} else if (type === 'tool_approval' || (reply.toolCall && (type === 'prompt' || type === 'choice'))) {
|
|
683
|
+
// §13: exact tool-approval format
|
|
684
|
+
const toolName = reply.toolCall?.name || 'shell';
|
|
685
|
+
const toolArgs = reply.toolCall?.args || reply.toolCall?.command || '';
|
|
686
|
+
const toolDesc = toolArgs ? `${toolName}: ${toolArgs}` : (reply.note || toolName);
|
|
687
|
+
choiceActionRef.current = (value) => {
|
|
688
|
+
if (value === '__session_allow__') {
|
|
689
|
+
cfg.allowedTools = [...(cfg.allowedTools || []), toolName];
|
|
690
|
+
runTurn('yes, proceed with this tool call');
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (value != null) runTurn(String(value));
|
|
694
|
+
};
|
|
695
|
+
setChoice({
|
|
696
|
+
title: '⚙ Tool request',
|
|
697
|
+
note: toolDesc,
|
|
698
|
+
sel: 0, text: '', focusText: false, allowText: false,
|
|
699
|
+
isToolApproval: true,
|
|
700
|
+
options: [
|
|
701
|
+
{ label: '1. Yes', value: 'yes, proceed with this tool call' },
|
|
702
|
+
{ label: '2. Yes, and approve shell for the rest of the session', value: '__session_allow__' },
|
|
703
|
+
{ label: '3. No, and tell Copilot what to do differently (Esc)', value: null },
|
|
657
704
|
],
|
|
658
705
|
});
|
|
659
706
|
} else if (type === 'prompt' || type === 'choice') {
|
|
@@ -763,8 +810,34 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
763
810
|
runTurn(prompt);
|
|
764
811
|
return;
|
|
765
812
|
}
|
|
766
|
-
if (q === '/theme') {
|
|
767
|
-
|
|
813
|
+
if (q === '/theme' || q.startsWith('/theme ')) {
|
|
814
|
+
const arg = q.slice('/theme'.length).trim();
|
|
815
|
+
const themes = ['default', 'dim', 'high-contrast', 'colorblind'];
|
|
816
|
+
if (arg && themes.includes(arg)) {
|
|
817
|
+
cfg.theme = arg;
|
|
818
|
+
persistCfg();
|
|
819
|
+
pushNotice(`${glyphs.check} Theme set to "${arg}". Restart iCli to fully apply.`, C.success);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const current = cfg.theme || 'default';
|
|
823
|
+
choiceActionRef.current = (value) => {
|
|
824
|
+
if (!value) return;
|
|
825
|
+
cfg.theme = value;
|
|
826
|
+
persistCfg();
|
|
827
|
+
pushNotice(`${glyphs.check} Theme set to "${value}". Restart iCli to fully apply.`, C.success);
|
|
828
|
+
};
|
|
829
|
+
setChoice({
|
|
830
|
+
title: 'Select colour theme',
|
|
831
|
+
note: `Current: ${current}`,
|
|
832
|
+
sel: Math.max(0, themes.indexOf(current)),
|
|
833
|
+
text: '', focusText: false, allowText: false,
|
|
834
|
+
options: [
|
|
835
|
+
{ label: 'default', value: 'default', hint: 'Auto-detected dark or light' },
|
|
836
|
+
{ label: 'dim', value: 'dim', hint: 'Reduced brightness, lower contrast' },
|
|
837
|
+
{ label: 'high-contrast', value: 'high-contrast', hint: 'Maximum contrast for readability' },
|
|
838
|
+
{ label: 'colorblind', value: 'colorblind', hint: 'Adjusted for colour-vision differences' },
|
|
839
|
+
],
|
|
840
|
+
});
|
|
768
841
|
return;
|
|
769
842
|
}
|
|
770
843
|
if (q === '/session') {
|
|
@@ -1383,7 +1456,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1383
1456
|
setChoice(null);
|
|
1384
1457
|
if (act) { act(value); return; }
|
|
1385
1458
|
if (value == null) {
|
|
1386
|
-
if (
|
|
1459
|
+
if (currentChoice?.isToolApproval || /⚙ Tool request|tool request|approval|shell:/i.test(`${currentChoice?.title || ''} ${currentChoice?.note || ''}`)) {
|
|
1387
1460
|
setChoice({
|
|
1388
1461
|
title: 'Tell Copilot what to do differently:',
|
|
1389
1462
|
note: '',
|
|
@@ -1616,6 +1689,20 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1616
1689
|
pushNotice('Composer primed with an ISP reference. Continue typing after #.', C.muted, true);
|
|
1617
1690
|
return;
|
|
1618
1691
|
}
|
|
1692
|
+
// §5: o = open in browser, / = open search for this tab
|
|
1693
|
+
if (activeTab !== 'session' && input === 'o') {
|
|
1694
|
+
const openCmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
1695
|
+
const base = (cfg.url || '').replace(/\/$/, '');
|
|
1696
|
+
const url = base ? `${base}/${activeTab}` : `https://ispbills.local/${activeTab}`;
|
|
1697
|
+
try { spawnSync(openCmd, [url], { detached: true, stdio: 'ignore', shell: false }); } catch {}
|
|
1698
|
+
pushNotice(`Opening ${url} in browser…`, C.muted, true);
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
if (activeTab !== 'session' && input === '/') {
|
|
1702
|
+
openSearch();
|
|
1703
|
+
pushNotice(`Search ${activeTab} tab…`, C.muted, true);
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1619
1706
|
if (key.tab && key.shift && !busy) {
|
|
1620
1707
|
const cur = MODES.indexOf(modeRef.current);
|
|
1621
1708
|
const next = MODES[(cur + 1) % MODES.length];
|
|
@@ -1693,17 +1780,21 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1693
1780
|
case 'notice': return html`<${Notice} key=${it.id} text=${it.text} color=${it.color} />`;
|
|
1694
1781
|
case 'info': return html`<${LabeledNotice} key=${it.id} label="ℹ Info" text=${it.text} color=${C.muted} bodyColor=${it.color || C.white} cols=${c} ts=${it.ts} />`;
|
|
1695
1782
|
case 'error': return html`<${LabeledNotice} key=${it.id} label="✖ Error" text=${it.text} color=${C.error} bodyColor=${it.color || C.white} cols=${c} ts=${it.ts} />`;
|
|
1783
|
+
case 'system': return html`<${LabeledNotice} key=${it.id} label="⚙ System" text=${it.text} color=${C.muted} bodyColor=${it.color || C.white} cols=${c} ts=${it.ts} />`;
|
|
1696
1784
|
case 'shell': return html`<${ShellOutput} key=${it.id} command=${it.command} output=${it.output} code=${it.code} cols=${c} ts=${it.ts} />`;
|
|
1697
1785
|
case 'help': return html`<${HelpOverlay} key=${it.id} cols=${c} ts=${it.ts} />`;
|
|
1698
1786
|
default: return null;
|
|
1699
1787
|
}
|
|
1700
1788
|
};
|
|
1701
1789
|
|
|
1702
|
-
const cwd = shortenPath(process.cwd());
|
|
1703
1790
|
const branch = branchRef.current;
|
|
1704
1791
|
const tokenCount = approxTokens(convo.current);
|
|
1705
1792
|
const tokenLabel = contextUsageLabel(tokenCount);
|
|
1706
1793
|
const modelLabel = streamerMode ? '' : shortModel(model || modelRef.current || 'server default');
|
|
1794
|
+
// §8: progressive path shortening — reserve space for right section
|
|
1795
|
+
const footerRightEstLen = (modelLabel.length || 0) + (tokenLabel.length || 0) + 8;
|
|
1796
|
+
const cwdMaxLen = Math.max(12, cols - footerRightEstLen - (branch ? branch.length + 6 : 0) - 4);
|
|
1797
|
+
const cwd = shortenPath(process.cwd(), cwdMaxLen);
|
|
1707
1798
|
const footerLeft = [cwd, branch ? `${glyphs.gitBranch}${branch}` : null].filter(Boolean).join(' ');
|
|
1708
1799
|
const footerRight = hint
|
|
1709
1800
|
? hint
|
|
@@ -1731,7 +1822,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1731
1822
|
<${StatusLines} lines=${live.status} busy=${busy} />
|
|
1732
1823
|
${live.reasoning ? html`<${Reasoning} text=${live.reasoning} show=${showReasoning} />` : null}
|
|
1733
1824
|
${live.reply?.steps ? html`<${Plan} reply=${live.reply} live=${true} />` : null}
|
|
1734
|
-
${live.text ? html`<${AssistantMessage} text=${live.text} cols=${cols} ts=${startedAt || Date.now()} />` : null}
|
|
1825
|
+
${live.text ? html`<${AssistantMessage} text=${live.text} cols=${cols} ts=${startedAt || Date.now()} streaming=${true} />` : null}
|
|
1735
1826
|
<${Thinking} label=${live.reply ? 'Planning' : live.text ? 'Responding' : 'Working'} startedAt=${startedAt} />
|
|
1736
1827
|
<//>`
|
|
1737
1828
|
: null}
|
|
@@ -1805,7 +1896,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1805
1896
|
}}
|
|
1806
1897
|
busy=${busy}
|
|
1807
1898
|
blocked=${Boolean(choice || search || sessionPicker || diffState || !trusted)}
|
|
1808
|
-
width=${cols}
|
|
1899
|
+
width=${Math.max(60, cols)}
|
|
1809
1900
|
mode=${composerMode}
|
|
1810
1901
|
activeTab=${activeTab}
|
|
1811
1902
|
presetText=${composerPreset.text}
|
package/src/tui/components.js
CHANGED
|
@@ -152,6 +152,8 @@ export function Banner({ cfg = {}, model, width = 80, compact = false, streamerM
|
|
|
152
152
|
const meta = [
|
|
153
153
|
['model', streamerMode ? 'hidden' : m], ['user', cfg.user?.name], ['role', cfg.user?.role],
|
|
154
154
|
].filter(([, v]) => v);
|
|
155
|
+
// §3: show /login instruction when unauthenticated
|
|
156
|
+
const isAuth = Boolean(cfg.user?.name || cfg.user?.email || cfg.apiKey || cfg._token);
|
|
155
157
|
|
|
156
158
|
if (compact) {
|
|
157
159
|
return html`
|
|
@@ -182,6 +184,20 @@ export function Banner({ cfg = {}, model, width = 80, compact = false, streamerM
|
|
|
182
184
|
<//>`)}
|
|
183
185
|
<//>
|
|
184
186
|
${cfg.url ? html`<${Text} color=${C.muted}>${cfg.url}<//>` : null}
|
|
187
|
+
|
|
188
|
+
<${Box} flexDirection="column" marginTop=${1} paddingX=${2} alignSelf="flex-start">
|
|
189
|
+
<${Text} color=${C.muted} bold>Quick start:<//>\n
|
|
190
|
+
<${Text} color=${C.muted}> <${Text} color=${C.accent}>@<//> mention files or devices<//>
|
|
191
|
+
<${Text} color=${C.muted}> <${Text} color=${C.accent}>/<//> slash commands<//>
|
|
192
|
+
<${Text} color=${C.muted}> <${Text} color=${C.accent}>?<//> quick help overlay<//>
|
|
193
|
+
<${Text} color=${C.muted}> <${Text} color=${C.accent}>!<//> run shell command<//>
|
|
194
|
+
<//>
|
|
195
|
+
${!isAuth
|
|
196
|
+
? html`<${Box} marginTop=${1}>
|
|
197
|
+
<${Text} color=${C.warning}>⚠ Not authenticated — run <//>
|
|
198
|
+
<${Text} color=${C.slash} bold>/login<//>
|
|
199
|
+
<${Text} color=${C.warning}> to connect.<//><//>`
|
|
200
|
+
: null}
|
|
185
201
|
<//>`;
|
|
186
202
|
}
|
|
187
203
|
|
|
@@ -211,13 +227,36 @@ export function UserMessage({ text, cols = 80, ts }) {
|
|
|
211
227
|
<//>`;
|
|
212
228
|
}
|
|
213
229
|
|
|
214
|
-
|
|
230
|
+
// §12: live streaming code-fence highlighter
|
|
231
|
+
function StreamingText({ text = '' }) {
|
|
232
|
+
const lines = String(text).split('\n');
|
|
233
|
+
let inFence = false;
|
|
234
|
+
let fenceLang = '';
|
|
235
|
+
const rendered = [];
|
|
236
|
+
for (let i = 0; i < lines.length; i++) {
|
|
237
|
+
const line = lines[i];
|
|
238
|
+
const fm = line.match(/^```(\w*)/);
|
|
239
|
+
if (fm) {
|
|
240
|
+
inFence = !inFence;
|
|
241
|
+
fenceLang = inFence ? (fm[1] || '') : '';
|
|
242
|
+
rendered.push(html`<${Text} key=${i} color=${C.muted}>${line}<//>`);
|
|
243
|
+
} else if (inFence) {
|
|
244
|
+
const isShell = /^(sh|bash|zsh|fish|console|shell)$/i.test(fenceLang);
|
|
245
|
+
rendered.push(html`<${Text} key=${i} color=${isShell ? C.success : C.brand}>${line}<//>`);
|
|
246
|
+
} else {
|
|
247
|
+
rendered.push(html`<${Text} key=${i}>${line}<//>`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return rendered;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function AssistantMessage({ text, cols = 80, ts, label = `${glyphs.copilotDot} iCopilot`, streaming = false }) {
|
|
215
254
|
return html`
|
|
216
255
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
217
256
|
<${Sep} cols=${cols} />
|
|
218
257
|
<${SpeakerLine} label=${label} color=${C.accent} cols=${cols} ts=${ts} />
|
|
219
|
-
<${Box} paddingX=${2}>
|
|
220
|
-
|
|
258
|
+
<${Box} paddingX=${2} flexDirection="column">
|
|
259
|
+
${streaming ? html`<${StreamingText} text=${text} />` : html`<${Markdown} content=${text} />`}
|
|
221
260
|
<//>
|
|
222
261
|
<//>`;
|
|
223
262
|
}
|
|
@@ -296,7 +335,7 @@ export function Reasoning({ text, show = true }) {
|
|
|
296
335
|
if (!show) {
|
|
297
336
|
return html`
|
|
298
337
|
<${Box} paddingX=${2} marginTop=${1}>
|
|
299
|
-
<${Text} color=${C.muted} italic>${glyphs.
|
|
338
|
+
<${Text} color=${C.muted} italic>${glyphs.copilotDot} Thinking (≈${tokenEst} tokens)… Ctrl+T to expand<//><//>`;
|
|
300
339
|
}
|
|
301
340
|
return html`
|
|
302
341
|
<${Box} flexDirection="column" marginTop=${1} borderStyle=${borderStyle()} borderColor=${C.separator}
|
|
@@ -468,7 +507,41 @@ export function Thinking({ label = 'Working', startedAt }) {
|
|
|
468
507
|
<//>`;
|
|
469
508
|
}
|
|
470
509
|
|
|
471
|
-
export function Choice({ title, note, options = [], sel = 0, allowText = false, text = '', focusText = false }) {
|
|
510
|
+
export function Choice({ title, note, options = [], sel = 0, allowText = false, text = '', focusText = false, isToolApproval = false }) {
|
|
511
|
+
// §13: tool approval uses exact spec format with Sep-style header
|
|
512
|
+
if (isToolApproval) {
|
|
513
|
+
return html`
|
|
514
|
+
<${Box} flexDirection="column" marginTop=${1}>
|
|
515
|
+
<${Sep} cols=${80} />
|
|
516
|
+
<${Box} paddingX=${2} marginTop=${1}>
|
|
517
|
+
<${Text} color=${C.accent} bold>⚙ Tool request<//>
|
|
518
|
+
<//>
|
|
519
|
+
${note ? html`<${Box} paddingX=${2} marginTop=${1}><${Text} color=${C.muted}>${note}<//><//>` : null}
|
|
520
|
+
<${Box} flexDirection="column" marginTop=${1} paddingX=${2}>
|
|
521
|
+
${options.map((o, i) => {
|
|
522
|
+
const on = !focusText && i === sel;
|
|
523
|
+
const label = o.label ?? o.value ?? String(o);
|
|
524
|
+
const tone = o.danger ? C.error : on ? C.success : C.white;
|
|
525
|
+
return html`
|
|
526
|
+
<${Box} key=${'o' + i}>
|
|
527
|
+
<${Text} color=${on ? C.success : C.muted}>${on ? glyphs.prompt + ' ' : ' '}<//>
|
|
528
|
+
<${Text} color=${tone} bold=${on}>${label}<//>
|
|
529
|
+
<//>`;
|
|
530
|
+
})}
|
|
531
|
+
${allowText
|
|
532
|
+
? html`<${Box} key="freetext" marginTop=${1}>
|
|
533
|
+
<${Text} color=${focusText ? C.accent : C.muted}>${focusText ? glyphs.prompt + ' ' : ' '}<//>
|
|
534
|
+
<${Text} color=${C.muted}>Tell Copilot what to do differently: <//>
|
|
535
|
+
<${Text} color=${C.white}>${text}<//>${focusText ? html`<${Text} inverse> <//>` : null}
|
|
536
|
+
<//>`
|
|
537
|
+
: null}
|
|
538
|
+
<//>
|
|
539
|
+
<${Box} marginTop=${1} paddingX=${2}>
|
|
540
|
+
<${Text} color=${C.muted}>${glyphs.up}${glyphs.down} choose ${midDot()}Enter confirm ${midDot()}Esc cancel<//>
|
|
541
|
+
<//>
|
|
542
|
+
<//>`;
|
|
543
|
+
}
|
|
544
|
+
|
|
472
545
|
return html`
|
|
473
546
|
<${Box} flexDirection="column" marginTop=${1} borderStyle=${borderStyle()} borderColor=${C.accent} paddingX=${1}>
|
|
474
547
|
${title ? html`<${Text} color=${C.accent} bold>${glyphs.diamond} ${title}<//>` : null}
|
package/src/tui/composer.js
CHANGED
|
@@ -44,6 +44,21 @@ function fileCompletions(pathToken, cwd) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// §14: fuzzy file search for @word (no path separator)
|
|
48
|
+
function fuzzyFileCompletions(token, cwd) {
|
|
49
|
+
const query = token.slice(1).toLowerCase();
|
|
50
|
+
if (!query) return [];
|
|
51
|
+
try {
|
|
52
|
+
const entries = readdirSync(cwd, { withFileTypes: true });
|
|
53
|
+
return entries
|
|
54
|
+
.filter((e) => e.name.toLowerCase().includes(query))
|
|
55
|
+
.slice(0, 8)
|
|
56
|
+
.map((e) => ({ tag: '@' + e.name, desc: e.isDirectory() ? 'directory' : 'file' }));
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
function insertAt(text, cursor, chunk) {
|
|
48
63
|
return text.slice(0, cursor) + chunk + text.slice(cursor);
|
|
49
64
|
}
|
|
@@ -159,14 +174,18 @@ export function Composer({
|
|
|
159
174
|
const token = upto.slice(tokStart);
|
|
160
175
|
const slashMode = value.startsWith('/') && tokStart === 0;
|
|
161
176
|
const mentionMode = token.startsWith('@');
|
|
162
|
-
// file path
|
|
163
|
-
const fileMode = mentionMode && /^@(\/|\.\/|~\/|\.\.\/)/.test(token);
|
|
177
|
+
// §14: file mode = explicit path prefix OR any path containing /
|
|
178
|
+
const fileMode = mentionMode && (/^@(\/|\.\/|~\/|\.\.\/)/.test(token) || (token.includes('/') && token.length > 1));
|
|
179
|
+
// Known entity mention: @device, @olt, @onu, @customer, etc.
|
|
180
|
+
const knownMention = mentionMode && !fileMode && MENTIONS.some((m) => m.tag.toLowerCase().startsWith(token.toLowerCase()));
|
|
164
181
|
const matches = slashMode
|
|
165
182
|
? SLASH_COMMANDS.filter((s) => s.cmd.startsWith(token))
|
|
166
183
|
: fileMode
|
|
167
184
|
? fileCompletions(token.slice(1), process.cwd())
|
|
168
|
-
:
|
|
169
|
-
? MENTIONS.filter((m) => m.tag.startsWith(token.toLowerCase()))
|
|
185
|
+
: knownMention
|
|
186
|
+
? MENTIONS.filter((m) => m.tag.toLowerCase().startsWith(token.toLowerCase()))
|
|
187
|
+
: mentionMode && token.length > 1
|
|
188
|
+
? fuzzyFileCompletions(token, process.cwd()) // §14: fuzzy file search
|
|
170
189
|
: [];
|
|
171
190
|
const menuOpen = matches.length > 0;
|
|
172
191
|
|
|
@@ -220,7 +239,10 @@ export function Composer({
|
|
|
220
239
|
setText(c, c.length);
|
|
221
240
|
return;
|
|
222
241
|
}
|
|
223
|
-
const
|
|
242
|
+
const match = matches[sel];
|
|
243
|
+
// §14: file paths get trailing space; entity mentions get ':'
|
|
244
|
+
const isFile = fileMode || match.desc === 'file' || match.desc === 'directory';
|
|
245
|
+
const ins = isFile ? match.tag + ' ' : match.tag + ':';
|
|
224
246
|
const v = value.slice(0, tokStart) + ins + value.slice(cursor);
|
|
225
247
|
setText(v, tokStart + ins.length);
|
|
226
248
|
};
|
|
@@ -501,7 +523,9 @@ export function Composer({
|
|
|
501
523
|
? null
|
|
502
524
|
: fileMode
|
|
503
525
|
? html`<${Box}><${Text} color=${C.brand} bold>@ file<//> <${Text} color=${C.muted}>fuzzy path match<//><//>`
|
|
504
|
-
:
|
|
526
|
+
: knownMention
|
|
527
|
+
? html`<${Box}><${Text} color=${C.brand} bold>@ mention<//> <${Text} color=${C.muted}>device · customer · entity<//><//>`
|
|
528
|
+
: html`<${Box}><${Text} color=${C.brand} bold>@ file<//> <${Text} color=${C.muted}>fuzzy match<//><//>` }
|
|
505
529
|
${paletteRows}
|
|
506
530
|
<//>`
|
|
507
531
|
: null}
|
package/src/tui/theme.js
CHANGED
|
@@ -47,7 +47,7 @@ const ASCII_EXTRA = {
|
|
|
47
47
|
collapse: '-', // collapse indicator
|
|
48
48
|
};
|
|
49
49
|
const UNICODE_EXTRA = {
|
|
50
|
-
copilotDot: '
|
|
50
|
+
copilotDot: '◈', // spec §6: ◈ Copilot speaker label (◈ in accent/purple)
|
|
51
51
|
busy: '⧖', // white hourglass for busy state
|
|
52
52
|
gitBranch: '\uE0A0 ', // Nerd Font powerline branch glyph
|
|
53
53
|
tabOpen: '[',
|