Skip to content

Commit f58a53a

Browse files
davindicodeclaude
andcommitted
UI polish: online/offline label, select mode, font size for previews, tunnel cleanup
- Header: show online/offline text with color next to status dot (always visible) - Code editor: Select mode toggle for mobile text selection (read-only Monaco) - Font size control now applies to markdown and notebook previews - DNS tip in README wrapped in collapsible details element - Browser tabs: closing a tab stops its port tunnel and logs cleanup - Terminal action buttons: scroll-aware long press (no accidental fires) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a57b308 commit f58a53a

5 files changed

Lines changed: 66 additions & 43 deletions

File tree

README.md

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -53,38 +53,35 @@ pnpm dev
5353

5454
The `start.sh` script installs dependencies if needed, builds the app, starts the server, and launches a Cloudflare Quick Tunnel — printing a public URL you can open on any device.
5555

56-
> **DNS tip:** Quick tunnel URLs (e.g. `xxxx.trycloudflare.com`) resolve instantly on Cloudflare DNS but may take time on ISP/router DNS servers, or fail entirely if a stale NXDOMAIN gets cached. Set your DNS to **1.1.1.1** (Cloudflare) for instant resolution — it's faster and more reliable than most ISP DNS:
57-
>
58-
> **macOS:**
59-
> ```bash
60-
> # Set DNS for Wi-Fi (most common)
61-
> networksetup -setdnsservers Wi-Fi 1.1.1.1 1.0.0.1
62-
> # Flush DNS cache
63-
> sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
64-
> # To revert to automatic/router DNS:
65-
> # networksetup -setdnsservers Wi-Fi empty
66-
> ```
67-
>
68-
> **Linux (systemd-resolved):**
69-
> ```bash
70-
> # Find your interface name (e.g. eth0, wlan0)
71-
> resolvectl dns $(ip route show default | awk '{print $5}') 1.1.1.1 1.0.0.1
72-
> # Or permanently via /etc/systemd/resolved.conf:
73-
> # [Resolve]
74-
> # DNS=1.1.1.1 1.0.0.1
75-
> # sudo systemctl restart systemd-resolved
76-
> ```
77-
>
78-
> **Windows (PowerShell as Admin):**
79-
> ```powershell
80-
> # Find your interface (e.g. "Wi-Fi" or "Ethernet")
81-
> Get-NetAdapter | Select Name, Status
82-
> # Set DNS
83-
> Set-DnsClientServerAddress -InterfaceAlias "Wi-Fi" -ServerAddresses 1.1.1.1,1.0.0.1
84-
> # Flush DNS cache
85-
> ipconfig /flushdns
86-
> # To revert: Set-DnsClientServerAddress -InterfaceAlias "Wi-Fi" -ResetServerAddresses
87-
> ```
56+
<details>
57+
<summary><strong>DNS tip:</strong> Tunnel URLs not resolving? Set your DNS to 1.1.1.1</summary>
58+
59+
Quick tunnel URLs (e.g. `xxxx.trycloudflare.com`) resolve instantly on Cloudflare DNS but may take time on ISP/router DNS servers, or fail entirely if a stale NXDOMAIN gets cached. Set your DNS to **1.1.1.1** (Cloudflare) for instant resolution — it's faster and more reliable than most ISP DNS.
60+
61+
**macOS:**
62+
```bash
63+
networksetup -setdnsservers Wi-Fi 1.1.1.1 1.0.0.1
64+
sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder
65+
# To revert: networksetup -setdnsservers Wi-Fi empty
66+
```
67+
68+
**Linux (systemd-resolved):**
69+
```bash
70+
resolvectl dns $(ip route show default | awk '{print $5}') 1.1.1.1 1.0.0.1
71+
# Or permanently via /etc/systemd/resolved.conf:
72+
# [Resolve]
73+
# DNS=1.1.1.1 1.0.0.1
74+
# sudo systemctl restart systemd-resolved
75+
```
76+
77+
**Windows (PowerShell as Admin):**
78+
```powershell
79+
Get-NetAdapter | Select Name, Status
80+
Set-DnsClientServerAddress -InterfaceAlias "Wi-Fi" -ServerAddresses 1.1.1.1,1.0.0.1
81+
ipconfig /flushdns
82+
# To revert: Set-DnsClientServerAddress -InterfaceAlias "Wi-Fi" -ResetServerAddresses
83+
```
84+
</details>
8885

8986
## Scripts
9087

app/components/Header.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ export default function Header() {
1212
<div className="flex items-center gap-2">
1313
<span
1414
className={`w-2 h-2 rounded-full ${connected ? "bg-green-500" : "bg-red-500"}`}
15-
title={connected ? "Connected" : "Disconnected"}
1615
/>
17-
<span className="text-xs text-gray-400 hidden sm:inline">
18-
{connected ? "Connected" : "Disconnected"}
16+
<span className={`text-xs ${connected ? "text-green-500" : "text-red-400"}`}>
17+
{connected ? "online" : "offline"}
1918
</span>
2019
</div>
2120
</header>

app/components/files/CodeEditor.tsx

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,16 @@ function getLanguage(path: string): string {
5252
return map[ext] || "plaintext";
5353
}
5454

55-
function MarkdownPreview({ content }: { content: string }) {
55+
function MarkdownPreview({ content, fontSize }: { content: string; fontSize: number }) {
5656
const html = useMemo(() => {
5757
marked.setOptions({ breaks: true, gfm: true });
5858
return marked.parse(content) as string;
5959
}, [content]);
6060

6161
return (
6262
<div
63-
className="prose prose-invert prose-sm max-w-none p-4 overflow-auto h-full bg-[#0d0d1a]"
63+
className="prose prose-invert max-w-none p-4 overflow-auto h-full bg-[#0d0d1a]"
64+
style={{ fontSize: `${fontSize}px` }}
6465
dangerouslySetInnerHTML={{ __html: html }}
6566
/>
6667
);
@@ -97,7 +98,7 @@ function joinSource(s: string | string[] | undefined): string {
9798
return Array.isArray(s) ? s.join("") : s;
9899
}
99100

100-
function NotebookPreview({ content }: { content: string }) {
101+
function NotebookPreview({ content, fontSize }: { content: string; fontSize: number }) {
101102
const cells = useMemo(() => {
102103
try {
103104
const nb = JSON.parse(content);
@@ -116,7 +117,7 @@ function NotebookPreview({ content }: { content: string }) {
116117
}
117118

118119
return (
119-
<div className="overflow-auto h-full bg-[#0d0d1a] p-4 space-y-3">
120+
<div className="overflow-auto h-full bg-[#0d0d1a] p-4 space-y-3" style={{ fontSize: `${fontSize}px` }}>
120121
{cells.map((cell, i) => (
121122
<div key={i} className="rounded border border-gray-700 overflow-hidden">
122123
{/* Cell header */}
@@ -192,6 +193,7 @@ export default function CodeEditor({ path, content, onSave, onClose }: CodeEdito
192193
const [value, setValue] = useState(content);
193194
const [dirty, setDirty] = useState(false);
194195
const [mode, setMode] = useState<"edit" | "preview">(canPreview ? "preview" : "edit");
196+
const [selectMode, setSelectMode] = useState(false);
195197
const [editorFontSize, setEditorFontSize] = useState(6);
196198

197199
const handleChange = (v: string | undefined) => {
@@ -236,6 +238,20 @@ export default function CodeEditor({ path, content, onSave, onClose }: CodeEdito
236238
</button>
237239
</div>
238240
)}
241+
{/* Select mode toggle (for mobile text selection) */}
242+
{mode === "edit" && (
243+
<button
244+
onClick={() => setSelectMode((s) => !s)}
245+
className={`px-2 py-0.5 text-xs rounded border transition-colors ${
246+
selectMode
247+
? "bg-yellow-600 text-white border-yellow-500"
248+
: "text-gray-400 hover:text-white border-gray-700"
249+
}`}
250+
title={selectMode ? "Exit select mode (read-only)" : "Select mode (read-only, enables text selection)"}
251+
>
252+
Select
253+
</button>
254+
)}
239255
{/* Font size */}
240256
<div className="flex items-center border border-gray-700 rounded overflow-hidden">
241257
<button
@@ -283,11 +299,11 @@ export default function CodeEditor({ path, content, onSave, onClose }: CodeEdito
283299
<div className="flex-1 min-h-0">
284300
{mode === "preview" && canPreview ? (
285301
ext === "ipynb" ? (
286-
<NotebookPreview content={value} />
302+
<NotebookPreview content={value} fontSize={editorFontSize} />
287303
) : ext === "html" || ext === "htm" ? (
288304
<HtmlPreview content={value} />
289305
) : (
290-
<MarkdownPreview content={value} />
306+
<MarkdownPreview content={value} fontSize={editorFontSize} />
291307
)
292308
) : (
293309
<Suspense
@@ -312,7 +328,9 @@ export default function CodeEditor({ path, content, onSave, onClose }: CodeEdito
312328
automaticLayout: true,
313329
selectionHighlight: true,
314330
occurrencesHighlight: "singleFile",
315-
dragAndDrop: true,
331+
dragAndDrop: !selectMode,
332+
readOnly: selectMode,
333+
domReadOnly: selectMode,
316334
}}
317335
/>
318336
</Suspense>

app/components/terminal/InputBox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ const GROUPS: QuickKeyGroup[] = [
8585
{ label: "o pane", key: "\x02o", title: "Next pane" },
8686
{ label: "x kill", key: "\x02x", title: "Kill pane" },
8787
{ label: "z zoom", key: "\x02z", title: "Toggle zoom pane" },
88-
{ label: "[ scroll", key: "\x02[", title: "Enter scroll/copy mode (q to exit)" },
88+
{ label: "[ scroll", key: "\x02[", title: "Enter scroll/copy mode (Esc to exit)" },
8989
{ label: "] paste", key: "\x02]", title: "Paste from tmux buffer" },
9090
{ label: ", rename", key: "\x02,", title: "Rename window" },
9191
{ label: "w list", key: "\x02w", title: "List windows" },

app/stores/browserStore.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { create } from "zustand";
2+
import { getSocket } from "~/lib/socket";
23

34
export interface BrowserTab {
45
id: string;
@@ -34,6 +35,7 @@ export const useBrowserStore = create<BrowserState>((set, get) => ({
3435

3536
removeTab: (id) => {
3637
const { tabs, activeTabId } = get();
38+
const removed = tabs.find((t) => t.id === id);
3739
const newTabs = tabs.filter((t) => t.id !== id);
3840
let newActive = activeTabId;
3941
if (activeTabId === id) {
@@ -43,6 +45,13 @@ export const useBrowserStore = create<BrowserState>((set, get) => ({
4345
: null;
4446
}
4547
set({ tabs: newTabs, activeTabId: newActive });
48+
// Stop tunnel if no other tab uses this port
49+
if (removed?.port) {
50+
const portStillUsed = newTabs.some((t) => t.port === removed.port);
51+
if (!portStillUsed) {
52+
getSocket().emit("stop_port_tunnel", { port: parseInt(removed.port, 10) });
53+
}
54+
}
4655
},
4756

4857
setActiveTab: (id) => set({ activeTabId: id }),

0 commit comments

Comments
 (0)