claude-code/utils/processUserInput/processBashCommand.tsx

140 lines
22 KiB
TypeScript
Raw Permalink Normal View History

import type { ContentBlockParam } from '@anthropic-ai/sdk/resources';
import { randomUUID } from 'crypto';
import * as React from 'react';
import { BashModeProgress } from 'src/components/BashModeProgress.js';
import type { SetToolJSXFn } from 'src/Tool.js';
import { BashTool } from 'src/tools/BashTool/BashTool.js';
import type { AttachmentMessage, SystemMessage, UserMessage } from 'src/types/message.js';
import type { ShellProgress } from 'src/types/tools.js';
import { logEvent } from '../../services/analytics/index.js';
import { errorMessage, ShellError } from '../errors.js';
import { createSyntheticUserCaveatMessage, createUserInterruptionMessage, createUserMessage, prepareUserContent } from '../messages.js';
import { resolveDefaultShell } from '../shell/resolveDefaultShell.js';
import { isPowerShellToolEnabled } from '../shell/shellToolUtils.js';
import { processToolResultBlock } from '../toolResultStorage.js';
import { escapeXml } from '../xml.js';
import type { ProcessUserInputContext } from './processUserInput.js';
export async function processBashCommand(inputString: string, precedingInputBlocks: ContentBlockParam[], attachmentMessages: AttachmentMessage[], context: ProcessUserInputContext, setToolJSX: SetToolJSXFn): Promise<{
messages: (UserMessage | AttachmentMessage | SystemMessage)[];
shouldQuery: boolean;
}> {
// Shell routing (docs/design/ps-shell-selection.md §5.2): consult
// defaultShell, fall back to bash. isPowerShellToolEnabled() applies the
// same platform + env-var gate as tools.ts so input-box routing matches
// tool-list visibility. Computed up front so telemetry records the
// actual shell, not the raw setting.
const usePowerShell = isPowerShellToolEnabled() && resolveDefaultShell() === 'powershell';
logEvent('tengu_input_bash', {
powershell: usePowerShell
});
const userMessage = createUserMessage({
content: prepareUserContent({
inputString: `<bash-input>${inputString}</bash-input>`,
precedingInputBlocks
})
});
// ctrl+b to background indicator
let jsx: React.ReactNode;
// Just show initial UI
setToolJSX({
jsx: <BashModeProgress input={inputString} progress={null} verbose={context.options.verbose} />,
shouldHidePromptInput: false
});
try {
const bashModeContext: ProcessUserInputContext = {
...context,
// TODO: Clean up this hack
setToolJSX: _ => {
jsx = _?.jsx;
}
};
// Progress UI — shared across both shell backends (both emit ShellProgress)
const onProgress = (progress: {
data: ShellProgress;
}) => {
setToolJSX({
jsx: <>
<BashModeProgress input={inputString!} progress={progress.data} verbose={context.options.verbose} />
{jsx}
</>,
shouldHidePromptInput: false,
showSpinner: false
});
};
// User-initiated `!` commands run outside sandbox. Both shell tools honor
// dangerouslyDisableSandbox (checked against areUnsandboxedCommandsAllowed()
// in shouldUseSandbox.ts). PS sandbox is Linux/macOS/WSL2 only — on Windows
// native, shouldUseSandbox() returns false regardless (unsupported platform).
// Lazy-require PowerShellTool so its ~300KB chunk only loads when the
// user has actually selected the powershell default shell.
type PSMod = typeof import('src/tools/PowerShellTool/PowerShellTool.js');
let PowerShellTool: PSMod['PowerShellTool'] | null = null;
if (usePowerShell) {
/* eslint-disable @typescript-eslint/no-require-imports */
PowerShellTool = (require('src/tools/PowerShellTool/PowerShellTool.js') as PSMod).PowerShellTool;
/* eslint-enable @typescript-eslint/no-require-imports */
}
const shellTool = PowerShellTool ?? BashTool;
const response = PowerShellTool ? await PowerShellTool.call({
command: inputString,
dangerouslyDisableSandbox: true
}, bashModeContext, undefined, undefined, onProgress) : await BashTool.call({
command: inputString,
dangerouslyDisableSandbox: true
}, bashModeContext, undefined, undefined, onProgress);
const data = response.data;
if (!data) {
throw new Error('No result received from shell command');
}
const stderr = data.stderr;
// Reuse the same formatting pipeline as inline !`cmd` bash (promptShellExecution)
// and model-initiated Bash. When BashTool.call() persists large output to disk,
// data.persistedOutputPath is set and the formatter wraps in <persisted-output>.
// Pass stderr:'' to keep it separate for the <bash-stderr> UI tag.
const mapped = await processToolResultBlock(shellTool, {
...data,
stderr: ''
}, randomUUID());
// mapped.content may contain our own <persisted-output> wrapper (trusted
// XML from buildLargeToolResultMessage). Escaping it would turn structural
// tags into &lt;persisted-output&gt;, breaking the model's parse and
// UserBashOutputMessage's extractTag. Escape the raw fallback only.
const stdout = typeof mapped.content === 'string' ? mapped.content : escapeXml(data.stdout);
return {
messages: [createSyntheticUserCaveatMessage(), userMessage, ...attachmentMessages, createUserMessage({
content: `<bash-stdout>${stdout}</bash-stdout><bash-stderr>${escapeXml(stderr)}</bash-stderr>`
})],
shouldQuery: false
};
} catch (e) {
if (e instanceof ShellError) {
if (e.interrupted) {
return {
messages: [createSyntheticUserCaveatMessage(), userMessage, createUserInterruptionMessage({
toolUse: false
}), ...attachmentMessages],
shouldQuery: false
};
}
return {
messages: [createSyntheticUserCaveatMessage(), userMessage, ...attachmentMessages, createUserMessage({
content: `<bash-stdout>${escapeXml(e.stdout)}</bash-stdout><bash-stderr>${escapeXml(e.stderr)}</bash-stderr>`
})],
shouldQuery: false
};
}
return {
messages: [createSyntheticUserCaveatMessage(), userMessage, ...attachmentMessages, createUserMessage({
content: `<bash-stderr>Command failed: ${escapeXml(errorMessage(e))}</bash-stderr>`
})],
shouldQuery: false
};
} finally {
setToolJSX(null);
}
}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJuYW1lcyI6WyJDb250ZW50QmxvY2tQYXJhbSIsInJhbmRvbVVVSUQiLCJSZWFjdCIsIkJhc2hNb2RlUHJvZ3Jlc3MiLCJTZXRUb29sSlNYRm4iLCJCYXNoVG9vbCIsIkF0dGFjaG1lbnRNZXNzYWdlIiwiU3lzdGVtTWVzc2FnZSIsIlVzZXJNZXNzYWdlIiwiU2hlbGxQcm9ncmVzcyIsImxvZ0V2ZW50IiwiZXJyb3JNZXNzYWdlIiwiU2hlbGxFcnJvciIsImNyZWF0ZVN5bnRoZXRpY1VzZXJDYXZlYXRNZXNzYWdlIiwiY3JlYXRlVXNlckludGVycnVwdGlvbk1lc3NhZ2UiLCJjcmVhdGVVc2VyTWVzc2FnZSIsInByZXBhcmVVc2VyQ29udGVudCIsInJlc29sdmVEZWZhdWx0U2hlbGwiLCJpc1Bvd2VyU2hlbGxUb29sRW5hYmxlZCIsInByb2Nlc3NUb29sUmVzdWx0QmxvY2siLCJlc2NhcGVYbWwiLCJQcm9jZXNzVXNlcklucHV0Q29udGV4dCIsInByb2Nlc3NCYXNoQ29tbWFuZCIsImlucHV0U3RyaW5nIiwicHJlY2VkaW5nSW5wdXRCbG9ja3MiLCJhdHRhY2htZW50TWVzc2FnZXMiLCJjb250ZXh0Iiwic2V0VG9vbEpTWCIsIlByb21pc2UiLCJtZXNzYWdlcyIsInNob3VsZFF1ZXJ5IiwidXNlUG93ZXJTaGVsbCIsInBvd2Vyc2hlbGwiLCJ1c2VyTWVzc2FnZSIsImNvbnRlbnQiLCJqc3giLCJSZWFjdE5vZGUiLCJvcHRpb25zIiwidmVyYm9zZSIsInNob3VsZEhpZGVQcm9tcHRJbnB1dCIsImJhc2hNb2RlQ29udGV4dCIsIl8iLCJvblByb2dyZXNzIiwicHJvZ3Jlc3MiLCJkYXRhIiwic2hvd1NwaW5uZXIiLCJQU01vZCIsIlBvd2VyU2hlbGxUb29sIiwicmVxdWlyZSIsInNoZWxsVG9vbCIsInJlc3BvbnNlIiwiY2FsbCIsImNvbW1hbmQiLCJkYW5nZXJvdXNseURpc2FibGVTYW5kYm94IiwidW5kZWZpbmVkIiwiRXJyb3IiLCJzdGRlcnIiLCJtYXBwZWQiLCJzdGRvdXQiLCJlIiwiaW50ZXJydXB0ZWQiLCJ0b29sVXNlIl0sInNvdXJjZXMiOlsicHJvY2Vzc0Jhc2hDb21tYW5kLnRzeCJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgdHlwZSB7IENvbnRlbnRCbG9ja1BhcmFtIH0gZnJvbSAnQGFudGhyb3BpYy1haS9zZGsvcmVzb3VyY2VzJ1xuaW1wb3J0IHsgcmFuZG9tVVVJRCB9IGZyb20gJ2NyeXB0bydcbmltcG9ydCAqIGFzIFJlYWN0IGZyb20gJ3JlYWN0J1xuaW1wb3J0IHsgQmFzaE1vZGVQcm9ncmVzcyB9IGZyb20gJ3NyYy9jb21wb25lbnRzL0Jhc2hNb2RlUHJvZ3Jlc3MuanMnXG5pbXBvcnQgdHlwZSB7IFNldFRvb2xKU1hGbiB9IGZyb20gJ3NyYy9Ub29sLmpzJ1xuaW1wb3J0IHsgQmFzaFRvb2wgfSBmcm9tICdzcmMvdG9vbHMvQmFzaFRvb2wvQmFzaFRvb2wuanMnXG5pbXBvcnQgdHlwZSB7XG4gIEF0dGFjaG1lbnRNZXNzYWdlLFxuICBTeXN0ZW1NZXNzYWdlLFxuICBVc2VyTWVzc2FnZSxcbn0gZnJvbSAnc3JjL3R5cGVzL21lc3NhZ2UuanMnXG5pbXBvcnQgdHlwZSB7IFNoZWxsUHJvZ3Jlc3MgfSBmcm9tICdzcmMvdHlwZXMvdG9vbHMuanMnXG5pbXBvcnQgeyBsb2dFdmVudCB9IGZyb20gJy4uLy4uL3NlcnZpY2VzL2FuYWx5dGljcy9pbmRleC5qcydcbmltcG9ydCB7IGVycm9yTWVzc2FnZSwgU2hlbGxFcnJvciB9IGZyb20gJy4uL2Vycm9ycy5qcydcbmltcG9ydCB7XG4gIGNyZWF0ZVN5bnRoZXRpY1VzZXJDYXZlYXRNZXNzYWdlLFxuICBjcmVhdGVVc2VySW50ZXJydXB0aW9uTWVzc2FnZSxcbiAgY3JlYXRlVXNlck1lc3NhZ2UsXG4gIHByZXBhcmVVc2VyQ29udGVudCxcbn0gZnJvbSAnLi4vbWVzc2FnZXMuanMnXG5pbXBvcnQgeyByZXNvbHZlRGVmYXVsdFNoZWxsIH0gZnJvbSAnLi4vc2hlbGwvcmVzb2x2ZURlZmF1bHRTaGVsbC5qcydcbmltcG9ydCB7IGlzUG93ZXJTaGVsbFRvb2xFbmFibGVkIH0gZnJvbSAnLi4vc2hlbGwvc2hlbGxUb29sVXRpbHMuanMnXG5pbXBvcnQgeyBwcm9jZXNzVG9vbFJlc3VsdEJsb2NrIH0gZnJvbSAnLi4vdG9vbFJlc3VsdFN0b3JhZ2UuanMnXG5pbXBvcnQgeyBlc2NhcGVYbWwgfSBmcm9tICcuLi94bWwuanMnXG5pbXBvcnQgdHlwZSB7IFByb2Nlc3NVc2VySW5wdXRDb250ZXh0IH0gZnJvbSAnLi9wcm9jZXNzVXNlcklucHV0LmpzJ1xuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcHJvY2Vzc0Jhc2hDb21tYW5kKFxuICBpbnB1dFN0cmluZzogc3RyaW5nLFxuICBwcmVjZWRpbmdJbnB1dEJsb2NrczogQ29udGVudEJsb2NrUGFyYW1bXSxcbiAgYXR0YWNobWVudE1lc3NhZ2VzOiBBdHRhY2htZW50TWVzc2FnZVtdLFxuICBjb250ZXh0OiBQcm9jZXNzVXNlcklucHV0Q29udGV4dCxcbiAgc2V0VG9vbEpTWDogU2V0VG9vbEpTWEZuLFxuKTogUHJvbWlzZTx7XG4gIG1lc3NhZ2VzOiAoVXNlck1lc3NhZ2UgfCBBdHRhY2htZW50TWVzc2FnZSB8IFN5c3RlbU1lc3NhZ2UpW11cbiAgc2hvdWxkUXVlcnk6IGJvb2xlYW5cbn0+IHtcbiAgLy8gU2hlbGwgcm91dGluZyAoZG9jcy9kZXNpZ24vcHMtc2hlbGwtc2VsZWN0aW9uLm1kIMKnNS4yKTogY29uc3VsdFxuICAvLyBkZWZhdWx0U2hlbGwsIGZhbGwgYmFjayB0byBiYXNoLiBpc1Bvd2VyU2hlbGxUb29sRW5hYmxlZCgpIGFwcGxpZXMgdGhlXG4gIC8vIHNhbWUgcGxhdGZvcm0gKyBlbnYtdmFyIGdhdGUgYXMgdG9vbHMudHMgc28gaW5wdXQtYm94IHJvdXRpbmcgbWF0Y2hlc1xuICAvLyB0b29sLWxpc3QgdmlzaWJpbGl0eS4gQ29tcHV0ZWQgdXAgZnJvbnQgc28gdGVsZW1ldHJ5IHJlY29yZHMgdGhlXG4gIC8vIGFjdHVhbCBzaGVsbCwgbm90IHRoZSByYXcgc2V0dGluZy5cbiAgY29uc3QgdXNlUG93ZXJTaGVsbCA9XG4gICAgaXNQb3dlclNoZWxsVG9vbEVuYWJsZWQoKSAmJiByZXNvbHZlRGVmYXVsdFNoZWxsKCkgPT09ICdwb3dlcnNoZWxsJ1xuXG4gIGxvZ0V2ZW50KCd0ZW5ndV9pbnB1dF9iYXNoJywgeyBwb3dlcnNoZWxsOiB1c2VQb3dlclNoZWxsIH0pXG5cbiAgY29uc3QgdXNlck1lc3NhZ2UgPSBjcmVhdGVVc2VyTWVzc2FnZSh7XG4gICAgY29udGVudDogcHJlcGFyZVVzZXJDb250ZW50KHtcbiAgICAgIGlucHV0U3RyaW5nOiBgPGJhc2gtaW5wdXQ+JHtpbnB1dFN0cmluZ308L2Jhc2gtaW5wdXQ+YCx