Skip to content

Commit 86f513a

Browse files
davindicodeclaude
andcommitted
Add PDF viewer, browser tab redesign, tmux/cmd quick keys, tunnel fix
- PDF viewer via inline iframe in file explorer - Browser panel redesigned: tab-driven UX, port input on new tab, refresh on active tab, blocked port validation as-you-type - Quick action keys: added tmux window buttons (0-5), common commands group (ls, nvidia-smi, htop, etc.), merged misc into Nav - Terminal tabs moved to footer (between output and input) - Font size: default 6, aA buttons instead of +/- - Cloudflare tunnel: force http2 protocol, temp HOME to bypass config - App port blocked in browser proxy via meta tag injection - start.sh: one-command production + tunnel launch - README: added logo, project-specific content Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ea7b7a4 commit 86f513a

16 files changed

Lines changed: 402 additions & 298 deletions

README.md

Lines changed: 50 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,68 @@
1-
# Welcome to React Router!
1+
<p align="center">
2+
<img src="public/logo-square.png" alt="otgcode" width="128" />
3+
</p>
24

3-
A modern, production-ready template for building full-stack React applications using React Router.
5+
<h1 align="center">otgcode</h1>
46

5-
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
7+
<p align="center">Mobile-terminal-UX first coding IDE. Control your VPS or local machine from anywhere — phone, tablet, or desktop browser.</p>
68

79
## Features
810

9-
- 🚀 Server-side rendering
10-
- ⚡️ Hot Module Replacement (HMR)
11-
- 📦 Asset bundling and optimization
12-
- 🔄 Data loading and mutations
13-
- 🔒 TypeScript by default
14-
- 🎉 TailwindCSS for styling
15-
- 📖 [React Router docs](https://reactrouter.com/)
11+
- **Terminal** — xterm.js with multi-session tabs, configurable font size, quick action keys (Ctrl combos, vim, nano, navigation)
12+
- **File Explorer** — multi-session tabs, breadcrumb navigation, create/rename/delete files and folders, file info, hidden files toggle
13+
- **Code Editor** — Monaco editor with syntax highlighting, markdown/HTML/Jupyter notebook preview mode
14+
- **Localhost Browser** — reverse proxy to preview any localhost port through the app, browser-style tabs
15+
- **Responsive** — 3-column resizable panels on desktop, tab bar on mobile
16+
- **Cloudflare Tunnel** — one command to expose your machine to the internet
1617

17-
## Getting Started
18-
19-
### Installation
20-
21-
Install the dependencies:
22-
23-
```bash
24-
npm install
25-
```
26-
27-
### Development
28-
29-
Start the development server with HMR:
18+
## Quick Start
3019

3120
```bash
32-
npm run dev
21+
# Clone and install
22+
git clone https://github.com/davindicode/otgcode.git
23+
cd otgcode
24+
pnpm install
25+
26+
# Create .env
27+
cp .env.example .env # or create manually:
28+
# OTG_PORT=7777
29+
# DEFAULT_SHELL=/bin/bash
30+
# DEFAULT_CWD=/home/user
31+
32+
# Development
33+
pnpm dev
34+
35+
# Production with tunnel (one command)
36+
./start.sh
3337
```
3438

35-
Your application will be available at `http://localhost:5173`.
36-
37-
## Building for Production
38-
39-
Create a production build:
40-
41-
```bash
42-
npm run build
43-
```
39+
## Scripts
4440

45-
## Deployment
41+
| Command | Description |
42+
|---------|-------------|
43+
| `pnpm dev` | Dev server with HMR on port 7777 |
44+
| `pnpm build` | Production build |
45+
| `pnpm start` | Production server |
46+
| `pnpm start:tunnel` | Production server + Cloudflare tunnel |
47+
| `./start.sh` | Build + start + tunnel (all-in-one) |
4648

47-
### Docker Deployment
49+
## Architecture
4850

49-
To build and run using Docker:
51+
Single Express server on one port handles everything:
5052

51-
```bash
52-
docker build -t my-app .
53-
54-
# Run the container
55-
docker run -p 3000:3000 my-app
56-
```
57-
58-
The containerized application can be deployed to any platform that supports Docker, including:
59-
60-
- AWS ECS
61-
- Google Cloud Run
62-
- Azure Container Apps
63-
- Digital Ocean App Platform
64-
- Fly.io
65-
- Railway
66-
67-
### DIY Deployment
68-
69-
If you're familiar with deploying Node applications, the built-in app server is production-ready.
70-
71-
Make sure to deploy the output of `npm run build`
72-
73-
```
74-
├── package.json
75-
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
76-
├── build/
77-
│ ├── client/ # Static assets
78-
│ └── server/ # Server-side code
79-
```
53+
- React Router v7 (framework mode) for the UI
54+
- Socket.IO for real-time terminal I/O via node-pty
55+
- REST API for file operations
56+
- Reverse proxy for localhost port forwarding
57+
- Cloudflare quick tunnel for remote access
8058

81-
## Styling
59+
## Tech Stack
8260

83-
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
61+
React Router v7, Express, Socket.IO, node-pty, xterm.js, Monaco Editor, Zustand, Tailwind CSS, TypeScript
8462

85-
---
63+
## Requirements
8664

87-
Built with ❤️ using React Router.
65+
- Node.js 20+
66+
- pnpm
67+
- C compiler (for node-pty on Linux: `apt install build-essential`)
68+
- cloudflared (optional, for tunnel)

app/components/browser/BrowserPage.tsx

Lines changed: 140 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,147 @@
1-
import PortSelector from "./PortSelector";
2-
import ProxyFrame from "./ProxyFrame";
3-
import BrowserTabs from "./BrowserTabs";
1+
import { useState, useRef, useEffect } from "react";
2+
import { useBrowserStore, type BrowserTab } from "~/stores/browserStore";
3+
import { isPortBlocked } from "~/lib/constants";
4+
5+
function TabInput({ tab }: { tab: BrowserTab }) {
6+
const [input, setInput] = useState(tab.port);
7+
const inputRef = useRef<HTMLInputElement>(null);
8+
const setTabPort = useBrowserStore((s) => s.setTabPort);
9+
10+
useEffect(() => {
11+
if (!tab.port) inputRef.current?.focus();
12+
}, []);
13+
14+
const val = input.trim();
15+
const isNumeric = val !== "" && /^\d+$/.test(val);
16+
const blocked = isNumeric ? isPortBlocked(parseInt(val, 10)) : null;
17+
const canConnect = isNumeric && !blocked;
18+
19+
const handleSubmit = () => {
20+
if (canConnect) setTabPort(tab.id, val);
21+
};
22+
23+
return (
24+
<div className="flex flex-col items-center justify-center h-full bg-[#0d0d1a] gap-4">
25+
<svg className="w-10 h-10 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
26+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
27+
</svg>
28+
<div className="flex items-center gap-2">
29+
<span className="text-sm text-gray-400">localhost:</span>
30+
<input
31+
ref={inputRef}
32+
type="text"
33+
value={input}
34+
onChange={(e) => setInput(e.target.value)}
35+
onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
36+
placeholder="port"
37+
className={`w-24 bg-[#1a1a2e] text-white border rounded px-3 py-2 text-sm focus:outline-none focus:ring-1 ${
38+
blocked ? "border-red-500 focus:ring-red-500" : "border-gray-600 focus:ring-blue-500"
39+
}`}
40+
autoFocus
41+
/>
42+
<button
43+
onClick={handleSubmit}
44+
disabled={!canConnect}
45+
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-700 disabled:text-gray-500 text-white rounded text-sm font-medium transition-colors"
46+
>
47+
Connect
48+
</button>
49+
</div>
50+
{blocked && (
51+
<span className="text-xs text-red-400">{blocked}</span>
52+
)}
53+
</div>
54+
);
55+
}
456

557
export default function BrowserPage() {
58+
const { tabs, activeTabId, addTab, removeTab, setActiveTab, refreshTab } = useBrowserStore();
59+
const activeTab = tabs.find((t) => t.id === activeTabId);
60+
661
return (
7-
<div className="flex flex-col h-full">
8-
<BrowserTabs />
9-
<PortSelector />
62+
<div className="flex flex-col h-full min-h-0">
63+
{/* Tab bar */}
64+
<div className="flex items-center bg-[#0d0d1a] border-b border-gray-800 overflow-x-auto scrollbar-none shrink-0">
65+
{tabs.map((tab) => {
66+
const isActive = tab.id === activeTabId;
67+
return (
68+
<div
69+
key={tab.id}
70+
onClick={() => setActiveTab(tab.id)}
71+
className={`flex items-center gap-1.5 px-3 py-1.5 cursor-pointer text-sm border-r border-gray-800 shrink-0 transition-colors ${
72+
isActive
73+
? "bg-[#16162a] text-white border-b-2 border-b-blue-500"
74+
: "text-gray-400 hover:text-white hover:bg-[#16162a]/50"
75+
}`}
76+
>
77+
<svg className="w-3 h-3 shrink-0 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
78+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9" />
79+
</svg>
80+
<span>{tab.port ? `:${tab.port}` : "new"}</span>
81+
{/* Refresh — only on active tab with a port */}
82+
{isActive && tab.port && (
83+
<button
84+
onClick={(e) => { e.stopPropagation(); refreshTab(tab.id); }}
85+
className="p-0.5 rounded hover:bg-gray-600 text-gray-500 hover:text-gray-200 transition-colors"
86+
title="Refresh"
87+
>
88+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
89+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
90+
</svg>
91+
</button>
92+
)}
93+
<button
94+
onClick={(e) => { e.stopPropagation(); removeTab(tab.id); }}
95+
className="p-0.5 rounded hover:bg-gray-600 text-gray-500 hover:text-gray-200 transition-colors"
96+
title="Close tab"
97+
>
98+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
99+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
100+
</svg>
101+
</button>
102+
</div>
103+
);
104+
})}
105+
<button
106+
onClick={addTab}
107+
className="px-3 py-1.5 text-gray-400 hover:text-white hover:bg-[#16162a]/50 transition-colors shrink-0"
108+
title="New browser tab"
109+
>
110+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
111+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
112+
</svg>
113+
</button>
114+
</div>
115+
116+
{/* Content */}
10117
<div className="flex-1 relative min-h-0">
11-
<ProxyFrame />
118+
{!activeTab ? (
119+
<div className="flex flex-col items-center justify-center h-full bg-[#0d0d1a] text-gray-500 gap-3">
120+
<svg className="w-12 h-12 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
121+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
122+
</svg>
123+
<span className="text-sm">Click + to preview a localhost port</span>
124+
</div>
125+
) : !activeTab.port ? (
126+
<TabInput tab={activeTab} />
127+
) : (
128+
/* Render all tabs with port, show active */
129+
tabs.filter((t) => t.port).map((tab) => (
130+
<div
131+
key={tab.id}
132+
className="absolute inset-0"
133+
style={{ visibility: tab.id === activeTabId ? "visible" : "hidden" }}
134+
>
135+
<iframe
136+
key={`${tab.port}-${tab.refreshKey}`}
137+
src={`/proxy/${tab.port}/`}
138+
className="w-full h-full border-0 bg-[#0d0d1a]"
139+
title={`localhost:${tab.port}`}
140+
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals"
141+
/>
142+
</div>
143+
))
144+
)}
12145
</div>
13146
</div>
14147
);

app/components/browser/BrowserTabs.tsx

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)