287
* Used when shell integration is not available.
288
*/
290
>
target: IShellCommandTarget,
291
>
command: string,
292
>
timeoutMs: number,
293
>
terminalManager: IAgentHostTerminalManager,
294
>
logService: ILogService,
295
>
): Promise<IShellCommandResult> {
296
>
const sentinelId = makeSentinelId();
297
>
const sentinelCmd = buildSentinelCommand(sentinelId, target.shellType);
298
>
const disposables = new DisposableStore();
299
>
300
>
const contentBefore = terminalManager.getContent(target.terminalUri) ?? '';
301
>
const offsetBefore = contentBefore.length;
302
>
303
>
const result = new Promise<IShellCommandResult>(resolve => {
304
>
let resolved = false;
305
>
const finish = (result: IShellCommandResult) => {
306
>
if (resolved) {
307
return;
308
}
310
>
disposables.dispose();
311
>
resolve(result);
312
>
};
313
>
314
>
const checkForSentinel = () => {
315
>
const fullContent = terminalManager.getContent(target.terminalUri) ?? '';
316
>
// Clamp offset: the terminal manager trims content when it exceeds
317
>
// 100k chars (slices to last 80k). If trimming happened after we
318
>
// captured offsetBefore, scan from the start of the current buffer.
319
>
const clampedOffset = Math.min(offsetBefore, fullContent.length);
320
>
const newContent = fullContent.substring(clampedOffset);
321
>
const parsed = parseSentinel(newContent, sentinelId);
322
>
if (parsed.found) {
323
const output = prepareOutputForModel(parsed.outputBeforeSentinel);
324
logService.info(`[ShellCommand] Command completed with exit code ${parsed.exitCode}`);
325
finish({ status: 'completed', exitCode: parsed.exitCode, output });
326
}
328
>
329
>
disposables.add(terminalManager.onData(target.terminalUri, () => {
330
checkForSentinel();
332
>
333
>
registerAltBufferHandler(target, terminalManager, logService, disposables, finish);
334
>
335
>
disposables.add(terminalManager.onExit(target.terminalUri, (exitCode: number) => {
336
logService.info(`[ShellCommand] Shell exited unexpectedly with code ${exitCode}`);
337
const fullContent = terminalManager.getContent(target.terminalUri) ?? '';
338
const newContent = fullContent.substring(offsetBefore);
339
finish({ status: 'shellExited', exitCode, output: prepareOutputForModel(newContent) });
341
>
342
>
disposables.add(terminalManager.onClaimChanged(target.terminalUri, (claim) => {
343
if (claim.kind === TerminalClaimKind.Session && !claim.toolCallId) {
344
logService.info(`[ShellCommand] Continuing in background (claim narrowed)`);
345
finish({ status: 'background', output: '' });
346
}
348
>
349
>
const timer = setTimeout(() => {
350
logService.warn(`[ShellCommand] Command timed out after ${timeoutMs}ms`);
351
const fullContent = terminalManager.getContent(target.terminalUri) ?? '';
352
const newContent = fullContent.substring(offsetBefore);
353
finish({ status: 'timeout', output: prepareOutputForModel(newContent) });
355
>
disposables.add(toDisposable(() => clearTimeout(timer)));
356
>
357
>
checkForSentinel();
358
>
});
359
>
360
>
try {
361
>
await terminalManager.sendText(target.terminalUri, `${prefixForHistorySuppression(target.shellType)}${command}`, {
362
>
shouldExecute: true,
363
>
bracketedPasteMode: shouldUseBracketedPasteMode(command),
364
>
});
365
>
await terminalManager.sendText(target.terminalUri, sentinelCmd, { shouldExecute: true });
366
>
} catch (err) {
367
disposables.dispose();
368
throw err;
369
}
371
>
return result;
372
>
}