mirror of
https://github.com/EthanShoeDev/fressh.git
synced 2026-01-10 22:02:50 +00:00
shell integration
This commit is contained in:
@@ -141,6 +141,10 @@ function ShellDetail() {
|
||||
logger.warn('sendData failed', e);
|
||||
router.back();
|
||||
});
|
||||
|
||||
xtermRef.current?.getRecentCommands(10).then((commands) => {
|
||||
logger.info('recent commands', commands);
|
||||
});
|
||||
},
|
||||
[shell, router, modifierKeysActive],
|
||||
);
|
||||
|
||||
@@ -45,6 +45,9 @@ export const useSshStore = create<SshRegistryStore>((set) => ({
|
||||
return { shells: rest };
|
||||
});
|
||||
},
|
||||
}).catch((e) => {
|
||||
logger.error('error starting shell', e.name, e.message);
|
||||
throw e;
|
||||
});
|
||||
const storeKey = `${connection.connectionId}-${shell.channelId}`;
|
||||
set((s) => ({
|
||||
|
||||
132
docs/projects/shell-integration.md
Normal file
132
docs/projects/shell-integration.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Add command detection and history to the RN xterm WebView
|
||||
|
||||
## What we'll build
|
||||
|
||||
- In-WebView OSC-633 parser for VS Code shell integration sequences (A/B/C/D/E,
|
||||
P=Cwd) to get exact command boundaries when present. Falls back to heuristics
|
||||
(Enter-based + prompt learning + alt-screen guard) when sequences are absent.
|
||||
No persistent server install required; optional ephemeral per-session sourcing
|
||||
is supported later.
|
||||
- Ring buffers in WebView to store last N commands and their outputs, with size
|
||||
caps.
|
||||
- New bridge messages and imperative methods so RN can query: last N commands,
|
||||
last N outputs, and a specific command’s output.
|
||||
|
||||
## Key files to change
|
||||
|
||||
- packages/react-native-xtermjs-webview/src-internal/main.tsx
|
||||
- Register xterm OSC handler for 633; parse “A/B/C/D/E” and “P;Cwd=…”.
|
||||
- Track command state and outputs; implement a capped in-memory store.
|
||||
- Respect a runtime flag from injected options to fully disable command
|
||||
tracking/history.
|
||||
- Add message handler for queries (from RN) and send responses.
|
||||
|
||||
- packages/react-native-xtermjs-webview/src/bridge.ts
|
||||
- Extend `BridgeOutboundMessage` (RN→WebView) with query messages.
|
||||
- Extend `BridgeInboundMessage` (WebView→RN) with responses and optional
|
||||
events.
|
||||
|
||||
- packages/react-native-xtermjs-webview/src/index.tsx
|
||||
- Add prop `enableCommandHistory?: boolean` (default true). When false, do not
|
||||
enable OSC handlers/heuristics or allocate history in the WebView.
|
||||
- Extend `XtermWebViewHandle` with:
|
||||
- `getRecentCommands(limit?: number)`
|
||||
- `getRecentOutputs(limit?: number)`
|
||||
- `getCommandOutput(id: string)`
|
||||
- `clearHistory()`
|
||||
- Implement a simple request/response over `injectJavaScript` using
|
||||
correlation IDs.
|
||||
|
||||
- apps/mobile/src/app/(tabs)/shell/detail.tsx
|
||||
- Show example usage via the existing `xtermRef` to fetch recent
|
||||
commands/outputs on demand.
|
||||
|
||||
## Implementation details
|
||||
|
||||
- OSC-633 parsing
|
||||
- Use xterm proposed API to register an OSC handler when available:
|
||||
`terminal.parser.registerOscHandler(633, handler)`.
|
||||
- Handle sequences per VS Code docs: `A` (prompt start), `B` (prompt end), `C`
|
||||
(pre-exec), `D[;code]` (post-exec), `E;<escapedCmd>[;nonce]`, `P;Cwd=…`.
|
||||
- References: VS Code docs and sources.
|
||||
|
||||
- Heuristic fallback (no sequences)
|
||||
- Track local keystrokes (we already have them) to build a transient “input
|
||||
line buffer”.
|
||||
- On Enter (\r or \r\n) outside alt-screen (CSI ? 1049/47/1047 toggles), emit
|
||||
a best-effort “command started”.
|
||||
- Detect new prompt by screen change patterns (stable prompt prefix on the
|
||||
left) to close a command when possible; otherwise time out and roll forward.
|
||||
- Disable detection while in full-screen TUIs (alt screen), and ignore
|
||||
bracketed paste blocks.
|
||||
|
||||
- Storage
|
||||
- Maintain two ring buffers with caps (defaults: 100 commands, 1 MB output per
|
||||
command; configurable via injected options later).
|
||||
- Each entry:
|
||||
`{ id, command, startTime, endTime?, exitCode?, cwd?, outputBytes[] }`.
|
||||
|
||||
- Bridge extensions
|
||||
- RN→WebView: `{ type: 'history:getCommands', limit? }`,
|
||||
`{ type: 'history:getOutputs', limit? }`,
|
||||
`{ type: 'history:getOutput', id }`, `{ type: 'history:clear' }`.
|
||||
- WebView→RN responses: `{ type: 'history:commands', corr, items }`,
|
||||
`{ type: 'history:outputs', corr, items }`,
|
||||
`{ type: 'history:output', corr, item }`,
|
||||
`{ type: 'history:cleared', corr }`.
|
||||
- Optional event stream for live updates: `{ type: 'history:event', event }`.
|
||||
|
||||
- Imperative handle
|
||||
- Implement methods that send queries with a correlation ID and await the
|
||||
matching response via `onMessage`.
|
||||
|
||||
## Critical code touchpoints
|
||||
|
||||
- Where to hook OSC parsing and input/output
|
||||
|
||||
```150:176:packages/react-native-xtermjs-webview/src-internal/main.tsx
|
||||
term.onData((data) => {
|
||||
sendToRn({ type: 'input', str: data });
|
||||
});
|
||||
```
|
||||
|
||||
- Where to expose new methods
|
||||
|
||||
```231:246:packages/react-native-xtermjs-webview/src/index.tsx
|
||||
useImperativeHandle(ref, () => ({
|
||||
write,
|
||||
writeMany,
|
||||
flush,
|
||||
clear: () => sendToWebView({ type: 'clear' }),
|
||||
focus: () => {
|
||||
sendToWebView({ type: 'focus' });
|
||||
webRef.current?.requestFocus();
|
||||
},
|
||||
resize: (size: { cols: number; rows: number }) => {
|
||||
sendToWebView({ type: 'resize', cols: size.cols, rows: size.rows });
|
||||
autoFitFn();
|
||||
appliedSizeRef.current = size;
|
||||
},
|
||||
fit,
|
||||
}));
|
||||
```
|
||||
|
||||
## Optional enhancement (no install, ephemeral session-only)
|
||||
|
||||
- On session open, send a one-shot, in-memory sourced shell snippet
|
||||
(bash/zsh/fish/pwsh) to enable OSC 633 for that session only. No files written
|
||||
server-side. If disabled by user, fallback to heuristics.
|
||||
|
||||
## References
|
||||
|
||||
- VS Code Shell Integration docs (OSC 633, iTerm/FinalTerm sequences)
|
||||
[Terminal Shell Integration](https://code.visualstudio.com/docs/terminal/shell-integration)
|
||||
- VS Code sources: `shellIntegrationAddon.ts`, `commandDetectionCapability.ts`,
|
||||
`terminalEnvironment.ts`:
|
||||
- [shellIntegrationAddon.ts](https://github.com/microsoft/vscode/blob/main/src/vs/platform/terminal/common/xterm/shellIntegrationAddon.ts)
|
||||
- [commandDetectionCapability.ts](https://github.com/microsoft/vscode/blob/main/src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts)
|
||||
- [terminalEnvironment.ts](https://github.com/microsoft/vscode/blob/main/src/vs/platform/terminal/node/terminalEnvironment.ts)
|
||||
- Shell scripts (for optional ephemeral sourcing):
|
||||
- [shellIntegration-bash.sh](https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh)
|
||||
- [shellIntegration.ps1](https://cocalc.com/github/microsoft/vscode/blob/main/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1?utm_source=chatgpt.com)
|
||||
- [Fish integration discussion](https://github.com/microsoft/vscode/issues/184659?utm_source=chatgpt.com)
|
||||
@@ -3,8 +3,11 @@ import { Terminal, type ITerminalOptions } from '@xterm/xterm';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
import {
|
||||
bStrToBinary,
|
||||
binaryToBStr,
|
||||
type BridgeInboundMessage,
|
||||
type BridgeOutboundMessage,
|
||||
type CommandMeta,
|
||||
type OutputMeta,
|
||||
} from '../src/bridge';
|
||||
|
||||
declare global {
|
||||
@@ -52,7 +55,11 @@ window.onload = () => {
|
||||
|
||||
window.__FRESSH_XTERM_BRIDGE__ = true;
|
||||
|
||||
const injectedObject = JSON.parse(injectedObjectJson) as ITerminalOptions;
|
||||
const injectedObject = JSON.parse(
|
||||
injectedObjectJson,
|
||||
) as ITerminalOptions & {
|
||||
__fresshEnableCommandHistory?: boolean;
|
||||
};
|
||||
|
||||
// ---- Xterm setup
|
||||
const term = new Terminal(injectedObject);
|
||||
@@ -67,10 +74,169 @@ window.onload = () => {
|
||||
window.terminal = term;
|
||||
window.fitAddon = fitAddon;
|
||||
|
||||
let sawOsc633 = false;
|
||||
let inputBuffer = '';
|
||||
term.onData((data) => {
|
||||
sendToRn({ type: 'input', str: data });
|
||||
if (enableHistory && !sawOsc633) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const ch = data[i];
|
||||
if (ch === '\\r' || ch === '\\n') {
|
||||
if (current) finishCommand(undefined);
|
||||
const cmd = inputBuffer.trim();
|
||||
inputBuffer = '';
|
||||
if (cmd.length > 0) startCommand(cmd);
|
||||
} else {
|
||||
inputBuffer += ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Command history tracking (OSC 633 and minimal heuristics)
|
||||
const enableHistory = injectedObject.__fresshEnableCommandHistory ?? true;
|
||||
|
||||
type MutableCommand = CommandMeta & {
|
||||
_output: Uint8Array;
|
||||
_truncated: boolean;
|
||||
};
|
||||
const maxCommands = 100;
|
||||
const maxBytesPerCommand = 1 * 1024 * 1024; // 1MB
|
||||
|
||||
const commands: MutableCommand[] = [];
|
||||
let current: MutableCommand | null = null;
|
||||
let cwd: string | undefined = undefined;
|
||||
|
||||
function pushCommand(cmd: MutableCommand) {
|
||||
if (commands.length >= maxCommands) commands.shift();
|
||||
commands.push(cmd);
|
||||
}
|
||||
|
||||
function startCommand(command?: string) {
|
||||
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
current = {
|
||||
id,
|
||||
command,
|
||||
startTime: Date.now(),
|
||||
cwd,
|
||||
_output: new Uint8Array(0),
|
||||
_truncated: false,
|
||||
};
|
||||
sendToRn({
|
||||
type: 'history:event',
|
||||
event: { kind: 'commandStarted', meta: { ...current } },
|
||||
});
|
||||
}
|
||||
|
||||
function finishCommand(exitCode?: number) {
|
||||
if (!current) return;
|
||||
current.endTime = Date.now();
|
||||
if (exitCode != null) current.exitCode = exitCode;
|
||||
pushCommand(current);
|
||||
sendToRn({
|
||||
type: 'history:event',
|
||||
event: { kind: 'commandFinished', meta: { ...current } },
|
||||
});
|
||||
current = null;
|
||||
}
|
||||
|
||||
function appendOutput(bytes: Uint8Array) {
|
||||
if (!current) return;
|
||||
if (current._truncated) return;
|
||||
const newLen = current._output.length + bytes.length;
|
||||
if (newLen > maxBytesPerCommand) {
|
||||
const allowed = Math.max(
|
||||
0,
|
||||
maxBytesPerCommand - current._output.length,
|
||||
);
|
||||
if (allowed > 0) {
|
||||
const merged = new Uint8Array(current._output.length + allowed);
|
||||
merged.set(current._output, 0);
|
||||
merged.set(bytes.subarray(0, allowed), current._output.length);
|
||||
current._output = merged;
|
||||
}
|
||||
current._truncated = true;
|
||||
return;
|
||||
}
|
||||
const merged = new Uint8Array(newLen);
|
||||
merged.set(current._output, 0);
|
||||
merged.set(bytes, current._output.length);
|
||||
current._output = merged;
|
||||
}
|
||||
|
||||
if (enableHistory) {
|
||||
// OSC 633 handler
|
||||
try {
|
||||
term.parser.registerOscHandler(633, (data: string) => {
|
||||
sawOsc633 = true;
|
||||
// data like: 'A' | 'B' | 'C' | 'D;0' | 'E;...escapedCmd[;nonce]' | 'P;Cwd=/path'
|
||||
if (!data) return true;
|
||||
const semi = data.indexOf(';');
|
||||
const tag = semi === -1 ? data : data.slice(0, semi);
|
||||
const rest = semi === -1 ? '' : data.slice(semi + 1);
|
||||
switch (tag) {
|
||||
case 'P': {
|
||||
// property
|
||||
// format: Cwd=<cwd>
|
||||
const eq = rest.indexOf('=');
|
||||
if (eq !== -1) {
|
||||
const key = rest.slice(0, eq);
|
||||
const value = rest.slice(eq + 1);
|
||||
if (key === 'Cwd') {
|
||||
cwd = value;
|
||||
sendToRn({
|
||||
type: 'history:event',
|
||||
event: { kind: 'cwdChanged', cwd: value },
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case 'E': {
|
||||
// explicit command line
|
||||
// command is escaped: requires unescaping \ and \xAB (hex)
|
||||
let cmdRaw = rest;
|
||||
const semi2 = cmdRaw.indexOf(';');
|
||||
if (semi2 !== -1) cmdRaw = cmdRaw.slice(0, semi2);
|
||||
// unescape
|
||||
const unescaped = cmdRaw
|
||||
.replace(/\\x([0-9a-fA-F]{2})/g, (_m, p1) =>
|
||||
String.fromCharCode(parseInt(p1, 16)),
|
||||
)
|
||||
.replace(/\\\\/g, '\\');
|
||||
// save for upcoming run if we start immediately
|
||||
if (current) current.command = unescaped;
|
||||
else startCommand(unescaped);
|
||||
return true;
|
||||
}
|
||||
case 'C': {
|
||||
// pre-execution
|
||||
if (!current) startCommand();
|
||||
return true;
|
||||
}
|
||||
case 'D': {
|
||||
// execution finished
|
||||
let code: number | undefined = undefined;
|
||||
if (rest) {
|
||||
const n = Number(rest);
|
||||
if (!Number.isNaN(n)) code = n;
|
||||
}
|
||||
finishCommand(code);
|
||||
return true;
|
||||
}
|
||||
// 'A' and 'B' (prompt markers) are ignored for storage here
|
||||
default:
|
||||
return true; // swallow
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
sendToRn({
|
||||
type: 'debug',
|
||||
message: `OSC633 handler error: ${String(e)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old handler if any (just in case)
|
||||
if (window.__FRESSH_XTERM_MSG_HANDLER__)
|
||||
window.removeEventListener(
|
||||
@@ -78,7 +244,7 @@ window.onload = () => {
|
||||
window.__FRESSH_XTERM_MSG_HANDLER__!,
|
||||
);
|
||||
|
||||
// RN -> WebView handler (write, resize, setFont, setTheme, setOptions, clear, focus)
|
||||
// RN -> WebView handler (write, resize, setFont, setTheme, setOptions, clear, focus, history queries)
|
||||
const handler = (e: MessageEvent<BridgeOutboundMessage>) => {
|
||||
try {
|
||||
const msg = e.data;
|
||||
@@ -89,6 +255,7 @@ window.onload = () => {
|
||||
const termWrite = (bStr: string) => {
|
||||
const bytes = bStrToBinary(bStr);
|
||||
term.write(bytes);
|
||||
if (enableHistory) appendOutput(bytes);
|
||||
};
|
||||
|
||||
switch (msg.type) {
|
||||
@@ -140,6 +307,59 @@ window.onload = () => {
|
||||
term.focus();
|
||||
break;
|
||||
}
|
||||
case 'history:getCommands': {
|
||||
const items: CommandMeta[] = commands.map((c) => ({
|
||||
id: c.id,
|
||||
command: c.command,
|
||||
startTime: c.startTime,
|
||||
endTime: c.endTime,
|
||||
exitCode: c.exitCode,
|
||||
cwd: c.cwd,
|
||||
}));
|
||||
const lim = msg.limit ?? items.length;
|
||||
sendToRn({
|
||||
type: 'history:commands',
|
||||
corr: msg.corr,
|
||||
items: items.slice(-lim),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'history:getOutputs': {
|
||||
const items: OutputMeta[] = commands.map((c) => ({
|
||||
id: c.id,
|
||||
byteLength: c._output.length,
|
||||
}));
|
||||
const lim = msg.limit ?? items.length;
|
||||
sendToRn({
|
||||
type: 'history:outputs',
|
||||
corr: msg.corr,
|
||||
items: items.slice(-lim),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'history:getOutput': {
|
||||
const found = commands.find((c) => c.id === msg.id);
|
||||
if (!found) {
|
||||
sendToRn({
|
||||
type: 'history:output',
|
||||
corr: msg.corr,
|
||||
item: undefined,
|
||||
});
|
||||
break;
|
||||
}
|
||||
sendToRn({
|
||||
type: 'history:output',
|
||||
corr: msg.corr,
|
||||
item: { id: found.id, bytesB64: binaryToBStr(found._output) },
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'history:clear': {
|
||||
commands.length = 0;
|
||||
current = null;
|
||||
sendToRn({ type: 'history:cleared', corr: msg.corr });
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
sendToRn({
|
||||
|
||||
@@ -1,11 +1,42 @@
|
||||
import { Base64 } from 'js-base64';
|
||||
type ITerminalOptions = import('@xterm/xterm').ITerminalOptions;
|
||||
type ITerminalInitOnlyOptions = import('@xterm/xterm').ITerminalInitOnlyOptions;
|
||||
|
||||
// ---- History payload shapes
|
||||
export type CommandMeta = {
|
||||
id: string;
|
||||
command?: string;
|
||||
startTime: number; // epoch ms
|
||||
endTime?: number; // epoch ms
|
||||
exitCode?: number;
|
||||
cwd?: string;
|
||||
};
|
||||
|
||||
export type OutputMeta = {
|
||||
id: string;
|
||||
byteLength: number;
|
||||
};
|
||||
|
||||
export type OutputItemB64 = {
|
||||
id: string;
|
||||
bytesB64: string; // base64-encoded bytes
|
||||
};
|
||||
|
||||
export type HistoryEvent =
|
||||
| { kind: 'commandStarted'; meta: CommandMeta }
|
||||
| { kind: 'commandFinished'; meta: CommandMeta }
|
||||
| { kind: 'cwdChanged'; cwd: string };
|
||||
|
||||
// Messages posted from the WebView (xterm page) to React Native
|
||||
export type BridgeInboundMessage =
|
||||
| { type: 'initialized' }
|
||||
| { type: 'input'; str: string }
|
||||
| { type: 'debug'; message: string };
|
||||
| { type: 'debug'; message: string }
|
||||
| { type: 'history:commands'; corr: string; items: CommandMeta[] }
|
||||
| { type: 'history:outputs'; corr: string; items: OutputMeta[] }
|
||||
| { type: 'history:output'; corr: string; item?: OutputItemB64 }
|
||||
| { type: 'history:cleared'; corr: string }
|
||||
| { type: 'history:event'; event: HistoryEvent };
|
||||
|
||||
// Messages injected from React Native into the WebView (xterm page)
|
||||
export type BridgeOutboundMessage =
|
||||
@@ -18,7 +49,11 @@ export type BridgeOutboundMessage =
|
||||
opts: Partial<Omit<ITerminalOptions, keyof ITerminalInitOnlyOptions>>;
|
||||
}
|
||||
| { type: 'clear' }
|
||||
| { type: 'focus' };
|
||||
| { type: 'focus' }
|
||||
| { type: 'history:getCommands'; corr: string; limit?: number }
|
||||
| { type: 'history:getOutputs'; corr: string; limit?: number }
|
||||
| { type: 'history:getOutput'; corr: string; id: string }
|
||||
| { type: 'history:clear'; corr: string };
|
||||
|
||||
export const binaryToBStr = (binary: Uint8Array): string =>
|
||||
Base64.fromUint8Array(binary);
|
||||
|
||||
@@ -40,6 +40,14 @@ export type XtermWebViewHandle = {
|
||||
focus: () => void;
|
||||
resize: (size: { cols: number; rows: number }) => void;
|
||||
fit: () => void;
|
||||
getRecentCommands: (
|
||||
limit?: number,
|
||||
) => Promise<import('./bridge').CommandMeta[]>;
|
||||
getRecentOutputs: (
|
||||
limit?: number,
|
||||
) => Promise<import('./bridge').OutputMeta[]>;
|
||||
getCommandOutput: (id: string) => Promise<Uint8Array | null>;
|
||||
clearHistory: () => Promise<void>;
|
||||
};
|
||||
|
||||
const defaultWebViewProps: WebViewOptions = {
|
||||
@@ -95,6 +103,7 @@ export type XtermJsWebViewProps = {
|
||||
rows: number;
|
||||
};
|
||||
autoFit?: boolean;
|
||||
enableCommandHistory?: boolean; // default true
|
||||
};
|
||||
|
||||
function xTermOptionsEquals(
|
||||
@@ -126,10 +135,30 @@ export function XtermJsWebView({
|
||||
logger,
|
||||
size,
|
||||
autoFit = true,
|
||||
enableCommandHistory = true,
|
||||
}: XtermJsWebViewProps) {
|
||||
const webRef = useRef<WebView>(null);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
// Request/response correlation for history APIs
|
||||
const pendingRef = useRef(
|
||||
new Map<
|
||||
string,
|
||||
{
|
||||
resolve: (v: unknown) => void;
|
||||
reject: (e: unknown) => void;
|
||||
type:
|
||||
| 'history:commands'
|
||||
| 'history:outputs'
|
||||
| 'history:output'
|
||||
| 'history:cleared';
|
||||
}
|
||||
>(),
|
||||
);
|
||||
const genCorr = useCallback(() => {
|
||||
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}, []);
|
||||
|
||||
// ---- RN -> WebView message sender
|
||||
const sendToWebView = useCallback(
|
||||
(obj: BridgeOutboundMessage) => {
|
||||
@@ -243,6 +272,53 @@ export function XtermJsWebView({
|
||||
appliedSizeRef.current = size;
|
||||
},
|
||||
fit,
|
||||
getRecentCommands: (limit?: number) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const corr = genCorr();
|
||||
pendingRef.current.set(corr, {
|
||||
resolve: (v: unknown) =>
|
||||
resolve(v as unknown as import('./bridge').CommandMeta[]),
|
||||
reject: (e: unknown) => reject(e as unknown as never),
|
||||
type: 'history:commands',
|
||||
});
|
||||
sendToWebView({ type: 'history:getCommands', corr, limit });
|
||||
});
|
||||
},
|
||||
getRecentOutputs: (limit?: number) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const corr = genCorr();
|
||||
pendingRef.current.set(corr, {
|
||||
resolve: (v: unknown) =>
|
||||
resolve(v as unknown as import('./bridge').OutputMeta[]),
|
||||
reject: (e: unknown) => reject(e as unknown as never),
|
||||
type: 'history:outputs',
|
||||
});
|
||||
sendToWebView({ type: 'history:getOutputs', corr, limit });
|
||||
});
|
||||
},
|
||||
getCommandOutput: (id: string) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const corr = genCorr();
|
||||
pendingRef.current.set(corr, {
|
||||
resolve: (v: unknown) =>
|
||||
resolve((v as unknown as Uint8Array | null) ?? null),
|
||||
reject: (e: unknown) => reject(e as unknown as never),
|
||||
type: 'history:output',
|
||||
});
|
||||
sendToWebView({ type: 'history:getOutput', corr, id });
|
||||
});
|
||||
},
|
||||
clearHistory: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const corr = genCorr();
|
||||
pendingRef.current.set(corr, {
|
||||
resolve: () => resolve(),
|
||||
reject: (e: unknown) => reject(e as unknown as never),
|
||||
type: 'history:cleared',
|
||||
});
|
||||
sendToWebView({ type: 'history:clear', corr });
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
const mergedXTermOptions = useMemo(
|
||||
@@ -287,6 +363,25 @@ export function XtermJsWebView({
|
||||
logger?.log?.(`received debug msg from webview: `, msg.message);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
msg.type === 'history:commands' ||
|
||||
msg.type === 'history:outputs' ||
|
||||
msg.type === 'history:output' ||
|
||||
msg.type === 'history:cleared'
|
||||
) {
|
||||
const pending = pendingRef.current.get(msg.corr);
|
||||
if (pending && pending.type === msg.type) {
|
||||
pendingRef.current.delete(msg.corr);
|
||||
if (msg.type === 'history:commands') pending.resolve(msg.items);
|
||||
else if (msg.type === 'history:outputs') pending.resolve(msg.items);
|
||||
else if (msg.type === 'history:output') {
|
||||
if (!msg.item) pending.resolve(null);
|
||||
else pending.resolve(bStrToBinary(msg.item.bytesB64));
|
||||
} else if (msg.type === 'history:cleared')
|
||||
pending.resolve(undefined);
|
||||
return;
|
||||
}
|
||||
}
|
||||
webViewOptions?.onMessage?.(e);
|
||||
} catch (error) {
|
||||
logger?.warn?.(
|
||||
@@ -349,7 +444,10 @@ export function XtermJsWebView({
|
||||
source={{ html: htmlString }}
|
||||
onMessage={onMessage}
|
||||
style={style}
|
||||
injectedJavaScriptObject={mergedXTermOptions}
|
||||
injectedJavaScriptObject={{
|
||||
...mergedXTermOptions,
|
||||
__fresshEnableCommandHistory: enableCommandHistory,
|
||||
}}
|
||||
injectedJavaScriptBeforeContentLoaded={
|
||||
mergedXTermOptions.theme?.background
|
||||
? `
|
||||
|
||||
Reference in New Issue
Block a user