Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/node/osc633Parser.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
/**
7
* Lightweight parser for OSC 633 (VS Code shell integration) sequences in raw
8
* PTY output. Designed for the agent host where we don't have a full xterm.js
9
* instance - it scans data chunks for the sequences, extracts events, and
10
* removes the sequences from the data stream.
11
*
12
* Handles partial sequences that span across data chunk boundaries.
13
*/
14
15
/** OSC 633 event types we care about. */
16
export const enum Osc633EventType {
17
/** 633;A - Prompt start. Used to detect shell integration is active. */
18
PromptStart,
19
/** 633;B - Command start (where user inputs command). */
20
CommandStart,
21
/** 633;C - Command executed (output begins). */
22
CommandExecuted,
23
/** 633;D[;exitCode] - Command finished. */
24
CommandFinished,
25
/** 633;E;commandLine[;nonce] - Explicit command line. */
26
CommandLine,
27
/** 633;P;Key=Value - Property (e.g. Cwd). */
28
Property,
29
}
30
31
export interface IOsc633PromptStartEvent {
32
type: Osc633EventType.PromptStart;
33
}
34
35
export interface IOsc633CommandStartEvent {
36
type: Osc633EventType.CommandStart;
37
}
38
39
export interface IOsc633CommandExecutedEvent {
40
type: Osc633EventType.CommandExecuted;
41
}
42
43
export interface IOsc633CommandFinishedEvent {
44
type: Osc633EventType.CommandFinished;
45
exitCode: number | undefined;
46
}
47
48
export interface IOsc633CommandLineEvent {
49
type: Osc633EventType.CommandLine;
50
commandLine: string;
51
nonce: string | undefined;
52
}
53
54
export interface IOsc633PropertyEvent {
55
type: Osc633EventType.Property;
56
key: string;
57
value: string;
58
}
59
60
export type Osc633Event =
61
| IOsc633PromptStartEvent
62
| IOsc633CommandStartEvent
63
| IOsc633CommandExecutedEvent
64
| IOsc633CommandFinishedEvent
65
| IOsc633CommandLineEvent
66
| IOsc633PropertyEvent;
67
68
export interface IOsc633ParseResult {
69
/** Data with all OSC 633 sequences stripped. */
70
cleanedData: string;
71
/** Parsed events in order of appearance. */
72
events: Osc633Event[];
73
}
74
75
/**
76
* Decode escaped values in OSC 633 messages.
77
* Handles `\\` -> `\` and `\xAB` -> character with code 0xAB.
78
*/
79
function deserializeOscMessage(message: string): string {
80
if (message.indexOf('\\') === -1) {
81
return message;
82
}
83
return message.replaceAll(
84
/\\(\\|x([0-9a-f]{2}))/gi,
85
(_match: string, op: string, hex?: string) => hex ? String.fromCharCode(parseInt(hex, 16)) : op,
86
);
87
}
88
89
function parseOsc633Payload(payload: string): Osc633Event | undefined {
90
const semiIdx = payload.indexOf(';');
91
if ((semiIdx === -1 ? payload.length : semiIdx) !== 1) {
92
return undefined;
93
}
94
95
const command = payload[0];
96
const argsRaw = semiIdx === -1 ? '' : payload.substring(semiIdx + 1);
97
98
switch (command) {
99
case 'A':
100
return { type: Osc633EventType.PromptStart };
101
case 'B':
102
return { type: Osc633EventType.CommandStart };
103
case 'C':
104
return { type: Osc633EventType.CommandExecuted };
105
case 'D': {
106
const exitCode = argsRaw.length > 0 ? parseInt(argsRaw, 10) : undefined;
107
return {
108
type: Osc633EventType.CommandFinished,
109
exitCode: exitCode !== undefined && !isNaN(exitCode) ? exitCode : undefined,
110
};
111
}
112
case 'E': {
113
const nonceIdx = argsRaw.indexOf(';');
114
const commandLine = deserializeOscMessage(nonceIdx === -1 ? argsRaw : argsRaw.substring(0, nonceIdx));
115
const nonce = nonceIdx === -1 ? undefined : argsRaw.substring(nonceIdx + 1);
116
return { type: Osc633EventType.CommandLine, commandLine, nonce };
117
}
118
case 'P': {
119
const deserialized = deserializeOscMessage(argsRaw);
120
const eqIdx = deserialized.indexOf('=');
121
if (eqIdx === -1) {
122
return undefined;
123
}
124
return {
125
type: Osc633EventType.Property,
126
key: deserialized.substring(0, eqIdx),
127
value: deserialized.substring(eqIdx + 1),
128
};
129
}
130
default:
131
return undefined;
132
}
133
}
134
135
// OSC introducer is ESC ] (0x1b 0x5d)
136
const ESC = '\x1b';
137
const OSC_START = ESC + ']';
138
// Terminators: BEL (0x07) or ST (ESC \)
139
const BEL = '\x07';
140
const ST = ESC + '\\';
141
142
/**
143
* Stateful parser that handles data chunks, correctly dealing with
144
* partial sequences that span multiple chunks.
145
*/
146
export class Osc633Parser {
147
/** Buffer for an incomplete OSC sequence (from ESC] up to but not including the terminator). */
148
private _pendingOsc = '';
149
/** Whether we are currently accumulating an OSC sequence. */
150
private _inOsc = false;
151
/** Set when the previous chunk ended with ESC inside an OSC body (potential ST start). */
152
private _pendingEscInOsc = false;
153
154
/**
155
* Parse a chunk of PTY data.
156
* Returns cleaned data (all OSC 633 sequences removed) and extracted events.
157
*/
158
parse(data: string): IOsc633ParseResult {
159
const events: Osc633Event[] = [];
160
if (!this._inOsc && data.indexOf(OSC_START) === -1) {
161
return { cleanedData: data, events };
162
}
163
164
let cleaned = '';
165
let i = 0;
166
167
while (i < data.length) {
168
if (this._inOsc) {
169
// Handle ESC that was pending from the previous chunk.
170
if (this._pendingEscInOsc) {
171
this._pendingEscInOsc = false;
172
if (data[i] === '\\') {
173
// ESC \ = ST terminator, sequence is complete.
174
i++;
175
this._inOsc = false;
176
const payload = this._pendingOsc;
177
this._pendingOsc = '';
178
this._handleOscPayload(payload, events, { value: cleaned, append(s: string) { cleaned = s; } }, ST);
179
continue;
180
}
181
// ESC was not followed by \, malformed: complete the OSC anyway.
182
this._inOsc = false;
183
const payload = this._pendingOsc;
184
this._pendingOsc = '';
185
this._handleOscPayload(payload, events, { value: cleaned, append(s: string) { cleaned = s; } });
186
continue;
187
}
188
189
// We're inside an OSC sequence, look for the terminator.
190
const result = this._consumeOscBody(data, i);
191
i = result.nextIndex;
192
if (result.complete) {
193
this._inOsc = false;
194
const payload = this._pendingOsc;
195
this._pendingOsc = '';
196
this._handleOscPayload(payload, events, { value: cleaned, append(s: string) { cleaned = s; } }, result.terminator);
197
} else if (result.pendingEsc) {
198
this._pendingEscInOsc = true;
199
}
200
// If not complete, _pendingOsc has been extended, and we're at end of data.
201
continue;
202
}
203
204
// Look for the next ESC ] which starts an OSC sequence
205
const escIdx = data.indexOf(OSC_START, i);
206
if (escIdx === -1) {
207
cleaned += data.substring(i);
208
i = data.length;
209
continue;
210
}
211
212
// Copy everything before the OSC start to cleaned output.
213
cleaned += data.substring(i, escIdx);
214
215
// Start of OSC: check if it's 633.
216
i = escIdx + 2; // skip past ESC ]
217
this._pendingOsc = '';
218
this._inOsc = true;
219
220
// Try to consume the OSC body in this same chunk.
221
const result = this._consumeOscBody(data, i);
222
i = result.nextIndex;
223
if (result.complete) {
224
this._inOsc = false;
225
const payload = this._pendingOsc;
226
this._pendingOsc = '';
227
// If it's a 633 sequence, extract event; otherwise put it back in cleaned.
228
this._handleOscPayload(payload, events, { value: cleaned, append(s: string) { cleaned = s; } }, result.terminator);
229
} else if (result.pendingEsc) {
230
this._pendingEscInOsc = true;
231
}
232
// If not complete, we're at end of data and _pendingOsc is buffered.
233
}
234
235
return { cleanedData: cleaned, events };
236
}
237
238
/**
239
* Consume characters from the OSC body, appending to _pendingOsc until a
240
* terminator (BEL or ST) is found.
241
*/
242
private _consumeOscBody(data: string, startIdx: number): { nextIndex: number; complete: boolean; pendingEsc?: boolean; terminator?: string } {
243
const belIdx = data.indexOf(BEL, startIdx);
244
const escIdx = data.indexOf(ESC, startIdx);
245
246
if (belIdx !== -1 && (escIdx === -1 || belIdx < escIdx)) {
247
this._pendingOsc += data.substring(startIdx, belIdx);
248
return { nextIndex: belIdx + 1, complete: true, terminator: BEL };
249
}
250
251
if (escIdx !== -1) {
252
if (escIdx + 1 >= data.length) {
253
this._pendingOsc += data.substring(startIdx, escIdx);
254
return { nextIndex: data.length, complete: false, pendingEsc: true };
255
}
256
257
this._pendingOsc += data.substring(startIdx, escIdx);
258
if (data[escIdx + 1] === '\\') {
259
return { nextIndex: escIdx + 2, complete: true, terminator: ST };
260
}
261
262
return { nextIndex: escIdx, complete: true };
263
}
264
265
this._pendingOsc += data.substring(startIdx);
266
return { nextIndex: data.length, complete: false };
267
}
268
269
/**
270
* Process a complete OSC payload. If it's a 633; sequence, extract the
271
* event. Otherwise, reconstruct the original bytes and pass them through
272
* to cleaned output.
273
*/
274
private _handleOscPayload(
275
payload: string,
276
events: Osc633Event[],
277
cleanedRef: { value: string; append(s: string): void } | undefined,
278
terminator = BEL,
279
): void {
280
if (payload.startsWith('633;')) {
281
const oscContent = payload.substring(4); // strip "633;"
282
const event = parseOsc633Payload(oscContent);
283
if (event) {
284
events.push(event);
285
}
286
// 633 sequences are always stripped from output
287
} else {
288
// Non-633 OSC: put back the original bytes.
289
if (cleanedRef) {
290
cleanedRef.append(cleanedRef.value + OSC_START + payload + terminator);
291
}
292
}
293
}
294
}
295
296