Skip to content

Commit

Permalink
feat: improve terminal feedback and interactivity
Browse files Browse the repository at this point in the history
Add checkmark indicator when dev server is ready
Enable interactive terminal input with command execution
Show proper completion states for all commands
Add support for basic terminal features (backspace, Ctrl+C)
Fix status updates in both terminal and UI
Improve command feedback with clear success/failure indicators
  • Loading branch information
vgcman16 committed Oct 25, 2024
1 parent 3955343 commit 3770135
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 9 deletions.
36 changes: 36 additions & 0 deletions app/components/workbench/terminal/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { forwardRef, memo, useEffect, useImperativeHandle, useRef } from 'react'
import type { Theme } from '~/lib/stores/theme';
import { createScopedLogger } from '~/utils/logger';
import { getTerminalTheme } from './theme';
import { workbenchStore } from '~/lib/stores/workbench';

const logger = createScopedLogger('Terminal');

Expand All @@ -25,6 +26,7 @@ export const Terminal = memo(
const terminalElementRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<XTerm>();
const fitAddonRef = useRef<FitAddon>();
const inputBufferRef = useRef<string>('');

useEffect(() => {
const element = terminalElementRef.current!;
Expand Down Expand Up @@ -52,6 +54,40 @@ export const Terminal = memo(
terminal.loadAddon(webLinksAddon);
terminal.open(element);

// Handle user input
terminal.onData((data) => {
if (readonly) return;

// Handle special keys
if (data === '\r') { // Enter key
const command = inputBufferRef.current.trim();
if (command) {
terminal.write('\r\n');
workbenchStore.handleTerminalInput(command);
}
inputBufferRef.current = '';
return;
}

if (data === '\u007f') { // Backspace
if (inputBufferRef.current.length > 0) {
inputBufferRef.current = inputBufferRef.current.slice(0, -1);
terminal.write('\b \b');
}
return;
}

if (data === '\u0003') { // Ctrl+C
terminal.write('^C\r\n$ ');
inputBufferRef.current = '';
return;
}

// Regular input
inputBufferRef.current += data;
terminal.write(data);
});

// Initial fit
setTimeout(() => {
fitAddon.fit();
Expand Down
93 changes: 84 additions & 9 deletions app/lib/runtime/action-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type ActionsMap = MapStore<Record<string, ActionState>>;
export class ActionRunner {
#webcontainer: Promise<WebContainer>;
#currentExecutionPromise: Promise<void> = Promise.resolve();
#currentProcess: any = null;
#currentActionId: string | null = null;

actions: ActionsMap = map({});

Expand Down Expand Up @@ -97,13 +99,14 @@ export class ActionRunner {
const terminal = workbenchStore.terminal;
if (terminal) {
const errorMessage = error instanceof Error ? error.message : String(error);
terminal.write(`\r\nError: ${errorMessage}\r\n`);
terminal.write(`\r\nError: ${errorMessage}\r\n$ `);
}
});
}

async #executeAction(actionId: string) {
const action = this.actions.get()[actionId];
this.#currentActionId = actionId;

this.#updateAction(actionId, { status: 'running' });

Expand All @@ -119,7 +122,10 @@ export class ActionRunner {
}
}

this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
// Don't update status for npm run dev - it will be updated when server is ready
if (!(action.type === 'shell' && action.content.includes('npm run dev'))) {
this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.#updateAction(actionId, { status: 'failed', error: errorMessage });
Expand Down Expand Up @@ -153,31 +159,55 @@ export class ActionRunner {
});

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

const process = await webcontainer.spawn('jsh', ['-c', action.content], {
env: { npm_config_yes: true },
});

this.#currentProcess = process;

action.abortSignal.addEventListener('abort', () => {
process.kill();
});

// Set up a flag to track if we've shown the ready message
let readyMessageShown = false;

process.output.pipeTo(
new WritableStream({
write(data) {
write: (data) => {
console.log(data);
terminal.write(data);

// Check for dev server ready message
if (!readyMessageShown &&
(data.includes('Local:') || data.includes('localhost:')) &&
action.content.includes('npm run dev')) {
terminal.write('\r\n✓ Development server is ready\r\n');
readyMessageShown = true;
// Update the action status to complete when dev server is ready
if (this.#currentActionId) {
this.#updateAction(this.#currentActionId, { status: 'complete' });
}
}
},
}),
);

const exitCode = await process.exit;
this.#currentProcess = null;

if (exitCode !== 0) {
terminal.write('\r\n❌ Command failed\r\n$ ');
throw new Error(`Command failed with exit code ${exitCode}`);
}

// Only show completion message for non-dev-server commands
if (!action.content.includes('npm run dev')) {
terminal.write('\r\n✓ Command completed\r\n$ ');
}

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

Expand Down Expand Up @@ -212,31 +242,76 @@ export class ActionRunner {
if (folder !== '.') {
try {
await webcontainer.fs.mkdir(folder, { recursive: true });
terminal.write(`\r\nCreated folder: ${folder}\r\n`);
terminal.write(`\r\nCreated folder: ${folder}`);
logger.debug('Created folder', folder);
} 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`);
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`);
terminal.write(`\r\nCreated file: ${action.filePath}\r\n✓ File operation completed\r\n$ `);
logger.debug(`File written ${action.filePath}`);
} 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`);
terminal.write(`\r\nError creating file: ${errorMessage}\r\n$ `);
throw error;
}
}

#updateAction(id: string, newState: ActionStateUpdate) {
const actions = this.actions.get();

this.actions.setKey(id, { ...actions[id], ...newState });
}

// Handle user input from terminal
async handleTerminalInput(command: string) {
if (!command) return;

const webcontainer = await this.#webcontainer;
const terminal = workbenchStore.terminal;

if (!terminal) return;

const process = await webcontainer.spawn('jsh', ['-c', command], {
env: { npm_config_yes: true },
});

this.#currentProcess = process;

// Set up a flag to track if we've shown the ready message
let readyMessageShown = false;

process.output.pipeTo(
new WritableStream({
write(data) {
console.log(data);
terminal.write(data);

// Check for dev server ready message
if (!readyMessageShown &&
(data.includes('Local:') || data.includes('localhost:')) &&
command.includes('npm run dev')) {
terminal.write('\r\n✓ Development server is ready\r\n');
readyMessageShown = true;
}
},
}),
);

const exitCode = await process.exit;
this.#currentProcess = null;

if (exitCode !== 0) {
terminal.write('\r\n❌ Command failed\r\n$ ');
} else if (!command.includes('npm run dev')) {
// Only show completion message for non-dev-server commands
terminal.write('\r\n✓ Command completed\r\n$ ');
}
}
}
10 changes: 10 additions & 0 deletions app/lib/stores/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class WorkbenchStore {
#editorStore = new EditorStore(this.#filesStore);
#terminalStore = new TerminalStore(webcontainer);
#currentTerminal: XTerm | null = null;
#currentRunner: ActionRunner | null = null;

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

Expand All @@ -49,6 +50,8 @@ export class WorkbenchStore {
import.meta.hot.data.showWorkbench = this.showWorkbench;
import.meta.hot.data.currentView = this.currentView;
}
// Initialize a default action runner for terminal commands
this.#currentRunner = new ActionRunner(webcontainer);
}

get terminal() {
Expand Down Expand Up @@ -97,6 +100,13 @@ export class WorkbenchStore {
this.toggleTerminal(true);
}

// Handle terminal input
async handleTerminalInput(command: string) {
if (this.#currentRunner) {
await this.#currentRunner.handleTerminalInput(command);
}
}

onTerminalResize(cols: number, rows: number) {
this.#terminalStore.onTerminalResize(cols, rows);
}
Expand Down

0 comments on commit 3770135

Please sign in to comment.