Skip to content

Commit 680ff17

Browse files
author
DavidQ
committed
DEV_CONSOLE_AUTOCOMPLETE_TAB_PATCH
- Added Tab autocomplete - Supports cycling suggestions - Integrated with cursor-based input
1 parent baf86a8 commit 680ff17

3 files changed

Lines changed: 188 additions & 26 deletions

File tree

docs/dev/CODEX_COMMANDS.md

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,30 @@ MODEL: GPT-5.4-codex
22
REASONING: high
33

44
COMMAND:
5-
Patch dev console input system.
5+
Add TAB autocomplete to dev console.
66

77
Modify:
88
tools/dev/devConsoleIntegration.js
99

10-
Add:
11-
- cursor-based input editing (left/right arrows)
12-
- backspace/delete support
13-
- command history (up/down)
14-
- scroll mode when not typing
15-
- preventDefault for arrow keys
10+
Requirements:
11+
- Tab triggers autocomplete
12+
- Autocomplete uses command registry (if available)
13+
- If multiple matches → cycle suggestions
14+
- If one match → complete command
15+
- Do not break existing input/cursor logic
16+
- Prevent default tab behavior
1617

17-
Rules:
18-
- Do NOT modify engine core
19-
- Keep combo keys unchanged
20-
- Keep logic isolated to this file only
18+
Behavior:
19+
- Partial input "sce" → Tab → "scene."
20+
- "scene.r" → Tab → "scene.reload"
21+
- Multiple matches → cycle each Tab press
22+
23+
Constraints:
24+
- No engine core changes
25+
- Keep logic isolated to this file
26+
- Preserve combo keys
2127

2228
Validation:
23-
- arrows move cursor
24-
- backspace/delete works
25-
- history works
26-
- scroll works
27-
- no browser scrolling
29+
- Tab completes commands
30+
- No browser focus change
31+
- Cursor remains correct

docs/dev/COMMIT_COMMENT.txt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
DEV_CONSOLE_ARROW_KEYS_EDIT_AND_SCROLL_PATCH
1+
DEV_CONSOLE_AUTOCOMPLETE_TAB_PATCH
22

3-
- Added cursor editing
4-
- Added scroll mode
5-
- Improved keyboard handling
3+
- Added Tab autocomplete
4+
- Supports cycling suggestions
5+
- Integrated with cursor-based input

tools/dev/devConsoleIntegration.js

Lines changed: 164 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,11 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
229229
let consoleScrollOffset = 0;
230230
let consoleTypingMode = true;
231231
let consoleCommandHistory = [];
232+
let consoleAutocompleteState = {
233+
basePrefix: "",
234+
matches: [],
235+
tokenStart: 0
236+
};
232237
const keyboardEventTarget = getKeyboardEventTarget();
233238
const commandRegistry = createDevConsoleCommandRegistry({
234239
packs: [
@@ -249,6 +254,11 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
249254
consoleOutputHistory = [];
250255
consoleScrollOffset = 0;
251256
consoleTypingMode = true;
257+
consoleAutocompleteState = {
258+
basePrefix: "",
259+
matches: [],
260+
tokenStart: 0
261+
};
252262
}
253263

254264
function normalizeRuntimeDelegationResult(commandName, execution) {
@@ -308,6 +318,54 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
308318
source.forEach((line) => pushConsoleOutputLine(line));
309319
}
310320

321+
function resetConsoleAutocompleteState() {
322+
consoleAutocompleteState = {
323+
basePrefix: "",
324+
matches: [],
325+
tokenStart: 0
326+
};
327+
}
328+
329+
function getCommandRegistryNames() {
330+
const names = new Set(["help", "status"]);
331+
if (typeof commandRegistry?.listCommands === "function") {
332+
const commands = commandRegistry.listCommands();
333+
const source = Array.isArray(commands) ? commands : [];
334+
source.forEach((entry) => {
335+
const name = sanitizeText(typeof entry === "string" ? entry : entry?.name);
336+
if (name) {
337+
names.add(name);
338+
}
339+
});
340+
}
341+
return Array.from(names).sort((left, right) => left.localeCompare(right));
342+
}
343+
344+
function findLongestCommonPrefix(values) {
345+
const source = Array.isArray(values) ? values.filter(Boolean) : [];
346+
if (source.length === 0) {
347+
return "";
348+
}
349+
if (source.length === 1) {
350+
return source[0];
351+
}
352+
353+
let prefix = source[0];
354+
for (let index = 1; index < source.length; index += 1) {
355+
const candidate = source[index];
356+
let prefixIndex = 0;
357+
const maxLength = Math.min(prefix.length, candidate.length);
358+
while (prefixIndex < maxLength && prefix[prefixIndex] === candidate[prefixIndex]) {
359+
prefixIndex += 1;
360+
}
361+
prefix = prefix.slice(0, prefixIndex);
362+
if (!prefix) {
363+
break;
364+
}
365+
}
366+
return prefix;
367+
}
368+
311369
function captureCommandHistory(command) {
312370
if (!command) {
313371
return;
@@ -319,6 +377,7 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
319377
}
320378
consoleHistoryCursor = -1;
321379
consoleScrollOffset = 0;
380+
resetConsoleAutocompleteState();
322381
}
323382

324383
function clampConsoleCursor() {
@@ -336,6 +395,7 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
336395
} else {
337396
clampConsoleCursor();
338397
}
398+
resetConsoleAutocompleteState();
339399
}
340400

341401
function insertConsoleText(text) {
@@ -408,6 +468,99 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
408468
return `${before}|${after}`;
409469
}
410470

471+
function replaceConsoleRange(startIndex, endIndex, replacement) {
472+
const safeStart = Math.max(0, Math.min(startIndex, consoleInputBuffer.length));
473+
const safeEnd = Math.max(safeStart, Math.min(endIndex, consoleInputBuffer.length));
474+
const before = consoleInputBuffer.slice(0, safeStart);
475+
const after = consoleInputBuffer.slice(safeEnd);
476+
const next = `${before}${replacement}${after}`;
477+
setConsoleInputBuffer(next, false);
478+
consoleCursorIndex = safeStart + String(replacement ?? "").length;
479+
clampConsoleCursor();
480+
}
481+
482+
function getCommandTokenAtCursor() {
483+
const beforeCursor = consoleInputBuffer.slice(0, consoleCursorIndex);
484+
const afterCursor = consoleInputBuffer.slice(consoleCursorIndex);
485+
const start = beforeCursor.lastIndexOf(" ") + 1;
486+
const nextSpaceOffset = afterCursor.indexOf(" ");
487+
const end = nextSpaceOffset >= 0
488+
? consoleCursorIndex + nextSpaceOffset
489+
: consoleInputBuffer.length;
490+
const token = consoleInputBuffer.slice(start, end);
491+
return {
492+
start,
493+
end,
494+
token: sanitizeText(token)
495+
};
496+
}
497+
498+
function autocompleteConsoleInput() {
499+
const tokenInfo = getCommandTokenAtCursor();
500+
if (tokenInfo.start !== 0) {
501+
return false;
502+
}
503+
504+
const commandPrefix = tokenInfo.token.toLowerCase();
505+
const names = getCommandRegistryNames();
506+
if (!commandPrefix) {
507+
return false;
508+
}
509+
510+
const matched = names.filter((name) => name.toLowerCase().startsWith(commandPrefix));
511+
if (matched.length === 0) {
512+
return false;
513+
}
514+
515+
if (matched.length === 1) {
516+
replaceConsoleRange(tokenInfo.start, tokenInfo.end, matched[0]);
517+
resetConsoleAutocompleteState();
518+
return true;
519+
}
520+
521+
const currentAutocompleteMatches = Array.isArray(consoleAutocompleteState.matches)
522+
? consoleAutocompleteState.matches
523+
: [];
524+
const cycleMatches = currentAutocompleteMatches.length > 1
525+
&& consoleAutocompleteState.tokenStart === tokenInfo.start
526+
&& tokenInfo.token.toLowerCase().startsWith(sanitizeText(consoleAutocompleteState.basePrefix).toLowerCase())
527+
&& currentAutocompleteMatches.every((name) => matched.includes(name))
528+
&& matched.every((name) => currentAutocompleteMatches.includes(name));
529+
530+
if (cycleMatches) {
531+
const currentIndex = currentAutocompleteMatches.indexOf(tokenInfo.token);
532+
const nextIndex = currentIndex >= 0
533+
? (currentIndex + 1) % currentAutocompleteMatches.length
534+
: 0;
535+
replaceConsoleRange(tokenInfo.start, tokenInfo.end, currentAutocompleteMatches[nextIndex]);
536+
consoleAutocompleteState = {
537+
basePrefix: sanitizeText(consoleAutocompleteState.basePrefix) || tokenInfo.token,
538+
matches: currentAutocompleteMatches.slice(),
539+
tokenStart: tokenInfo.start
540+
};
541+
return true;
542+
}
543+
544+
const commonPrefix = findLongestCommonPrefix(matched);
545+
if (commonPrefix.length > tokenInfo.token.length) {
546+
replaceConsoleRange(tokenInfo.start, tokenInfo.end, commonPrefix);
547+
consoleAutocompleteState = {
548+
basePrefix: commonPrefix,
549+
matches: matched.slice(),
550+
tokenStart: tokenInfo.start
551+
};
552+
return true;
553+
}
554+
555+
replaceConsoleRange(tokenInfo.start, tokenInfo.end, matched[0]);
556+
consoleAutocompleteState = {
557+
basePrefix: tokenInfo.token,
558+
matches: matched.slice(),
559+
tokenStart: tokenInfo.start
560+
};
561+
return true;
562+
}
563+
411564
function appendExecutionToConsole(command, execution) {
412565
if (!command) {
413566
return;
@@ -496,6 +649,12 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
496649
return;
497650
}
498651

652+
if (code === "Tab") {
653+
autocompleteConsoleInput();
654+
event.preventDefault();
655+
return;
656+
}
657+
499658
if (code === "Enter") {
500659
consoleTypingMode = true;
501660
submitConsoleInput();
@@ -531,13 +690,15 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
531690
if (code === "ArrowLeft") {
532691
consoleTypingMode = true;
533692
consoleCursorIndex = Math.max(0, consoleCursorIndex - 1);
693+
resetConsoleAutocompleteState();
534694
event.preventDefault();
535695
return;
536696
}
537697

538698
if (code === "ArrowRight") {
539699
consoleTypingMode = true;
540700
consoleCursorIndex = Math.min(consoleInputBuffer.length, consoleCursorIndex + 1);
701+
resetConsoleAutocompleteState();
541702
event.preventDefault();
542703
return;
543704
}
@@ -551,6 +712,7 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
551712
} else {
552713
scrollConsoleHistory(1);
553714
}
715+
resetConsoleAutocompleteState();
554716
event.preventDefault();
555717
return;
556718
}
@@ -564,11 +726,7 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
564726
} else {
565727
scrollConsoleHistory(-1);
566728
}
567-
event.preventDefault();
568-
return;
569-
}
570-
571-
if (code === "Tab") {
729+
resetConsoleAutocompleteState();
572730
event.preventDefault();
573731
return;
574732
}
@@ -754,7 +912,7 @@ export function createSampleGameDevConsoleIntegration(options = {}) {
754912

755913
const commandHint = "Shift+` console | Ctrl+Shift+` overlay | Ctrl+Shift+R reload";
756914
const inputHint = consoleTypingMode
757-
? "Typing: Left/Right cursor | Up/Down history | Shift+Up/Down scroll | Backspace/Delete | Esc mode"
915+
? "Typing: Tab autocomplete | Left/Right cursor | Up/Down history | Shift+Up/Down scroll | Backspace/Delete | Esc mode"
758916
: "Scroll: Up/Down output | Esc mode";
759917
const statusHint = `mode: ${consoleTypingMode ? "typing" : "scroll"} | scroll: ${consoleScrollOffset} | last: ${lastCommandBinding || "none"} | status: ${sanitizeText(lastCommandExecution?.status) || "idle"} | history: ${consoleCommandHistory.length}`;
760918

0 commit comments

Comments
 (0)