btcp-browser-agent 0.1.0 → 0.1.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/package.json +8 -9
- package/packages/core/dist/actions.d.ts +97 -0
- package/packages/core/dist/actions.js +940 -0
- package/packages/core/dist/errors.d.ts +138 -0
- package/packages/core/dist/errors.js +157 -0
- package/packages/core/dist/index.d.ts +120 -0
- package/packages/core/dist/index.js +134 -0
- package/packages/core/dist/ref-map.d.ts +16 -0
- package/packages/core/dist/ref-map.js +91 -0
- package/packages/core/dist/snapshot.d.ts +37 -0
- package/packages/core/dist/snapshot.js +751 -0
- package/packages/core/dist/types.d.ts +396 -0
- package/packages/core/dist/types.js +7 -0
- package/packages/extension/dist/background.d.ts +227 -0
- package/packages/extension/dist/background.js +737 -0
- package/packages/extension/dist/content.d.ts +18 -0
- package/packages/extension/dist/content.js +149 -0
- package/packages/extension/dist/index.d.ts +228 -0
- package/packages/extension/dist/index.js +350 -0
- package/packages/extension/dist/session-manager.d.ts +87 -0
- package/packages/extension/dist/session-manager.js +322 -0
- package/packages/extension/{src/session-types.ts → dist/session-types.d.ts} +113 -144
- package/packages/extension/dist/session-types.js +5 -0
- package/packages/extension/dist/types.d.ts +88 -0
- package/packages/extension/dist/types.js +7 -0
- package/CLAUDE.md +0 -230
- package/SKILL.md +0 -143
- package/SNAPSHOT_IMPROVEMENTS.md +0 -302
- package/USAGE.md +0 -146
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/docs/browser-cli-design.md +0 -500
- package/examples/chrome-extension/CHANGELOG.md +0 -210
- package/examples/chrome-extension/DEBUG.md +0 -231
- package/examples/chrome-extension/ERROR_FIXED.md +0 -147
- package/examples/chrome-extension/QUICK_TEST.md +0 -189
- package/examples/chrome-extension/README.md +0 -149
- package/examples/chrome-extension/SESSION_ONLY_MODE.md +0 -305
- package/examples/chrome-extension/TEST_WITH_YOUR_TABS.md +0 -97
- package/examples/chrome-extension/build.js +0 -43
- package/examples/chrome-extension/manifest.json +0 -37
- package/examples/chrome-extension/package-lock.json +0 -1063
- package/examples/chrome-extension/package.json +0 -21
- package/examples/chrome-extension/popup.html +0 -195
- package/examples/chrome-extension/src/background.ts +0 -12
- package/examples/chrome-extension/src/content.ts +0 -7
- package/examples/chrome-extension/src/popup.ts +0 -303
- package/examples/chrome-extension/src/scenario-google-github.ts +0 -389
- package/examples/chrome-extension/test-page.html +0 -127
- package/examples/chrome-extension/tests/README.md +0 -206
- package/examples/chrome-extension/tests/scenario-google-to-github-star.ts +0 -380
- package/examples/chrome-extension/tsconfig.json +0 -14
- package/examples/snapshots/README.md +0 -207
- package/examples/snapshots/amazon-com-detail.html +0 -9528
- package/examples/snapshots/amazon-com-detail.snapshot.txt +0 -997
- package/examples/snapshots/convert-snapshots.ts +0 -97
- package/examples/snapshots/edition-cnn-com.html +0 -13292
- package/examples/snapshots/edition-cnn-com.snapshot.txt +0 -562
- package/examples/snapshots/github-com-microsoft-vscode.html +0 -2916
- package/examples/snapshots/github-com-microsoft-vscode.snapshot.txt +0 -455
- package/examples/snapshots/google-search.html +0 -20012
- package/examples/snapshots/google-search.snapshot.txt +0 -195
- package/examples/snapshots/metadata.json +0 -86
- package/examples/snapshots/npr-org-templates.html +0 -2031
- package/examples/snapshots/npr-org-templates.snapshot.txt +0 -224
- package/examples/snapshots/stackoverflow-com.html +0 -5216
- package/examples/snapshots/stackoverflow-com.snapshot.txt +0 -2404
- package/examples/snapshots/test-all-mode.html +0 -46
- package/examples/snapshots/test-all-mode.snapshot.txt +0 -5
- package/examples/snapshots/validate.test.ts +0 -296
- package/packages/cli/package.json +0 -42
- package/packages/cli/src/__tests__/cli.test.ts +0 -434
- package/packages/cli/src/__tests__/errors.test.ts +0 -226
- package/packages/cli/src/__tests__/executor.test.ts +0 -275
- package/packages/cli/src/__tests__/formatter.test.ts +0 -260
- package/packages/cli/src/__tests__/parser.test.ts +0 -288
- package/packages/cli/src/__tests__/suggestions.test.ts +0 -255
- package/packages/cli/src/commands/back.ts +0 -22
- package/packages/cli/src/commands/check.ts +0 -33
- package/packages/cli/src/commands/clear.ts +0 -33
- package/packages/cli/src/commands/click.ts +0 -32
- package/packages/cli/src/commands/closetab.ts +0 -31
- package/packages/cli/src/commands/eval.ts +0 -41
- package/packages/cli/src/commands/fill.ts +0 -30
- package/packages/cli/src/commands/focus.ts +0 -33
- package/packages/cli/src/commands/forward.ts +0 -22
- package/packages/cli/src/commands/goto.ts +0 -34
- package/packages/cli/src/commands/help.ts +0 -162
- package/packages/cli/src/commands/hover.ts +0 -34
- package/packages/cli/src/commands/index.ts +0 -129
- package/packages/cli/src/commands/newtab.ts +0 -35
- package/packages/cli/src/commands/press.ts +0 -40
- package/packages/cli/src/commands/reload.ts +0 -25
- package/packages/cli/src/commands/screenshot.ts +0 -27
- package/packages/cli/src/commands/scroll.ts +0 -64
- package/packages/cli/src/commands/select.ts +0 -35
- package/packages/cli/src/commands/snapshot.ts +0 -21
- package/packages/cli/src/commands/tab.ts +0 -32
- package/packages/cli/src/commands/tabs.ts +0 -26
- package/packages/cli/src/commands/text.ts +0 -27
- package/packages/cli/src/commands/title.ts +0 -17
- package/packages/cli/src/commands/type.ts +0 -38
- package/packages/cli/src/commands/uncheck.ts +0 -33
- package/packages/cli/src/commands/url.ts +0 -17
- package/packages/cli/src/commands/wait.ts +0 -54
- package/packages/cli/src/errors.ts +0 -164
- package/packages/cli/src/executor.ts +0 -68
- package/packages/cli/src/formatter.ts +0 -215
- package/packages/cli/src/index.ts +0 -257
- package/packages/cli/src/parser.ts +0 -195
- package/packages/cli/src/suggestions.ts +0 -207
- package/packages/cli/src/terminal/Terminal.ts +0 -365
- package/packages/cli/src/terminal/index.ts +0 -5
- package/packages/cli/src/types.ts +0 -155
- package/packages/cli/tsconfig.json +0 -20
- package/packages/core/package.json +0 -35
- package/packages/core/src/actions.ts +0 -1210
- package/packages/core/src/errors.ts +0 -296
- package/packages/core/src/index.test.ts +0 -638
- package/packages/core/src/index.ts +0 -220
- package/packages/core/src/ref-map.ts +0 -107
- package/packages/core/src/snapshot.ts +0 -873
- package/packages/core/src/types.ts +0 -536
- package/packages/core/tsconfig.json +0 -23
- package/packages/extension/README.md +0 -129
- package/packages/extension/package.json +0 -43
- package/packages/extension/src/background.ts +0 -888
- package/packages/extension/src/content.ts +0 -172
- package/packages/extension/src/index.ts +0 -579
- package/packages/extension/src/session-manager.ts +0 -385
- package/packages/extension/src/types.ts +0 -162
- package/packages/extension/tsconfig.json +0 -28
- package/src/index.ts +0 -64
- package/tsconfig.build.json +0 -12
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -13
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @btcp/cli - Command suggestions and fuzzy matching
|
|
3
|
-
*
|
|
4
|
-
* Helps users self-correct typos and discover commands.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { commands } from './commands/index.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Calculate Levenshtein distance between two strings
|
|
11
|
-
*/
|
|
12
|
-
function levenshteinDistance(a: string, b: string): number {
|
|
13
|
-
const matrix: number[][] = [];
|
|
14
|
-
|
|
15
|
-
for (let i = 0; i <= b.length; i++) {
|
|
16
|
-
matrix[i] = [i];
|
|
17
|
-
}
|
|
18
|
-
for (let j = 0; j <= a.length; j++) {
|
|
19
|
-
matrix[0][j] = j;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
for (let i = 1; i <= b.length; i++) {
|
|
23
|
-
for (let j = 1; j <= a.length; j++) {
|
|
24
|
-
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
25
|
-
matrix[i][j] = matrix[i - 1][j - 1];
|
|
26
|
-
} else {
|
|
27
|
-
matrix[i][j] = Math.min(
|
|
28
|
-
matrix[i - 1][j - 1] + 1, // substitution
|
|
29
|
-
matrix[i][j - 1] + 1, // insertion
|
|
30
|
-
matrix[i - 1][j] + 1 // deletion
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return matrix[b.length][a.length];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Find similar commands to the given input
|
|
41
|
-
*/
|
|
42
|
-
export function findSimilarCommands(input: string, maxSuggestions = 3): string[] {
|
|
43
|
-
const inputLower = input.toLowerCase();
|
|
44
|
-
const commandNames = Object.keys(commands);
|
|
45
|
-
|
|
46
|
-
// Calculate distances and filter
|
|
47
|
-
const scored = commandNames
|
|
48
|
-
.map((name) => ({
|
|
49
|
-
name,
|
|
50
|
-
distance: levenshteinDistance(inputLower, name),
|
|
51
|
-
startsWith: name.startsWith(inputLower),
|
|
52
|
-
contains: name.includes(inputLower),
|
|
53
|
-
}))
|
|
54
|
-
.filter((item) => {
|
|
55
|
-
// Only suggest if reasonably close
|
|
56
|
-
const maxDistance = Math.max(2, Math.floor(input.length / 2));
|
|
57
|
-
return item.distance <= maxDistance || item.startsWith || item.contains;
|
|
58
|
-
})
|
|
59
|
-
.sort((a, b) => {
|
|
60
|
-
// Prioritize: startsWith > contains > distance
|
|
61
|
-
if (a.startsWith && !b.startsWith) return -1;
|
|
62
|
-
if (!a.startsWith && b.startsWith) return 1;
|
|
63
|
-
if (a.contains && !b.contains) return -1;
|
|
64
|
-
if (!a.contains && b.contains) return 1;
|
|
65
|
-
return a.distance - b.distance;
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
return scored.slice(0, maxSuggestions).map((item) => item.name);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get contextual suggestions based on command and error
|
|
73
|
-
*/
|
|
74
|
-
export function getContextualSuggestion(
|
|
75
|
-
commandName: string,
|
|
76
|
-
error: string,
|
|
77
|
-
args: string[]
|
|
78
|
-
): string | null {
|
|
79
|
-
const errorLower = error.toLowerCase();
|
|
80
|
-
|
|
81
|
-
// Selector not found
|
|
82
|
-
if (errorLower.includes('not found') || errorLower.includes('no element')) {
|
|
83
|
-
return `Element not found. Try:\n 1. Run 'snapshot' to see available elements with @ref IDs\n 2. Use a different selector or wait for the element: 'wait ${args[0] || '<selector>'} --state visible'`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Invalid selector
|
|
87
|
-
if (errorLower.includes('selector') || errorLower.includes('invalid')) {
|
|
88
|
-
return `Invalid selector. Supported formats:\n - @ref:5 (from snapshot)\n - #id (CSS ID selector)\n - .class (CSS class selector)\n - button, input (tag names)`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Navigation errors
|
|
92
|
-
if (errorLower.includes('navigate') || errorLower.includes('url')) {
|
|
93
|
-
return `Navigation failed. Make sure:\n 1. URL includes protocol (https://)\n 2. The page is accessible\n 3. Try: 'goto https://example.com'`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Timeout
|
|
97
|
-
if (errorLower.includes('timeout')) {
|
|
98
|
-
return `Operation timed out. Try:\n 1. Increase wait time: 'wait 5000'\n 2. Check if element exists: 'snapshot'\n 3. Wait for specific state: 'wait ${args[0] || '<selector>'} --state visible'`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Permission/security
|
|
102
|
-
if (errorLower.includes('permission') || errorLower.includes('security') || errorLower.includes('blocked')) {
|
|
103
|
-
return `Action blocked. This may be due to:\n 1. Cross-origin restrictions\n 2. Browser security policies\n 3. The element may be in an iframe`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Command categories for organized help
|
|
111
|
-
*/
|
|
112
|
-
export const commandCategories = {
|
|
113
|
-
navigation: {
|
|
114
|
-
name: 'Navigation',
|
|
115
|
-
description: 'Navigate between pages and control browser history',
|
|
116
|
-
commands: ['goto', 'back', 'forward', 'reload', 'url', 'title'],
|
|
117
|
-
},
|
|
118
|
-
inspection: {
|
|
119
|
-
name: 'Page Inspection',
|
|
120
|
-
description: 'Inspect page content and capture screenshots',
|
|
121
|
-
commands: ['snapshot', 'screenshot', 'text'],
|
|
122
|
-
},
|
|
123
|
-
interaction: {
|
|
124
|
-
name: 'Element Interaction',
|
|
125
|
-
description: 'Click, type, and interact with page elements',
|
|
126
|
-
commands: ['click', 'type', 'fill', 'clear', 'hover', 'focus', 'press'],
|
|
127
|
-
},
|
|
128
|
-
forms: {
|
|
129
|
-
name: 'Form Controls',
|
|
130
|
-
description: 'Work with form inputs like checkboxes and dropdowns',
|
|
131
|
-
commands: ['check', 'uncheck', 'select'],
|
|
132
|
-
},
|
|
133
|
-
scrolling: {
|
|
134
|
-
name: 'Scrolling',
|
|
135
|
-
description: 'Scroll the page or scroll elements into view',
|
|
136
|
-
commands: ['scroll'],
|
|
137
|
-
},
|
|
138
|
-
tabs: {
|
|
139
|
-
name: 'Tab Management',
|
|
140
|
-
description: 'Manage browser tabs',
|
|
141
|
-
commands: ['tabs', 'tab', 'newtab', 'closetab'],
|
|
142
|
-
},
|
|
143
|
-
utility: {
|
|
144
|
-
name: 'Utility',
|
|
145
|
-
description: 'Wait, evaluate JavaScript, and get help',
|
|
146
|
-
commands: ['wait', 'eval', 'help'],
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Get category for a command
|
|
152
|
-
*/
|
|
153
|
-
export function getCommandCategory(commandName: string): string | null {
|
|
154
|
-
for (const [key, category] of Object.entries(commandCategories)) {
|
|
155
|
-
if (category.commands.includes(commandName)) {
|
|
156
|
-
return key;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Common workflow suggestions
|
|
164
|
-
*/
|
|
165
|
-
export const workflowSuggestions = {
|
|
166
|
-
afterNavigation: [
|
|
167
|
-
'snapshot # See page structure and get element refs',
|
|
168
|
-
'screenshot # Capture visual state',
|
|
169
|
-
'wait 1000 # Wait for page to settle',
|
|
170
|
-
],
|
|
171
|
-
afterSnapshot: [
|
|
172
|
-
'click @ref:N # Click an element from snapshot',
|
|
173
|
-
'type @ref:N "..." # Type into an input',
|
|
174
|
-
'fill @ref:N "..." # Fill an input field',
|
|
175
|
-
],
|
|
176
|
-
afterClick: [
|
|
177
|
-
'snapshot # See updated page state',
|
|
178
|
-
'wait 500 # Wait for animations/updates',
|
|
179
|
-
],
|
|
180
|
-
afterError: [
|
|
181
|
-
'snapshot # Check current page state',
|
|
182
|
-
'url # Verify current URL',
|
|
183
|
-
'tabs # Check open tabs',
|
|
184
|
-
],
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Get next step suggestions based on last command
|
|
189
|
-
*/
|
|
190
|
-
export function getNextStepSuggestions(lastCommand: string): string[] {
|
|
191
|
-
switch (lastCommand) {
|
|
192
|
-
case 'goto':
|
|
193
|
-
case 'reload':
|
|
194
|
-
case 'back':
|
|
195
|
-
case 'forward':
|
|
196
|
-
return workflowSuggestions.afterNavigation;
|
|
197
|
-
case 'snapshot':
|
|
198
|
-
return workflowSuggestions.afterSnapshot;
|
|
199
|
-
case 'click':
|
|
200
|
-
case 'type':
|
|
201
|
-
case 'fill':
|
|
202
|
-
case 'press':
|
|
203
|
-
return workflowSuggestions.afterClick;
|
|
204
|
-
default:
|
|
205
|
-
return [];
|
|
206
|
-
}
|
|
207
|
-
}
|
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Terminal UI Component
|
|
3
|
-
*
|
|
4
|
-
* An in-browser terminal interface for the CLI.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { HistoryEntry, TerminalConfig, CommandResult } from '../types.js';
|
|
8
|
-
import { formatResult } from '../formatter.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Terminal options
|
|
12
|
-
*/
|
|
13
|
-
export interface TerminalOptions {
|
|
14
|
-
/** Execute a command and return the result */
|
|
15
|
-
onExecute: (input: string) => Promise<CommandResult>;
|
|
16
|
-
/** Optional configuration */
|
|
17
|
-
config?: Partial<TerminalConfig>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Default terminal configuration
|
|
22
|
-
*/
|
|
23
|
-
const defaultConfig: TerminalConfig = {
|
|
24
|
-
theme: 'dark',
|
|
25
|
-
fontSize: 14,
|
|
26
|
-
historySize: 1000,
|
|
27
|
-
prompt: '$ ',
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Terminal state
|
|
32
|
-
*/
|
|
33
|
-
interface TerminalState {
|
|
34
|
-
history: HistoryEntry[];
|
|
35
|
-
commandHistory: string[];
|
|
36
|
-
historyIndex: number;
|
|
37
|
-
inputBuffer: string;
|
|
38
|
-
isExecuting: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Terminal UI class
|
|
43
|
-
*/
|
|
44
|
-
export class Terminal {
|
|
45
|
-
private container: HTMLElement;
|
|
46
|
-
private config: TerminalConfig;
|
|
47
|
-
private state: TerminalState;
|
|
48
|
-
private onExecute: (input: string) => Promise<CommandResult>;
|
|
49
|
-
|
|
50
|
-
// DOM elements
|
|
51
|
-
private outputEl!: HTMLElement;
|
|
52
|
-
private inputLineEl!: HTMLElement;
|
|
53
|
-
private promptEl!: HTMLElement;
|
|
54
|
-
private inputEl!: HTMLInputElement;
|
|
55
|
-
|
|
56
|
-
constructor(container: HTMLElement, options: TerminalOptions) {
|
|
57
|
-
this.container = container;
|
|
58
|
-
this.config = { ...defaultConfig, ...options.config };
|
|
59
|
-
this.onExecute = options.onExecute;
|
|
60
|
-
|
|
61
|
-
this.state = {
|
|
62
|
-
history: [],
|
|
63
|
-
commandHistory: [],
|
|
64
|
-
historyIndex: -1,
|
|
65
|
-
inputBuffer: '',
|
|
66
|
-
isExecuting: false,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
this.render();
|
|
70
|
-
this.bindEvents();
|
|
71
|
-
this.focus();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Render the terminal UI
|
|
76
|
-
*/
|
|
77
|
-
private render(): void {
|
|
78
|
-
const isDark = this.config.theme === 'dark';
|
|
79
|
-
|
|
80
|
-
this.container.innerHTML = `
|
|
81
|
-
<div class="btcp-terminal" style="
|
|
82
|
-
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
|
83
|
-
font-size: ${this.config.fontSize}px;
|
|
84
|
-
line-height: 1.4;
|
|
85
|
-
background: ${isDark ? '#1e1e1e' : '#ffffff'};
|
|
86
|
-
color: ${isDark ? '#d4d4d4' : '#1e1e1e'};
|
|
87
|
-
padding: 12px;
|
|
88
|
-
height: 100%;
|
|
89
|
-
overflow: hidden;
|
|
90
|
-
display: flex;
|
|
91
|
-
flex-direction: column;
|
|
92
|
-
box-sizing: border-box;
|
|
93
|
-
">
|
|
94
|
-
<div class="btcp-terminal-output" style="
|
|
95
|
-
flex: 1;
|
|
96
|
-
overflow-y: auto;
|
|
97
|
-
white-space: pre-wrap;
|
|
98
|
-
word-wrap: break-word;
|
|
99
|
-
"></div>
|
|
100
|
-
<div class="btcp-terminal-input-line" style="
|
|
101
|
-
display: flex;
|
|
102
|
-
align-items: center;
|
|
103
|
-
margin-top: 8px;
|
|
104
|
-
flex-shrink: 0;
|
|
105
|
-
">
|
|
106
|
-
<span class="btcp-terminal-prompt" style="
|
|
107
|
-
color: ${isDark ? '#6a9955' : '#008000'};
|
|
108
|
-
margin-right: 8px;
|
|
109
|
-
user-select: none;
|
|
110
|
-
">${this.config.prompt}</span>
|
|
111
|
-
<input class="btcp-terminal-input" type="text" style="
|
|
112
|
-
flex: 1;
|
|
113
|
-
background: transparent;
|
|
114
|
-
border: none;
|
|
115
|
-
outline: none;
|
|
116
|
-
font-family: inherit;
|
|
117
|
-
font-size: inherit;
|
|
118
|
-
color: inherit;
|
|
119
|
-
padding: 0;
|
|
120
|
-
margin: 0;
|
|
121
|
-
" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
`;
|
|
125
|
-
|
|
126
|
-
this.outputEl = this.container.querySelector('.btcp-terminal-output')!;
|
|
127
|
-
this.inputLineEl = this.container.querySelector('.btcp-terminal-input-line')!;
|
|
128
|
-
this.promptEl = this.container.querySelector('.btcp-terminal-prompt')!;
|
|
129
|
-
this.inputEl = this.container.querySelector('.btcp-terminal-input')!;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Bind event listeners
|
|
134
|
-
*/
|
|
135
|
-
private bindEvents(): void {
|
|
136
|
-
// Handle input
|
|
137
|
-
this.inputEl.addEventListener('keydown', (e) => this.handleKeyDown(e));
|
|
138
|
-
|
|
139
|
-
// Click to focus
|
|
140
|
-
this.container.addEventListener('click', () => this.focus());
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Handle key down events
|
|
145
|
-
*/
|
|
146
|
-
private async handleKeyDown(e: KeyboardEvent): Promise<void> {
|
|
147
|
-
if (this.state.isExecuting) {
|
|
148
|
-
e.preventDefault();
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
switch (e.key) {
|
|
153
|
-
case 'Enter':
|
|
154
|
-
e.preventDefault();
|
|
155
|
-
await this.executeInput();
|
|
156
|
-
break;
|
|
157
|
-
|
|
158
|
-
case 'ArrowUp':
|
|
159
|
-
e.preventDefault();
|
|
160
|
-
this.navigateHistory(-1);
|
|
161
|
-
break;
|
|
162
|
-
|
|
163
|
-
case 'ArrowDown':
|
|
164
|
-
e.preventDefault();
|
|
165
|
-
this.navigateHistory(1);
|
|
166
|
-
break;
|
|
167
|
-
|
|
168
|
-
case 'c':
|
|
169
|
-
if (e.ctrlKey) {
|
|
170
|
-
e.preventDefault();
|
|
171
|
-
this.cancel();
|
|
172
|
-
}
|
|
173
|
-
break;
|
|
174
|
-
|
|
175
|
-
case 'l':
|
|
176
|
-
if (e.ctrlKey) {
|
|
177
|
-
e.preventDefault();
|
|
178
|
-
this.clearScreen();
|
|
179
|
-
}
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Execute the current input
|
|
186
|
-
*/
|
|
187
|
-
private async executeInput(): Promise<void> {
|
|
188
|
-
const input = this.inputEl.value.trim();
|
|
189
|
-
|
|
190
|
-
// Show the input in history
|
|
191
|
-
this.appendOutput(`${this.config.prompt}${input}`, 'input');
|
|
192
|
-
|
|
193
|
-
// Clear input
|
|
194
|
-
this.inputEl.value = '';
|
|
195
|
-
|
|
196
|
-
if (!input) {
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Add to command history
|
|
201
|
-
this.state.commandHistory.push(input);
|
|
202
|
-
this.state.historyIndex = this.state.commandHistory.length;
|
|
203
|
-
|
|
204
|
-
// Handle built-in terminal commands
|
|
205
|
-
if (input === 'clear') {
|
|
206
|
-
this.clearScreen();
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Execute command
|
|
211
|
-
this.state.isExecuting = true;
|
|
212
|
-
this.setPrompt('...');
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
const result = await this.onExecute(input);
|
|
216
|
-
const formatted = formatResult(result);
|
|
217
|
-
|
|
218
|
-
this.appendOutput(formatted.content, formatted.type === 'error' ? 'error' : 'output');
|
|
219
|
-
} catch (error) {
|
|
220
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
221
|
-
this.appendOutput(`Error: ${message}`, 'error');
|
|
222
|
-
} finally {
|
|
223
|
-
this.state.isExecuting = false;
|
|
224
|
-
this.setPrompt(this.config.prompt);
|
|
225
|
-
this.focus();
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Navigate command history
|
|
231
|
-
*/
|
|
232
|
-
private navigateHistory(direction: number): void {
|
|
233
|
-
const newIndex = this.state.historyIndex + direction;
|
|
234
|
-
|
|
235
|
-
if (newIndex < 0) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (newIndex >= this.state.commandHistory.length) {
|
|
240
|
-
this.state.historyIndex = this.state.commandHistory.length;
|
|
241
|
-
this.inputEl.value = this.state.inputBuffer;
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Save current input if navigating from end
|
|
246
|
-
if (this.state.historyIndex === this.state.commandHistory.length) {
|
|
247
|
-
this.state.inputBuffer = this.inputEl.value;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
this.state.historyIndex = newIndex;
|
|
251
|
-
this.inputEl.value = this.state.commandHistory[newIndex];
|
|
252
|
-
|
|
253
|
-
// Move cursor to end
|
|
254
|
-
this.inputEl.setSelectionRange(this.inputEl.value.length, this.inputEl.value.length);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Append output to the terminal
|
|
259
|
-
*/
|
|
260
|
-
private appendOutput(content: string, type: 'input' | 'output' | 'error' | 'info'): void {
|
|
261
|
-
const isDark = this.config.theme === 'dark';
|
|
262
|
-
|
|
263
|
-
let color = isDark ? '#d4d4d4' : '#1e1e1e';
|
|
264
|
-
if (type === 'error') {
|
|
265
|
-
color = isDark ? '#f14c4c' : '#cd3131';
|
|
266
|
-
} else if (type === 'info') {
|
|
267
|
-
color = isDark ? '#3794ff' : '#0066bf';
|
|
268
|
-
} else if (type === 'input') {
|
|
269
|
-
color = isDark ? '#9cdcfe' : '#0066bf';
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const entry: HistoryEntry = {
|
|
273
|
-
type,
|
|
274
|
-
content,
|
|
275
|
-
timestamp: Date.now(),
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
this.state.history.push(entry);
|
|
279
|
-
|
|
280
|
-
// Trim history if needed
|
|
281
|
-
while (this.state.history.length > this.config.historySize) {
|
|
282
|
-
this.state.history.shift();
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Append to DOM
|
|
286
|
-
const line = document.createElement('div');
|
|
287
|
-
line.style.color = color;
|
|
288
|
-
line.style.marginBottom = '2px';
|
|
289
|
-
line.textContent = content;
|
|
290
|
-
this.outputEl.appendChild(line);
|
|
291
|
-
|
|
292
|
-
// Scroll to bottom
|
|
293
|
-
this.outputEl.scrollTop = this.outputEl.scrollHeight;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Set the prompt text
|
|
298
|
-
*/
|
|
299
|
-
private setPrompt(prompt: string): void {
|
|
300
|
-
this.promptEl.textContent = prompt;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Cancel current operation
|
|
305
|
-
*/
|
|
306
|
-
private cancel(): void {
|
|
307
|
-
if (this.state.isExecuting) {
|
|
308
|
-
this.appendOutput('^C', 'info');
|
|
309
|
-
} else {
|
|
310
|
-
this.inputEl.value = '';
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Clear the screen
|
|
316
|
-
*/
|
|
317
|
-
public clearScreen(): void {
|
|
318
|
-
this.outputEl.innerHTML = '';
|
|
319
|
-
this.state.history = [];
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Focus the input
|
|
324
|
-
*/
|
|
325
|
-
public focus(): void {
|
|
326
|
-
this.inputEl.focus();
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Write output programmatically
|
|
331
|
-
*/
|
|
332
|
-
public write(content: string, type: 'output' | 'error' | 'info' = 'output'): void {
|
|
333
|
-
this.appendOutput(content, type);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Execute a command programmatically
|
|
338
|
-
*/
|
|
339
|
-
public async run(command: string): Promise<CommandResult> {
|
|
340
|
-
this.inputEl.value = command;
|
|
341
|
-
await this.executeInput();
|
|
342
|
-
return { success: true };
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Get the history
|
|
347
|
-
*/
|
|
348
|
-
public getHistory(): HistoryEntry[] {
|
|
349
|
-
return [...this.state.history];
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Get command history
|
|
354
|
-
*/
|
|
355
|
-
public getCommandHistory(): string[] {
|
|
356
|
-
return [...this.state.commandHistory];
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Destroy the terminal
|
|
361
|
-
*/
|
|
362
|
-
public destroy(): void {
|
|
363
|
-
this.container.innerHTML = '';
|
|
364
|
-
}
|
|
365
|
-
}
|