adding xterm for interactive tasking responses

This commit is contained in:
its-a-feature
2026-05-03 15:24:00 -05:00
parent 749f37dec9
commit ae4b756fc0
4 changed files with 297 additions and 12 deletions
+55 -2
View File
@@ -23,6 +23,8 @@
"@mui/x-charts": "^8.1.0",
"@mui/x-data-grid": "^8.1.0",
"@mui/x-date-pickers": "^8.1.0",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"@xyflow/react": "^12.6.0",
"ace-builds": "^1.40.1",
"anser": "^2.3.2",
@@ -168,6 +170,7 @@
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
@@ -855,6 +858,7 @@
"integrity": "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.25.9"
},
@@ -1776,6 +1780,7 @@
"integrity": "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.25.9",
"@babel/helper-module-imports": "^7.25.9",
@@ -2646,6 +2651,7 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -2689,6 +2695,7 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -2885,6 +2892,7 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
@@ -3582,6 +3590,7 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.0.2.tgz",
"integrity": "sha512-rjJlJ13+3LdLfobRplkXbjIFEIkn6LgpetgU/Cs3Xd8qINCCQK9qXQIjjQ6P0FXFTPFzEVMj0VgBR1mN+FhOcA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.0",
"@mui/core-downloads-tracker": "^7.0.2",
@@ -3692,6 +3701,7 @@
"resolved": "https://registry.npmjs.org/@mui/system/-/system-7.0.2.tgz",
"integrity": "sha512-yFUraAWYWuKIISPPEVPSQ1NLeqmTT4qiQ+ktmyS8LO/KwHxB+NNVOacEZaIofh5x1NxY8rzphvU5X2heRZ/RDA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.0",
"@mui/private-theming": "^7.0.2",
@@ -5032,6 +5042,7 @@
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.62.0",
@@ -5087,6 +5098,7 @@
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
@@ -5467,6 +5479,21 @@
"node": ">=8"
}
},
"node_modules/@xterm/addon-fit": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz",
"integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==",
"license": "MIT"
},
"node_modules/@xterm/xterm": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
"integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==",
"license": "MIT",
"workspaces": [
"addons/*"
]
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -5552,6 +5579,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5658,6 +5686,7 @@
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -6814,6 +6843,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -8238,6 +8268,7 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -8402,7 +8433,8 @@
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/debug": {
"version": "4.4.0",
@@ -9266,6 +9298,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -10974,6 +11007,7 @@
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz",
"integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
@@ -10998,6 +11032,7 @@
"resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.4.tgz",
"integrity": "sha512-8b4OZtNOvv8+NZva8HXamrc0y1jluYC0+13gdh7198FKjVzXyTvVc95DCwGzaKEfn3YuWZxUqjJlHe3qKM/F2g==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20"
},
@@ -11590,6 +11625,7 @@
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -12422,6 +12458,7 @@
"integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^27.5.1",
"import-local": "^3.0.2",
@@ -14138,6 +14175,7 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"license": "MIT",
"peer": true,
"engines": {
"node": "*"
}
@@ -15008,6 +15046,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@@ -16269,6 +16308,7 @@
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -16484,6 +16524,7 @@
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -16714,6 +16755,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -16915,6 +16957,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -17005,6 +17048,7 @@
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -17053,6 +17097,7 @@
"integrity": "sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
@@ -17304,7 +17349,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
@@ -17780,6 +17826,7 @@
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"rollup": "dist/bin/rollup"
},
@@ -18043,6 +18090,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -19885,6 +19933,7 @@
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -20374,6 +20423,7 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz",
"integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -20447,6 +20497,7 @@
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5",
@@ -20858,6 +20909,7 @@
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -21177,6 +21229,7 @@
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"devOptional": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},
+2
View File
@@ -28,6 +28,8 @@
"@mui/x-charts": "^8.1.0",
"@mui/x-data-grid": "^8.1.0",
"@mui/x-date-pickers": "^8.1.0",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"@xyflow/react": "^12.6.0",
"ace-builds": "^1.40.1",
"anser": "^2.3.2",
@@ -14,6 +14,9 @@ import Anser from "anser";
import PaletteIcon from '@mui/icons-material/Palette';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import './ResponseDisplayInteractiveANSI.css';
import {Terminal} from '@xterm/xterm';
import {FitAddon} from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css';
import KeyboardReturnIcon from '@mui/icons-material/KeyboardReturn';
import InputLabel from "@mui/material/InputLabel";
import FormControl from "@mui/material/FormControl";
@@ -224,6 +227,225 @@ export const GetOutputFormatAll = ({data, useASNIColor, messagesEndRef, showTask
}
const INTERACTIVE_TERMINAL_SCROLLBACK = 5000;
const INTERACTIVE_TERMINAL_MAX_UNWRAPPED_COLS = 300;
const ANSI_STYLE_CODE_REGEX = /\x1b\[[0-?]*[ -/]*m/g;
const TERMINAL_CONTROL_CODE_REGEX = /\x1b\][^\x07]*(?:\x07|\x1b\\)|\x1b\[[0-?]*[ -/]*[@-~]|\x1b[ -/]*[@-~]/g;
const stripAnsiStyleCodes = (value) => {
return value.replace(ANSI_STYLE_CODE_REGEX, "");
}
const getTerminalText = (value) => {
return value === undefined || value === null ? "" : String(value);
}
const getInteractiveTerminalTheme = (theme) => {
return {
background: theme.outputBackgroundColor,
foreground: theme.outputTextColor,
cursor: theme.palette.info.main,
selectionBackground: theme.palette.info.main + "55",
}
}
const getInteractiveTerminalEntrySignature = (entry) => {
if(entry.response !== undefined){
return `response:${entry.id}:${entry.timestamp}:${entry.is_error}:${entry.response.length}`;
}
return `task:${entry.id}:${entry.status}:${entry.original_params}:${entry.display_params}`;
}
const getInteractiveTerminalMeasurementText = (value) => {
return getTerminalText(value).replace(TERMINAL_CONTROL_CODE_REGEX, "");
}
const getInteractiveTerminalEntryMeasurementText = (entry) => {
if(entry.response !== undefined){
return getInteractiveTerminalMeasurementText(entry.response);
}
return getInteractiveTerminalMeasurementText(entry.original_params || entry.display_params);
}
const getLongestInteractiveTerminalLineLength = (entries) => {
return entries.reduce( (longestLineLength, entry) => {
return getInteractiveTerminalEntryMeasurementText(entry).split(/\r\n|\n|\r/).reduce( (longestEntryLineLength, line) => {
return Math.max(longestEntryLineLength, Array.from(line.replaceAll("\t", " ")).length);
}, longestLineLength);
}, 0);
}
const getInteractiveTerminalTaskStatus = ({task, useASNIColor}) => {
if(task.status === "completed" || task.status === "success"){
return useASNIColor ? "\x1b[32m[completed]\x1b[0m " : "[completed] ";
}
if(task.status === "submitted"){
return useASNIColor ? "\x1b[33m[submitted]\x1b[0m " : "[submitted] ";
}
if(task.status === "error"){
return useASNIColor ? "\x1b[31m[error]\x1b[0m " : "[error] ";
}
return task.status ? `[${task.status}] ` : "";
}
const formatInteractiveTerminalEntry = ({entry, useASNIColor, showTaskStatus}) => {
if(entry.response !== undefined){
const response = useASNIColor ? getTerminalText(entry.response) : stripAnsiStyleCodes(getTerminalText(entry.response));
if(entry.is_error && useASNIColor){
return `\x1b[37;41m${response}\x1b[0m`;
}
return response;
}
const status = showTaskStatus ? getInteractiveTerminalTaskStatus({task: entry, useASNIColor}) : "";
const prompt = useASNIColor ? "\x1b[36m> \x1b[0m" : "> ";
return `\r\n${status}${prompt}${getTerminalText(entry.original_params || entry.display_params)}\r\n`;
}
const InteractiveTerminalDisplay = ({data, useASNIColor, showTaskStatus, wrapText, autoScroll, theme}) => {
const terminalScrollContainerRef = React.useRef(null);
const terminalElementRef = React.useRef(null);
const terminalRef = React.useRef(null);
const fitAddonRef = React.useRef(null);
const resizeObserverRef = React.useRef(null);
const replayStateRef = React.useRef({settingsKey: "", signatures: []});
const autoScrollRef = React.useRef(autoScroll);
const wrapTextRef = React.useRef(wrapText);
const unwrappedColumnCountRef = React.useRef(0);
const [terminalReady, setTerminalReady] = React.useState(false);
const longestLineLength = React.useMemo( () => {
return getLongestInteractiveTerminalLineLength(data);
}, [data]);
const fitTerminal = React.useCallback( () => {
const terminal = terminalRef.current;
const fitAddon = fitAddonRef.current;
const terminalElement = terminalElementRef.current;
const scrollContainer = terminalScrollContainerRef.current;
if(!terminal || !fitAddon || !terminalElement || !scrollContainer){
return;
}
try{
terminalElement.style.width = "100%";
const proposedDimensions = fitAddon.proposeDimensions();
if(!proposedDimensions){
return;
}
const rows = Math.max(1, proposedDimensions.rows);
if(wrapTextRef.current){
fitAddon.fit();
scrollContainer.scrollLeft = 0;
} else {
const visibleCols = Math.max(1, proposedDimensions.cols);
const cols = Math.max(visibleCols, unwrappedColumnCountRef.current);
terminalElement.style.width = `${Math.ceil((cols / visibleCols) * scrollContainer.clientWidth)}px`;
terminal.resize(cols, rows);
}
}catch(error){
console.error(error);
}
}, []);
React.useEffect( () => {
autoScrollRef.current = autoScroll;
if(autoScroll){
terminalRef.current?.scrollToBottom();
}
}, [autoScroll]);
React.useEffect( () => {
wrapTextRef.current = wrapText;
fitTerminal();
}, [wrapText, fitTerminal]);
React.useEffect( () => {
if(wrapText){
unwrappedColumnCountRef.current = 0;
} else {
unwrappedColumnCountRef.current = Math.min(Math.max(longestLineLength + 2, 1), INTERACTIVE_TERMINAL_MAX_UNWRAPPED_COLS);
}
fitTerminal();
}, [fitTerminal, longestLineLength, wrapText]);
React.useEffect( () => {
if(!terminalElementRef.current){
return;
}
const terminal = new Terminal({
allowTransparency: true,
convertEol: true,
cursorBlink: true,
disableStdin: true,
fontFamily: 'Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: 13,
scrollback: INTERACTIVE_TERMINAL_SCROLLBACK,
theme: getInteractiveTerminalTheme(theme),
});
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(terminalElementRef.current);
terminalRef.current = terminal;
fitAddonRef.current = fitAddon;
resizeObserverRef.current = new ResizeObserver(() => fitTerminal());
if(terminalScrollContainerRef.current){
resizeObserverRef.current.observe(terminalScrollContainerRef.current);
}
window.requestAnimationFrame(() => {
fitTerminal();
setTerminalReady(true);
});
return () => {
resizeObserverRef.current?.disconnect();
resizeObserverRef.current = null;
fitAddonRef.current = null;
terminalRef.current = null;
terminal.dispose();
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
React.useEffect( () => {
if(terminalRef.current){
terminalRef.current.options.theme = getInteractiveTerminalTheme(theme);
}
}, [theme]);
React.useEffect( () => {
if(!terminalReady || !terminalRef.current){
return;
}
const terminal = terminalRef.current;
fitTerminal();
const nextSignatures = data.map(getInteractiveTerminalEntrySignature);
const settingsKey = `${useASNIColor}:${showTaskStatus}:${wrapText}`;
const previousReplayState = replayStateRef.current;
const canAppend = previousReplayState.settingsKey === settingsKey &&
nextSignatures.length >= previousReplayState.signatures.length &&
previousReplayState.signatures.every((signature, index) => signature === nextSignatures[index]);
const previousViewportY = terminal.buffer.active.viewportY;
const entriesToWrite = canAppend ? data.slice(previousReplayState.signatures.length) : data;
if(!canAppend){
terminal.reset();
}
const terminalOutput = entriesToWrite.map( (entry) => formatInteractiveTerminalEntry({
entry,
useASNIColor,
showTaskStatus,
})).join("");
replayStateRef.current = {
settingsKey,
signatures: nextSignatures,
};
const afterWrite = () => {
if(autoScrollRef.current){
terminal.scrollToBottom();
} else {
terminal.scrollToLine(previousViewportY);
}
};
if(terminalOutput.length > 0){
terminal.write(terminalOutput, afterWrite);
} else {
afterWrite();
}
}, [data, showTaskStatus, terminalReady, useASNIColor, wrapText]);
return (
<div
ref={terminalScrollContainerRef}
className={"MythicInteractiveTerminal"}
style={{
height: "100%",
minHeight: "50px",
overflowX: wrapText ? "hidden" : "auto",
overflowY: "hidden",
width: "100%",
}}>
<div ref={terminalElementRef} style={{height: "100%", width: "100%"}} />
</div>
)
}
const InteractiveMessageTypes = [
{"name": "None", "value": -1, "text": "None"},
{"name": "Tab", "value": 13, "text": "^I"},
@@ -267,7 +489,6 @@ export const ResponseDisplayInteractive = (props) =>{
const [search, setSearch] = React.useState("");
const [totalCount, setTotalCount] = React.useState(0);
const totalCountRef = React.useRef(0);
const messagesEndRef = React.useRef();
const [currentPage, setCurrentPage] = React.useState(1);
const currentPageRef = React.useRef(1);
const taskResponseCountRef = React.useRef(props.task.response_count || 0);
@@ -503,16 +724,15 @@ export const ResponseDisplayInteractive = (props) =>{
{props.searchOutput &&
<SearchBar onSubmitSearch={onSubmitSearch}/>
}
<div style={{overflowY: "auto", width: "100%", marginBottom: "5px", height: props.expand ? "100%": undefined,
flexGrow: 1, paddingLeft: "10px", minHeight: "50px"}} ref={props.responseRef}
<div style={{overflow: "hidden", width: "100%", marginBottom: "5px", height: props.expand ? "100%": undefined,
flexGrow: 1, minHeight: "50px"}} ref={props.responseRef}
id={`ptytask${props.task.id}`}>
<GetOutputFormatAll data={visibleOutput}
useASNIColor={useASNIColor}
messagesEndRef={messagesEndRef}
showTaskStatus={showTaskStatus}
wrapText={wrapText}
autoScroll={autoScroll}/>
<div ref={messagesEndRef}/>
<InteractiveTerminalDisplay data={visibleOutput}
useASNIColor={useASNIColor}
showTaskStatus={showTaskStatus}
wrapText={wrapText}
autoScroll={autoScroll}
theme={theme}/>
</div>
{!props.task?.is_interactive_task &&
<div style={{width: "100%", display: "inline-flex", alignItems: "flex-end"}}>
+10
View File
@@ -269,6 +269,16 @@ tspan {
overflow: hidden;
text-overflow: ellipsis;
}
.MythicInteractiveTerminal {
background-color: ${(props) => props.theme.outputBackgroundColor};
}
.MythicInteractiveTerminal .xterm {
height: 100%;
padding: 6px 8px;
}
.MythicInteractiveTerminal .xterm-viewport {
background-color: transparent !important;
}
.Toastify__toast {
word-break: break-all;
white-space: pre-wrap !important;