Skip to content

Commit c238adb

Browse files
authored
🤖 Open markdown links externally and add PWA support (#359)
Fixes the issue where clicking links in markdown (like web/server docs) would replace the entire app window instead of opening externally. ## Changes **Markdown Links:** - Added custom anchor component to markdown renderer - All links now open with `target='_blank'` and `rel='noopener noreferrer'` **PWA Support:** - Added manifest.json with app metadata, icons, and shortcuts - Created service worker for offline support and caching strategy - Registered service worker in main.tsx - Updated index.html with PWA meta tags ## Testing 1. Click any link in markdown content - should open in external browser/tab 2. Install as PWA from browser (if supported) 3. Verify offline functionality with service worker _Generated with `cmux`_
1 parent d43e30a commit c238adb

File tree

7 files changed

+118
-0
lines changed

7 files changed

+118
-0
lines changed

index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta name="description" content="Parallel agentic development with Electron + React" />
7+
<meta name="theme-color" content="#1e1e1e" />
8+
<link rel="manifest" href="/manifest.json" />
9+
<link rel="apple-touch-icon" href="/icon-192.png" />
610
<title>cmux - coder multiplexer</title>
711
<style>
812
body {

public/icon-192.png

7.61 KB
Loading

public/icon-512.png

23.4 KB
Loading

public/manifest.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "cmux - coder multiplexer",
3+
"short_name": "cmux",
4+
"description": "Parallel agentic development with Electron + React",
5+
"start_url": "/",
6+
"display": "standalone",
7+
"background_color": "#1e1e1e",
8+
"theme_color": "#1e1e1e",
9+
"orientation": "any",
10+
"icons": [
11+
{
12+
"src": "/icon-192.png",
13+
"sizes": "192x192",
14+
"type": "image/png",
15+
"purpose": "any maskable"
16+
},
17+
{
18+
"src": "/icon-512.png",
19+
"sizes": "512x512",
20+
"type": "image/png",
21+
"purpose": "any maskable"
22+
}
23+
],
24+
"categories": ["development", "productivity", "utilities"],
25+
"shortcuts": [
26+
{
27+
"name": "New Workspace",
28+
"short_name": "New",
29+
"description": "Create a new workspace",
30+
"url": "/?action=new",
31+
"icons": []
32+
}
33+
]
34+
}
35+

public/service-worker.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// cmux Service Worker for PWA support
2+
const CACHE_NAME = 'cmux-v1';
3+
const urlsToCache = [
4+
'/',
5+
'/index.html',
6+
];
7+
8+
// Install event - cache core assets
9+
self.addEventListener('install', (event) => {
10+
event.waitUntil(
11+
caches.open(CACHE_NAME)
12+
.then((cache) => cache.addAll(urlsToCache))
13+
.then(() => self.skipWaiting())
14+
);
15+
});
16+
17+
// Activate event - clean up old caches
18+
self.addEventListener('activate', (event) => {
19+
event.waitUntil(
20+
caches.keys()
21+
.then((cacheNames) => {
22+
return Promise.all(
23+
cacheNames.map((cacheName) => {
24+
if (cacheName !== CACHE_NAME) {
25+
return caches.delete(cacheName);
26+
}
27+
})
28+
);
29+
})
30+
.then(() => self.clients.claim())
31+
);
32+
});
33+
34+
// Fetch event - network first, fallback to cache
35+
self.addEventListener('fetch', (event) => {
36+
event.respondWith(
37+
fetch(event.request)
38+
.then((response) => {
39+
// Clone the response before caching
40+
const responseToCache = response.clone();
41+
caches.open(CACHE_NAME)
42+
.then((cache) => {
43+
cache.put(event.request, responseToCache);
44+
});
45+
return response;
46+
})
47+
.catch(() => {
48+
// If network fails, try cache
49+
return caches.match(event.request);
50+
})
51+
);
52+
});
53+

src/components/Messages/MarkdownComponents.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ interface SummaryProps {
2727
children?: ReactNode;
2828
}
2929

30+
interface AnchorProps {
31+
href?: string;
32+
children?: ReactNode;
33+
}
34+
3035
interface CodeBlockProps {
3136
code: string;
3237
language: string;
@@ -89,6 +94,13 @@ export const markdownComponents = {
8994
// Pass through pre element - let code component handle the wrapping
9095
pre: ({ children }: PreProps) => <>{children}</>,
9196

97+
// Custom anchor to open links externally
98+
a: ({ href, children }: AnchorProps) => (
99+
<a href={href} target="_blank" rel="noopener noreferrer">
100+
{children}
101+
</a>
102+
),
103+
92104
// Custom details/summary for collapsible sections
93105
details: ({ children, open }: DetailsProps) => (
94106
<details

src/main.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,17 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
3838
<App />
3939
</React.StrictMode>
4040
);
41+
42+
// Register service worker for PWA support
43+
if ("serviceWorker" in navigator) {
44+
window.addEventListener("load", () => {
45+
navigator.serviceWorker
46+
.register("/service-worker.js")
47+
.then((registration) => {
48+
console.log("Service Worker registered:", registration);
49+
})
50+
.catch((error) => {
51+
console.log("Service Worker registration failed:", error);
52+
});
53+
});
54+
}

0 commit comments

Comments
 (0)