codacus commited on
Commit
719384c
·
1 Parent(s): d1f3e8c

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
- <div className="i-svg-spinners:90-ring-with-bg"></div>
 
 
 
 
 
 
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
- console.log('starting bolt terminal');
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 Start Application");
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: ReadableStream<string> | undefined
 
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
- if (state && state.sessionId !== sessionId && state.active) {
96
- this.terminal.input('\x03');
 
 
 
 
97
  }
98
- this.executionState.set({ sessionId, active: true })
99
  this.terminal.input(command.trim() + '\n');
100
- let resp = await this.getCurrentExecutionResult()
 
 
 
 
 
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.getReader()
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 exitMatch = fullOutput.match(/\]654;exit=-?\d+:(\d+)/);
167
- if (exitMatch) {
168
- console.log(exitMatch);
169
- exitCode = parseInt(exitMatch[1], 10);
170
- tappedStream.releaseLock()
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
  }