Alasvetovalikko kpn-terminaalin autocompletioniin

TAB avaa dropdown-valikon käytettävissä olevista vaihtoehdoista:
- Nuolilla (ylös/alas) navigointi
- Enter tai TAB valitsee korostetun vaihtoehdon
- Esc sulkee valikon
- Klikkaus valitsee suoraan
- Yksi vaihtoehto → täydennetään suoraan ilman valikkoa

Valikko näyttää kontekstin mukaan: alikomennot, mallit/agentit
tai esimerkkiprompteja. Sulkeutuu automaattisesti kun klikataan muualle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaakko Vanhala
2026-04-05 09:53:47 +03:00
parent aa6c4739dd
commit 3d1b406e8d

View File

@@ -1105,10 +1105,11 @@
</div> </div>
<div class="terminal-panel" id="agent-terminal" style="margin-top:0;border-top:none;border-radius:0"> <div class="terminal-panel" id="agent-terminal" style="margin-top:0;border-top:none;border-radius:0">
</div> </div>
<div style="display:flex;align-items:center;background:#010409;border:1px solid var(--border-color);border-top:none;border-radius:0 0 6px 6px;padding:8px 12px;font-family:'Courier New',monospace;font-size:14px"> <div style="position:relative;display:flex;align-items:center;background:#010409;border:1px solid var(--border-color);border-top:none;border-radius:0 0 6px 6px;padding:8px 12px;font-family:'Courier New',monospace;font-size:14px">
<span style="color:#d29922;margin-right:8px;flex-shrink:0">$</span> <span style="color:#d29922;margin-right:8px;flex-shrink:0">$</span>
<input id="term-input" type="text" placeholder="kpn run coder &quot;kirjoita hello world&quot;" spellcheck="false" <input id="term-input" type="text" placeholder="kpn run coder &quot;kirjoita hello world&quot;" spellcheck="false" autocomplete="off"
style="flex:1;background:transparent;border:none;outline:none;color:var(--success-color);font-family:inherit;font-size:inherit"> style="flex:1;background:transparent;border:none;outline:none;color:var(--success-color);font-family:inherit;font-size:inherit">
<div id="term-dropdown" style="display:none;position:absolute;bottom:100%;left:30px;background:#161b22;border:1px solid #30363d;border-radius:6px;max-height:200px;overflow-y:auto;font-size:13px;min-width:200px;z-index:100;box-shadow:0 4px 12px rgba(0,0,0,0.4)"></div>
</div> </div>
</div> </div>
</div><!-- /panel-agents --> </div><!-- /panel-agents -->
@@ -1982,13 +1983,105 @@
return false; return false;
} }
// Dropdown-autocompletionin tila
const dropdown = document.getElementById('term-dropdown');
let dropdownItems = [];
let dropdownIdx = -1;
let dropdownPrefix = ''; // Inputin alku joka säilyy valinnan yhteydessä
function getCandidates(val) {
const words = val.trimEnd().split(/\s+/);
for (let depth = words.length; depth >= 1; depth--) {
const prefix = words.slice(0, depth).join(' ');
const partial = words[depth] || '';
// Esimerkkipromptit
if (kpnExamples[prefix] && !partial) {
return { items: kpnExamples[prefix], prefix: prefix + ' ' };
}
// Komennot
const candidates = kpnCommands[prefix];
if (candidates) {
const matches = partial ? candidates.filter(c => c.startsWith(partial)) : candidates;
if (matches.length > 0) {
return { items: matches, prefix: prefix + ' ' };
}
}
}
if (!val.trim()) return { items: kpnCommands['kpn'] || [], prefix: 'kpn ' };
return { items: [], prefix: val };
}
function showDropdown(items, prefix) {
if (!dropdown || items.length === 0) { hideDropdown(); return; }
dropdownItems = items;
dropdownPrefix = prefix;
dropdownIdx = -1;
dropdown.innerHTML = items.map((item, i) =>
`<div class="term-dd-item" data-idx="${i}" style="padding:6px 12px;cursor:pointer;color:#c9d1d9;white-space:nowrap;border-bottom:1px solid #21262d">${esc(item)}</div>`
).join('');
dropdown.style.display = 'block';
// Klikkaus-handlerit
dropdown.querySelectorAll('.term-dd-item').forEach(el => {
el.addEventListener('mouseenter', () => highlightDropdown(parseInt(el.dataset.idx)));
el.addEventListener('click', () => { selectDropdown(); termInput.focus(); });
});
}
function hideDropdown() {
if (dropdown) { dropdown.style.display = 'none'; dropdown.innerHTML = ''; }
dropdownItems = [];
dropdownIdx = -1;
}
function highlightDropdown(idx) {
dropdownIdx = idx;
dropdown.querySelectorAll('.term-dd-item').forEach((el, i) => {
el.style.background = i === idx ? '#30363d' : 'transparent';
el.style.color = i === idx ? '#58a6ff' : '#c9d1d9';
});
// Varmistetaan näkyvyys
const active = dropdown.children[idx];
if (active) active.scrollIntoView({ block: 'nearest' });
}
function selectDropdown() {
if (dropdownIdx >= 0 && dropdownIdx < dropdownItems.length) {
termInput.value = dropdownPrefix + dropdownItems[dropdownIdx] + (dropdownItems[dropdownIdx].startsWith('"') ? '' : ' ');
}
hideDropdown();
}
termInput?.addEventListener('keydown', (e) => { termInput?.addEventListener('keydown', (e) => {
if (e.key === 'Tab' && e.shiftKey) { // Dropdown auki: nuolet navigoi, Enter/Tab valitsee, Esc sulkee
// Shift-TAB: poista viimeinen sana (tai lainausmerkkien sisältö) if (dropdown && dropdown.style.display === 'block') {
if (e.key === 'ArrowDown') {
e.preventDefault(); e.preventDefault();
highlightDropdown(Math.min(dropdownIdx + 1, dropdownItems.length - 1));
return;
}
if (e.key === 'ArrowUp') {
e.preventDefault();
highlightDropdown(Math.max(dropdownIdx - 1, 0));
return;
}
if ((e.key === 'Enter' || e.key === 'Tab') && dropdownIdx >= 0) {
e.preventDefault();
selectDropdown();
return;
}
if (e.key === 'Escape') {
e.preventDefault();
hideDropdown();
return;
}
}
if (e.key === 'Tab' && e.shiftKey) {
e.preventDefault();
hideDropdown();
const val = termInput.value.trimEnd(); const val = termInput.value.trimEnd();
if (!val) return; if (!val) return;
// Jos päättyy lainausmerkkeihin, poista koko lainattu osa
const quoteMatch = val.match(/^(.+\s)".*"?$|^(.+\s)'.*'?$/); const quoteMatch = val.match(/^(.+\s)".*"?$|^(.+\s)'.*'?$/);
if (quoteMatch) { if (quoteMatch) {
termInput.value = (quoteMatch[1] || quoteMatch[2]).trimEnd() + ' '; termInput.value = (quoteMatch[1] || quoteMatch[2]).trimEnd() + ' ';
@@ -1998,18 +2091,26 @@
} }
} else if (e.key === 'Tab') { } else if (e.key === 'Tab') {
e.preventDefault(); e.preventDefault();
tabComplete(termInput); // Näytä dropdown tai täydennä jos vain yksi vaihtoehto
const { items, prefix } = getCandidates(termInput.value);
if (items.length === 1) {
termInput.value = prefix + items[0] + (items[0].startsWith('"') ? '' : ' ');
hideDropdown();
} else if (items.length > 1) {
showDropdown(items, prefix);
}
} else if (e.key === 'Enter') { } else if (e.key === 'Enter') {
hideDropdown();
const cmd = termInput.value.trim(); const cmd = termInput.value.trim();
if (cmd) termExec(cmd); if (cmd) termExec(cmd);
termInput.value = ''; termInput.value = '';
} else if (e.key === 'ArrowUp') { } else if (e.key === 'ArrowUp' && !dropdown?.style.display?.includes('block')) {
e.preventDefault(); e.preventDefault();
if (termHistIdx < termHistory.length - 1) { if (termHistIdx < termHistory.length - 1) {
termHistIdx++; termHistIdx++;
termInput.value = termHistory[termHistIdx]; termInput.value = termHistory[termHistIdx];
} }
} else if (e.key === 'ArrowDown') { } else if (e.key === 'ArrowDown' && !dropdown?.style.display?.includes('block')) {
e.preventDefault(); e.preventDefault();
if (termHistIdx > 0) { if (termHistIdx > 0) {
termHistIdx--; termHistIdx--;
@@ -2021,6 +2122,11 @@
} }
}); });
// Suljetaan dropdown kun klikataan muualle
document.addEventListener('click', (e) => {
if (!termInput?.contains(e.target) && !dropdown?.contains(e.target)) hideDropdown();
});
// Klikkaa terminaalipaneelia → fokusoi input // Klikkaa terminaalipaneelia → fokusoi input
termPanel?.addEventListener('click', () => termInput?.focus()); termPanel?.addEventListener('click', () => termInput?.focus());