Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dd88264
feat: WYSIWYG inline editing with rich text support
peter6055 Jul 28, 2026
afa0b56
fix: remove browser-added line wraps at word-boundary characters
peter6055 Jul 28, 2026
731a8a3
fix: toolbar state updates and line break round-trip issues
peter6055 Jul 28, 2026
2e0e7e6
refactor: remove inline styling toolbar from WYSIWYG editor
peter6055 Jul 28, 2026
fb469b6
style: fix prettier formatting
peter6055 Jul 28, 2026
4bd31e4
fix(telemetry): persist usage analytics and debug reporting toggles i…
peter6055 Jul 28, 2026
6c5d270
remove: incorrect config and file
peter6055 Jul 28, 2026
c3e2b1c
feat(editor): add formatting toolbar to inline text editor with usage…
peter6055 Jul 31, 2026
9d27c1c
fix: match flowchart inline editor text layout to Mermaid output
peter6055 Aug 2, 2026
d9cbfdc
fix(editor): move inline toolbar closer and preserve alignment on for…
peter6055 Aug 4, 2026
07b4fa4
chore(testing): add random-port test server infra and Playwright glob…
peter6055 Aug 5, 2026
16b5dad
feat(dashboard): auto-load newly created diagrams across tabs
peter6055 Aug 5, 2026
225e42d
fix(editor): force copy cursor on diagram nodes in comment mode
peter6055 Aug 6, 2026
7b7a2f1
fix(editor): improve inline editor double-click handling and shape co…
peter6055 Aug 6, 2026
df229f7
feat(editor): vertically center inline editor text
peter6055 Aug 6, 2026
9358d41
fix(security): sanitize HTML before storing in diagram source
peter6055 Aug 7, 2026
eb4e42a
fix(security): handle unclosed HTML comments in node fallback
peter6055 Aug 7, 2026
6fdc68e
fix(security): address all 13 CodeRabbit review comments on PR #126
peter6055 Aug 7, 2026
7c3ff71
fix: address remaining CodeRabbit review comments on PR #126
peter6055 Aug 7, 2026
a1594b5
fix(editor): handle collapsed ranges next to element children in form…
peter6055 Aug 7, 2026
8b1eb75
Merge branch 'main' into feat/resolve-inline-editing-2-feature-improv…
peter6055 Aug 7, 2026
89498e5
style: reformat files for consistent prettier output
peter6055 Aug 7, 2026
176a979
fix(security): address CodeQL and Copilot review feedback on PR #126
peter6055 Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@ data/
.opencode/

.config/

# local test-server build artifacts
tmp/
59 changes: 43 additions & 16 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Before planning or implementing any Mermaid diagram logic (parsers, rendering, f
When implementing UI features, rendering logic, or complex client-side changes, you MUST follow this robust operational loop:

1. **Implement**: Write the code and implement the changes.
2. **Execute Interactive Testing (Browser)**: Use the best available browser automation tooling in the current runtime to perform the exact user flow for the feature. Prefer the in-app Browser tooling when available; otherwise use the loaded browser automation alternative.
2. **Execute Interactive Testing (Browser)**: Use the best available browser automation tooling in the current runtime to perform the exact user flow for the feature. Prefer project Playwright (`npx playwright test` or a standalone script against `npm run test:dev`); use the in-app Browser tooling only for quick recon. If the in-app Browser MCP hangs or times out, switch to Playwright immediately — see [`reference/standards/browser-mcp.md`](reference/standards/browser-mcp.md).
3. **Capture Comprehensive Visuals**: Capture screenshots at each meaningful verification checkpoint so the interaction flow has a visual record.
4. **Return Results**: Evaluate the results (including all screenshots and DOM observations).
5. **Scale Testing (If Needed)**: You can spawn multiple subagents to test the implementation to different degrees or in parallel if the feature is complex.
Expand All @@ -65,56 +65,83 @@ When implementing UI features, rendering logic, or complex client-side changes,
## Two-Server Protocol

- **User's server** (port **3434**): runs via `npm run dev`. Never touch this.
- **Agent's test server** (port **3435**): runs via `npm run test:dev`. Agent starts/stops ONLY this.
- **Agent's test server** (random free port): runs via `npm run test:dev`. Agent starts/stops ONLY this.

Both `npm run dev` and `npm run test:dev` now automatically kill any existing process on their respective port before starting, so stale servers are cleaned up automatically.

**CRITICAL**: Never kill or interact with the user's server (port 3434, tmux session `livemaid`). The agent uses a separate port and tmux session (`livemaid-test`).
Comment thread
peter6055 marked this conversation as resolved.

## Agent Test Server Startup
## Random Port Test Server

> **Note**: OpenCode's bash tool may hang when backgrounding processes with `&`.
> Use `tmux` to fully decouple the server from the tool's process tree.
`npm run test:dev` picks a random port in the **20000–30000** range (`Math.random()`) and starts `next dev` on it. The port is checked for availability first and re-rolled on collision (up to 20 attempts), so multiple agents running their own test server concurrently don't collide. Each test server also uses a unique temporary `distDir` so multiple agents can run their own test server against the same project directory without Next.js detecting a conflict. The port is printed in the console output:

```
Starting test dev server on http://localhost:<port>
```

Use `tmux` to fully decouple the server from the tool's process tree, then read the port from the log:

```bash
# Kill only the agent's old test server (NOT the user's server on 3434)
tmux kill-session -t livemaid-test 2>/dev/null

# Start agent's test server on port 3435 in a detached tmux session
# (port 3435 is automatically freed by the `test:dev` script)
# Start agent's test server on a random free port in a detached tmux session
cd /path/to/project && tmux new-session -d -s livemaid-test 'npm run test:dev'

# Wait for the port to appear in tmux output (bounded extraction loop)
PORT=""
for i in $(seq 1 20); do
PORT=$(tmux capture-pane -t livemaid-test -p -S -50 | grep -oE 'http://localhost:[0-9]+' | head -1 | cut -d: -f3)
if [ -n "$PORT" ]; then break; fi
sleep 1
done
if [ -z "$PORT" ]; then
echo "ERROR: No port found after 20s. tmux output:" >&2
tmux capture-pane -t livemaid-test -p -S -50 >&2
exit 1
fi

# Poll until ready
for i in $(seq 1 30); do
if curl -s -o /dev/null --max-time 2 http://localhost:3435 2>/dev/null; then
echo "Dev server ready after ${i}s"
if curl -s -o /dev/null --max-time 2 "http://localhost:${PORT}" 2>/dev/null; then
echo "Dev server ready after ${i}s on port ${PORT}"
break
fi
sleep 1
done

# Retry once if needed
if ! curl -s -o /dev/null --max-time 2 http://localhost:3435 2>/dev/null; then
if ! curl -s -o /dev/null --max-time 2 "http://localhost:${PORT}" 2>/dev/null; then
echo "First attempt timed out, retrying..."
tmux kill-session -t livemaid-test 2>/dev/null
tmux new-session -d -s livemaid-test 'npm run test:dev'
PORT=""
for i in $(seq 1 20); do
PORT=$(tmux capture-pane -t livemaid-test -p -S -50 | grep -oE 'http://localhost:[0-9]+' | head -1 | cut -d: -f3)
if [ -n "$PORT" ]; then break; fi
sleep 1
done
if [ -z "$PORT" ]; then
echo "ERROR: No port found on retry after 20s. tmux output:" >&2
tmux capture-pane -t livemaid-test -p -S -50 >&2
exit 1
fi
for i in $(seq 1 30); do
if curl -s -o /dev/null --max-time 2 http://localhost:3435 2>/dev/null; then
echo "Dev server ready on retry after ${i}s"
if curl -s -o /dev/null --max-time 2 "http://localhost:${PORT}" 2>/dev/null; then
echo "Dev server ready on retry after ${i}s on port ${PORT}"
break
fi
sleep 1
done
if ! curl -s -o /dev/null --max-time 2 http://localhost:3435 2>/dev/null; then
if ! curl -s -o /dev/null --max-time 2 "http://localhost:${PORT}" 2>/dev/null; then
echo "Dev server still unavailable after retry" >&2
exit 1
fi
fi
```

- Use `tmux new-session -d -s livemaid-test 'npm run test:dev'` to start the agent's server.
- Poll `http://localhost:3435` until it responds with any HTTP status.
- If not responding after 30s, kill and retry once.
- When running Playwright, use the `playwright-global-setup.mjs` script which automatically picks a random 20000–30000 port, starts the dev server with a unique `distDir`, and sets `PLAYWRIGHT_BASE_URL`.
- Multiple agents can therefore run `npm run test:dev` / Playwright concurrently without port or Next.js project-lock conflicts.
- When done testing, kill ONLY the agent's server: `tmux kill-session -t livemaid-test`.
- Never run `pkill -f "next dev"` — it would kill the user's server too.

Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const eslintConfig = defineConfig([
"build/**",
"next-env.d.ts",
"playwright-report/**",
"tmp/**",
]),
{
rules: {
Expand Down
12 changes: 4 additions & 8 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import type { NextConfig } from "next";

const DEV_ORIGINS_DEFAULTS = [
"devenv-ubuntu-a.tail1f8d1a.ts.net",
"127.0.0.1",
"localhost",
"0.0.0.0",
];

const nextConfig: NextConfig = {
/* config options here */
output: "standalone",
// Use a unique distDir per test server so multiple Next.js dev servers can run
// concurrently without conflicting on the same project directory.
distDir: process.env.NEXT_TEST_DIST_DIR || ".next",
allowedDevOrigins: process.env.ALLOWED_DEV_ORIGINS
? process.env.ALLOWED_DEV_ORIGINS.split(",").map((s) => s.trim())
: DEV_ORIGINS_DEFAULTS,
: [],
turbopack: {
root: process.cwd(),
},
Expand Down
13 changes: 7 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"scripts": {
"dev": "fuser -k 3434/tcp 2>/dev/null; next dev --hostname 0.0.0.0 -p 3434",
"test:dev": "fuser -k 3435/tcp 2>/dev/null; next dev --hostname 0.0.0.0 -p 3435",
"test:dev": "node scripts/start-test-server.mjs",
"build": "node scripts/build.mjs",
"start": "next start",
"lint": "eslint",
Expand Down Expand Up @@ -44,7 +44,8 @@
"shadcn": "^4.8.3",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0"
"tw-animate-css": "^1.4.0",
"dompurify": "^3.4.11"
},
"overrides": {
"dompurify": "^3.4.11",
Expand Down
11 changes: 3 additions & 8 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,15 @@ import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./src/test",
testMatch: "**/*.spec.ts",
timeout: 30000,
timeout: 60000,
fullyParallel: true,
retries: 1,
workers: 1,
reporter: [["html", { outputFolder: "playwright-report" }]],
globalSetup: require.resolve("./scripts/playwright-global-setup.mjs"),
use: {
baseURL: "http://localhost:3435",
baseURL: process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3435",
Comment thread
peter6055 marked this conversation as resolved.
trace: "on-first-retry",
screenshot: "only-on-failure",
},
webServer: {
command: "npm run test:dev",
port: 3435,
reuseExistingServer: true,
timeout: 120000,
},
});
3 changes: 3 additions & 0 deletions reference/features/editor/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@
double-rendered text.
- Inline editors should visually inherit the underlying text area's typography, alignment, and
bounds closely enough to avoid visible jumps.
- Flowchart inline editors mirror Mermaid's `foreignObject` text layout: `display: table`,
`white-space: break-spaces`, and the same font stack so line breaks match what Mermaid will
render on commit.
63 changes: 63 additions & 0 deletions reference/standards/browser-mcp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Browser Automation: In-App Browser MCP vs Playwright

## Decision Rule

- **Prefer project Playwright** (`npx playwright test`, or a short standalone script against
`npm run test:dev`) for any real UI/feature validation. It is the reliable path in this runtime.
- Use the **in-app Browser MCP** only for quick exploratory reconnaissance (a few clicks/screenshots).
- If the Browser MCP hangs or times out, **switch to Playwright immediately — do not keep retrying
the MCP session.** A wedged MCP session usually stays wedged until the browser process is killed.

## Agent Responsibilities (browser testing)

- **Own the session lifecycle.** The agent starts, monitors, and cleans up its own browser session and
test server. Never leave orphaned Chromium processes or `livemaid-test` tmux sessions behind.
- **Never touch the user's server** (port 3434, tmux `livemaid`). Only the agent's test server
(`npm run test:dev`) may be started/stopped, and only via tmux `livemaid-test`.
- **Don't retry a wedged MCP session.** One hang is enough: switch to project Playwright immediately
instead of burning time/timeouts on further `snapshot`/`click`/`stop` calls.
- **Clean up after crashes.** Kill stale Chromium and clear `SingletonLock`/`SingletonSocket` from the
browser profile before relaunching (commands below).
- **Warm routes before navigating** with the MCP so cold-compile timeouts don't trigger hangs.
- **Use explicit, generous navigation settings** (`waitUntil: "domcontentloaded"`, 60–120s timeout)
rather than MCP defaults.
- **Verify, then close.** Close the browser context and kill the test server when validation is done
so later agents don't inherit wedged state.

## Why the Browser MCP Hangs (observed in this environment)

1. **Cold compile vs short navigation timeout (main trigger).**
Next.js/Turbopack's first compile of a route can take 30–40s+ (observed `GET / 200 in 37.5s`).
The Browser MCP `goto` defaults to `waitUntil: "load"` with a ~30s timeout, so navigation to a
not-yet-compiled route times out and leaves the session in a bad state.

2. **Dead session after timeout/abort.**
Once a navigation times out or the user aborts mid-navigation, the CDP connection is wedged but
the MCP wrapper still treats the session as open. Subsequent `snapshot`, `click`, or `stop` calls
then hang waiting on `body`/CDP indefinitely.

3. **Stale persistent-profile lock.**
`/root/.opencode/browser-profile/` can retain `SingletonLock` / `SingletonSocket` from a crashed
Chromium (e.g. a headed launch that died on missing `$DISPLAY`), blocking or flaking later launches.

4. **Headed mode without an X server.**
`$DISPLAY` is empty in this runtime. Headed launch fails immediately
(`Missing X server or $DISPLAY`). `Xvfb` is installed but not managed by the MCP. Headless works.

## Workarounds

- **Warm the route before navigating.** `curl -s -o /dev/null -w "%{http_code}" --max-time 90 http://localhost:PORT/`
forces the cold compile; then the MCP `goto` completes quickly.
- **Use relaxed navigation.** `waitUntil: "domcontentloaded"` with a 60–120s timeout instead of the
default `load`/30s.
- **Recover a wedged session:** kill stale Chromium, then clear the singleton locks before relaunch:
```bash
pkill -f "chrome.*opencode/browser-profile"
rm -f /root/.opencode/browser-profile/SingletonLock \
/root/.opencode/browser-profile/SingletonSocket \
/root/.opencode/browser-profile/SingletonCookie
```
- **Prefer Playwright specs.** The project suite already handles cold compiles correctly:
`scripts/playwright-global-setup.mjs` waits for "Ready in", uses a random port + unique `distDir`,
and specs use generous timeouts/retries. Cold-compile flakiness on first `page.goto` still exists
(passes on retry); if it matters, add a warm-up `page.goto` before the assertions.
27 changes: 11 additions & 16 deletions reference/standards/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,24 @@
After UI or interaction changes:

1. Implement the change.
2. Run interactive browser testing on the real user flow.
2. Run interactive browser testing on the real user flow. Prefer project Playwright; see
[`browser-mcp.md`](./browser-mcp.md) for why the in-app Browser MCP can hang and how to recover.
3. Capture screenshots at meaningful checkpoints.
4. Fix every error discovered before considering the task done.
5. Iterate until stable.

## Dev Servers

| Server | Port | Command | Owner |
| ---------- | ---- | ------------------ | ----------------------------------------- |
| User dev | 3434 | `npm run dev` | Human (tmux `livemaid`) — **never touch** |
| Agent test | 3435 | `npm run test:dev` | Agent (tmux `livemaid-test`) |
| Server | Port | Command | Owner |
| ---------- | ------------------ | ------------------ | ----------------------------------------- |
| User dev | 3434 | `npm run dev` | Human (tmux `livemaid`) — **never touch** |
| Agent test | random 20000–30000 | `npm run test:dev` | Agent (tmux `livemaid-test`) |

Never run `pkill -f "next dev"` — it kills both servers.

Agent test server startup:
`npm run test:dev` picks a random free port in 20000–30000 and uses a unique temporary `distDir`
(see AGENTS.md "Random Port Test Server" for the full startup/poll/cleanup flow). When running
Playwright, `scripts/playwright-global-setup.mjs` handles server startup and sets `PLAYWRIGHT_BASE_URL`
automatically.

```bash
tmux kill-session -t livemaid-test 2>/dev/null
cd /path/to/project && tmux new-session -d -s livemaid-test 'npm run test:dev'
for i in $(seq 1 30); do
if curl -s -o /dev/null --max-time 2 http://localhost:3435 2>/dev/null; then break; fi
sleep 1
done
```
Never run `pkill -f "next dev"` — it kills both servers.

When done testing: `tmux kill-session -t livemaid-test`.
30 changes: 30 additions & 0 deletions scripts/get-random-port.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import net from "node:net";

const DEFAULT_MIN = 20000;
const DEFAULT_MAX = 30000;
const MAX_ATTEMPTS = 20;

function randomPort(min = DEFAULT_MIN, max = DEFAULT_MAX) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

function isPortAvailable(port) {
return new Promise((resolve) => {
const server = net.createServer();
server.once("error", () => resolve(false));
server.once("listening", () => server.close(() => resolve(true)));
server.listen(port, "0.0.0.0");
});
}

export async function getRandomPort(min = DEFAULT_MIN, max = DEFAULT_MAX) {
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
const port = randomPort(min, max);
if (await isPortAvailable(port)) {
return port;
}
}
throw new Error(
`Could not find a free port in range ${min}-${max} after ${MAX_ATTEMPTS} attempts`,
);
}
Loading
Loading