playwright-repl 0.2.0 → 0.2.1
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/CHANGELOG.md +18 -0
- package/examples/ghost-completion-demo.mjs +122 -0
- package/package.json +1 -1
- package/src/completion-data.mjs +50 -0
- package/src/index.mjs +16 -15
- package/src/repl.mjs +94 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.1 — Ghost Completion
|
|
4
|
+
|
|
5
|
+
**2026-02-17**
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **Ghost completion**: Fish-shell style inline suggestions — type a prefix and see dimmed suggestion text after the cursor
|
|
10
|
+
- **Tab** cycles through matches (e.g., `go` → goto, go-back, go-forward)
|
|
11
|
+
- **Right Arrow** accepts the current suggestion
|
|
12
|
+
- **Tab on empty line** cycles through all commands
|
|
13
|
+
- Aliases excluded from ghost suggestions (still work when typed)
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
|
|
17
|
+
- Removed readline's built-in Tab completer (replaced entirely by ghost completion)
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
3
21
|
## v0.2.0 — MCP Server
|
|
4
22
|
|
|
5
23
|
**2026-02-16**
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Ghost completion demo — standalone test for inline suggestions.
|
|
4
|
+
*
|
|
5
|
+
* Run: node examples/ghost-completion-demo.mjs
|
|
6
|
+
*
|
|
7
|
+
* Type a prefix (e.g. "go") and see dimmed suggestion text.
|
|
8
|
+
* Tab cycles through matches, Right Arrow accepts.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import readline from 'node:readline';
|
|
12
|
+
|
|
13
|
+
const COMMANDS = [
|
|
14
|
+
'click', 'check', 'close', 'console', 'cookie-get', 'cookie-list',
|
|
15
|
+
'dblclick', 'drag', 'eval', 'fill', 'goto', 'go-back', 'go-forward',
|
|
16
|
+
'hover', 'network', 'open', 'press', 'reload', 'screenshot', 'select',
|
|
17
|
+
'snapshot', 'type', 'uncheck', 'upload',
|
|
18
|
+
'.help', '.aliases', '.status', '.exit',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const rl = readline.createInterface({
|
|
22
|
+
input: process.stdin,
|
|
23
|
+
output: process.stdout,
|
|
24
|
+
prompt: '\x1b[36mpw>\x1b[0m ',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ─── Ghost completion via _ttyWrite ─────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
let ghost = '';
|
|
30
|
+
let matches = [];
|
|
31
|
+
let matchIdx = 0;
|
|
32
|
+
|
|
33
|
+
function getMatches(input) {
|
|
34
|
+
if (input.length > 0 && !input.includes(' ')) {
|
|
35
|
+
return COMMANDS.filter(cmd => cmd.startsWith(input) && cmd !== input);
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderGhost(suffix) {
|
|
41
|
+
ghost = suffix;
|
|
42
|
+
rl.output.write(`\x1b[2m${ghost}\x1b[0m\x1b[${ghost.length}D`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const origTtyWrite = rl._ttyWrite.bind(rl);
|
|
46
|
+
rl._ttyWrite = function (s, key) {
|
|
47
|
+
if (ghost && key) {
|
|
48
|
+
// Right-arrow-at-end accepts ghost suggestion
|
|
49
|
+
if (key.name === 'right' && rl.cursor === rl.line.length) {
|
|
50
|
+
const text = ghost;
|
|
51
|
+
rl.output.write('\x1b[K');
|
|
52
|
+
ghost = '';
|
|
53
|
+
matches = [];
|
|
54
|
+
rl._insertString(text);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Tab cycles through matches (or accepts if only one)
|
|
59
|
+
if (key.name === 'tab') {
|
|
60
|
+
if (matches.length > 1) {
|
|
61
|
+
rl.output.write('\x1b[K');
|
|
62
|
+
matchIdx = (matchIdx + 1) % matches.length;
|
|
63
|
+
const input = rl.line || '';
|
|
64
|
+
renderGhost(matches[matchIdx].slice(input.length));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Single match — accept it
|
|
68
|
+
const text = ghost;
|
|
69
|
+
rl.output.write('\x1b[K');
|
|
70
|
+
ghost = '';
|
|
71
|
+
matches = [];
|
|
72
|
+
rl._insertString(text);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Tab on empty input — show all commands as ghost suggestions
|
|
78
|
+
if (key && key.name === 'tab') {
|
|
79
|
+
if ((rl.line || '') === '') {
|
|
80
|
+
matches = COMMANDS;
|
|
81
|
+
matchIdx = 0;
|
|
82
|
+
renderGhost(matches[0]);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Clear existing ghost text before readline processes the key
|
|
88
|
+
if (ghost) {
|
|
89
|
+
rl.output.write('\x1b[K');
|
|
90
|
+
ghost = '';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Let readline handle the key normally
|
|
94
|
+
origTtyWrite(s, key);
|
|
95
|
+
|
|
96
|
+
// Render new ghost text if cursor is at end of line
|
|
97
|
+
const input = rl.line || '';
|
|
98
|
+
matches = getMatches(input);
|
|
99
|
+
matchIdx = 0;
|
|
100
|
+
if (matches.length > 0 && rl.cursor === rl.line.length) {
|
|
101
|
+
renderGhost(matches[0].slice(input.length));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ─── REPL loop ──────────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
console.log('Ghost completion demo — Tab cycles matches, Right Arrow accepts\n');
|
|
108
|
+
rl.prompt();
|
|
109
|
+
|
|
110
|
+
rl.on('line', (line) => {
|
|
111
|
+
if (line.trim() === '.exit') {
|
|
112
|
+
rl.close();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.log(` → ${line.trim() || '(empty)'}`);
|
|
116
|
+
rl.prompt();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
rl.on('close', () => {
|
|
120
|
+
console.log('\nBye!');
|
|
121
|
+
process.exit(0);
|
|
122
|
+
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completion data — builds the list of items for dropdown autocomplete.
|
|
3
|
+
*
|
|
4
|
+
* Sources: COMMANDS from resolve.mjs, ALIASES from parser.mjs,
|
|
5
|
+
* plus REPL meta-commands (.help, .exit, etc.).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { COMMANDS } from './resolve.mjs';
|
|
9
|
+
import { ALIASES } from './parser.mjs';
|
|
10
|
+
|
|
11
|
+
// ─── Meta-commands ───────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const META_COMMANDS = [
|
|
14
|
+
{ cmd: '.help', desc: 'Show available commands' },
|
|
15
|
+
{ cmd: '.aliases', desc: 'Show command aliases' },
|
|
16
|
+
{ cmd: '.status', desc: 'Show connection status' },
|
|
17
|
+
{ cmd: '.reconnect', desc: 'Reconnect to daemon' },
|
|
18
|
+
{ cmd: '.record', desc: 'Start recording commands' },
|
|
19
|
+
{ cmd: '.save', desc: 'Stop recording and save' },
|
|
20
|
+
{ cmd: '.pause', desc: 'Pause/resume recording' },
|
|
21
|
+
{ cmd: '.discard', desc: 'Discard current recording' },
|
|
22
|
+
{ cmd: '.replay', desc: 'Replay a recorded session' },
|
|
23
|
+
{ cmd: '.exit', desc: 'Exit REPL' },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// ─── Build completion items ──────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns a sorted array of `{ cmd, desc }` for all completable items:
|
|
30
|
+
* commands, aliases (with "→ target" description), and meta-commands.
|
|
31
|
+
*/
|
|
32
|
+
export function buildCompletionItems() {
|
|
33
|
+
const items = [];
|
|
34
|
+
|
|
35
|
+
// Primary commands
|
|
36
|
+
for (const [name, info] of Object.entries(COMMANDS)) {
|
|
37
|
+
items.push({ cmd: name, desc: info.desc });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Aliases — show "→ target" as description
|
|
41
|
+
for (const [alias, target] of Object.entries(ALIASES)) {
|
|
42
|
+
items.push({ cmd: alias, desc: `→ ${target}` });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Meta-commands
|
|
46
|
+
items.push(...META_COMMANDS);
|
|
47
|
+
|
|
48
|
+
items.sort((a, b) => a.cmd.localeCompare(b.cmd));
|
|
49
|
+
return items;
|
|
50
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* playwright-repl — public API
|
|
3
|
-
*
|
|
4
|
-
* Usage as CLI:
|
|
5
|
-
* npx playwright-repl [options]
|
|
6
|
-
*
|
|
7
|
-
* Usage as library:
|
|
8
|
-
* import { DaemonConnection, parseInput, SessionRecorder } from 'playwright-repl';
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export { DaemonConnection } from './connection.mjs';
|
|
12
|
-
export { parseInput, ALIASES, ALL_COMMANDS } from './parser.mjs';
|
|
13
|
-
export { SessionRecorder, SessionPlayer } from './recorder.mjs';
|
|
14
|
-
export { socketPath, isDaemonRunning, startDaemon, findWorkspaceDir } from './workspace.mjs';
|
|
15
|
-
export { startRepl } from './repl.mjs';
|
|
1
|
+
/**
|
|
2
|
+
* playwright-repl — public API
|
|
3
|
+
*
|
|
4
|
+
* Usage as CLI:
|
|
5
|
+
* npx playwright-repl [options]
|
|
6
|
+
*
|
|
7
|
+
* Usage as library:
|
|
8
|
+
* import { DaemonConnection, parseInput, SessionRecorder } from 'playwright-repl';
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { DaemonConnection } from './connection.mjs';
|
|
12
|
+
export { parseInput, ALIASES, ALL_COMMANDS } from './parser.mjs';
|
|
13
|
+
export { SessionRecorder, SessionPlayer } from './recorder.mjs';
|
|
14
|
+
export { socketPath, isDaemonRunning, startDaemon, findWorkspaceDir } from './workspace.mjs';
|
|
15
|
+
export { startRepl } from './repl.mjs';
|
|
16
|
+
export { buildCompletionItems } from './completion-data.mjs';
|
package/src/repl.mjs
CHANGED
|
@@ -9,11 +9,12 @@ import path from 'node:path';
|
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import { execSync } from 'node:child_process';
|
|
11
11
|
|
|
12
|
-
import { replVersion
|
|
12
|
+
import { replVersion } from './resolve.mjs';
|
|
13
13
|
import { DaemonConnection } from './connection.mjs';
|
|
14
14
|
import { socketPath, daemonProfilesDir, isDaemonRunning, startDaemon } from './workspace.mjs';
|
|
15
15
|
import { parseInput, ALIASES, ALL_COMMANDS } from './parser.mjs';
|
|
16
16
|
import { SessionManager } from './recorder.mjs';
|
|
17
|
+
import { buildCompletionItems } from './completion-data.mjs';
|
|
17
18
|
import { c } from './colors.mjs';
|
|
18
19
|
|
|
19
20
|
// ─── Verify commands → run-code translation ─────────────────────────────────
|
|
@@ -518,26 +519,98 @@ export function promptStr(ctx) {
|
|
|
518
519
|
return `${prefix}${c.cyan}pw>${c.reset} `;
|
|
519
520
|
}
|
|
520
521
|
|
|
521
|
-
// ───
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
522
|
+
// ─── Ghost completion (inline suggestion) ───────────────────────────────────
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Attaches ghost-text completion to a readline interface.
|
|
526
|
+
* Shows dimmed inline suggestion after the cursor; Tab or Right Arrow accepts it.
|
|
527
|
+
*
|
|
528
|
+
* Uses _ttyWrite wrapper instead of _writeToOutput because Node 22+ optimizes
|
|
529
|
+
* single-character appends and doesn't always trigger a full line refresh.
|
|
530
|
+
*
|
|
531
|
+
* @param {readline.Interface} rl
|
|
532
|
+
* @param {Array<{cmd: string, desc: string}>} items - from buildCompletionItems()
|
|
533
|
+
*/
|
|
534
|
+
function attachGhostCompletion(rl, items) {
|
|
535
|
+
if (!process.stdin.isTTY) return; // no ghost text for piped input
|
|
536
|
+
|
|
537
|
+
const cmds = items.filter(i => !i.desc.startsWith('→')).map(i => i.cmd);
|
|
538
|
+
let ghost = '';
|
|
539
|
+
let matches = []; // all matching commands for current input
|
|
540
|
+
let matchIdx = 0; // which match is currently shown
|
|
541
|
+
|
|
542
|
+
function getMatches(input) {
|
|
543
|
+
if (input.length > 0 && !input.includes(' ')) {
|
|
544
|
+
return cmds.filter(cmd => cmd.startsWith(input) && cmd !== input);
|
|
545
|
+
}
|
|
546
|
+
return [];
|
|
532
547
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const hits = helpText.filter(o => o.startsWith(lastPart));
|
|
538
|
-
return [hits, lastPart];
|
|
548
|
+
|
|
549
|
+
function renderGhost(suffix) {
|
|
550
|
+
ghost = suffix;
|
|
551
|
+
rl.output.write(`\x1b[2m${ghost}\x1b[0m\x1b[${ghost.length}D`);
|
|
539
552
|
}
|
|
540
|
-
|
|
553
|
+
|
|
554
|
+
const origTtyWrite = rl._ttyWrite.bind(rl);
|
|
555
|
+
rl._ttyWrite = function (s, key) {
|
|
556
|
+
if (ghost && key) {
|
|
557
|
+
// Right-arrow-at-end accepts ghost suggestion
|
|
558
|
+
if (key.name === 'right' && rl.cursor === rl.line.length) {
|
|
559
|
+
const text = ghost;
|
|
560
|
+
rl.output.write('\x1b[K');
|
|
561
|
+
ghost = '';
|
|
562
|
+
matches = [];
|
|
563
|
+
rl._insertString(text);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Tab cycles through matches
|
|
568
|
+
if (key.name === 'tab' && matches.length > 1) {
|
|
569
|
+
rl.output.write('\x1b[K');
|
|
570
|
+
matchIdx = (matchIdx + 1) % matches.length;
|
|
571
|
+
const input = rl.line || '';
|
|
572
|
+
renderGhost(matches[matchIdx].slice(input.length));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Tab with single match accepts it
|
|
577
|
+
if (key.name === 'tab' && matches.length === 1) {
|
|
578
|
+
const text = ghost;
|
|
579
|
+
rl.output.write('\x1b[K');
|
|
580
|
+
ghost = '';
|
|
581
|
+
matches = [];
|
|
582
|
+
rl._insertString(text);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Tab on empty input — show all commands as ghost suggestions
|
|
588
|
+
if (key && key.name === 'tab') {
|
|
589
|
+
if ((rl.line || '') === '') {
|
|
590
|
+
matches = cmds;
|
|
591
|
+
matchIdx = 0;
|
|
592
|
+
renderGhost(matches[0]);
|
|
593
|
+
}
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Clear existing ghost text before readline processes the key
|
|
598
|
+
if (ghost) {
|
|
599
|
+
rl.output.write('\x1b[K');
|
|
600
|
+
ghost = '';
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Let readline handle the key normally
|
|
604
|
+
origTtyWrite(s, key);
|
|
605
|
+
|
|
606
|
+
// Render new ghost text if cursor is at end of line
|
|
607
|
+
const input = rl.line || '';
|
|
608
|
+
matches = getMatches(input);
|
|
609
|
+
matchIdx = 0;
|
|
610
|
+
if (matches.length > 0 && rl.cursor === rl.line.length) {
|
|
611
|
+
renderGhost(matches[0].slice(input.length));
|
|
612
|
+
}
|
|
613
|
+
};
|
|
541
614
|
}
|
|
542
615
|
|
|
543
616
|
// ─── REPL ────────────────────────────────────────────────────────────────────
|
|
@@ -585,7 +658,6 @@ export async function startRepl(opts = {}) {
|
|
|
585
658
|
output: process.stdout,
|
|
586
659
|
prompt: promptStr(ctx),
|
|
587
660
|
historySize: 500,
|
|
588
|
-
completer,
|
|
589
661
|
});
|
|
590
662
|
ctx.rl = rl;
|
|
591
663
|
|
|
@@ -594,6 +666,8 @@ export async function startRepl(opts = {}) {
|
|
|
594
666
|
for (const line of hist) rl.history.push(line);
|
|
595
667
|
} catch {}
|
|
596
668
|
|
|
669
|
+
attachGhostCompletion(rl, buildCompletionItems());
|
|
670
|
+
|
|
597
671
|
// ─── Start ───────────────────────────────────────────────────────
|
|
598
672
|
|
|
599
673
|
if (opts.replay) {
|