Skip to content

Commit a4c10ff

Browse files
committed
feat: add mobile support
1 parent fb30451 commit a4c10ff

10 files changed

Lines changed: 163 additions & 68 deletions

File tree

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,15 @@ A standard OpenCode plugin script that hooks into the event bus:
6464
- `session` events (status changes)
6565
- `fs` events (file modifications)
6666

67-
## Installation
67+
## 📱 Mobile Support
68+
69+
Monitor your agents from your phone or tablet!
70+
71+
- **Network Access**: The server automatically binds to your LAN IP.
72+
- **QR Code**: In the dashboard, click the Network URL in the top-right corner to reveal a QR code. Scan it to instantly connect.
73+
- **Responsive Design**: The dashboard adapts to portrait and landscape modes on mobile devices.
74+
75+
![Mobile View](docs/mobile_preview.png)
6876

6977
## Installation
7078

client/src/App.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useState } from "react";
2+
import QRCode from "react-qr-code";
13
import { AgentPanel } from "./components/AgentPanel";
24
import { ScreenFrame } from "./components/ScreenFrame";
35
import { SessionPanel } from "./components/SessionPanel";
@@ -19,10 +21,13 @@ const App = () => {
1921
appVersion,
2022
lastTodoSummary,
2123
bossMessage,
24+
networkIp,
2225
setSelectedAgentId,
2326
setSelectedSessionId,
2427
} = useOfficeState();
2528

29+
const [showQr, setShowQr] = useState(false);
30+
2631
const activeLabel = activeSessionId ? activeSessionId.slice(0, 10) : "N/A";
2732
const version = appVersion ? `v${appVersion}` : "v1.0.0";
2833
const todoSummary = lastTodoSummary
@@ -43,6 +48,31 @@ const App = () => {
4348
<div className={`status-badge ${connected ? "live" : ""}`}>
4449
<span className="text-[8px] opacity-70 mb-1">SYSTEM STATUS</span>
4550
<span className="font-bold">{connected ? "ONLINE" : "OFFLINE"}</span>
51+
{networkIp && connected && (
52+
<div className="relative mt-2 border-t border-white/20 pt-1 flex flex-col items-center">
53+
<button
54+
onClick={() => setShowQr(!showQr)}
55+
className="text-[8px] font-normal opacity-80 hover:opacity-100 hover:text-accent cursor-pointer underline decoration-dotted"
56+
title="Click to scan QR code"
57+
>
58+
http://{networkIp}:5100
59+
</button>
60+
61+
{showQr && (
62+
<div className="absolute top-8 right-0 bg-white p-2 border-2 border-foreground shadow-[4px_4px_0_rgba(0,0,0,0.2)] z-50">
63+
<div style={{ height: "auto", margin: "0 auto", maxWidth: 128, width: "100%" }}>
64+
<QRCode
65+
size={256}
66+
style={{ height: "auto", maxWidth: "100%", width: "100%" }}
67+
value={`http://${networkIp}:5100`}
68+
viewBox={`0 0 256 256`}
69+
/>
70+
</div>
71+
<div className="text-[8px] text-center mt-1 text-foreground font-bold">SCAN ME</div>
72+
</div>
73+
)}
74+
</div>
75+
)}
4676
</div>
4777
</header>
4878

client/src/PixiScene.tsx

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -568,14 +568,14 @@ const SceneLayer = ({
568568

569569
const getRandomWalkable = (allowChairs: boolean) => {
570570
if (!collisionMap) return { row: 32, col: 32 };
571-
for (let i = 0; i < 80; i++) {
572-
const r = Math.floor(Math.random() * collisionMap.rows);
573-
const c = Math.floor(Math.random() * collisionMap.cols);
574-
const state = collisionMap.grid[r][c];
575-
if (state <= 0) continue;
576-
if (!allowChairs && state === 2) continue;
577-
return { row: r, col: c };
578-
}
571+
for (let i = 0; i < 80; i++) {
572+
const r = Math.floor(Math.random() * collisionMap.rows);
573+
const c = Math.floor(Math.random() * collisionMap.cols);
574+
const state = collisionMap.grid[r][c];
575+
if (state <= 0) continue;
576+
if (!allowChairs && state === 2) continue;
577+
return { row: r, col: c };
578+
}
579579
return { row: 32, col: 32 };
580580
};
581581

@@ -1355,16 +1355,16 @@ const SceneLayer = ({
13551355
const motion = 0;
13561356
const direction = sprite.direction || "front";
13571357
const movement = Math.hypot(sprite.targetX - sprite.x, sprite.targetY - sprite.y);
1358-
const isRunning = movement > 0.8;
1359-
const isWalking = movement > 0.2;
1360-
const currentTile = pixelToTile(sprite.x, sprite.y);
1361-
const isOnWorkChair = Boolean(
1362-
collisionMap?.workNodes?.some(
1363-
(node) => node.row === currentTile.row && node.col === currentTile.col
1364-
)
1365-
);
1366-
const shouldJump = agent.status === "thinking" && isOnWorkChair;
1367-
const state = shouldJump ? "jump" : isRunning ? "run" : isWalking ? "walk" : "idle";
1358+
const isRunning = movement > 0.8;
1359+
const isWalking = movement > 0.2;
1360+
const currentTile = pixelToTile(sprite.x, sprite.y);
1361+
const isOnWorkChair = Boolean(
1362+
collisionMap?.workNodes?.some(
1363+
(node) => node.row === currentTile.row && node.col === currentTile.col
1364+
)
1365+
);
1366+
const shouldJump = agent.status === "thinking" && isOnWorkChair;
1367+
const state = shouldJump ? "jump" : isRunning ? "run" : isWalking ? "walk" : "idle";
13681368
const sheet = spriteSheets[state] || spriteSheets.idle;
13691369
const frameCount = sheet?.right.length || 1;
13701370
const frameSpeed = isRunning ? 90 : isWalking ? 120 : 200;
@@ -1452,7 +1452,7 @@ const SceneLayer = ({
14521452
const labelPaddingX = 4;
14531453
const chipHeight = 12;
14541454
const chipGap = 4;
1455-
const labelY = y + 34 + motion;
1455+
const labelY = y + 34 + motion;
14561456
const activityBubbleSize = 18;
14571457
const activityBubbleX = sprite.x - activityBubbleSize / 2;
14581458
const messageBubble: MessageBubbleData = {
@@ -1627,7 +1627,7 @@ export const PixiScene = (props: PixiSceneProps) => {
16271627
<Stage
16281628
width={dimensions.width}
16291629
height={dimensions.height}
1630-
style={{ width: dimensions.width, height: dimensions.height, display: "block" }}
1630+
style={{ display: "block" }}
16311631
options={{
16321632
antialias: false,
16331633
backgroundColor: 0x101615,

client/src/styles.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,13 @@ main {
224224
position: relative;
225225
}
226226

227+
.pixi-wrapper canvas {
228+
width: 100% !important;
229+
height: auto !important;
230+
object-fit: contain;
231+
max-height: 80vh; /* Prevent it from taking over too much vertical space */
232+
}
233+
227234
.pixi-wrapper::before {
228235
content: "OFFICE_VIEW";
229236
position: absolute;
@@ -366,5 +373,31 @@ main {
366373
@media (max-width: 900px) {
367374
main {
368375
grid-template-columns: 1fr;
376+
gap: 16px;
377+
}
378+
379+
.app {
380+
padding: 12px;
381+
}
382+
383+
header {
384+
margin-bottom: 16px;
385+
}
386+
387+
/* Make sidebar stack visibly underneath */
388+
.sidebar-stack {
389+
width: 100%;
369390
}
370391
}
392+
393+
@media (max-width: 600px) {
394+
/* Smaller font for mobile */
395+
:root {
396+
font-size: 14px;
397+
}
398+
399+
.gamish-card {
400+
border-width: 2px;
401+
}
402+
}
403+

client/src/useOfficeState.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type OfficeStatePayload = {
7171
appVersion?: string | null;
7272
lastTodoSummary?: TodoSummary | null;
7373
bossMessage?: BossMessage | null;
74+
networkIp?: string | null;
7475
};
7576

7677
const createSocketUrl = () => {
@@ -90,6 +91,7 @@ export const useOfficeState = () => {
9091
const [appVersion, setAppVersion] = useState<string | null>(null);
9192
const [lastTodoSummary, setLastTodoSummary] = useState<TodoSummary | null>(null);
9293
const [bossMessage, setBossMessage] = useState<BossMessage | null>(null);
94+
const [networkIp, setNetworkIp] = useState<string | null>(null);
9395

9496
useEffect(() => {
9597
let socket: WebSocket | null = null;
@@ -120,6 +122,7 @@ export const useOfficeState = () => {
120122
setAppVersion(nextState.appVersion || null);
121123
setLastTodoSummary(nextState.lastTodoSummary || null);
122124
setBossMessage(nextState.bossMessage || null);
125+
setNetworkIp(nextState.networkIp || null);
123126
if (nextState.activeSessionId) {
124127
const nextActive = nextState.activeSessionId ?? null;
125128
setSelectedSessionId((current) => current ?? nextActive);
@@ -187,6 +190,7 @@ export const useOfficeState = () => {
187190
appVersion,
188191
lastTodoSummary,
189192
bossMessage,
193+
networkIp,
190194
setSelectedAgentId,
191195
setSelectedSessionId,
192196
};

docs/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ <h2>Screenshots</h2>
3232
<div class="placeholder">Screenshot 3</div>
3333
<figcaption>Boss message + panels</figcaption>
3434
</figure>
35+
<figure class="card">
36+
<img src="mobile_preview.png" alt="Mobile Dashboard View" class="screenshot" />
37+
<figcaption>Responsive Mobile Dashboard</figcaption>
38+
</figure>
3539
</div>
3640
</section>
3741

@@ -42,6 +46,7 @@ <h2>Features</h2>
4246
<li>Streams OpenCode events in real time</li>
4347
<li>Agent movement, messages, and status indicators</li>
4448
<li>Session and todo panels for at-a-glance context</li>
49+
<li>Mobile Ready: Connect instantly via local network + QR Code</li>
4550
</ul>
4651
</section>
4752

docs/mobile_preview.png

692 KB
Loading

0 commit comments

Comments
 (0)