Skip to content

Commit 071fa70

Browse files
authored
Merge pull request #1 from davindicode/post-v0.1.0-fixes
Release v0.1.1
2 parents 2d30db2 + 5c0895c commit 071fa70

46 files changed

Lines changed: 2976 additions & 795 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,14 @@ jobs:
2424

2525
- run: pnpm install --frozen-lockfile
2626

27+
- name: Lint & format check
28+
run: pnpm run lint
29+
2730
- name: TypeScript check
2831
run: pnpm run typecheck
2932

33+
- name: Test
34+
run: pnpm test
35+
3036
- name: Build
3137
run: pnpm run build

CHANGELOG.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Changelog
2+
3+
All notable changes to this project are documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
## [0.1.1] - 2026-06-13
11+
12+
### Added
13+
- **Download** action in the file explorer dropdown — download any file without
14+
opening it first (works on mobile and desktop).
15+
- **Copy path** action in the file explorer dropdown — copy the absolute path of
16+
a file or folder to paste into the CLI, with a clipboard fallback for contexts
17+
where the Clipboard API is unavailable.
18+
- Editor **Plain** view mode backed by a native textarea, so mobile native
19+
selection and Select-All work for copying code/logs (Monaco's custom-rendered
20+
editor does not support this on touch). Available on desktop too.
21+
- Image viewer: desktop **click-and-drag to pan** a zoomed image (hand tool),
22+
matching the touch swipe/pinch controls on mobile.
23+
24+
### Changed
25+
- Arrow keys are now pinned to the end of the persistent terminal key bar
26+
(Enter · Bksp → Esc · Tab · PgUp · PgDn · y · n → ↑ · ↓ · ← · →).
27+
- Moved y/n keys into the persistent nav bar and removed duplicate code-tab keys.
28+
29+
### Fixed
30+
- Cloudflare quick tunnel robustness: cloudflared output is now logged, the
31+
promise resolves immediately on early exit instead of a 30s silent wait, and
32+
startup retries up to 3 times before falling back to local-only.
33+
- Quick tunnel is isolated from any system-wide `cloudflared` config so a global
34+
ingress catch-all can no longer hijack quick-tunnel traffic.
35+
- File explorer dropdown menu now stays attached to its entry on scroll, clamps
36+
to the pane boundary instead of overflowing on mobile, and flips upward for
37+
bottom entries so it is never clipped.
38+
- Markdown preview now renders local images/video referenced by relative or
39+
absolute on-disk paths (e.g. a README's `public/logo.png`) by routing them
40+
through the file API, instead of showing a broken-media icon.
41+
- Per-terminal `cd` picker state and correct tmux pane resolution.
42+
43+
## [0.1.0] - 2026-04-19
44+
45+
- Initial public release.
46+
47+
[Unreleased]: https://github.com/davindicode/otgcode/compare/v0.1.1...HEAD
48+
[0.1.1]: https://github.com/davindicode/otgcode/compare/v0.1.0...v0.1.1
49+
[0.1.0]: https://github.com/davindicode/otgcode/releases/tag/v0.1.0

CONTRIBUTING.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Contributing to OTG Code
2+
3+
Thanks for your interest in improving OTG Code! This guide covers the dev
4+
setup, conventions, and PR workflow.
5+
6+
## Prerequisites
7+
8+
- **Node.js** `>= 20.19`
9+
- **pnpm** `>= 10` (`corepack enable` will provide it)
10+
11+
## Setup
12+
13+
```bash
14+
pnpm install
15+
```
16+
17+
### Run it
18+
19+
```bash
20+
pnpm dev # local dev server (no tunnel)
21+
bash start.sh # production build + server + Cloudflare quick tunnel
22+
```
23+
24+
By default the app runs on http://localhost:7777 (override with `OTG_PORT`).
25+
26+
## Checks (run before opening a PR)
27+
28+
CI runs these on every PR; they must pass.
29+
30+
```bash
31+
pnpm lint # Biome lint + format check (biome ci)
32+
pnpm format # auto-fix formatting and safe lint issues
33+
pnpm typecheck # React Router typegen + tsc
34+
pnpm run build # production build
35+
```
36+
37+
## Code style
38+
39+
Formatting and linting are handled by [Biome](https://biomejs.dev) — config in
40+
`biome.json`. Run `pnpm format` to auto-apply. Don't hand-format; let Biome
41+
decide. Match the conventions of the surrounding code.
42+
43+
## Commit messages — Conventional Commits
44+
45+
This project uses [Conventional Commits](https://www.conventionalcommits.org).
46+
It keeps history readable and makes the `CHANGELOG.md` easy to assemble per
47+
release.
48+
49+
Common types:
50+
51+
- `feat:` — a new feature
52+
- `fix:` — a bug fix
53+
- `chore:`, `docs:`, `style:`, `refactor:`, `test:`, `ci:` — supporting changes
54+
55+
Example: `fix: flip file menu upward near the bottom of the pane`
56+
57+
Security fixes should use `fix:` and are listed under a **Security** section in
58+
the changelog — there is no separate security-advisory file.
59+
60+
## Pull request workflow
61+
62+
1. Branch off `main`.
63+
2. Make your change; keep commits focused and Conventionally named.
64+
3. Ensure `pnpm lint`, `pnpm typecheck`, and `pnpm run build` pass.
65+
4. Open a PR against `main` with a short description of what and why.
66+
67+
## Releasing (maintainers)
68+
69+
Releases are cut manually:
70+
71+
1. Update the version in `package.json` and add a section to `CHANGELOG.md`
72+
(group changes under Added / Changed / Fixed / Security).
73+
2. Merge to `main`.
74+
3. Tag and push: `git tag -a vX.Y.Z -m "Release vX.Y.Z" && git push origin vX.Y.Z`.
75+
4. Create a GitHub Release for the tag, using the changelog section as the notes.
76+
77+
The in-app version (shown in the header) is read from `package.json` at build
78+
time, so bumping `package.json` is enough to update it.
79+
80+
## Project layout
81+
82+
```
83+
app/ React Router (SSR) frontend
84+
components/ UI — browser/, files/, terminal/
85+
routes/ routes + routes/api/ (file ops, tmux, system info)
86+
stores/ Zustand stores
87+
lib/ shared client helpers (clipboard, socket, constants)
88+
server/ Node backend
89+
index.ts Express + Socket.IO entry
90+
pty-manager.ts node-pty terminal sessions
91+
socket-handlers.ts realtime terminal I/O
92+
proxy.ts reverse proxy
93+
tunnel.ts Cloudflare quick-tunnel management
94+
start.sh builds, installs cloudflared, runs server + tunnel
95+
```

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<p align="center">Work on your VPS or home machine from anywhere — phone, tablet, or desktop — with zero client-side installation. A mobile-optimised terminal built for coding CLIs, with file explorer, code editor, and localhost proxy — all in a single browser tab, accessible via a single Cloudflare Quick Tunnel.</p>
99

1010
<p align="center">
11-
<img src="https://img.shields.io/badge/version-0.1.0-blue" alt="Version">
11+
<img src="https://img.shields.io/github/package-json/v/davindicode/otgcode?color=blue" alt="Version">
1212
<img src="https://img.shields.io/badge/status-alpha-orange" alt="Status: alpha">
1313
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green" alt="License"></a>
1414
<img src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" alt="Node >= 20">

app/components/AppShell.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { useState, useEffect } from "react";
1+
import { useEffect, useState } from "react";
2+
import { useUiStore } from "~/stores/uiStore";
3+
import BrowserPage from "./browser/BrowserPage";
4+
import FilesPage from "./files/FilesPage";
25
import Header from "./Header";
36
import MobileTabBar from "./MobileTabBar";
47
import ResizablePanels from "./ResizablePanels";
5-
import TerminalPage from "./terminal/TerminalPage";
6-
import FilesPage from "./files/FilesPage";
7-
import BrowserPage from "./browser/BrowserPage";
88
import InputBox from "./terminal/InputBox";
9-
import { useUiStore } from "~/stores/uiStore";
9+
import TerminalPage from "./terminal/TerminalPage";
1010

1111
// Desktop layout requires landscape orientation AND at least 768px width,
1212
// OR at least 1024px width in any orientation.
@@ -15,7 +15,7 @@ const DESKTOP_QUERY = "(min-width: 1024px), (min-width: 768px) and (orientation:
1515

1616
function useIsDesktop() {
1717
const [isDesktop, setIsDesktop] = useState(
18-
typeof window !== "undefined" ? window.matchMedia(DESKTOP_QUERY).matches : true
18+
typeof window !== "undefined" ? window.matchMedia(DESKTOP_QUERY).matches : true,
1919
);
2020
useEffect(() => {
2121
const mq = window.matchMedia(DESKTOP_QUERY);
@@ -59,10 +59,7 @@ export default function AppShell() {
5959
<TerminalPage />
6060
<InputBox />
6161
</div>
62-
<div
63-
className="flex-1 flex flex-col min-h-0"
64-
style={{ display: activeTab === "files" ? "flex" : "none" }}
65-
>
62+
<div className="flex-1 flex flex-col min-h-0" style={{ display: activeTab === "files" ? "flex" : "none" }}>
6663
<FilesPage />
6764
</div>
6865
<div

app/components/ClientOnly.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, type ReactNode } from "react";
1+
import { type ReactNode, useEffect, useState } from "react";
22

33
export default function ClientOnly({ children }: { children: () => ReactNode }) {
44
const [mounted, setMounted] = useState(false);

app/components/Header.tsx

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useRef } from "react";
1+
import { useEffect, useRef, useState } from "react";
22
import { useTerminalStore } from "~/stores/terminalStore";
33

44
declare const __APP_VERSION__: string;
@@ -23,11 +23,16 @@ function SystemInfoPopup({ onClose }: { onClose: () => void }) {
2323
}, [onClose]);
2424

2525
return (
26-
<div ref={popupRef} className="absolute right-2 top-10 z-50 bg-[#16162a] border border-gray-700 rounded-lg shadow-xl w-72 max-h-80 overflow-y-auto">
26+
<div
27+
ref={popupRef}
28+
className="absolute right-2 top-10 z-50 bg-[#16162a] border border-gray-700 rounded-lg shadow-xl w-72 max-h-80 overflow-y-auto"
29+
>
2730
<div className="flex items-center justify-between px-3 py-2 border-b border-gray-700">
2831
<span className="text-xs font-medium text-gray-300">System Info</span>
2932
<button onClick={onClose} className="text-gray-500 hover:text-white">
30-
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
33+
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
34+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
35+
</svg>
3136
</button>
3237
</div>
3338
{!info ? (
@@ -39,12 +44,14 @@ function SystemInfoPopup({ onClose }: { onClose: () => void }) {
3944
</div>
4045
) : (
4146
<div className="px-3 py-2 space-y-1">
42-
{Object.entries(info).map(([key, val]) => val ? (
43-
<div key={key} className="flex justify-between gap-2 text-[11px]">
44-
<span className="text-gray-500 shrink-0">{key}</span>
45-
<span className="text-gray-300 text-right break-all">{val}</span>
46-
</div>
47-
) : null)}
47+
{Object.entries(info).map(([key, val]) =>
48+
val ? (
49+
<div key={key} className="flex justify-between gap-2 text-[11px]">
50+
<span className="text-gray-500 shrink-0">{key}</span>
51+
<span className="text-gray-300 text-right break-all">{val}</span>
52+
</div>
53+
) : null,
54+
)}
4855
</div>
4956
)}
5057
</div>
@@ -56,9 +63,7 @@ export default function Header() {
5663
const sessions = useTerminalStore((s) => s.sessions);
5764
const [showInfo, setShowInfo] = useState(false);
5865

59-
const hasActiveSessions = Object.values(sessions).some(
60-
(s) => s.status === "connected" || s.status === "connecting"
61-
);
66+
const hasActiveSessions = Object.values(sessions).some((s) => s.status === "connected" || s.status === "connecting");
6267

6368
const status = !socketConnected
6469
? "offline"
@@ -82,7 +87,11 @@ export default function Header() {
8287
title="System info"
8388
>
8489
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
85-
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
90+
<path
91+
strokeLinecap="round"
92+
strokeLinejoin="round"
93+
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
94+
/>
8695
</svg>
8796
</button>
8897
<span
@@ -94,13 +103,11 @@ export default function Header() {
94103
: "bg-red-500"
95104
}`}
96105
/>
97-
<span className={`text-xs ${
98-
status === "online"
99-
? "text-green-500"
100-
: status === "reconnecting"
101-
? "text-yellow-500"
102-
: "text-red-400"
103-
}`}>
106+
<span
107+
className={`text-xs ${
108+
status === "online" ? "text-green-500" : status === "reconnecting" ? "text-yellow-500" : "text-red-400"
109+
}`}
110+
>
104111
{status}
105112
</span>
106113
</div>

app/components/MobileTabBar.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import { useUiStore, type TabId } from "~/stores/uiStore";
1+
import { type TabId, useUiStore } from "~/stores/uiStore";
22

33
const tabs: { id: TabId; label: string; icon: React.ReactNode }[] = [
44
{
55
id: "files",
66
label: "Files",
77
icon: (
88
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
9-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
9+
<path
10+
strokeLinecap="round"
11+
strokeLinejoin="round"
12+
strokeWidth={2}
13+
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"
14+
/>
1015
</svg>
1116
),
1217
},
@@ -15,7 +20,12 @@ const tabs: { id: TabId; label: string; icon: React.ReactNode }[] = [
1520
label: "Terminal",
1621
icon: (
1722
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
18-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
23+
<path
24+
strokeLinecap="round"
25+
strokeLinejoin="round"
26+
strokeWidth={2}
27+
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
28+
/>
1929
</svg>
2030
),
2131
},
@@ -24,7 +34,12 @@ const tabs: { id: TabId; label: string; icon: React.ReactNode }[] = [
2434
label: "Localhost",
2535
icon: (
2636
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
27-
<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-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
37+
<path
38+
strokeLinecap="round"
39+
strokeLinejoin="round"
40+
strokeWidth={2}
41+
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"
42+
/>
2843
</svg>
2944
),
3045
},

app/components/RenamableTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useRef, useEffect } from "react";
1+
import { useEffect, useRef, useState } from "react";
22

33
interface RenamableTabProps {
44
name: string;

0 commit comments

Comments
 (0)