feat(bolt-terminal) bolt terminal integrated with the system
Browse files
app/components/chat/Artifact.tsx
CHANGED
|
@@ -151,7 +151,13 @@ const ActionList = memo(({ actions }: ActionListProps) => {
|
|
| 151 |
<div className="flex items-center gap-1.5 text-sm">
|
| 152 |
<div className={classNames('text-lg', getIconColor(action.status))}>
|
| 153 |
{status === 'running' ? (
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
) : status === 'pending' ? (
|
| 156 |
<div className="i-ph:circle-duotone"></div>
|
| 157 |
) : status === 'complete' ? (
|
|
@@ -177,7 +183,7 @@ const ActionList = memo(({ actions }: ActionListProps) => {
|
|
| 177 |
</div>
|
| 178 |
) : null}
|
| 179 |
</div>
|
| 180 |
-
{type === 'shell' && (
|
| 181 |
<ShellCodeBlock
|
| 182 |
classsName={classNames('mt-1', {
|
| 183 |
'mb-3.5': !isLast,
|
|
|
|
| 151 |
<div className="flex items-center gap-1.5 text-sm">
|
| 152 |
<div className={classNames('text-lg', getIconColor(action.status))}>
|
| 153 |
{status === 'running' ? (
|
| 154 |
+
<>
|
| 155 |
+
{type !== 'start' ? (
|
| 156 |
+
<div className="i-svg-spinners:90-ring-with-bg"></div>
|
| 157 |
+
) : (
|
| 158 |
+
<div className="i-ph:terminal-window-duotone"></div>
|
| 159 |
+
)}
|
| 160 |
+
</>
|
| 161 |
) : status === 'pending' ? (
|
| 162 |
<div className="i-ph:circle-duotone"></div>
|
| 163 |
) : status === 'complete' ? (
|
|
|
|
| 183 |
</div>
|
| 184 |
) : null}
|
| 185 |
</div>
|
| 186 |
+
{(type === 'shell' || type === 'start') && (
|
| 187 |
<ShellCodeBlock
|
| 188 |
classsName={classNames('mt-1', {
|
| 189 |
'mb-3.5': !isLast,
|
app/components/workbench/EditorPanel.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import { themeStore } from '~/lib/stores/theme';
|
|
| 18 |
import { workbenchStore } from '~/lib/stores/workbench';
|
| 19 |
import { classNames } from '~/utils/classNames';
|
| 20 |
import { WORK_DIR } from '~/utils/constants';
|
| 21 |
-
import { renderLogger } from '~/utils/logger';
|
| 22 |
import { isMobile } from '~/utils/mobile';
|
| 23 |
import { FileBreadcrumb } from './FileBreadcrumb';
|
| 24 |
import { FileTree } from './FileTree';
|
|
@@ -255,7 +255,7 @@ export const EditorPanel = memo(
|
|
| 255 |
{Array.from({ length: terminalCount + 1 }, (_, index) => {
|
| 256 |
const isActive = activeTerminal === index;
|
| 257 |
if (index == 0) {
|
| 258 |
-
|
| 259 |
|
| 260 |
return (
|
| 261 |
<Terminal
|
|
|
|
| 18 |
import { workbenchStore } from '~/lib/stores/workbench';
|
| 19 |
import { classNames } from '~/utils/classNames';
|
| 20 |
import { WORK_DIR } from '~/utils/constants';
|
| 21 |
+
import { logger, renderLogger } from '~/utils/logger';
|
| 22 |
import { isMobile } from '~/utils/mobile';
|
| 23 |
import { FileBreadcrumb } from './FileBreadcrumb';
|
| 24 |
import { FileTree } from './FileTree';
|
|
|
|
| 255 |
{Array.from({ length: terminalCount + 1 }, (_, index) => {
|
| 256 |
const isActive = activeTerminal === index;
|
| 257 |
if (index == 0) {
|
| 258 |
+
logger.info('Starting bolt terminal');
|
| 259 |
|
| 260 |
return (
|
| 261 |
<Terminal
|
app/lib/.server/llm/stream-text.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { getModel } from '~/lib/.server/llm/model';
|
|
| 5 |
import { MAX_TOKENS } from './constants';
|
| 6 |
import { getSystemPrompt } from './prompts';
|
| 7 |
import { MODEL_LIST, DEFAULT_MODEL, DEFAULT_PROVIDER } from '~/utils/constants';
|
|
|
|
| 8 |
|
| 9 |
interface ToolResult<Name extends string, Args, Result> {
|
| 10 |
toolCallId: string;
|
|
@@ -40,6 +41,7 @@ function extractModelFromMessage(message: Message): { model: string; content: st
|
|
| 40 |
|
| 41 |
export function streamText(messages: Messages, env: Env, options?: StreamingOptions) {
|
| 42 |
let currentModel = DEFAULT_MODEL;
|
|
|
|
| 43 |
const processedMessages = messages.map((message) => {
|
| 44 |
if (message.role === 'user') {
|
| 45 |
const { model, content } = extractModelFromMessage(message);
|
|
|
|
| 5 |
import { MAX_TOKENS } from './constants';
|
| 6 |
import { getSystemPrompt } from './prompts';
|
| 7 |
import { MODEL_LIST, DEFAULT_MODEL, DEFAULT_PROVIDER } from '~/utils/constants';
|
| 8 |
+
import { logger } from '~/utils/logger';
|
| 9 |
|
| 10 |
interface ToolResult<Name extends string, Args, Result> {
|
| 11 |
toolCallId: string;
|
|
|
|
| 41 |
|
| 42 |
export function streamText(messages: Messages, env: Env, options?: StreamingOptions) {
|
| 43 |
let currentModel = DEFAULT_MODEL;
|
| 44 |
+
logger.debug('model List', JSON.stringify(MODEL_LIST, null, 2))
|
| 45 |
const processedMessages = messages.map((message) => {
|
| 46 |
if (message.role === 'user') {
|
| 47 |
const { model, content } = extractModelFromMessage(message);
|
app/lib/runtime/action-runner.ts
CHANGED
|
@@ -116,7 +116,7 @@ export class ActionRunner {
|
|
| 116 |
break;
|
| 117 |
}
|
| 118 |
case 'start': {
|
| 119 |
-
await this.#runStartAction(action)
|
| 120 |
break;
|
| 121 |
}
|
| 122 |
}
|
|
@@ -124,6 +124,7 @@ export class ActionRunner {
|
|
| 124 |
this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
|
| 125 |
} catch (error) {
|
| 126 |
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
|
|
|
|
| 127 |
|
| 128 |
// re-throw the error to be caught in the promise chain
|
| 129 |
throw error;
|
|
@@ -140,8 +141,9 @@ export class ActionRunner {
|
|
| 140 |
unreachable('Shell terminal not found');
|
| 141 |
}
|
| 142 |
const resp = await shell.executeCommand(this.runnerId.get(), action.content)
|
|
|
|
| 143 |
if (resp?.exitCode != 0) {
|
| 144 |
-
throw new Error("Failed To
|
| 145 |
|
| 146 |
}
|
| 147 |
}
|
|
@@ -159,10 +161,12 @@ export class ActionRunner {
|
|
| 159 |
unreachable('Shell terminal not found');
|
| 160 |
}
|
| 161 |
const resp = await shell.executeCommand(this.runnerId.get(), action.content)
|
|
|
|
|
|
|
| 162 |
if (resp?.exitCode != 0) {
|
| 163 |
throw new Error("Failed To Start Application");
|
| 164 |
-
|
| 165 |
}
|
|
|
|
| 166 |
}
|
| 167 |
|
| 168 |
async #runFileAction(action: ActionState) {
|
|
@@ -193,24 +197,6 @@ export class ActionRunner {
|
|
| 193 |
logger.error('Failed to write file\n\n', error);
|
| 194 |
}
|
| 195 |
}
|
| 196 |
-
async getCurrentExecutionResult(output: ReadableStreamDefaultReader<string>) {
|
| 197 |
-
let fullOutput = '';
|
| 198 |
-
let exitCode: number = 0;
|
| 199 |
-
while (true) {
|
| 200 |
-
const { value, done } = await output.read();
|
| 201 |
-
if (done) break;
|
| 202 |
-
const text = value || '';
|
| 203 |
-
fullOutput += text;
|
| 204 |
-
// Check if command completion signal with exit code
|
| 205 |
-
const exitMatch = fullOutput.match(/\]654;exit=-?\d+:(\d+)/);
|
| 206 |
-
if (exitMatch) {
|
| 207 |
-
exitCode = parseInt(exitMatch[1], 10);
|
| 208 |
-
break;
|
| 209 |
-
}
|
| 210 |
-
}
|
| 211 |
-
return { output: fullOutput, exitCode };
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
#updateAction(id: string, newState: ActionStateUpdate) {
|
| 215 |
const actions = this.actions.get();
|
| 216 |
|
|
|
|
| 116 |
break;
|
| 117 |
}
|
| 118 |
case 'start': {
|
| 119 |
+
await this.#runStartAction(action)
|
| 120 |
break;
|
| 121 |
}
|
| 122 |
}
|
|
|
|
| 124 |
this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
|
| 125 |
} catch (error) {
|
| 126 |
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
|
| 127 |
+
logger.error(`[${action.type}]:Action failed\n\n`, error);
|
| 128 |
|
| 129 |
// re-throw the error to be caught in the promise chain
|
| 130 |
throw error;
|
|
|
|
| 141 |
unreachable('Shell terminal not found');
|
| 142 |
}
|
| 143 |
const resp = await shell.executeCommand(this.runnerId.get(), action.content)
|
| 144 |
+
logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
|
| 145 |
if (resp?.exitCode != 0) {
|
| 146 |
+
throw new Error("Failed To Execute Shell Command");
|
| 147 |
|
| 148 |
}
|
| 149 |
}
|
|
|
|
| 161 |
unreachable('Shell terminal not found');
|
| 162 |
}
|
| 163 |
const resp = await shell.executeCommand(this.runnerId.get(), action.content)
|
| 164 |
+
logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
|
| 165 |
+
|
| 166 |
if (resp?.exitCode != 0) {
|
| 167 |
throw new Error("Failed To Start Application");
|
|
|
|
| 168 |
}
|
| 169 |
+
return resp
|
| 170 |
}
|
| 171 |
|
| 172 |
async #runFileAction(action: ActionState) {
|
|
|
|
| 197 |
logger.error('Failed to write file\n\n', error);
|
| 198 |
}
|
| 199 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
#updateAction(id: string, newState: ActionStateUpdate) {
|
| 201 |
const actions = this.actions.get();
|
| 202 |
|
app/utils/shell.ts
CHANGED
|
@@ -60,8 +60,9 @@ export class BoltShell {
|
|
| 60 |
#webcontainer: WebContainer | undefined
|
| 61 |
#terminal: ITerminal | undefined
|
| 62 |
#process: WebContainerProcess | undefined
|
| 63 |
-
executionState = atom<{ sessionId: string, active: boolean } | undefined>()
|
| 64 |
-
#outputStream:
|
|
|
|
| 65 |
constructor() {
|
| 66 |
this.#readyPromise = new Promise((resolve) => {
|
| 67 |
this.#initialized = resolve
|
|
@@ -78,7 +79,8 @@ export class BoltShell {
|
|
| 78 |
}
|
| 79 |
let { process, output } = await this.newBoltShellProcess(webcontainer, terminal)
|
| 80 |
this.#process = process
|
| 81 |
-
this.#outputStream = output
|
|
|
|
| 82 |
this.#initialized?.()
|
| 83 |
}
|
| 84 |
get terminal() {
|
|
@@ -92,12 +94,21 @@ export class BoltShell {
|
|
| 92 |
return
|
| 93 |
}
|
| 94 |
let state = this.executionState.get()
|
| 95 |
-
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
}
|
| 98 |
-
|
| 99 |
this.terminal.input(command.trim() + '\n');
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
this.executionState.set({ sessionId, active: false })
|
| 102 |
return resp
|
| 103 |
|
|
@@ -114,6 +125,7 @@ export class BoltShell {
|
|
| 114 |
});
|
| 115 |
|
| 116 |
const input = process.input.getWriter();
|
|
|
|
| 117 |
const [internalOutput, terminalOutput] = process.output.tee();
|
| 118 |
|
| 119 |
const jshReady = withResolvers<void>();
|
|
@@ -151,10 +163,14 @@ export class BoltShell {
|
|
| 151 |
return { process, output: internalOutput };
|
| 152 |
}
|
| 153 |
async getCurrentExecutionResult() {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
let fullOutput = '';
|
| 155 |
let exitCode: number = 0;
|
| 156 |
-
if (!this.#outputStream) return;
|
| 157 |
-
let tappedStream = this.#outputStream
|
| 158 |
|
| 159 |
while (true) {
|
| 160 |
const { value, done } = await tappedStream.read();
|
|
@@ -163,11 +179,11 @@ export class BoltShell {
|
|
| 163 |
fullOutput += text;
|
| 164 |
|
| 165 |
// Check if command completion signal with exit code
|
| 166 |
-
const
|
| 167 |
-
if (
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
break;
|
| 172 |
}
|
| 173 |
}
|
|
|
|
| 60 |
#webcontainer: WebContainer | undefined
|
| 61 |
#terminal: ITerminal | undefined
|
| 62 |
#process: WebContainerProcess | undefined
|
| 63 |
+
executionState = atom<{ sessionId: string, active: boolean, executionPrms?: Promise<any> } | undefined>()
|
| 64 |
+
#outputStream: ReadableStreamDefaultReader<string> | undefined
|
| 65 |
+
#shellInputStream: WritableStreamDefaultWriter<string> | undefined
|
| 66 |
constructor() {
|
| 67 |
this.#readyPromise = new Promise((resolve) => {
|
| 68 |
this.#initialized = resolve
|
|
|
|
| 79 |
}
|
| 80 |
let { process, output } = await this.newBoltShellProcess(webcontainer, terminal)
|
| 81 |
this.#process = process
|
| 82 |
+
this.#outputStream = output.getReader()
|
| 83 |
+
await this.waitTillOscCode('interactive')
|
| 84 |
this.#initialized?.()
|
| 85 |
}
|
| 86 |
get terminal() {
|
|
|
|
| 94 |
return
|
| 95 |
}
|
| 96 |
let state = this.executionState.get()
|
| 97 |
+
|
| 98 |
+
//interrupt the current execution
|
| 99 |
+
// this.#shellInputStream?.write('\x03');
|
| 100 |
+
this.terminal.input('\x03');
|
| 101 |
+
if (state && state.executionPrms) {
|
| 102 |
+
await state.executionPrms
|
| 103 |
}
|
| 104 |
+
//start a new execution
|
| 105 |
this.terminal.input(command.trim() + '\n');
|
| 106 |
+
|
| 107 |
+
//wait for the execution to finish
|
| 108 |
+
let executionPrms = this.getCurrentExecutionResult()
|
| 109 |
+
this.executionState.set({ sessionId, active: true, executionPrms })
|
| 110 |
+
|
| 111 |
+
let resp = await executionPrms
|
| 112 |
this.executionState.set({ sessionId, active: false })
|
| 113 |
return resp
|
| 114 |
|
|
|
|
| 125 |
});
|
| 126 |
|
| 127 |
const input = process.input.getWriter();
|
| 128 |
+
this.#shellInputStream = input;
|
| 129 |
const [internalOutput, terminalOutput] = process.output.tee();
|
| 130 |
|
| 131 |
const jshReady = withResolvers<void>();
|
|
|
|
| 163 |
return { process, output: internalOutput };
|
| 164 |
}
|
| 165 |
async getCurrentExecutionResult() {
|
| 166 |
+
let { output, exitCode } = await this.waitTillOscCode('exit')
|
| 167 |
+
return { output, exitCode };
|
| 168 |
+
}
|
| 169 |
+
async waitTillOscCode(waitCode: string) {
|
| 170 |
let fullOutput = '';
|
| 171 |
let exitCode: number = 0;
|
| 172 |
+
if (!this.#outputStream) return { output: fullOutput, exitCode };
|
| 173 |
+
let tappedStream = this.#outputStream
|
| 174 |
|
| 175 |
while (true) {
|
| 176 |
const { value, done } = await tappedStream.read();
|
|
|
|
| 179 |
fullOutput += text;
|
| 180 |
|
| 181 |
// Check if command completion signal with exit code
|
| 182 |
+
const [, osc, , pid, code] = text.match(/\x1b\]654;([^\x07=]+)=?((-?\d+):(\d+))?\x07/) || [];
|
| 183 |
+
if (osc === 'exit') {
|
| 184 |
+
exitCode = parseInt(code, 10);
|
| 185 |
+
}
|
| 186 |
+
if (osc === waitCode) {
|
| 187 |
break;
|
| 188 |
}
|
| 189 |
}
|