Skip to content

Commit e66a2fc

Browse files
committed
yo
1 parent b78d9be commit e66a2fc

8 files changed

Lines changed: 397 additions & 0 deletions

File tree

apps/demo/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Stale Closure Demo
2+
3+
A minimal chat app with a subtle stale closure bug — built to demonstrate [Debug Agent](https://github.com/millionco/debug-agent).
4+
5+
## The Bug
6+
7+
The **Send button** and **Enter key** work perfectly. But the **⌘/Ctrl+Enter** keyboard shortcut silently sends the wrong message (or nothing at all).
8+
9+
The root cause is non-obvious from reading the code alone — it requires runtime evidence to diagnose.
10+
11+
## Setup
12+
13+
```bash
14+
pnpm install
15+
pnpm --filter @debug-agent/demo dev
16+
```
17+
18+
## Reproduce
19+
20+
1. Open `http://localhost:5173`
21+
2. Type "hello" and click **Send** → works fine
22+
3. Type "goodbye" and press **⌘+Enter** (or **Ctrl+Enter**) → sends a blank/wrong message
23+
24+
## Debug with Debug Agent
25+
26+
```bash
27+
npx debug-agent@latest init
28+
```
29+
30+
Then ask your agent to debug: _"The ⌘+Enter shortcut sends the wrong message or nothing"_

apps/demo/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Chat App</title>
7+
</head>
8+
<body>
9+
<div id="root"></div>
10+
<script type="module" src="/src/main.tsx"></script>
11+
</body>
12+
</html>

apps/demo/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@debug-agent/demo",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "vite build",
8+
"preview": "vite preview"
9+
},
10+
"dependencies": {
11+
"react": "^19.1.0",
12+
"react-dom": "^19.1.0"
13+
},
14+
"devDependencies": {
15+
"@types/react": "^19.1.2",
16+
"@types/react-dom": "^19.1.2",
17+
"@vitejs/plugin-react": "^4.5.2",
18+
"typescript": "^5.8.3",
19+
"vite": "^6.3.2"
20+
}
21+
}

apps/demo/src/app.tsx

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { useEffect, useRef, useState } from "react";
2+
3+
interface Message {
4+
text: string;
5+
sender: "user" | "bot";
6+
timestamp: number;
7+
}
8+
9+
const BOT_REPLIES = [
10+
"Got it, thanks!",
11+
"Interesting, tell me more.",
12+
"That makes sense.",
13+
"I'll look into that.",
14+
"Thanks for letting me know!",
15+
];
16+
17+
const pickBotReply = () => BOT_REPLIES[Math.floor(Math.random() * BOT_REPLIES.length)];
18+
19+
const formatTime = (timestamp: number) =>
20+
new Date(timestamp).toLocaleTimeString([], {
21+
hour: "2-digit",
22+
minute: "2-digit",
23+
});
24+
25+
const isMac = typeof navigator !== "undefined" && /Mac/.test(navigator.userAgent);
26+
27+
export const App = () => {
28+
const [messages, setMessages] = useState<Message[]>([]);
29+
const [inputValue, setInputValue] = useState("");
30+
const messagesEndRef = useRef<HTMLDivElement>(null);
31+
32+
const addBotReply = () => {
33+
setTimeout(() => {
34+
setMessages((previous) => [
35+
...previous,
36+
{ text: pickBotReply(), sender: "bot", timestamp: Date.now() },
37+
]);
38+
}, 800);
39+
};
40+
41+
const handleSendFromButton = () => {
42+
const trimmed = inputValue.trim();
43+
if (!trimmed) return;
44+
45+
setMessages((previous) => [
46+
...previous,
47+
{ text: trimmed, sender: "user", timestamp: Date.now() },
48+
]);
49+
setInputValue("");
50+
addBotReply();
51+
};
52+
53+
useEffect(() => {
54+
const handleKeyDown = (event: KeyboardEvent) => {
55+
const modifierPressed = isMac ? event.metaKey : event.ctrlKey;
56+
if (event.key === "Enter" && modifierPressed) {
57+
event.preventDefault();
58+
const trimmed = inputValue.trim();
59+
if (!trimmed) return;
60+
61+
setMessages((previous) => [
62+
...previous,
63+
{ text: trimmed, sender: "user", timestamp: Date.now() },
64+
]);
65+
setInputValue("");
66+
addBotReply();
67+
}
68+
};
69+
70+
window.addEventListener("keydown", handleKeyDown);
71+
return () => window.removeEventListener("keydown", handleKeyDown);
72+
}, []);
73+
74+
useEffect(() => {
75+
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
76+
}, [messages]);
77+
78+
return (
79+
<div className="chat-container">
80+
<div className="chat-header">
81+
<h1>Chat</h1>
82+
<div className="shortcut-hint">
83+
<kbd className="kbd">{isMac ? "⌘" : "Ctrl"}</kbd>
84+
<span>+</span>
85+
<kbd className="kbd">Enter</kbd>
86+
<span>to send</span>
87+
</div>
88+
</div>
89+
90+
<div className="messages">
91+
{messages.length === 0 && (
92+
<div className="messages-empty">Send a message to start chatting</div>
93+
)}
94+
{messages.map((message, index) => (
95+
<div
96+
key={index}
97+
className={`message ${message.sender === "user" ? "message-sent" : "message-received"}`}
98+
>
99+
{message.text}
100+
<div className="message-timestamp">{formatTime(message.timestamp)}</div>
101+
</div>
102+
))}
103+
<div ref={messagesEndRef} />
104+
</div>
105+
106+
<div className="input-area">
107+
<input
108+
value={inputValue}
109+
onChange={(event) => setInputValue(event.target.value)}
110+
onKeyDown={(event) => {
111+
if (event.key === "Enter" && !event.metaKey && !event.ctrlKey) {
112+
event.preventDefault();
113+
handleSendFromButton();
114+
}
115+
}}
116+
placeholder="Type a message..."
117+
/>
118+
<button
119+
className="send-button"
120+
onClick={handleSendFromButton}
121+
disabled={!inputValue.trim()}
122+
>
123+
Send
124+
</button>
125+
</div>
126+
</div>
127+
);
128+
};

apps/demo/src/index.css

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
*,
2+
*::before,
3+
*::after {
4+
box-sizing: border-box;
5+
margin: 0;
6+
padding: 0;
7+
}
8+
9+
:root {
10+
--bg: #fafafa;
11+
--surface: #ffffff;
12+
--border: #e5e5e5;
13+
--text: #171717;
14+
--text-muted: #a3a3a3;
15+
--accent: #2563eb;
16+
--accent-hover: #1d4ed8;
17+
--sent-bg: #2563eb;
18+
--sent-text: #ffffff;
19+
--received-bg: #f0f0f0;
20+
--received-text: #171717;
21+
--radius: 12px;
22+
}
23+
24+
body {
25+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
26+
background: var(--bg);
27+
color: var(--text);
28+
-webkit-font-smoothing: antialiased;
29+
}
30+
31+
.chat-container {
32+
max-width: 480px;
33+
margin: 0 auto;
34+
height: 100dvh;
35+
display: flex;
36+
flex-direction: column;
37+
background: var(--surface);
38+
border-inline: 1px solid var(--border);
39+
}
40+
41+
.chat-header {
42+
padding: 16px 20px;
43+
border-bottom: 1px solid var(--border);
44+
display: flex;
45+
align-items: center;
46+
justify-content: space-between;
47+
flex-shrink: 0;
48+
}
49+
50+
.chat-header h1 {
51+
font-size: 17px;
52+
font-weight: 600;
53+
letter-spacing: -0.02em;
54+
}
55+
56+
.shortcut-hint {
57+
font-size: 12px;
58+
color: var(--text-muted);
59+
display: flex;
60+
align-items: center;
61+
gap: 4px;
62+
}
63+
64+
.kbd {
65+
display: inline-flex;
66+
align-items: center;
67+
padding: 2px 6px;
68+
font-size: 11px;
69+
font-family: inherit;
70+
background: var(--bg);
71+
border: 1px solid var(--border);
72+
border-radius: 4px;
73+
color: var(--text-muted);
74+
}
75+
76+
.messages {
77+
flex: 1;
78+
overflow-y: auto;
79+
padding: 16px 20px;
80+
display: flex;
81+
flex-direction: column;
82+
gap: 8px;
83+
}
84+
85+
.messages-empty {
86+
flex: 1;
87+
display: flex;
88+
align-items: center;
89+
justify-content: center;
90+
color: var(--text-muted);
91+
font-size: 14px;
92+
}
93+
94+
.message {
95+
max-width: 75%;
96+
padding: 10px 14px;
97+
font-size: 15px;
98+
line-height: 1.4;
99+
word-wrap: break-word;
100+
}
101+
102+
.message-sent {
103+
align-self: flex-end;
104+
background: var(--sent-bg);
105+
color: var(--sent-text);
106+
border-radius: var(--radius) var(--radius) 4px var(--radius);
107+
}
108+
109+
.message-received {
110+
align-self: flex-start;
111+
background: var(--received-bg);
112+
color: var(--received-text);
113+
border-radius: var(--radius) var(--radius) var(--radius) 4px;
114+
}
115+
116+
.message-timestamp {
117+
font-size: 11px;
118+
color: var(--text-muted);
119+
margin-top: 2px;
120+
text-align: right;
121+
}
122+
123+
.message-sent .message-timestamp {
124+
color: rgba(255, 255, 255, 0.6);
125+
}
126+
127+
.input-area {
128+
padding: 12px 16px;
129+
border-top: 1px solid var(--border);
130+
display: flex;
131+
gap: 8px;
132+
flex-shrink: 0;
133+
}
134+
135+
.input-area input {
136+
flex: 1;
137+
padding: 10px 14px;
138+
font-size: 15px;
139+
font-family: inherit;
140+
border: 1px solid var(--border);
141+
border-radius: 10px;
142+
outline: none;
143+
background: var(--bg);
144+
color: var(--text);
145+
transition: border-color 0.15s;
146+
}
147+
148+
.input-area input:focus {
149+
border-color: var(--accent);
150+
}
151+
152+
.input-area input::placeholder {
153+
color: var(--text-muted);
154+
}
155+
156+
.send-button {
157+
padding: 10px 20px;
158+
font-size: 15px;
159+
font-weight: 500;
160+
font-family: inherit;
161+
background: var(--accent);
162+
color: white;
163+
border: none;
164+
border-radius: 10px;
165+
cursor: pointer;
166+
transition: background 0.15s;
167+
flex-shrink: 0;
168+
}
169+
170+
.send-button:hover {
171+
background: var(--accent-hover);
172+
}
173+
174+
.send-button:disabled {
175+
opacity: 0.4;
176+
cursor: not-allowed;
177+
}

apps/demo/src/main.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { StrictMode } from "react";
2+
import { createRoot } from "react-dom/client";
3+
import { App } from "./app";
4+
import "./index.css";
5+
6+
createRoot(document.getElementById("root")!).render(
7+
<StrictMode>
8+
<App />
9+
</StrictMode>,
10+
);

apps/demo/tsconfig.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "ESNext",
5+
"moduleResolution": "bundler",
6+
"jsx": "react-jsx",
7+
"strict": true,
8+
"esModuleInterop": true,
9+
"skipLibCheck": true,
10+
"noEmit": true
11+
},
12+
"include": ["src"]
13+
}

0 commit comments

Comments
 (0)