playwright-repl 0.1.1 → 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/{RELEASES.md → CHANGELOG.md} +45 -1
- package/README.md +49 -7
- package/bin/mcp-server.cjs +32 -0
- package/examples/ghost-completion-demo.mjs +122 -0
- package/package.json +4 -3
- package/src/completion-data.mjs +50 -0
- package/src/index.mjs +16 -15
- package/src/repl.mjs +94 -20
|
@@ -1,4 +1,48 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Changelog
|
|
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
|
+
|
|
21
|
+
## v0.2.0 — MCP Server
|
|
22
|
+
|
|
23
|
+
**2026-02-16**
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
- **MCP Server**: Ships a stdio MCP server (`playwright-mcp-server`) that exposes Playwright's full browser automation toolkit to AI agents (Claude, Cursor, etc.)
|
|
28
|
+
- Supports `--headed` flag for visible browser mode
|
|
29
|
+
|
|
30
|
+
### Configuration
|
|
31
|
+
|
|
32
|
+
VS Code / Cursor — add to `.vscode/mcp.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"servers": {
|
|
37
|
+
"playwright": {
|
|
38
|
+
"command": "npx",
|
|
39
|
+
"args": ["-p", "playwright-repl", "playwright-mcp-server", "--headed"]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
2
46
|
|
|
3
47
|
## v0.1.1 — Bug Fixes
|
|
4
48
|
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# playwright-repl
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
Interactive REPL for Playwright browser automation — keyword-driven testing from your terminal.
|
|
4
6
|
|
|
5
7
|
Inspired by [playwright-cli](https://github.com/anthropics/playwright-cli), reusing its command vocabulary and Playwright MCP daemon. Where playwright-cli is designed for AI agents (one command per process), playwright-repl is designed for **humans** — a persistent session with recording, replay, and instant feedback.
|
|
@@ -128,6 +130,52 @@ playwright-repl --session checkout-flow --headed
|
|
|
128
130
|
| `-q, --silent` | Suppress banner and status messages |
|
|
129
131
|
| `-h, --help` | Show help |
|
|
130
132
|
|
|
133
|
+
## MCP Server
|
|
134
|
+
|
|
135
|
+
`playwright-repl` also ships an MCP server that exposes Playwright's full browser automation toolkit to AI agents (Claude, Cursor, etc.) over stdio.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Run directly
|
|
139
|
+
npx -p playwright-repl playwright-mcp-server
|
|
140
|
+
|
|
141
|
+
# With visible browser
|
|
142
|
+
npx -p playwright-repl playwright-mcp-server --headed
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### VS Code / Cursor Configuration
|
|
146
|
+
|
|
147
|
+
Add to `.vscode/mcp.json` in your project:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"servers": {
|
|
152
|
+
"playwright": {
|
|
153
|
+
"command": "npx",
|
|
154
|
+
"args": ["-p", "playwright-repl", "playwright-mcp-server"]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Or with a visible browser:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"servers": {
|
|
165
|
+
"playwright": {
|
|
166
|
+
"command": "npx",
|
|
167
|
+
"args": ["-p", "playwright-repl", "playwright-mcp-server", "--headed"]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### MCP Server Options
|
|
174
|
+
|
|
175
|
+
| Option | Description |
|
|
176
|
+
|--------|-------------|
|
|
177
|
+
| `--headed` | Run browser in headed (visible) mode |
|
|
178
|
+
|
|
131
179
|
## Commands
|
|
132
180
|
|
|
133
181
|
### Navigation
|
|
@@ -336,13 +384,7 @@ playwright-repl --replay examples/05-ci-pipe.pw --silent
|
|
|
336
384
|
|
|
337
385
|
## Architecture
|
|
338
386
|
|
|
339
|
-
|
|
340
|
-
┌──────────────┐ Unix Socket ┌──────────────────┐ CDP ┌─────────┐
|
|
341
|
-
│ playwright- │◄──── JSON/newline ───►│ Daemon Process │◄────────────►│ Browser │
|
|
342
|
-
│ repl │ │ (Playwright │ │(Chrome/ │
|
|
343
|
-
│ │ │ MCP backend) │ │ FF/WK) │
|
|
344
|
-
└──────────────┘ └──────────────────┘ └─────────┘
|
|
345
|
-
```
|
|
387
|
+

|
|
346
388
|
|
|
347
389
|
The REPL replaces only the **client half** of playwright-cli. The daemon, browser, and all tool handlers are unchanged — both CLI and REPL produce identical wire messages.
|
|
348
390
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thin MCP server entrypoint — uses Playwright's built-in MCP infrastructure.
|
|
5
|
+
* Starts a stdio MCP server with all browser tools available.
|
|
6
|
+
*
|
|
7
|
+
* Uses only publicly exported paths:
|
|
8
|
+
* - playwright/lib/mcp/index → createConnection()
|
|
9
|
+
* - playwright-core/lib/mcpBundle → StdioServerTransport
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { createConnection } = require('playwright/lib/mcp/index');
|
|
13
|
+
const { StdioServerTransport } = require('playwright-core/lib/mcpBundle');
|
|
14
|
+
|
|
15
|
+
// ─── Parse CLI args ───
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const headed = args.includes('--headed');
|
|
19
|
+
|
|
20
|
+
(async () => {
|
|
21
|
+
const server = await createConnection({
|
|
22
|
+
browser: {
|
|
23
|
+
launchOptions: { headless: !headed },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const transport = new StdioServerTransport();
|
|
28
|
+
await server.connect(transport);
|
|
29
|
+
})().catch(e => {
|
|
30
|
+
console.error(e.message);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
@@ -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
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playwright-repl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Interactive REPL for Playwright browser automation — keyword-driven testing from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"playwright-repl": "./bin/playwright-repl.mjs"
|
|
7
|
+
"playwright-repl": "./bin/playwright-repl.mjs",
|
|
8
|
+
"playwright-mcp-server": "./bin/mcp-server.cjs"
|
|
8
9
|
},
|
|
9
10
|
"main": "./src/index.mjs",
|
|
10
11
|
"exports": {
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
"examples/",
|
|
37
38
|
"LICENSE",
|
|
38
39
|
"README.md",
|
|
39
|
-
"
|
|
40
|
+
"CHANGELOG.md"
|
|
40
41
|
],
|
|
41
42
|
"engines": {
|
|
42
43
|
"node": ">=18.0.0"
|
|
@@ -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) {
|