Skip to content

Commit 2c04e4a

Browse files
feat: make file operations browser-compatible
- Update fileUpload to accept string/Blob/File instead of file paths - Update fileDownload to return content instead of writing to disk - Add convenience methods for common browser use cases - Remove Node.js fs and path dependencies - Add FileDownloadResult interface - Update examples to remove Node.js-specific code - Add comprehensive browser compatibility documentation Fixes file operations to work natively in browser environments without Node.js polyfills or dependencies. Co-authored-by: openhands <openhands@all-hands.dev>
1 parent f3369ae commit 2c04e4a

File tree

7 files changed

+304
-44
lines changed

7 files changed

+304
-44
lines changed

BROWSER_COMPATIBILITY.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Browser Compatibility Guide
2+
3+
This document outlines the browser-compatible file operations API in the OpenHands TypeScript Client.
4+
5+
## Overview
6+
7+
The TypeScript client has been updated to work natively in browser environments without Node.js dependencies. The main changes involve file upload and download operations that now work with browser-native data types like `Blob`, `File`, and strings instead of file system paths.
8+
9+
## File Upload API
10+
11+
### `fileUpload(content, destinationPath, fileName?)`
12+
13+
Upload content to the remote workspace.
14+
15+
**Parameters:**
16+
- `content: string | Blob | File` - The content to upload
17+
- `destinationPath: string` - Where to save the file on the remote workspace
18+
- `fileName?: string` - Optional filename (auto-detected for File objects)
19+
20+
**Examples:**
21+
22+
```typescript
23+
const workspace = new RemoteWorkspace({
24+
host: 'http://localhost:3000',
25+
workingDir: '/tmp',
26+
apiKey: 'your-api-key'
27+
});
28+
29+
// Upload text content
30+
await workspace.fileUpload('Hello, World!', '/tmp/hello.txt', 'hello.txt');
31+
32+
// Upload a File object (from file input)
33+
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
34+
const file = fileInput.files[0];
35+
await workspace.fileUpload(file, '/tmp/uploads/');
36+
37+
// Upload a Blob
38+
const blob = new Blob(['Some data'], { type: 'text/plain' });
39+
await workspace.fileUpload(blob, '/tmp/data.txt', 'data.txt');
40+
```
41+
42+
### Convenience Methods
43+
44+
#### `uploadText(text, destinationPath, fileName?)`
45+
Shorthand for uploading text content.
46+
47+
```typescript
48+
await workspace.uploadText('Hello, World!', '/tmp/hello.txt');
49+
```
50+
51+
#### `uploadFileObject(file, destinationPath)`
52+
Shorthand for uploading File objects.
53+
54+
```typescript
55+
const file = fileInput.files[0];
56+
await workspace.uploadFileObject(file, '/tmp/uploads/');
57+
```
58+
59+
## File Download API
60+
61+
### `fileDownload(sourcePath)`
62+
63+
Download a file from the remote workspace. Returns content as string or Blob.
64+
65+
**Parameters:**
66+
- `sourcePath: string` - Path to the file on the remote workspace
67+
68+
**Returns:** `Promise<FileDownloadResult>`
69+
70+
```typescript
71+
interface FileDownloadResult {
72+
success: boolean;
73+
source_path: string;
74+
content: string | Blob;
75+
file_size?: number;
76+
error?: string;
77+
}
78+
```
79+
80+
**Example:**
81+
82+
```typescript
83+
const result = await workspace.fileDownload('/tmp/data.txt');
84+
if (result.success) {
85+
console.log('File content:', result.content);
86+
}
87+
```
88+
89+
### Convenience Methods
90+
91+
#### `downloadAsText(sourcePath)`
92+
Download file content as a string.
93+
94+
```typescript
95+
const text = await workspace.downloadAsText('/tmp/hello.txt');
96+
console.log(text); // "Hello, World!"
97+
```
98+
99+
#### `downloadAsBlob(sourcePath)`
100+
Download file content as a Blob.
101+
102+
```typescript
103+
const blob = await workspace.downloadAsBlob('/tmp/image.png');
104+
// Use blob for further processing
105+
```
106+
107+
#### `downloadAndSave(sourcePath, saveAsFileName?)`
108+
Download a file and trigger browser download dialog.
109+
110+
```typescript
111+
// This will prompt the user to save the file
112+
await workspace.downloadAndSave('/tmp/report.pdf', 'my-report.pdf');
113+
```
114+
115+
## Migration from Node.js API
116+
117+
### Before (Node.js only)
118+
```typescript
119+
// Old API - required file system paths
120+
await workspace.fileUpload('/local/path/file.txt', '/remote/path/file.txt');
121+
await workspace.fileDownload('/remote/path/file.txt', '/local/path/file.txt');
122+
```
123+
124+
### After (Browser compatible)
125+
```typescript
126+
// New API - works with browser data types
127+
const fileInput = document.getElementById('file') as HTMLInputElement;
128+
const file = fileInput.files[0];
129+
await workspace.fileUpload(file, '/remote/path/file.txt');
130+
131+
const result = await workspace.fileDownload('/remote/path/file.txt');
132+
if (result.success) {
133+
// Use result.content (string or Blob)
134+
console.log(result.content);
135+
}
136+
```
137+
138+
## Browser Testing
139+
140+
A test file `test-browser.html` is included to verify browser compatibility. Open it in a browser after building the project to test the API without a running server.
141+
142+
## Node.js Compatibility
143+
144+
The new API is also compatible with Node.js environments. You can still use the client in Node.js applications by providing appropriate data types:
145+
146+
```typescript
147+
import fs from 'fs';
148+
149+
// Read file content and upload
150+
const content = await fs.promises.readFile('/local/file.txt', 'utf8');
151+
await workspace.fileUpload(content, '/remote/file.txt', 'file.txt');
152+
153+
// Download and save
154+
const result = await workspace.fileDownload('/remote/file.txt');
155+
if (result.success && typeof result.content === 'string') {
156+
await fs.promises.writeFile('/local/downloaded.txt', result.content);
157+
}
158+
```

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
A TypeScript client library for the OpenHands Agent Server API. Mirrors the structure and functionality of the Python [OpenHands Software Agent SDK](https://github.com/OpenHands/software-agent-sdk),
1515
but only supports remote conversations.
1616

17+
## ✨ Browser Compatible
18+
19+
This client is **fully browser-compatible** and works without Node.js dependencies. File operations use browser-native APIs like `Blob`, `File`, and `FormData` instead of file system operations. Perfect for web applications, React apps, and other browser-based projects.
20+
21+
See [BROWSER_COMPATIBILITY.md](./BROWSER_COMPATIBILITY.md) for detailed usage examples.
22+
1723
## Installation
1824

1925
This package is published to GitHub Packages. You have two installation options:

examples/basic-usage.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import { Conversation, Agent, Workspace, AgentExecutionStatus } from '../src/ind
66

77
async function main() {
88
// Define the agent configuration
9+
// Note: In a browser environment, you would get these values from your app's configuration
910
const agent = new Agent({
1011
llm: {
1112
model: 'gpt-4',
12-
api_key: process.env.OPENAI_API_KEY || 'your-openai-api-key',
13+
api_key: 'your-openai-api-key', // Replace with your actual API key
1314
},
1415
});
1516

@@ -18,7 +19,7 @@ async function main() {
1819
const workspace = new Workspace({
1920
host: 'http://localhost:3000',
2021
workingDir: '/tmp',
21-
apiKey: process.env.SESSION_API_KEY || 'your-session-api-key',
22+
apiKey: 'your-session-api-key', // Replace with your actual session API key
2223
});
2324

2425
// Create a new conversation
@@ -87,7 +88,7 @@ async function loadExistingConversation() {
8788
const agent = new Agent({
8889
llm: {
8990
model: 'gpt-4',
90-
api_key: process.env.OPENAI_API_KEY || 'your-openai-api-key',
91+
api_key: 'your-openai-api-key', // Replace with your actual API key
9192
},
9293
});
9394

@@ -96,7 +97,7 @@ async function loadExistingConversation() {
9697
const workspace = new Workspace({
9798
host: 'http://localhost:3000',
9899
workingDir: '/tmp',
99-
apiKey: process.env.SESSION_API_KEY || 'your-session-api-key',
100+
apiKey: 'your-session-api-key', // Replace with your actual session API key
100101
});
101102

102103
const conversation = new Conversation(agent, workspace, {
@@ -120,6 +121,6 @@ async function loadExistingConversation() {
120121
}
121122

122123
// Run the example
123-
if (require.main === module) {
124-
main().catch(console.error);
125-
}
124+
// Note: In a browser environment, you would call main() directly or from an event handler
125+
// For Node.js environments, you can use import.meta.main (ES modules) or check if this is the main module
126+
main().catch(console.error);

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export type { AgentOptions } from './agent/agent';
5555
export { EventSortOrder, AgentExecutionStatus } from './types/base';
5656

5757
// Workspace models
58-
export type { CommandResult, FileOperationResult, GitChange, GitDiff } from './models/workspace';
58+
export type { CommandResult, FileOperationResult, FileDownloadResult, GitChange, GitDiff } from './models/workspace';
5959

6060
// Conversation models
6161
export type {

src/models/workspace.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ export interface FileOperationResult {
1818
error?: string;
1919
}
2020

21+
export interface FileDownloadResult {
22+
success: boolean;
23+
source_path: string;
24+
content: string | Blob;
25+
file_size?: number;
26+
error?: string;
27+
}
28+
2129
export interface GitChange {
2230
path: string;
2331
status: 'added' | 'modified' | 'deleted' | 'renamed';

0 commit comments

Comments
 (0)