Skip to content

Commit bbf2ae1

Browse files
committed
Responsive chat column
1 parent fbc5c71 commit bbf2ae1

File tree

6 files changed

+187
-43
lines changed

6 files changed

+187
-43
lines changed

Roo-Code/src/integrations/terminal/ExecaTerminalProcess.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { execa, ExecaError } from "execa"
22
import psTree from "ps-tree"
33
import process from "process"
4+
import os from "os"
5+
import fs from "fs/promises"
46

57
import type { RooTerminal } from "./types"
68
import { BaseTerminalProcess } from "./BaseTerminalProcess"
9+
import { createAndExecuteScript, getShell } from "../../utils/shell"
710

811
export class ExecaTerminalProcess extends BaseTerminalProcess {
912
private terminalRef: WeakRef<RooTerminal>
@@ -35,6 +38,27 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
3538
public override async run(command: string) {
3639
this.command = command
3740

41+
let commandToExecute = command
42+
let scriptPath: string | undefined
43+
44+
// Detect complex commands that might need a script file
45+
const shellOperators = ["&&", "||", ";", "source ", "`", "$("]
46+
const needsScript = shellOperators.some((op) => command.includes(op))
47+
48+
if (needsScript) {
49+
try {
50+
scriptPath = await createAndExecuteScript(command, this.terminal.getCurrentWorkingDirectory())
51+
commandToExecute = `${getShell()} ${scriptPath}` // Execute the script using the detected shell
52+
console.log(`[ExecaTerminalProcess#run] Executing complex command via script: ${scriptPath}`)
53+
} catch (error) {
54+
console.error(`[ExecaTerminalProcess#run] Failed to create and execute script: ${error}`)
55+
this.emit("shell_execution_complete", { exitCode: 1 })
56+
this.emit("completed", this.fullOutput)
57+
this.emit("continue")
58+
return
59+
}
60+
}
61+
3862
try {
3963
this.isHot = true
4064

@@ -48,7 +72,7 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
4872
LANG: "en_US.UTF-8",
4973
LC_ALL: "en_US.UTF-8",
5074
},
51-
})`${command}`
75+
})`${commandToExecute}`
5276

5377
this.pid = this.subprocess.pid
5478

@@ -148,6 +172,16 @@ export class ExecaTerminalProcess extends BaseTerminalProcess {
148172
this.emit("completed", this.fullOutput)
149173
this.emit("continue")
150174
this.subprocess = undefined
175+
176+
// Clean up the temporary script file if it was created
177+
if (scriptPath) {
178+
try {
179+
await fs.unlink(scriptPath)
180+
console.log(`[ExecaTerminalProcess#run] Cleaned up temporary script: ${scriptPath}`)
181+
} catch (cleanupError) {
182+
console.warn(`[ExecaTerminalProcess#run] Failed to clean up temporary script ${scriptPath}: ${cleanupError}`)
183+
}
184+
}
151185
}
152186

153187
public override continue() {

Roo-Code/src/utils/shell.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,26 @@ export function getShell(): string {
368368

369369
return shell
370370
}
371+
372+
/**
373+
* Executes a complex multi-line command by writing it to a temporary script file
374+
* and then executing the script. This is useful for commands that contain shell
375+
* operators like '&&', '||', ';', or 'source', which might not be handled
376+
* correctly when passed directly to execa with `shell: true` on all platforms.
377+
*
378+
* @param command The multi-line command string to execute.
379+
* @param cwd The current working directory for the command.
380+
* @returns The path to the temporary script file.
381+
*/
382+
export async function createAndExecuteScript(command: string, cwd: string): Promise<string> {
383+
const tempDir = path.join(os.tmpdir(), "roo-code-scripts")
384+
await fs.mkdir(tempDir, { recursive: true })
385+
386+
const scriptFileName = `command-${Date.now()}.sh`
387+
const scriptPath = path.join(tempDir, scriptFileName)
388+
389+
// Write the command to the temporary script file
390+
await fs.writeFile(scriptPath, command, { mode: 0o755 }) // Make it executable
391+
392+
return scriptPath
393+
}

src/components/ChatPanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export function ChatPanel({
184184
// Default frontend mode
185185
return (
186186
<div className="flex h-full">
187-
<div className="flex flex-col flex-1">
187+
<div className="flex flex-col flex-1 min-w-0" style={{ minWidth: '300px' }}>
188188
<ChatHeader
189189
isVersionPaneOpen={isVersionPaneOpen}
190190
isPreviewOpen={isPreviewOpen}
@@ -196,7 +196,7 @@ export function ChatPanel({
196196
/>
197197
<div className="flex flex-1 overflow-hidden">
198198
{!isVersionPaneOpen && (
199-
<div className="flex-1 flex flex-col min-w-0">
199+
<div className={`${(isFullstackMode || isFrontendMode) && isTodoPanelOpen ? 'min-w-0' : 'flex-1'} flex flex-col min-w-0`}>
200200
<MessagesList
201201
messages={messages}
202202
messagesEndRef={messagesEndRef}
@@ -212,7 +212,7 @@ export function ChatPanel({
212212
/>
213213
</div>
214214
</div>
215-
{(isFullstackMode || isFrontendMode) && (
215+
{(isFullstackMode || isFrontendMode) && isTodoPanelOpen && (
216216
<TodoListPanel
217217
isOpen={isTodoPanelOpen}
218218
onClose={() => setIsTodoPanelOpen(false)}

src/components/TodoListPanel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export function TodoListPanel({ isOpen, onClose }: TodoListPanelProps) {
138138
if (!isOpen) return null;
139139

140140
return (
141-
<div className="w-80 h-full bg-background border-l border-border flex flex-col flex-shrink-0">
141+
<div className="w-64 sm:w-72 md:w-80 lg:w-96 h-full bg-background border-l border-border flex flex-col flex-shrink-0">
142142
<div className="p-4 border-b border-border">
143143
<div className="flex items-center justify-between mb-4">
144144
<h2 className="text-lg font-semibold">Todo List</h2>

src/ipc/handlers/app_handlers.ts

Lines changed: 119 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
} from "../ipc_types";
1313
import fs from "node:fs";
1414
import path from "node:path";
15+
import os from "node:os";
1516
import { getDyadAppPath, getUserDataPath } from "../../paths/paths";
1617
import { ChildProcess, spawn, execSync } from "node:child_process";
1718
import git from "isomorphic-git";
@@ -398,6 +399,88 @@ python app.py
398399
}
399400
}
400401

402+
/**
403+
* Execute a command, using a temporary script file for complex commands with shell operators.
404+
* This allows handling of multi-line commands, pipes, conditionals, and other shell features.
405+
*/
406+
async function executeComplexCommand(
407+
command: string,
408+
workingDir: string,
409+
env: NodeJS.ProcessEnv
410+
): Promise<ChildProcess> {
411+
// Check if the command contains shell operators that require script execution
412+
const hasShellOperators = /(&&|\|\||source|\||;|\$\(|`.*`)/.test(command);
413+
414+
if (!hasShellOperators) {
415+
// For simple commands, use spawn directly
416+
logger.debug(`Using spawn for simple command: ${command}`);
417+
return spawn(command, [], {
418+
cwd: workingDir,
419+
shell: true,
420+
stdio: "pipe",
421+
detached: false,
422+
env,
423+
});
424+
}
425+
426+
// For complex commands, create a temporary script file
427+
logger.debug(`Using script file for complex command: ${command}`);
428+
429+
try {
430+
// Create temporary script file
431+
const tempDir = os.tmpdir();
432+
const scriptName = `dyad-script-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.sh`;
433+
const scriptPath = path.join(tempDir, scriptName);
434+
435+
// Write the command to the script file
436+
const scriptContent = `#!/bin/bash
437+
# Temporary script generated by AliFullStack
438+
cd "${workingDir}"
439+
${command}
440+
`;
441+
442+
await fsPromises.writeFile(scriptPath, scriptContent, 'utf-8');
443+
444+
// Make the script executable
445+
await fsPromises.chmod(scriptPath, 0o755);
446+
447+
logger.debug(`Created temporary script: ${scriptPath}`);
448+
449+
// Execute the script
450+
const process = spawn(scriptPath, [], {
451+
cwd: workingDir,
452+
shell: true,
453+
stdio: "pipe",
454+
detached: false,
455+
env,
456+
});
457+
458+
// Clean up the script file after the process exits
459+
process.on('exit', async (code, signal) => {
460+
try {
461+
await fsPromises.unlink(scriptPath);
462+
logger.debug(`Cleaned up temporary script: ${scriptPath}`);
463+
} catch (cleanupError) {
464+
logger.warn(`Failed to clean up temporary script ${scriptPath}:`, cleanupError);
465+
}
466+
});
467+
468+
process.on('error', async (error) => {
469+
try {
470+
await fsPromises.unlink(scriptPath);
471+
logger.debug(`Cleaned up temporary script after error: ${scriptPath}`);
472+
} catch (cleanupError) {
473+
logger.warn(`Failed to clean up temporary script ${scriptPath}:`, cleanupError);
474+
}
475+
});
476+
477+
return process;
478+
} catch (error) {
479+
logger.error(`Failed to create temporary script for complex command:`, error);
480+
throw error;
481+
}
482+
}
483+
401484
async function executeAppLocalNode({
402485
appPath,
403486
appId,
@@ -677,13 +760,17 @@ async function executeAppLocalNode({
677760

678761
logger.info(`Final backend command: ${backendCommand}`);
679762

680-
const backendProcess = spawn(backendCommand, [], {
681-
cwd: backendPath,
682-
shell: true,
683-
stdio: "pipe",
684-
detached: false,
685-
env: getShellEnv(),
686-
});
763+
// Check if the command contains shell operators that require script execution
764+
const hasShellOperators = /(&&|\|\||source|\||;|\$\(|`.*`)/.test(backendCommand);
765+
const backendProcess = hasShellOperators
766+
? await executeComplexCommand(backendCommand, backendPath, getShellEnv())
767+
: spawn(backendCommand, [], {
768+
cwd: backendPath,
769+
shell: true,
770+
stdio: "pipe",
771+
detached: false,
772+
env: getShellEnv(),
773+
});
687774

688775
if (backendProcess.pid) {
689776
const backendProcessId = processCounter.increment();
@@ -710,7 +797,7 @@ async function executeAppLocalNode({
710797
});
711798

712799
// Also send backend startup logs to system messages for visibility
713-
backendProcess.stdout?.on("data", (data) => {
800+
backendProcess.stdout?.on("data", (data: Buffer) => {
714801
const message = util.stripVTControlCharacters(data.toString());
715802
safeSend(event.sender, "app:output", {
716803
type: "stdout",
@@ -719,7 +806,7 @@ async function executeAppLocalNode({
719806
});
720807
});
721808

722-
backendProcess.stderr?.on("data", (data) => {
809+
backendProcess.stderr?.on("data", (data: Buffer) => {
723810
const message = util.stripVTControlCharacters(data.toString());
724811
safeSend(event.sender, "app:output", {
725812
type: "stderr",
@@ -778,7 +865,7 @@ async function executeAppLocalNode({
778865
});
779866

780867
// Also send frontend startup logs to system messages for visibility
781-
frontendProcess.stdout?.on("data", (data) => {
868+
frontendProcess.stdout?.on("data", (data: Buffer) => {
782869
const message = util.stripVTControlCharacters(data.toString());
783870
safeSend(event.sender, "app:output", {
784871
type: "stdout",
@@ -787,7 +874,7 @@ async function executeAppLocalNode({
787874
});
788875
});
789876

790-
frontendProcess.stderr?.on("data", (data) => {
877+
frontendProcess.stderr?.on("data", (data: Buffer) => {
791878
const message = util.stripVTControlCharacters(data.toString());
792879
safeSend(event.sender, "app:output", {
793880
type: "stderr",
@@ -926,18 +1013,13 @@ async function executeAppLocalNode({
9261013
}
9271014
}
9281015

929-
const spawnedProcess = spawn(command, [], {
930-
cwd: workingDir,
931-
shell: true,
932-
stdio: "pipe", // Ensure stdio is piped so we can capture output/errors and detect close
933-
detached: false, // Ensure child process is attached to the main process lifecycle unless explicitly backgrounded
934-
});
1016+
const spawnedProcess = await executeComplexCommand(command, workingDir, getShellEnv());
9351017

9361018
// Check if process spawned correctly
9371019
if (!spawnedProcess.pid) {
9381020
// Attempt to capture any immediate errors if possible
9391021
let errorOutput = "";
940-
spawnedProcess.stderr?.on("data", (data) => (errorOutput += data));
1022+
spawnedProcess.stderr?.on("data", (data: Buffer) => (errorOutput += data));
9411023
await new Promise((resolve) => spawnedProcess.on("error", resolve)); // Wait for error event
9421024
throw new Error(
9431025
`Failed to spawn process for app ${appId}. Error: ${
@@ -1005,7 +1087,7 @@ function listenToProcess({
10051087
}, 15000); // 15 seconds
10061088

10071089
// Log output
1008-
spawnedProcess.stdout?.on("data", async (data) => {
1090+
spawnedProcess.stdout?.on("data", async (data: Buffer) => {
10091091
const rawMessage = util.stripVTControlCharacters(data.toString());
10101092
const message = rawMessage; // Remove prefix since addTerminalOutput handles it
10111093

@@ -1630,22 +1712,24 @@ export function registerAppHandlers() {
16301712
logger.warn(`App ${app.id} created without Git repository`);
16311713
}
16321714

1633-
// Start autonomous development process
1634-
try {
1635-
logger.info(`Starting autonomous development for app ${app.id}`);
1636-
const requirements: string[] = []; // Requirements will be gathered during development
1637-
developmentOrchestrator.startAutonomousDevelopment(
1638-
app.id,
1639-
"react", // default frontend framework
1640-
params.selectedBackendFramework || undefined,
1641-
requirements
1642-
);
1643-
logger.info(`Autonomous development started for app ${app.id}`);
1644-
} catch (devError) {
1645-
logger.error(`Failed to start autonomous development for app ${app.id}:`, devError);
1646-
// Don't fail app creation if autonomous development fails to start
1647-
logger.warn(`App ${app.id} created but autonomous development failed to start`);
1648-
}
1715+
// Start autonomous development process asynchronously to prevent UI blocking
1716+
setTimeout(() => {
1717+
try {
1718+
logger.info(`Starting autonomous development for app ${app.id}`);
1719+
const requirements: string[] = []; // Requirements will be gathered during development
1720+
developmentOrchestrator.startAutonomousDevelopment(
1721+
app.id,
1722+
"react", // default frontend framework
1723+
params.selectedBackendFramework || undefined,
1724+
requirements
1725+
);
1726+
logger.info(`Autonomous development started for app ${app.id}`);
1727+
} catch (devError) {
1728+
logger.error(`Failed to start autonomous development for app ${app.id}:`, devError);
1729+
// Don't fail app creation if autonomous development fails to start
1730+
logger.warn(`App ${app.id} created but autonomous development failed to start`);
1731+
}
1732+
}, 0);
16491733

16501734
return { app, chatId: chat.id };
16511735
},

src/ipc/utils/start_proxy_server.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ export async function startProxy(
4141
} = opts;
4242

4343
// Get the correct path to the worker file in both development and production
44-
let workerPath: string;
4544
const electron = getElectron();
4645

46+
let workerPath: string;
47+
4748
if (electron && !process.env.NODE_ENV?.includes("development")) {
4849
// In production/built app, the worker is inside the ASAR archive
4950
// __dirname will be inside the ASAR, so we need to navigate to the worker directory
@@ -55,19 +56,21 @@ export async function startProxy(
5556
path.resolve(process.resourcesPath, "app.asar", "worker", "proxy_server.js"), // Explicit ASAR path
5657
];
5758

59+
let foundPath: string | null = null;
5860
for (const testPath of possiblePaths) {
5961
try {
6062
require.resolve(testPath);
61-
workerPath = testPath;
63+
foundPath = testPath;
6264
break;
6365
} catch (e) {
6466
// Continue to next path
6567
}
6668
}
6769

68-
if (!workerPath) {
70+
if (!foundPath) {
6971
throw new Error(`Could not find proxy_server.js worker file. Tried paths: ${possiblePaths.join(', ')}`);
7072
}
73+
workerPath = foundPath;
7174
} else {
7275
// In development, use the project root
7376
workerPath = path.resolve(process.cwd(), "worker", "proxy_server.js");

0 commit comments

Comments
 (0)