agent-sh 0.2.0 → 0.3.0
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/README.md +21 -0
- package/dist/acp-client.d.ts +24 -0
- package/dist/acp-client.js +144 -38
- package/dist/context-manager.d.ts +5 -3
- package/dist/context-manager.js +62 -31
- package/dist/event-bus.d.ts +4 -0
- package/dist/extension-loader.js +3 -14
- package/dist/extensions/shell-exec.js +27 -22
- package/dist/extensions/tui-renderer.d.ts +1 -1
- package/dist/extensions/tui-renderer.js +125 -26
- package/dist/index.js +184 -37
- package/dist/input-handler.d.ts +10 -0
- package/dist/input-handler.js +169 -10
- package/dist/mcp-server.js +37 -8
- package/dist/settings.d.ts +33 -0
- package/dist/settings.js +43 -0
- package/dist/shell.d.ts +1 -0
- package/dist/shell.js +44 -4
- package/dist/types.d.ts +2 -0
- package/dist/utils/ansi.d.ts +4 -1
- package/dist/utils/ansi.js +60 -2
- package/dist/utils/line-editor.d.ts +21 -1
- package/dist/utils/line-editor.js +193 -99
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/tool-display.d.ts +2 -0
- package/dist/utils/tool-display.js +48 -20
- package/examples/pi-agent-sh.ts +166 -0
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -24,13 +24,27 @@ function sendError(id, code, message) {
|
|
|
24
24
|
send({ id, error: { code, message } });
|
|
25
25
|
}
|
|
26
26
|
// ── Tool definition ─────────────────────────────────────────────
|
|
27
|
+
const SHELL_CWD_TOOL = {
|
|
28
|
+
name: "shell_cwd",
|
|
29
|
+
description: "Get the user's current working directory in their live shell. " +
|
|
30
|
+
"IMPORTANT: Your internal working directory may differ from the user's actual shell cwd — " +
|
|
31
|
+
"the user may have cd'd after your session started. Call this tool to get the real cwd " +
|
|
32
|
+
"before file operations if you're unsure.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
27
38
|
const USER_SHELL_TOOL = {
|
|
28
39
|
name: "user_shell",
|
|
29
40
|
description: "Execute a command in the user's live terminal session. " +
|
|
30
41
|
"Use this for commands that should affect the user's shell state: " +
|
|
31
42
|
"cd, export, source, pushd/popd, alias, etc. " +
|
|
32
43
|
"The command runs in the user's actual shell with their full environment " +
|
|
33
|
-
"(aliases, functions, PATH), not an isolated subprocess."
|
|
44
|
+
"(aliases, functions, PATH), not an isolated subprocess. " +
|
|
45
|
+
"NOTE: Your internal cwd may be stale — the user may have cd'd. " +
|
|
46
|
+
"Check the shell context for [shell cwd:...] labels or call shell_cwd " +
|
|
47
|
+
"to determine the real working directory. Use absolute paths when possible.",
|
|
34
48
|
inputSchema: {
|
|
35
49
|
type: "object",
|
|
36
50
|
properties: {
|
|
@@ -46,10 +60,11 @@ const SHELL_RECALL_TOOL = {
|
|
|
46
60
|
name: "shell_recall",
|
|
47
61
|
description: "Retrieve past shell commands, agent responses, and tool executions from the session history. " +
|
|
48
62
|
"Use this to look up truncated output, search for previous commands or errors, " +
|
|
49
|
-
"or browse recent exchanges.
|
|
50
|
-
|
|
51
|
-
'"
|
|
52
|
-
'"
|
|
63
|
+
"or browse recent exchanges. Each entry shows [shell cwd:...] so you can see " +
|
|
64
|
+
"which directory commands were run in. Operations: " +
|
|
65
|
+
'"browse" lists recent exchange summaries with line counts, ' +
|
|
66
|
+
'"search" finds exchanges matching a regex query, ' +
|
|
67
|
+
'"expand" retrieves content by exchange ID (use start/end for specific line ranges).',
|
|
53
68
|
inputSchema: {
|
|
54
69
|
type: "object",
|
|
55
70
|
properties: {
|
|
@@ -60,13 +75,21 @@ const SHELL_RECALL_TOOL = {
|
|
|
60
75
|
},
|
|
61
76
|
query: {
|
|
62
77
|
type: "string",
|
|
63
|
-
description: 'Search query (required for "search" operation)',
|
|
78
|
+
description: 'Search query — supports regex (required for "search" operation)',
|
|
64
79
|
},
|
|
65
80
|
ids: {
|
|
66
81
|
type: "array",
|
|
67
82
|
items: { type: "number" },
|
|
68
83
|
description: 'Exchange IDs to expand (required for "expand" operation)',
|
|
69
84
|
},
|
|
85
|
+
start: {
|
|
86
|
+
type: "number",
|
|
87
|
+
description: "Start line number, 1-indexed (optional, for expand)",
|
|
88
|
+
},
|
|
89
|
+
end: {
|
|
90
|
+
type: "number",
|
|
91
|
+
description: "End line number, inclusive (optional, for expand)",
|
|
92
|
+
},
|
|
70
93
|
},
|
|
71
94
|
},
|
|
72
95
|
};
|
|
@@ -127,14 +150,18 @@ async function handleRequest(id, method, params) {
|
|
|
127
150
|
// Client acknowledgement — nothing to do
|
|
128
151
|
break;
|
|
129
152
|
case "tools/list":
|
|
130
|
-
sendResult(id, { tools: [USER_SHELL_TOOL, SHELL_RECALL_TOOL] });
|
|
153
|
+
sendResult(id, { tools: [SHELL_CWD_TOOL, USER_SHELL_TOOL, SHELL_RECALL_TOOL] });
|
|
131
154
|
break;
|
|
132
155
|
case "tools/call": {
|
|
133
156
|
const toolName = params?.name;
|
|
134
157
|
const args = params?.arguments ?? {};
|
|
135
158
|
try {
|
|
136
159
|
let text;
|
|
137
|
-
if (toolName === "
|
|
160
|
+
if (toolName === "shell_cwd") {
|
|
161
|
+
const result = await callSocket("shell/cwd", {});
|
|
162
|
+
text = `User's current working directory: ${result.cwd}`;
|
|
163
|
+
}
|
|
164
|
+
else if (toolName === "user_shell") {
|
|
138
165
|
const command = args.command;
|
|
139
166
|
if (!command || typeof command !== "string") {
|
|
140
167
|
sendError(id, -32602, "Missing required parameter: command");
|
|
@@ -148,6 +175,8 @@ async function handleRequest(id, method, params) {
|
|
|
148
175
|
operation: args.operation || "browse",
|
|
149
176
|
query: args.query,
|
|
150
177
|
ids: args.ids,
|
|
178
|
+
start: args.start,
|
|
179
|
+
end: args.end,
|
|
151
180
|
});
|
|
152
181
|
text = result.result || "(no results)";
|
|
153
182
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare const CONFIG_DIR: string;
|
|
2
|
+
export interface Settings {
|
|
3
|
+
/** Extensions to load (npm packages or file paths). */
|
|
4
|
+
extensions?: string[];
|
|
5
|
+
/** Max agent query history entries to keep. */
|
|
6
|
+
historySize?: number;
|
|
7
|
+
/** Recent exchanges included in agent context window. */
|
|
8
|
+
contextWindowSize?: number;
|
|
9
|
+
/** Context budget in bytes (~4 chars per token). */
|
|
10
|
+
contextBudget?: number;
|
|
11
|
+
/** Shell output lines before truncation kicks in. */
|
|
12
|
+
shellTruncateThreshold?: number;
|
|
13
|
+
/** Lines kept from start of truncated shell output. */
|
|
14
|
+
shellHeadLines?: number;
|
|
15
|
+
/** Lines kept from end of truncated shell output. */
|
|
16
|
+
shellTailLines?: number;
|
|
17
|
+
/** Max lines for recall expand before requiring line ranges. */
|
|
18
|
+
recallExpandMaxLines?: number;
|
|
19
|
+
/** Max command output lines shown inline in TUI. */
|
|
20
|
+
maxCommandOutputLines?: number;
|
|
21
|
+
/** Max read tool output lines shown inline in TUI (0 = hide). */
|
|
22
|
+
readOutputMaxLines?: number;
|
|
23
|
+
/** Max diff lines shown before "ctrl+o to expand". */
|
|
24
|
+
diffMaxLines?: number;
|
|
25
|
+
/** Register MCP server for bridge tools (shell_cwd, user_shell, shell_recall). Default true. */
|
|
26
|
+
enableMcp?: boolean;
|
|
27
|
+
}
|
|
28
|
+
declare const DEFAULTS: Required<Settings>;
|
|
29
|
+
/** Load settings from disk (cached after first call). */
|
|
30
|
+
export declare function getSettings(): Settings & typeof DEFAULTS;
|
|
31
|
+
/** Reset cached settings (for testing or after external edit). */
|
|
32
|
+
export declare function reloadSettings(): void;
|
|
33
|
+
export {};
|
package/dist/settings.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User settings loaded from ~/.agent-sh/settings.json.
|
|
3
|
+
*
|
|
4
|
+
* Settings are loaded once at startup and available synchronously
|
|
5
|
+
* throughout the app. Unknown keys are preserved on write.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".agent-sh");
|
|
11
|
+
const SETTINGS_PATH = path.join(CONFIG_DIR, "settings.json");
|
|
12
|
+
const DEFAULTS = {
|
|
13
|
+
extensions: [],
|
|
14
|
+
historySize: 500,
|
|
15
|
+
contextWindowSize: 20,
|
|
16
|
+
contextBudget: 16384,
|
|
17
|
+
shellTruncateThreshold: 10,
|
|
18
|
+
shellHeadLines: 5,
|
|
19
|
+
shellTailLines: 5,
|
|
20
|
+
recallExpandMaxLines: 100,
|
|
21
|
+
maxCommandOutputLines: 5,
|
|
22
|
+
readOutputMaxLines: 0,
|
|
23
|
+
diffMaxLines: 20,
|
|
24
|
+
enableMcp: true,
|
|
25
|
+
};
|
|
26
|
+
let cached = null;
|
|
27
|
+
/** Load settings from disk (cached after first call). */
|
|
28
|
+
export function getSettings() {
|
|
29
|
+
if (!cached) {
|
|
30
|
+
try {
|
|
31
|
+
const raw = fs.readFileSync(SETTINGS_PATH, "utf-8");
|
|
32
|
+
cached = JSON.parse(raw);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
cached = {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { ...DEFAULTS, ...cached };
|
|
39
|
+
}
|
|
40
|
+
/** Reset cached settings (for testing or after external edit). */
|
|
41
|
+
export function reloadSettings() {
|
|
42
|
+
cached = null;
|
|
43
|
+
}
|
package/dist/shell.d.ts
CHANGED
package/dist/shell.js
CHANGED
|
@@ -10,6 +10,7 @@ export class Shell {
|
|
|
10
10
|
inputHandler;
|
|
11
11
|
outputParser;
|
|
12
12
|
paused = false;
|
|
13
|
+
echoSkip = false;
|
|
13
14
|
agentActive = false;
|
|
14
15
|
isZsh = false;
|
|
15
16
|
tmpDir;
|
|
@@ -98,6 +99,18 @@ export class Shell {
|
|
|
98
99
|
].join("\n") + "\n");
|
|
99
100
|
shellArgs = ["--rcfile", path.join(this.tmpDir, ".bashrc")];
|
|
100
101
|
}
|
|
102
|
+
// Pause stdin before spawning PTY to avoid TTY contention on macOS.
|
|
103
|
+
// The PTY will become the controlling terminal for the child shell.
|
|
104
|
+
const wasRaw = process.stdin.isTTY && process.stdin.isRaw;
|
|
105
|
+
if (process.stdin.isTTY) {
|
|
106
|
+
try {
|
|
107
|
+
process.stdin.setRawMode(false);
|
|
108
|
+
process.stdin.pause();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Ignore
|
|
112
|
+
}
|
|
113
|
+
}
|
|
101
114
|
this.ptyProcess = pty.spawn(shellBin, shellArgs, {
|
|
102
115
|
name: "xterm-256color",
|
|
103
116
|
cols: opts.cols,
|
|
@@ -105,6 +118,18 @@ export class Shell {
|
|
|
105
118
|
cwd: opts.cwd,
|
|
106
119
|
env,
|
|
107
120
|
});
|
|
121
|
+
// Restore stdin after PTY is created
|
|
122
|
+
if (process.stdin.isTTY) {
|
|
123
|
+
try {
|
|
124
|
+
process.stdin.resume();
|
|
125
|
+
if (wasRaw) {
|
|
126
|
+
process.stdin.setRawMode(true);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore - will be set up later in index.ts
|
|
131
|
+
}
|
|
132
|
+
}
|
|
108
133
|
this.bus = opts.bus;
|
|
109
134
|
this.outputParser = new OutputParser(opts.bus, opts.cwd);
|
|
110
135
|
// Ensure temp dir cleanup on abnormal exit (SIGKILL won't fire this,
|
|
@@ -178,9 +203,20 @@ export class Shell {
|
|
|
178
203
|
setupOutput() {
|
|
179
204
|
this.ptyProcess.onData((data) => {
|
|
180
205
|
this.outputParser.processData(data);
|
|
181
|
-
if (
|
|
182
|
-
|
|
206
|
+
if (this.paused)
|
|
207
|
+
return;
|
|
208
|
+
// During user_shell exec, skip the command echo (first line)
|
|
209
|
+
if (this.echoSkip) {
|
|
210
|
+
const nlIdx = data.indexOf("\n");
|
|
211
|
+
if (nlIdx === -1)
|
|
212
|
+
return;
|
|
213
|
+
this.echoSkip = false;
|
|
214
|
+
const rest = data.slice(nlIdx + 1);
|
|
215
|
+
if (rest)
|
|
216
|
+
process.stdout.write(rest);
|
|
217
|
+
return;
|
|
183
218
|
}
|
|
219
|
+
process.stdout.write(data);
|
|
184
220
|
});
|
|
185
221
|
}
|
|
186
222
|
setupInput() {
|
|
@@ -202,6 +238,7 @@ export class Shell {
|
|
|
202
238
|
this.bus.on("agent:processing-done", () => {
|
|
203
239
|
this.paused = false;
|
|
204
240
|
this.agentActive = false;
|
|
241
|
+
this.echoSkip = true;
|
|
205
242
|
this.freshPrompt();
|
|
206
243
|
});
|
|
207
244
|
// Permission prompts need stdout unpaused so the interactive UI renders,
|
|
@@ -217,10 +254,12 @@ export class Shell {
|
|
|
217
254
|
// stdout is paused during agent processing, so PTY output flows through
|
|
218
255
|
// OutputParser (for OSC detection) but never reaches the terminal.
|
|
219
256
|
this.bus.onPipeAsync("shell:exec-request", async (payload) => {
|
|
257
|
+
this.echoSkip = true;
|
|
258
|
+
this.paused = false;
|
|
259
|
+
process.stdout.write("\n");
|
|
220
260
|
const output = await new Promise((resolve, reject) => {
|
|
221
261
|
const timeout = setTimeout(() => {
|
|
222
262
|
this.bus.off("shell:command-done", handler);
|
|
223
|
-
// Kill any hung command
|
|
224
263
|
this.ptyProcess.write("\x03");
|
|
225
264
|
reject(new Error("Shell exec timed out after 30s"));
|
|
226
265
|
}, 30_000);
|
|
@@ -230,10 +269,11 @@ export class Shell {
|
|
|
230
269
|
resolve({ output: e.output, cwd: e.cwd });
|
|
231
270
|
};
|
|
232
271
|
this.bus.on("shell:command-done", handler);
|
|
233
|
-
// Start capture and write to PTY
|
|
234
272
|
this.outputParser.onCommandEntered(payload.command, this.outputParser.getCwd());
|
|
235
273
|
this.ptyProcess.write(payload.command + "\r");
|
|
236
274
|
});
|
|
275
|
+
this.paused = true;
|
|
276
|
+
this.echoSkip = false;
|
|
237
277
|
return { ...payload, output: output.output, cwd: output.cwd, done: true };
|
|
238
278
|
});
|
|
239
279
|
}
|
package/dist/types.d.ts
CHANGED
package/dist/utils/ansi.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ export declare const RED = "\u001B[31m";
|
|
|
6
6
|
export declare const GRAY = "\u001B[90m";
|
|
7
7
|
export declare const BOLD = "\u001B[1m";
|
|
8
8
|
export declare const RESET = "\u001B[0m";
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Measure visible string length in terminal columns.
|
|
11
|
+
* Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
|
|
12
|
+
*/
|
|
10
13
|
export declare function visibleLen(str: string): number;
|
|
11
14
|
/** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
|
|
12
15
|
export declare function stripAnsi(str: string): string;
|
package/dist/utils/ansi.js
CHANGED
|
@@ -8,9 +8,67 @@ export const GRAY = "\x1b[90m";
|
|
|
8
8
|
export const BOLD = "\x1b[1m";
|
|
9
9
|
export const RESET = "\x1b[0m";
|
|
10
10
|
// ── ANSI utility functions ───────────────────────────────────
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Check if a Unicode code point is a wide character (CJK, fullwidth, emoji, etc.)
|
|
13
|
+
* Returns 2 for wide chars, 1 for normal chars.
|
|
14
|
+
*/
|
|
15
|
+
function charWidth(codePoint) {
|
|
16
|
+
// CJK Unified Ideographs
|
|
17
|
+
if (codePoint >= 0x4e00 && codePoint <= 0x9fff)
|
|
18
|
+
return 2;
|
|
19
|
+
// CJK Unified Ideographs Extension A
|
|
20
|
+
if (codePoint >= 0x3400 && codePoint <= 0x4dbf)
|
|
21
|
+
return 2;
|
|
22
|
+
// Hangul Syllables
|
|
23
|
+
if (codePoint >= 0xac00 && codePoint <= 0xd7af)
|
|
24
|
+
return 2;
|
|
25
|
+
// CJK Unified Ideographs Extension B-F and other CJK blocks
|
|
26
|
+
if (codePoint >= 0x20000 && codePoint <= 0x2ebef)
|
|
27
|
+
return 2;
|
|
28
|
+
// Fullwidth ASCII variants
|
|
29
|
+
if (codePoint >= 0xff01 && codePoint <= 0xff5e)
|
|
30
|
+
return 2;
|
|
31
|
+
// Halfwidth Katakana (actually narrow, skip)
|
|
32
|
+
// Fullwidth bracket forms
|
|
33
|
+
if (codePoint >= 0xff5f && codePoint <= 0xff60)
|
|
34
|
+
return 2;
|
|
35
|
+
// Fullwidth symbol variants
|
|
36
|
+
if (codePoint >= 0xffe0 && codePoint <= 0xffe6)
|
|
37
|
+
return 2;
|
|
38
|
+
// Japanese hiragana and katakana
|
|
39
|
+
if (codePoint >= 0x3040 && codePoint <= 0x309f)
|
|
40
|
+
return 2;
|
|
41
|
+
if (codePoint >= 0x30a0 && codePoint <= 0x30ff)
|
|
42
|
+
return 2;
|
|
43
|
+
// CJK symbols and punctuation
|
|
44
|
+
if (codePoint >= 0x3000 && codePoint <= 0x303f)
|
|
45
|
+
return 2;
|
|
46
|
+
// Enclosed CJK letters and months
|
|
47
|
+
if (codePoint >= 0x3200 && codePoint <= 0x32ff)
|
|
48
|
+
return 2;
|
|
49
|
+
// CJK compatibility
|
|
50
|
+
if (codePoint >= 0x3300 && codePoint <= 0x33ff)
|
|
51
|
+
return 2;
|
|
52
|
+
// Hangul Jamo
|
|
53
|
+
if (codePoint >= 0x1100 && codePoint <= 0x11ff)
|
|
54
|
+
return 2;
|
|
55
|
+
// Hangul compatibility Jamo
|
|
56
|
+
if (codePoint >= 0x3130 && codePoint <= 0x318f)
|
|
57
|
+
return 2;
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Measure visible string length in terminal columns.
|
|
62
|
+
* Excludes SGR (color/style) sequences and accounts for CJK double-width chars.
|
|
63
|
+
*/
|
|
12
64
|
export function visibleLen(str) {
|
|
13
|
-
|
|
65
|
+
// First strip ANSI escape sequences
|
|
66
|
+
const cleanStr = str.replace(/\x1b\[[^m]*m/g, "");
|
|
67
|
+
let width = 0;
|
|
68
|
+
for (const char of cleanStr) {
|
|
69
|
+
width += charWidth(char.codePointAt(0) ?? 0);
|
|
70
|
+
}
|
|
71
|
+
return width;
|
|
14
72
|
}
|
|
15
73
|
/** Strip all ANSI escape sequences (SGR, OSC, CSI, private mode) and carriage returns. */
|
|
16
74
|
export function stripAnsi(str) {
|
|
@@ -16,6 +16,8 @@ export type LineEditAction = {
|
|
|
16
16
|
action: "delete-empty";
|
|
17
17
|
} | {
|
|
18
18
|
action: "tab";
|
|
19
|
+
} | {
|
|
20
|
+
action: "shift+tab";
|
|
19
21
|
} | {
|
|
20
22
|
action: "arrow-up";
|
|
21
23
|
} | {
|
|
@@ -24,12 +26,30 @@ export type LineEditAction = {
|
|
|
24
26
|
export declare class LineEditor {
|
|
25
27
|
buffer: string;
|
|
26
28
|
cursor: number;
|
|
29
|
+
private pendingSeq;
|
|
27
30
|
/** Process raw terminal input, return actions for the consumer. */
|
|
28
31
|
feed(data: string): LineEditAction[];
|
|
32
|
+
/** Check if there's a pending incomplete escape sequence. */
|
|
33
|
+
hasPendingEscape(): boolean;
|
|
34
|
+
/** Flush a pending sequence — treat bare \x1b as cancel, discard incomplete CSI. */
|
|
35
|
+
flushPendingEscape(): LineEditAction[];
|
|
29
36
|
clear(): void;
|
|
37
|
+
private readonly bindings;
|
|
38
|
+
/** Resolve a key name from the bindings table and execute it. */
|
|
39
|
+
private dispatch;
|
|
40
|
+
/** Map a legacy control character to a key name. */
|
|
41
|
+
private static readonly CTRL_MAP;
|
|
42
|
+
private handleControl;
|
|
43
|
+
/** Handle a kitty protocol CSI u sequence. Params format: "keycode;modifier". */
|
|
44
|
+
private handleKittyKey;
|
|
45
|
+
private insertAt;
|
|
46
|
+
private moveTo;
|
|
47
|
+
private deleteBackward;
|
|
48
|
+
private deleteForward;
|
|
49
|
+
private deleteRange;
|
|
30
50
|
/**
|
|
31
51
|
* Parse and handle a CSI sequence (\x1b[...) starting at `start`.
|
|
32
|
-
* Returns the number of bytes consumed.
|
|
52
|
+
* Returns the number of bytes consumed and whether the sequence was incomplete.
|
|
33
53
|
*/
|
|
34
54
|
private handleCSI;
|
|
35
55
|
private wordBackward;
|