mirror of
https://github.com/stackblitz/bolt.new
synced 2025-02-11 15:13:18 +00:00
feat(bolt-terminal) bolt terminal integrated with the system
This commit is contained in:
parent
d1f3e8cbec
commit
719384cfbd
@ -151,7 +151,13 @@ const ActionList = memo(({ actions }: ActionListProps) => {
|
|||||||
<div className="flex items-center gap-1.5 text-sm">
|
<div className="flex items-center gap-1.5 text-sm">
|
||||||
<div className={classNames('text-lg', getIconColor(action.status))}>
|
<div className={classNames('text-lg', getIconColor(action.status))}>
|
||||||
{status === 'running' ? (
|
{status === 'running' ? (
|
||||||
|
<>
|
||||||
|
{type !== 'start' ? (
|
||||||
<div className="i-svg-spinners:90-ring-with-bg"></div>
|
<div className="i-svg-spinners:90-ring-with-bg"></div>
|
||||||
|
) : (
|
||||||
|
<div className="i-ph:terminal-window-duotone"></div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : status === 'pending' ? (
|
) : status === 'pending' ? (
|
||||||
<div className="i-ph:circle-duotone"></div>
|
<div className="i-ph:circle-duotone"></div>
|
||||||
) : status === 'complete' ? (
|
) : status === 'complete' ? (
|
||||||
@ -177,7 +183,7 @@ const ActionList = memo(({ actions }: ActionListProps) => {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{type === 'shell' && (
|
{(type === 'shell' || type === 'start') && (
|
||||||
<ShellCodeBlock
|
<ShellCodeBlock
|
||||||
classsName={classNames('mt-1', {
|
classsName={classNames('mt-1', {
|
||||||
'mb-3.5': !isLast,
|
'mb-3.5': !isLast,
|
||||||
|
@ -18,7 +18,7 @@ import { themeStore } from '~/lib/stores/theme';
|
|||||||
import { workbenchStore } from '~/lib/stores/workbench';
|
import { workbenchStore } from '~/lib/stores/workbench';
|
||||||
import { classNames } from '~/utils/classNames';
|
import { classNames } from '~/utils/classNames';
|
||||||
import { WORK_DIR } from '~/utils/constants';
|
import { WORK_DIR } from '~/utils/constants';
|
||||||
import { renderLogger } from '~/utils/logger';
|
import { logger, renderLogger } from '~/utils/logger';
|
||||||
import { isMobile } from '~/utils/mobile';
|
import { isMobile } from '~/utils/mobile';
|
||||||
import { FileBreadcrumb } from './FileBreadcrumb';
|
import { FileBreadcrumb } from './FileBreadcrumb';
|
||||||
import { FileTree } from './FileTree';
|
import { FileTree } from './FileTree';
|
||||||
@ -255,7 +255,7 @@ export const EditorPanel = memo(
|
|||||||
{Array.from({ length: terminalCount + 1 }, (_, index) => {
|
{Array.from({ length: terminalCount + 1 }, (_, index) => {
|
||||||
const isActive = activeTerminal === index;
|
const isActive = activeTerminal === index;
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
console.log('starting bolt terminal');
|
logger.info('Starting bolt terminal');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Terminal
|
<Terminal
|
||||||
|
@ -5,6 +5,7 @@ import { getModel } from '~/lib/.server/llm/model';
|
|||||||
import { MAX_TOKENS } from './constants';
|
import { MAX_TOKENS } from './constants';
|
||||||
import { getSystemPrompt } from './prompts';
|
import { getSystemPrompt } from './prompts';
|
||||||
import { MODEL_LIST, DEFAULT_MODEL, DEFAULT_PROVIDER } from '~/utils/constants';
|
import { MODEL_LIST, DEFAULT_MODEL, DEFAULT_PROVIDER } from '~/utils/constants';
|
||||||
|
import { logger } from '~/utils/logger';
|
||||||
|
|
||||||
interface ToolResult<Name extends string, Args, Result> {
|
interface ToolResult<Name extends string, Args, Result> {
|
||||||
toolCallId: string;
|
toolCallId: string;
|
||||||
@ -40,6 +41,7 @@ function extractModelFromMessage(message: Message): { model: string; content: st
|
|||||||
|
|
||||||
export function streamText(messages: Messages, env: Env, options?: StreamingOptions) {
|
export function streamText(messages: Messages, env: Env, options?: StreamingOptions) {
|
||||||
let currentModel = DEFAULT_MODEL;
|
let currentModel = DEFAULT_MODEL;
|
||||||
|
logger.debug('model List', JSON.stringify(MODEL_LIST, null, 2))
|
||||||
const processedMessages = messages.map((message) => {
|
const processedMessages = messages.map((message) => {
|
||||||
if (message.role === 'user') {
|
if (message.role === 'user') {
|
||||||
const { model, content } = extractModelFromMessage(message);
|
const { model, content } = extractModelFromMessage(message);
|
||||||
|
@ -116,7 +116,7 @@ export class ActionRunner {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'start': {
|
case 'start': {
|
||||||
await this.#runStartAction(action);
|
await this.#runStartAction(action)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,6 +124,7 @@ export class ActionRunner {
|
|||||||
this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
|
this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
|
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
|
||||||
|
logger.error(`[${action.type}]:Action failed\n\n`, error);
|
||||||
|
|
||||||
// re-throw the error to be caught in the promise chain
|
// re-throw the error to be caught in the promise chain
|
||||||
throw error;
|
throw error;
|
||||||
@ -140,8 +141,9 @@ export class ActionRunner {
|
|||||||
unreachable('Shell terminal not found');
|
unreachable('Shell terminal not found');
|
||||||
}
|
}
|
||||||
const resp = await shell.executeCommand(this.runnerId.get(), action.content)
|
const resp = await shell.executeCommand(this.runnerId.get(), action.content)
|
||||||
|
logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
|
||||||
if (resp?.exitCode != 0) {
|
if (resp?.exitCode != 0) {
|
||||||
throw new Error("Failed To Start Application");
|
throw new Error("Failed To Execute Shell Command");
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,10 +161,12 @@ export class ActionRunner {
|
|||||||
unreachable('Shell terminal not found');
|
unreachable('Shell terminal not found');
|
||||||
}
|
}
|
||||||
const resp = await shell.executeCommand(this.runnerId.get(), action.content)
|
const resp = await shell.executeCommand(this.runnerId.get(), action.content)
|
||||||
|
logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
|
||||||
|
|
||||||
if (resp?.exitCode != 0) {
|
if (resp?.exitCode != 0) {
|
||||||
throw new Error("Failed To Start Application");
|
throw new Error("Failed To Start Application");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
async #runFileAction(action: ActionState) {
|
async #runFileAction(action: ActionState) {
|
||||||
@ -193,24 +197,6 @@ export class ActionRunner {
|
|||||||
logger.error('Failed to write file\n\n', error);
|
logger.error('Failed to write file\n\n', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async getCurrentExecutionResult(output: ReadableStreamDefaultReader<string>) {
|
|
||||||
let fullOutput = '';
|
|
||||||
let exitCode: number = 0;
|
|
||||||
while (true) {
|
|
||||||
const { value, done } = await output.read();
|
|
||||||
if (done) break;
|
|
||||||
const text = value || '';
|
|
||||||
fullOutput += text;
|
|
||||||
// Check if command completion signal with exit code
|
|
||||||
const exitMatch = fullOutput.match(/\]654;exit=-?\d+:(\d+)/);
|
|
||||||
if (exitMatch) {
|
|
||||||
exitCode = parseInt(exitMatch[1], 10);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { output: fullOutput, exitCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
#updateAction(id: string, newState: ActionStateUpdate) {
|
#updateAction(id: string, newState: ActionStateUpdate) {
|
||||||
const actions = this.actions.get();
|
const actions = this.actions.get();
|
||||||
|
|
||||||
|
@ -60,8 +60,9 @@ export class BoltShell {
|
|||||||
#webcontainer: WebContainer | undefined
|
#webcontainer: WebContainer | undefined
|
||||||
#terminal: ITerminal | undefined
|
#terminal: ITerminal | undefined
|
||||||
#process: WebContainerProcess | undefined
|
#process: WebContainerProcess | undefined
|
||||||
executionState = atom<{ sessionId: string, active: boolean } | undefined>()
|
executionState = atom<{ sessionId: string, active: boolean, executionPrms?: Promise<any> } | undefined>()
|
||||||
#outputStream: ReadableStream<string> | undefined
|
#outputStream: ReadableStreamDefaultReader<string> | undefined
|
||||||
|
#shellInputStream: WritableStreamDefaultWriter<string> | undefined
|
||||||
constructor() {
|
constructor() {
|
||||||
this.#readyPromise = new Promise((resolve) => {
|
this.#readyPromise = new Promise((resolve) => {
|
||||||
this.#initialized = resolve
|
this.#initialized = resolve
|
||||||
@ -78,7 +79,8 @@ export class BoltShell {
|
|||||||
}
|
}
|
||||||
let { process, output } = await this.newBoltShellProcess(webcontainer, terminal)
|
let { process, output } = await this.newBoltShellProcess(webcontainer, terminal)
|
||||||
this.#process = process
|
this.#process = process
|
||||||
this.#outputStream = output
|
this.#outputStream = output.getReader()
|
||||||
|
await this.waitTillOscCode('interactive')
|
||||||
this.#initialized?.()
|
this.#initialized?.()
|
||||||
}
|
}
|
||||||
get terminal() {
|
get terminal() {
|
||||||
@ -92,12 +94,21 @@ export class BoltShell {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let state = this.executionState.get()
|
let state = this.executionState.get()
|
||||||
if (state && state.sessionId !== sessionId && state.active) {
|
|
||||||
|
//interrupt the current execution
|
||||||
|
// this.#shellInputStream?.write('\x03');
|
||||||
this.terminal.input('\x03');
|
this.terminal.input('\x03');
|
||||||
|
if (state && state.executionPrms) {
|
||||||
|
await state.executionPrms
|
||||||
}
|
}
|
||||||
this.executionState.set({ sessionId, active: true })
|
//start a new execution
|
||||||
this.terminal.input(command.trim() + '\n');
|
this.terminal.input(command.trim() + '\n');
|
||||||
let resp = await this.getCurrentExecutionResult()
|
|
||||||
|
//wait for the execution to finish
|
||||||
|
let executionPrms = this.getCurrentExecutionResult()
|
||||||
|
this.executionState.set({ sessionId, active: true, executionPrms })
|
||||||
|
|
||||||
|
let resp = await executionPrms
|
||||||
this.executionState.set({ sessionId, active: false })
|
this.executionState.set({ sessionId, active: false })
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@ -114,6 +125,7 @@ export class BoltShell {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const input = process.input.getWriter();
|
const input = process.input.getWriter();
|
||||||
|
this.#shellInputStream = input;
|
||||||
const [internalOutput, terminalOutput] = process.output.tee();
|
const [internalOutput, terminalOutput] = process.output.tee();
|
||||||
|
|
||||||
const jshReady = withResolvers<void>();
|
const jshReady = withResolvers<void>();
|
||||||
@ -151,10 +163,14 @@ export class BoltShell {
|
|||||||
return { process, output: internalOutput };
|
return { process, output: internalOutput };
|
||||||
}
|
}
|
||||||
async getCurrentExecutionResult() {
|
async getCurrentExecutionResult() {
|
||||||
|
let { output, exitCode } = await this.waitTillOscCode('exit')
|
||||||
|
return { output, exitCode };
|
||||||
|
}
|
||||||
|
async waitTillOscCode(waitCode: string) {
|
||||||
let fullOutput = '';
|
let fullOutput = '';
|
||||||
let exitCode: number = 0;
|
let exitCode: number = 0;
|
||||||
if (!this.#outputStream) return;
|
if (!this.#outputStream) return { output: fullOutput, exitCode };
|
||||||
let tappedStream = this.#outputStream.getReader()
|
let tappedStream = this.#outputStream
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { value, done } = await tappedStream.read();
|
const { value, done } = await tappedStream.read();
|
||||||
@ -163,11 +179,11 @@ export class BoltShell {
|
|||||||
fullOutput += text;
|
fullOutput += text;
|
||||||
|
|
||||||
// Check if command completion signal with exit code
|
// Check if command completion signal with exit code
|
||||||
const exitMatch = fullOutput.match(/\]654;exit=-?\d+:(\d+)/);
|
const [, osc, , pid, code] = text.match(/\x1b\]654;([^\x07=]+)=?((-?\d+):(\d+))?\x07/) || [];
|
||||||
if (exitMatch) {
|
if (osc === 'exit') {
|
||||||
console.log(exitMatch);
|
exitCode = parseInt(code, 10);
|
||||||
exitCode = parseInt(exitMatch[1], 10);
|
}
|
||||||
tappedStream.releaseLock()
|
if (osc === waitCode) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user