Skip to content

Commit

Permalink
fix: Improve terminal output and command execution visibility
Browse files Browse the repository at this point in the history
Enhances terminal functionality and command execution feedback:

Add real-time command output display in terminal
Show commands before execution with proper formatting
Improve terminal initialization and state management
Add proper error handling and display
Maintain terminal visibility during command execution
Add scrollback buffer and proper terminal cleanup
Fix terminal fitting and theme handling
Add initial terminal prompt and clear
This change ensures users can see commands being executed and their output in real-time, with proper error handling and state management. The terminal now maintains proper visibility and provides better feedback during command execution.
  • Loading branch information
vgcman16 committed Oct 25, 2024
1 parent 396fe55 commit 3955343
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 27 deletions.
30 changes: 29 additions & 1 deletion app/components/workbench/terminal/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ export const Terminal = memo(
forwardRef<TerminalRef, TerminalProps>(({ className, theme, readonly, onTerminalReady, onTerminalResize }, ref) => {
const terminalElementRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<XTerm>();
const fitAddonRef = useRef<FitAddon>();

useEffect(() => {
const element = terminalElementRef.current!;

const fitAddon = new FitAddon();
fitAddonRef.current = fitAddon;
const webLinksAddon = new WebLinksAddon();

const terminal = new XTerm({
Expand All @@ -38,6 +40,10 @@ export const Terminal = memo(
theme: getTerminalTheme(readonly ? { cursor: '#00000000' } : {}),
fontSize: 12,
fontFamily: 'Menlo, courier-new, courier, monospace',
scrollback: 5000,
rows: 24,
cols: 80,
allowProposedApi: true
});

terminalRef.current = terminal;
Expand All @@ -46,6 +52,11 @@ export const Terminal = memo(
terminal.loadAddon(webLinksAddon);
terminal.open(element);

// Initial fit
setTimeout(() => {
fitAddon.fit();
}, 0);

const resizeObserver = new ResizeObserver(() => {
fitAddon.fit();
onTerminalResize?.(terminal.cols, terminal.rows);
Expand All @@ -55,6 +66,11 @@ export const Terminal = memo(

logger.info('Attach terminal');

// Clear terminal and show prompt
terminal.clear();
terminal.write('\x1b[2J\x1b[H');
terminal.write('\r\n$ ');

onTerminalReady?.(terminal);

return () => {
Expand All @@ -68,15 +84,27 @@ export const Terminal = memo(

// we render a transparent cursor in case the terminal is readonly
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});

terminal.options.disableStdin = readonly;

// Refit terminal when theme changes
if (fitAddonRef.current) {
setTimeout(() => {
fitAddonRef.current?.fit();
}, 0);
}
}, [theme, readonly]);

useImperativeHandle(ref, () => {
return {
reloadStyles: () => {
const terminal = terminalRef.current!;
terminal.options.theme = getTerminalTheme(readonly ? { cursor: '#00000000' } : {});
// Refit terminal when styles change
if (fitAddonRef.current) {
setTimeout(() => {
fitAddonRef.current?.fit();
}, 0);
}
},
};
}, []);
Expand Down
66 changes: 61 additions & 5 deletions app/lib/runtime/action-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { BoltAction } from '~/types/actions';
import { createScopedLogger } from '~/utils/logger';
import { unreachable } from '~/utils/unreachable';
import type { ActionCallbackData } from './message-parser';
import { workbenchStore } from '~/lib/stores/workbench';
import { Terminal as XTerm } from '@xterm/xterm';

const logger = createScopedLogger('ActionRunner');

Expand Down Expand Up @@ -90,8 +92,13 @@ export class ActionRunner {
.then(() => {
return this.#executeAction(actionId);
})
.catch((error) => {
.catch((error: unknown) => {
console.error('Action failed:', error);
const terminal = workbenchStore.terminal;
if (terminal) {
const errorMessage = error instanceof Error ? error.message : String(error);
terminal.write(`\r\nError: ${errorMessage}\r\n`);
}
});
}

Expand All @@ -113,8 +120,9 @@ export class ActionRunner {
}

this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
} catch (error) {
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.#updateAction(actionId, { status: 'failed', error: errorMessage });

// re-throw the error to be caught in the promise chain
throw error;
Expand All @@ -128,6 +136,25 @@ export class ActionRunner {

const webcontainer = await this.#webcontainer;

// Show terminal before running command
workbenchStore.toggleTerminal(true);

// Wait for terminal to be ready
const terminal = await new Promise<XTerm>((resolve) => {
const checkTerminal = () => {
const term = workbenchStore.terminal;
if (term) {
resolve(term);
} else {
setTimeout(checkTerminal, 100);
}
};
checkTerminal();
});

// Display the command being executed
terminal.write(`\r\n$ ${action.content}\r\n`);

const process = await webcontainer.spawn('jsh', ['-c', action.content], {
env: { npm_config_yes: true },
});
Expand All @@ -140,12 +167,17 @@ export class ActionRunner {
new WritableStream({
write(data) {
console.log(data);
terminal.write(data);
},
}),
);

const exitCode = await process.exit;

if (exitCode !== 0) {
throw new Error(`Command failed with exit code ${exitCode}`);
}

logger.debug(`Process terminated with code ${exitCode}`);
}

Expand All @@ -156,6 +188,22 @@ export class ActionRunner {

const webcontainer = await this.#webcontainer;

// Show terminal before creating file
workbenchStore.toggleTerminal(true);

// Wait for terminal to be ready
const terminal = await new Promise<XTerm>((resolve) => {
const checkTerminal = () => {
const term = workbenchStore.terminal;
if (term) {
resolve(term);
} else {
setTimeout(checkTerminal, 100);
}
};
checkTerminal();
});

let folder = nodePath.dirname(action.filePath);

// remove trailing slashes
Expand All @@ -164,17 +212,25 @@ export class ActionRunner {
if (folder !== '.') {
try {
await webcontainer.fs.mkdir(folder, { recursive: true });
terminal.write(`\r\nCreated folder: ${folder}\r\n`);
logger.debug('Created folder', folder);
} catch (error) {
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Failed to create folder\n\n', error);
terminal.write(`\r\nError creating folder: ${errorMessage}\r\n`);
throw error;
}
}

try {
await webcontainer.fs.writeFile(action.filePath, action.content);
terminal.write(`\r\nCreated file: ${action.filePath}\r\n`);
logger.debug(`File written ${action.filePath}`);
} catch (error) {
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Failed to write file\n\n', error);
terminal.write(`\r\nError creating file: ${errorMessage}\r\n`);
throw error;
}
}

Expand Down
61 changes: 40 additions & 21 deletions app/lib/stores/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TerminalStore } from './terminal';
import JSZip from 'jszip';
import { saveAs } from 'file-saver';
import { Octokit } from "@octokit/rest";
import { Terminal as XTerm } from '@xterm/xterm';

export interface ArtifactState {
id: string;
Expand All @@ -31,6 +32,7 @@ export class WorkbenchStore {
#filesStore = new FilesStore(webcontainer);
#editorStore = new EditorStore(this.#filesStore);
#terminalStore = new TerminalStore(webcontainer);
#currentTerminal: XTerm | null = null;

artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({});

Expand All @@ -49,6 +51,10 @@ export class WorkbenchStore {
}
}

get terminal() {
return this.#currentTerminal;
}

get previews() {
return this.#previewsStore.previews;
}
Expand Down Expand Up @@ -79,10 +85,16 @@ export class WorkbenchStore {

toggleTerminal(value?: boolean) {
this.#terminalStore.toggleTerminal(value);
// If we're showing the terminal, also show the workbench
if (value) {
this.showWorkbench.set(true);
}
}

attachTerminal(terminal: ITerminal) {
this.#terminalStore.attachTerminal(terminal);
attachTerminal(terminal: XTerm) {
this.#currentTerminal = terminal;
// Show terminal and workbench when attaching a terminal
this.toggleTerminal(true);
}

onTerminalResize(cols: number, rows: number) {
Expand Down Expand Up @@ -234,6 +246,9 @@ export class WorkbenchStore {
closed: false,
runner: new ActionRunner(webcontainer),
});

// Show terminal and workbench when adding an artifact
this.toggleTerminal(true);
}

updateArtifact({ messageId }: ArtifactCallbackData, state: Partial<ArtifactUpdateState>) {
Expand All @@ -256,6 +271,8 @@ export class WorkbenchStore {
}

artifact.runner.addAction(data);
// Show terminal and workbench when adding an action
this.toggleTerminal(true);
}

async runAction(data: ActionCallbackData) {
Expand All @@ -268,6 +285,8 @@ export class WorkbenchStore {
}

artifact.runner.runAction(data);
// Show terminal and workbench when running an action
this.toggleTerminal(true);
}

#getArtifact(id: string) {
Expand Down Expand Up @@ -336,7 +355,6 @@ export class WorkbenchStore {
}

async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {

try {
// Get the GitHub auth token from environment variables
const githubToken = ghToken;
Expand All @@ -351,18 +369,19 @@ export class WorkbenchStore {
const octokit = new Octokit({ auth: githubToken });

// Check if the repository already exists before creating it
let repo
let repoData;
try {
repo = await octokit.repos.get({ owner: owner, repo: repoName });
const { data } = await octokit.repos.get({ owner, repo: repoName });
repoData = data;
} catch (error) {
if (error instanceof Error && 'status' in error && error.status === 404) {
// Repository doesn't exist, so create a new one
const { data: newRepo } = await octokit.repos.createForAuthenticatedUser({
const { data } = await octokit.repos.createForAuthenticatedUser({
name: repoName,
private: false,
auto_init: true,
});
repo = newRepo;
repoData = data;
} else {
console.log('cannot create repo!');
throw error; // Some other error occurred
Expand All @@ -380,8 +399,8 @@ export class WorkbenchStore {
Object.entries(files).map(async ([filePath, dirent]) => {
if (dirent?.type === 'file' && dirent.content) {
const { data: blob } = await octokit.git.createBlob({
owner: repo.owner.login,
repo: repo.name,
owner: repoData.owner.login,
repo: repoData.name,
content: Buffer.from(dirent.content).toString('base64'),
encoding: 'base64',
});
Expand All @@ -396,18 +415,18 @@ export class WorkbenchStore {
throw new Error('No valid files to push');
}

// Get the latest commit SHA (assuming main branch, update dynamically if needed)
// Get the latest commit SHA
const { data: ref } = await octokit.git.getRef({
owner: repo.owner.login,
repo: repo.name,
ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
owner: repoData.owner.login,
repo: repoData.name,
ref: `heads/${repoData.default_branch || 'main'}`,
});
const latestCommitSha = ref.object.sha;

// Create a new tree
const { data: newTree } = await octokit.git.createTree({
owner: repo.owner.login,
repo: repo.name,
owner: repoData.owner.login,
repo: repoData.name,
base_tree: latestCommitSha,
tree: validBlobs.map((blob) => ({
path: blob!.path,
Expand All @@ -419,22 +438,22 @@ export class WorkbenchStore {

// Create a new commit
const { data: newCommit } = await octokit.git.createCommit({
owner: repo.owner.login,
repo: repo.name,
owner: repoData.owner.login,
repo: repoData.name,
message: 'Initial commit from your app',
tree: newTree.sha,
parents: [latestCommitSha],
});

// Update the reference
await octokit.git.updateRef({
owner: repo.owner.login,
repo: repo.name,
ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
owner: repoData.owner.login,
repo: repoData.name,
ref: `heads/${repoData.default_branch || 'main'}`,
sha: newCommit.sha,
});

alert(`Repository created and code pushed: ${repo.html_url}`);
alert(`Repository created and code pushed: ${repoData.html_url}`);
} catch (error) {
console.error('Error pushing to GitHub:', error instanceof Error ? error.message : String(error));
}
Expand Down

0 comments on commit 3955343

Please sign in to comment.