diff --git a/.stoneforge/sync/elements.jsonl b/.stoneforge/sync/elements.jsonl index f3529db..48ea31c 100644 --- a/.stoneforge/sync/elements.jsonl +++ b/.stoneforge/sync/elements.jsonl @@ -1,18 +1 @@ -{"id":"el-0000","type":"entity","createdAt":"2026-05-24T09:34:02.845Z","updatedAt":"2026-05-24T09:34:02.845Z","createdBy":"el-0000","tags":[],"metadata":{},"name":"operator","entityType":"human"} -{"id":"el-2jua","type":"entity","createdAt":"2026-05-24T09:44:24.375Z","updatedAt":"2026-05-24T10:29:09.942Z","createdBy":"el-0000","tags":[],"metadata":{"agent":{"agentRole":"director","sessionStatus":"running","provider":"opencode","model":"openai/gpt-5.5","channelId":"el-2vpd","lastActivityAt":"2026-05-24T10:29:09.939Z","sessionHistory":[{"id":"session-mpjlz79g-0001-9tz4","providerSessionId":"ses_1a689e409ffea1AaKRRY2e7GYP","status":"terminated","workingDirectory":"/home/alex/projects/headquarter","startedAt":"2026-05-24T10:02:20.170Z","endedAt":"2026-05-24T10:16:54.212Z"},{"id":"session-mpjlckjf-0001-cifg","status":"terminated","workingDirectory":"/home/alex/projects/headquarter","startedAt":"2026-05-24T09:44:44.286Z","endedAt":"2026-05-24T09:47:52.845Z"}]}},"name":"director","entityType":"agent"} -{"id":"el-4350","type":"entity","createdAt":"2026-05-24T12:03:32.237Z","updatedAt":"2026-05-24T12:03:32.238Z","createdBy":"el-0000","tags":[],"metadata":{"agent":{"agentRole":"steward","stewardFocus":"merge","sessionStatus":"idle","provider":"opencode","model":"kimi-for-coding/k2p6","channelId":"el-5mta"}},"name":"m-steward-1","entityType":"agent"} -{"id":"el-13ju","type":"entity","createdAt":"2026-05-24T12:03:36.368Z","updatedAt":"2026-05-24T12:03:36.368Z","createdBy":"el-0000","tags":[],"metadata":{"agent":{"agentRole":"steward","stewardFocus":"docs","sessionStatus":"idle","provider":"opencode","model":"kimi-for-coding/k2p6","channelId":"el-5j1i"}},"name":"d-steward-1","entityType":"agent"} -{"id":"el-51a8","type":"entity","createdAt":"2026-05-24T12:03:40.486Z","updatedAt":"2026-05-24T12:03:40.487Z","createdBy":"el-0000","tags":[],"metadata":{"agent":{"agentRole":"worker","workerMode":"ephemeral","sessionStatus":"idle","provider":"opencode","model":"kimi-for-coding/k2p6","channelId":"el-9amf"}},"name":"e-worker-1","entityType":"agent"} -{"id":"el-2i1s","type":"entity","createdAt":"2026-05-24T12:03:42.204Z","updatedAt":"2026-05-24T12:03:42.223Z","createdBy":"el-0000","tags":[],"metadata":{"agent":{"agentRole":"worker","workerMode":"ephemeral","sessionStatus":"idle","provider":"opencode","model":"kimi-for-coding/k2p6","channelId":"el-1na8"}},"name":"e-worker-2","entityType":"agent"} -{"id":"el-1of","type":"document","createdAt":"2026-05-24T09:44:58.758Z","updatedAt":"2026-05-24T09:44:58.759Z","createdBy":"el-2jua","tags":[],"metadata":{"purpose":"document-directory"},"title":"Documentation Directory","contentType":"markdown","content":"# Documentation Directory\n\nIndex of all workspace documents. Start with this document to navigate workspace knowledge.\n\n## Specs\n\n(none yet)\n\n## References\n\n| ID | Title |\n|----|-------|\n| el-1of | Documentation Directory (this document) |\n\n## How-To Guides\n\n(none yet)\n\n## Explanations\n\n(none yet)\n\n## Decision Logs\n\n(none yet)","version":2,"previousVersionId":"el-1of","category":"reference","status":"active","immutable":false} -{"id":"el-1io7","type":"document","createdAt":"2026-05-24T09:51:08.711Z","updatedAt":"2026-05-24T09:51:08.711Z","createdBy":"el-7z8w","tags":["dispatch-notification","task-assignment"],"metadata":{"dispatchNotification":true},"contentType":"text","content":"Task assigned: Remove tunnel functionality and ui elements from terminal sessions [Priority: 3]","version":1,"previousVersionId":null,"category":"other","status":"active","immutable":false} -{"id":"el-1jhy","type":"document","createdAt":"2026-05-24T11:00:44.885Z","updatedAt":"2026-05-24T11:00:44.885Z","createdBy":"el-7z8w","tags":["dispatch-notification","task-assignment"],"metadata":{"dispatchNotification":true},"contentType":"text","content":"Task assigned: Remove tunnel functionality and ui from terminal sessions [Priority: 3]","version":1,"previousVersionId":null,"category":"other","status":"active","immutable":false} -{"id":"el-18lc","type":"task","createdAt":"2026-05-24T10:07:25.455Z","updatedAt":"2026-05-24T12:03:51.474Z","createdBy":"el-0000","tags":[],"metadata":{"orchestrator":{"sessionHistory":[{"sessionId":"session-mpjo295o-0006-yuk4","providerSessionId":"ses_1a65b0534ffe1MQoY6emHeY9Qn","agentId":"el-7z8w","agentName":"e-worker-2","agentRole":"worker","startedAt":"2026-05-24T11:00:44.888Z"},{"sessionId":"session-mpjo2cw8-0007-3wjj","providerSessionId":"ses_1a65b0534ffe1MQoY6emHeY9Qn","agentId":"el-7z8w","agentName":"e-worker-2","agentRole":"worker","startedAt":"2026-05-24T11:00:47.467Z"},{"sessionId":"session-mpjo2grm-0008-7b8r","providerSessionId":"ses_1a65b0534ffe1MQoY6emHeY9Qn","agentId":"el-7z8w","agentName":"e-worker-2","agentRole":"worker","startedAt":"2026-05-24T11:00:52.495Z"},{"sessionId":"session-mpjo2km9-0009-n1vf","providerSessionId":"ses_1a65b0534ffe1MQoY6emHeY9Qn","agentId":"el-7z8w","agentName":"e-worker-2","agentRole":"worker","startedAt":"2026-05-24T11:00:57.468Z"}],"owningDirector":"el-2jua","resumeCount":3,"reconciliationCount":1}},"title":"Remove tunnel functionality and ui from terminal sessions","status":"open","priority":3,"complexity":3,"taskType":"task"} -{"id":"el-2afn","type":"message","createdAt":"2026-05-24T09:51:08.713Z","updatedAt":"2026-05-24T09:51:08.713Z","createdBy":"el-7z8w","tags":["dispatch-notification","task-assignment"],"metadata":{"type":"task-assignment","taskId":"el-3y4w","priority":3,"branch":"agent/e-worker-2/el-3y4w-remove-tunnel-functionality-an","worktree":"/home/alex/projects/headquarter/.stoneforge/.worktrees/e-worker-2-remove-tunnel-functionality-an","sessionId":"ses_1a69abe62ffe3VhShFnsV1mp1b","dispatchedAt":"2026-05-24T09:51:08.710Z","suppressInbox":true},"channelId":"el-4uqv","sender":"el-7z8w","contentRef":"el-1io7","attachments":[],"threadId":null} -{"id":"el-5lta","type":"message","createdAt":"2026-05-24T11:00:44.887Z","updatedAt":"2026-05-24T11:00:44.887Z","createdBy":"el-7z8w","tags":["dispatch-notification","task-assignment"],"metadata":{"type":"task-assignment","taskId":"el-18lc","priority":3,"branch":"agent/e-worker-2/el-18lc-remove-tunnel-functionality-an","worktree":"/home/alex/projects/headquarter/.stoneforge/.worktrees/e-worker-2-remove-tunnel-functionality-an","sessionId":"ses_1a65b0534ffe1MQoY6emHeY9Qn","dispatchedAt":"2026-05-24T11:00:44.884Z","suppressInbox":true},"channelId":"el-4uqv","sender":"el-7z8w","contentRef":"el-1jhy","attachments":[],"threadId":null} -{"id":"el-2vpd","type":"channel","createdAt":"2026-05-24T09:44:24.377Z","updatedAt":"2026-05-24T09:44:24.377Z","createdBy":"el-0000","tags":["agent-channel"],"metadata":{"agentId":"el-2jua","agentName":"director","purpose":"Agent direct messaging channel"},"name":"director:operator","description":null,"channelType":"direct","members":["el-0000","el-2jua"],"permissions":{"visibility":"private","joinPolicy":"invite-only","modifyMembers":[]}} -{"id":"el-5mta","type":"channel","createdAt":"2026-05-24T12:03:32.238Z","updatedAt":"2026-05-24T12:03:32.238Z","createdBy":"el-0000","tags":["agent-channel"],"metadata":{"agentId":"el-4350","agentName":"m-steward-1","purpose":"Agent direct messaging channel"},"name":"m-steward-1:operator","description":null,"channelType":"direct","members":["el-0000","el-4350"],"permissions":{"visibility":"private","joinPolicy":"invite-only","modifyMembers":[]}} -{"id":"el-5j1i","type":"channel","createdAt":"2026-05-24T12:03:36.368Z","updatedAt":"2026-05-24T12:03:36.368Z","createdBy":"el-0000","tags":["agent-channel"],"metadata":{"agentId":"el-13ju","agentName":"d-steward-1","purpose":"Agent direct messaging channel"},"name":"d-steward-1:operator","description":null,"channelType":"direct","members":["el-0000","el-13ju"],"permissions":{"visibility":"private","joinPolicy":"invite-only","modifyMembers":[]}} -{"id":"el-9amf","type":"channel","createdAt":"2026-05-24T12:03:40.487Z","updatedAt":"2026-05-24T12:03:40.487Z","createdBy":"el-0000","tags":["agent-channel"],"metadata":{"agentId":"el-51a8","agentName":"e-worker-1","purpose":"Agent direct messaging channel"},"name":"e-worker-1:operator","description":null,"channelType":"direct","members":["el-0000","el-51a8"],"permissions":{"visibility":"private","joinPolicy":"invite-only","modifyMembers":[]}} -{"id":"el-1na8","type":"channel","createdAt":"2026-05-24T12:03:42.222Z","updatedAt":"2026-05-24T12:03:42.222Z","createdBy":"el-0000","tags":["agent-channel"],"metadata":{"agentId":"el-2i1s","agentName":"e-worker-2","purpose":"Agent direct messaging channel"},"name":"e-worker-2:operator","description":null,"channelType":"direct","members":["el-0000","el-2i1s"],"permissions":{"visibility":"private","joinPolicy":"invite-only","modifyMembers":[]}} -{"id":"el-258","type":"library","createdAt":"2026-05-24T09:44:58.748Z","updatedAt":"2026-05-24T09:44:58.748Z","createdBy":"el-2jua","tags":[],"metadata":{},"name":"Documentation"} +{"id":"el-20no","type":"plan","createdAt":"2026-05-24T12:43:50.676Z","updatedAt":"2026-05-24T12:43:50.676Z","createdBy":"el-2jua","tags":["openspec","add-config-profiles"],"metadata":{},"title":"add-config-profiles","status":"active"} diff --git a/apps/api/src/api/terminal.py b/apps/api/src/api/terminal.py index 3f949a3..26d05f1 100644 --- a/apps/api/src/api/terminal.py +++ b/apps/api/src/api/terminal.py @@ -86,13 +86,14 @@ async def terminal_websocket( # Send connected status await websocket.send_json({"type": "status", "status": "connected"}) - # Start I/O loops + # Start I/O loops and heartbeat read_task = asyncio.create_task(_read_loop(session, websocket)) write_task = asyncio.create_task(_write_loop(session, websocket)) + heartbeat_task = asyncio.create_task(_heartbeat_loop(websocket)) # Wait for either task to complete (indicating disconnect or error) done, pending = await asyncio.wait( - [read_task, write_task], + [read_task, write_task, heartbeat_task], return_when=asyncio.FIRST_COMPLETED, ) @@ -181,6 +182,20 @@ async def _write_loop(session, websocket) -> None: pass +async def _heartbeat_loop(websocket: WebSocket) -> None: + """Send periodic ping messages to detect disconnections.""" + try: + while True: + await asyncio.sleep(30) # Ping every 30 seconds + try: + await websocket.send_json({"type": "ping"}) + except Exception: + # WebSocket is closed or broken + break + except Exception: + pass + + @router.post( "/projects/{project_id}/repositories/{repo_id}/instances/{instance_id}/terminal/reset", summary="Reset terminal session", diff --git a/apps/web/src/components/terminal.tsx b/apps/web/src/components/terminal.tsx index e007a75..e49955d 100644 --- a/apps/web/src/components/terminal.tsx +++ b/apps/web/src/components/terminal.tsx @@ -54,6 +54,8 @@ export const TerminalComponent: React.FC = ({ const stored = localStorage.getItem(FONT_SIZE_KEY); return stored ? parseInt(stored, 10) : isMobile ? 16 : 14; }); + const lastPingRef = useRef(0); + const heartbeatCheckRef = useRef(null); const calculateFontSize = useCallback(() => { if (!isMobile) return fontSize; @@ -75,6 +77,20 @@ export const TerminalComponent: React.FC = ({ setStatus("connected"); setError(null); reconnectAttemptsRef.current = 0; + lastPingRef.current = Date.now(); + + // Start heartbeat check + if (heartbeatCheckRef.current) { + window.clearInterval(heartbeatCheckRef.current); + } + heartbeatCheckRef.current = window.setInterval(() => { + const elapsed = Date.now() - lastPingRef.current; + if (elapsed > 60000) { + // No ping for 60 seconds, connection may be dead + console.warn("Terminal heartbeat timeout, reconnecting..."); + ws.close(4000, "Heartbeat timeout"); + } + }, 30000); }; ws.onmessage = (event) => { @@ -95,6 +111,12 @@ export const TerminalComponent: React.FC = ({ } else if (msg.status === "resetting") { setStatus("resetting"); } + } else if (msg.type === "ping") { + // Respond with pong and update last ping time + lastPingRef.current = Date.now(); + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "pong" })); + } } } catch { termRef.current?.write(event.data); @@ -104,6 +126,13 @@ export const TerminalComponent: React.FC = ({ ws.onclose = (event) => { setStatus("disconnected"); + + // Clean up heartbeat check + if (heartbeatCheckRef.current) { + window.clearInterval(heartbeatCheckRef.current); + heartbeatCheckRef.current = null; + } + if (event.code !== 1000) { setError(`Connection closed (code: ${event.code})`); diff --git a/docs/api/terminal.md b/docs/api/terminal.md new file mode 100644 index 0000000..ae4a3a7 --- /dev/null +++ b/docs/api/terminal.md @@ -0,0 +1,134 @@ +# Terminal API + +The Terminal API provides WebSocket-based terminal access to running tool instances. + +## WebSocket Endpoint + +### Connect to Terminal + +``` +GET /ws/tool-instances/{instance_id}/terminal +``` + +Establishes a WebSocket connection to an interactive terminal session inside a running tool instance container. + +**Authentication:** Requires valid session cookie. + +**Path Parameters:** +- `instance_id` (string, UUID): The tool instance ID + +**Connection Flow:** +1. Client connects to WebSocket endpoint +2. Server authenticates user and verifies instance ownership +3. Server creates or reattaches to existing terminal session +4. Server sends `{"type": "status", "status": "connected"}` message +5. Bidirectional communication begins + +**Message Types:** + +#### Client to Server + +**Terminal Input (bytes or string)** +- Send raw bytes for terminal input (key presses) +- Send text for terminal input (will be encoded as UTF-8) + +**Resize Command (JSON)** +```json +{ + "type": "resize", + "cols": 80, + "rows": 24 +} +``` + +**Reset Command (JSON)** +```json +{ + "type": "reset" +} +``` +Kills the current terminal session and starts a fresh one. + +**Pong Response (JSON)** +```json +{ + "type": "pong" +} +``` +Sent automatically in response to server ping messages. + +#### Server to Client + +**Terminal Output (bytes)** +Raw terminal output as binary data (Blob in browser). + +**Status Messages (JSON)** +```json +{"type": "status", "status": "connected"} +{"type": "status", "status": "resetting"} +``` + +**Ping Messages (JSON)** +```json +{"type": "ping"} +``` +Sent every 30 seconds to detect disconnections. Client should respond with `{"type": "pong"}`. + +### Session Persistence + +Terminal sessions persist across WebSocket disconnections: +- When a client disconnects, the terminal session remains active +- On reconnection, the client reattaches to the existing session +- Buffered output is replayed to the client on reconnection +- Sessions are cleaned up after 30 minutes of inactivity + +### Concurrent Connections + +Only one WebSocket connection is allowed per terminal session: +- New connections close existing connections with code 4000 +- Previous client receives "New connection established" reason + +## HTTP Endpoints + +### Reset Terminal Session + +``` +POST /api/projects/{project_id}/repositories/{repo_id}/instances/{instance_id}/terminal/reset +``` + +Resets the terminal session for a tool instance, killing the current shell and starting fresh. + +**Authentication:** Required + +**Path Parameters:** +- `project_id` (string, UUID): Project ID +- `repo_id` (string, UUID): Repository ID +- `instance_id` (string, UUID): Instance ID + +**Response:** +```json +{ + "status": "success", + "message": "Terminal session reset successfully", + "instance_id": "...", + "session_id": "..." +} +``` + +**Error Responses:** +- `404 Not Found`: Instance not found +- `400 Bad Request`: Instance is not running +- `500 Internal Server Error`: Failed to reset terminal session + +## Error Codes + +WebSocket close codes: +- `1000`: Normal closure +- `4000`: Error/reset +- `4001`: Invalid instance ID +- `4003`: Unauthorized/Forbidden +- `4004`: Instance not found or not running + +## Heartbeat + +The server sends ping messages every 30 seconds. If no ping is received for 60 seconds, the client should assume the connection is dead and reconnect. diff --git a/docs/features/terminal-troubleshooting.md b/docs/features/terminal-troubleshooting.md new file mode 100644 index 0000000..3925357 --- /dev/null +++ b/docs/features/terminal-troubleshooting.md @@ -0,0 +1,172 @@ +# Terminal Troubleshooting Guide + +## Common Issues + +### Cannot Connect to Terminal + +**Symptom:** Terminal shows "Connection closed" or "Error" status immediately. + +**Solutions:** +1. Verify the tool instance is running: + - Check instance status in the UI + - Start the instance if it's stopped + +2. Check browser console for errors: + - Open browser DevTools (F12) + - Look for WebSocket connection errors + - Check for CORS or authentication errors + +3. Verify network connectivity: + - Ensure you can reach the API server + - Check if WebSocket connections are blocked by firewall/proxy + - Try accessing from a different network + +**If the issue persists:** +- Reset the terminal session +- Refresh the page +- Check server logs for errors + +### Terminal Freezes or Becomes Unresponsive + +**Symptom:** Terminal accepts no input or stops updating. + +**Solutions:** +1. **Reset the terminal:** + - Click the Reset button in the terminal header + - Confirm the reset action + - Wait for the new session to start + +2. **Check for stuck processes:** + - Try Ctrl+C to interrupt any running process + - If that doesn't work, reset the terminal + +3. **Browser issues:** + - Close and reopen the browser tab + - Clear browser cache and cookies + - Try a different browser + +### Output Not Showing + +**Symptom:** Commands execute but no output appears. + +**Solutions:** +1. Check terminal focus: + - Click inside the terminal area + - Look for the cursor indicator + +2. Resize the terminal: + - The terminal may need a resize event to render properly + - Try resizing the browser window slightly + +3. Reset the terminal session + +### Reconnection Loop + +**Symptom:** Terminal keeps disconnecting and reconnecting repeatedly. + +**Solutions:** +1. Check instance health: + - The instance may be unhealthy or restarting + - Check instance logs for errors + +2. Network stability: + - Unstable network causes repeated disconnections + - Try on a more stable connection + +3. Multiple tabs: + - Only one tab can connect to a terminal session + - Close other tabs with the same terminal open + +## Diagnostic Steps + +### Check WebSocket Connection + +1. Open browser DevTools (F12) +2. Go to Network tab +3. Filter by "WS" (WebSocket) +4. Look for the terminal WebSocket connection +5. Check: + - Connection status (should be 101 Switching Protocols) + - Messages tab for ping/pong traffic + - Any error messages in the connection + +### Verify Terminal Session + +To check if a terminal session exists on the server: + +```bash +# Check server logs for session activity +docker logs hq-api | grep -i "terminal" +``` + +Look for: +- "Terminal session ready" - session created successfully +- "Reattaching to existing terminal session" - reconnecting to existing session +- "Cleaning up idle terminal session" - session expired + +### Test Basic Connectivity + +```bash +# Test if the WebSocket endpoint is reachable +curl -i -N \ + -H "Connection: Upgrade" \ + -H "Upgrade: websocket" \ + -H "Sec-WebSocket-Key: test" \ + -H "Sec-WebSocket-Version: 13" \ + https://your-api-domain/ws/tool-instances/test/terminal +``` + +Expected: HTTP 400 (invalid instance ID) or redirect to auth + +## Error Codes + +### WebSocket Close Codes + +- **1000**: Normal closure - connection closed cleanly +- **4000**: Generic error - check server logs +- **4001**: Invalid instance ID - verify the instance exists +- **4003**: Unauthorized - session expired or invalid +- **4004**: Instance not running - start the instance first + +### HTTP Status Codes + +- **404**: Instance not found - verify instance ID +- **400**: Instance not running - start the instance +- **500**: Server error - check server logs + +## Resetting Everything + +If all else fails: + +1. **Reset terminal session:** + ``` + POST /api/projects/{project_id}/repositories/{repo_id}/instances/{instance_id}/terminal/reset + ``` + +2. **Restart the tool instance:** + - Stop the instance + - Start the instance again + - Reconnect to the terminal + +3. **Clear browser data:** + - Clear cookies for the domain + - Clear local storage + - Hard refresh the page (Ctrl+F5) + +## Getting Help + +If issues persist: + +1. Collect diagnostic information: + - Browser console logs + - Network tab WebSocket messages + - Server logs (`docker logs hq-api`) + - Instance status and health + +2. Check the [Terminal API documentation](/docs/api/terminal.md) for protocol details + +3. Report issues with: + - Steps to reproduce + - Expected vs actual behavior + - Browser and OS version + - Instance type and configuration diff --git a/docs/features/terminal.md b/docs/features/terminal.md new file mode 100644 index 0000000..9f2b6c5 --- /dev/null +++ b/docs/features/terminal.md @@ -0,0 +1,83 @@ +# Terminal Sessions + +Terminal sessions provide interactive shell access to your running tool instances directly in the browser. + +## Persistent Sessions + +Terminal sessions are **persistent** - they survive browser refreshes, network interruptions, and tab switches. + +### How It Works + +- When you open a terminal, a shell session starts inside the tool instance container +- If you close the browser or lose connection, the session keeps running +- When you reconnect, you reattach to the same session with all previous output preserved +- Sessions automatically clean up after 30 minutes of inactivity + +### Reconnecting + +If your connection drops: +1. The terminal shows "Reconnecting..." status +2. The client automatically attempts to reconnect with exponential backoff +3. On successful reconnection, buffered output is replayed +4. You can continue working where you left off + +## Resetting the Terminal + +If your terminal becomes unresponsive or you want a fresh start: + +1. Click the **Reset** button in the terminal header +2. Confirm the reset action +3. The current shell is killed and a new one starts +4. All terminal history is cleared + +**Note:** Resetting only affects the terminal session, not the tool instance itself. Any files you've created remain intact. + +## Mobile Terminal + +On mobile devices, the terminal includes: +- Special keys panel (Ctrl, Alt, Tab, arrows, etc.) +- Font size controls +- Auto-hiding header for maximum screen space +- Touch-friendly interface + +## Keyboard Shortcuts + +Standard terminal shortcuts work as expected: +- `Ctrl+C`: Send interrupt signal +- `Ctrl+D`: Send EOF (close shell if empty) +- `Ctrl+L`: Clear screen +- `Ctrl+Z`: Suspend process + +Special keys can be accessed via the special keys panel on mobile or by using modifier combinations. + +## Troubleshooting + +### Connection Issues + +**"Connection closed" error:** +- The tool instance may have stopped - check the instance status +- Network issues - the client will auto-reconnect +- Session timeout - sessions expire after 30 minutes of inactivity + +**Terminal not responding:** +- Try resetting the terminal using the Reset button +- Check if the tool instance is still running +- Refresh the page to force reconnection + +### Display Issues + +**Text not visible:** +- Adjust font size using +/- buttons +- Check if the terminal has focus (click inside it) +- Try resizing the browser window + +**Characters not appearing:** +- Ensure the terminal has focus +- Check if a modifier key is stuck (Ctrl, Alt) +- Reset the terminal if stuck + +## Session Limits + +- **One connection per terminal:** Only one browser tab can connect to a terminal session at a time. Opening a new connection closes the old one. +- **30-minute idle timeout:** Sessions without activity are automatically cleaned up +- **Buffer size:** Up to 10KB of output is buffered for replay on reconnection diff --git a/openspec/changes/add-config-profiles/.openspec.yaml b/openspec/changes/add-config-profiles/.openspec.yaml new file mode 100644 index 0000000..6894814 --- /dev/null +++ b/openspec/changes/add-config-profiles/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-24 diff --git a/openspec/changes/add-config-profiles/design.md b/openspec/changes/add-config-profiles/design.md new file mode 100644 index 0000000..5300a36 --- /dev/null +++ b/openspec/changes/add-config-profiles/design.md @@ -0,0 +1,109 @@ +## Context + +The current system has tool configs for tool-type runtime fields and config folders for user-owned mounted files. Tool instance start currently discovers applicable tool configs and active config folders automatically, writes files/env vars into the instance directory, modifies Docker Compose, and starts the container. This creates useful building blocks but not a single user-facing launch profile that can be selected, composed, previewed, scoped to project/tool, or disabled for a launch. + +The target model is a config profile: a user-owned, selectable launch configuration that owns UTF-8 text files, mount roots, plain environment variables, and runtime hints. A profile can include other profiles in an ordered graph. Launch chooses one profile or `None`; included profiles provide stacking without making the start form multi-select. + +## Goals / Non-Goals + +**Goals:** + +- Provide one primary config abstraction for reusable launch setup. +- Allow one selected profile, or no profile, at session start. +- Allow ordered profile composition through includes with loop detection. +- Support portable, project-specific, tool-specific, and project+tool profiles through optional `project_id` and `tool_type_id` references. +- Resolve compatible defaults by specificity, falling back to the first created compatible profile. +- Store the selected profile on the instance so restart behavior is predictable. +- Replace legacy active config folder auto-mounting; compatibility with old config folder behavior is not required. +- Provide a settings editor for profile env vars, mount roots, text files, include order, defaults, and basic runtime hints. + +**Non-Goals:** + +- Secret storage or masking for env vars/files in v1. +- Binary file upload/editing in v1. +- Selecting multiple profiles directly at launch. +- Preserving legacy config folder semantics. +- Cross-user shared profiles. + +## Decisions + +### Config profiles own files directly + +Profiles will own their file content instead of referencing the existing `ConfigFolder` model. Reuse comes from profile composition: a profile such as `OpenCode Kimi` can include `Git identity` and `Shell defaults`. + +Alternative considered: keep `ConfigFolder` as a reusable file-bundle primitive. This adds another concept (`profile -> folder -> files`) and makes the UI harder to explain. Direct file ownership keeps the model centered on one abstraction. + +### Scope is derived from optional project/tool references + +Profiles do not need a separate scope enum. Scope is inferred from nullable references: + +- portable: no project and no tool +- tool: tool only +- project: project only +- project+tool: both project and tool + +This avoids storing redundant state and naturally supports `Headquarter OpenCode` profiles. + +### Launch selects one profile, composition happens inside profiles + +The start UI will expose a single config profile selector with `None` as an option. Profiles may include other profiles in ordered composition, so advanced stacking happens in the profile editor rather than the launch form. + +Alternative considered: allow selecting multiple profiles at launch. This is more flexible but makes start behavior harder to understand and raises ordering questions for every launch. + +### Includes use an ordered graph with cycle detection + +Profile includes will be represented as ordered edges. Resolution processes included profiles in position order, then applies the selected profile itself. Later layers override earlier layers. Cycles must be rejected when saving include relationships and guarded against again during launch resolution. + +### Mounts use target roots with relative UTF-8 text files + +Each profile mount has a target path, mode (`ro` or `rw`), and a map/list of relative file paths to UTF-8 text content. The resolver stages each resolved mount into the instance directory and adds Docker bind mounts to the compose file. + +Alternative considered: store absolute container paths on every file. Mount roots better match Docker volume behavior, simplify editing, and make merge/conflict rules clearer. + +### Deterministic override rules + +Resolution order is: + +1. tool defaults already provided by the tool type/compose template +2. included profiles in configured order, recursively resolved +3. selected profile itself +4. start-time/runtime overrides if a future workflow exposes them + +For env vars and runtime hints, later values replace earlier values. For mounts with the same target path, file maps are merged and later relative file paths win. For mount mode conflicts on the same target path, the later layer wins. + +### Defaults are selected by specificity + +The default selector will prefer explicit defaults by specificity: + +1. project+tool +2. project +3. tool +4. global/user +5. first created compatible profile +6. none + +If no explicit default exists, the first created compatible profile becomes the default launch selection. Users can still choose `None` to disable all profile config for a launch. + +### Instance stores selected profile + +Tool instances store the selected config profile ID, or null when `None` was selected. Restart uses the stored selection to avoid changing behavior when the user's default profile changes later. + +## Risks / Trade-offs + +- Existing config folder users may lose automatic mounts because legacy compatibility is explicitly out of scope. Mitigation: this is an accepted breaking change and can be handled by manually recreating profiles. +- Direct profile-owned files may duplicate content across profiles. Mitigation: include relationships provide reusable file-only profiles without adding another model. +- Plain env vars can contain secrets. Mitigation: label v1 env vars as non-secret/plain text and defer secret storage to a later change. +- Profile graph resolution can become complex. Mitigation: keep launch selection single-profile, use ordered includes, test cycle detection and override ordering thoroughly. +- Mount conflicts may surprise users. Mitigation: provide a resolved preview showing final env vars, mount targets, and overridden files before launch or in profile details. + +## Migration Plan + +- Add config profile tables and profile selection fields without preserving config folder behavior. +- Stop applying all active config folders during instance start. +- Apply selected config profile resolution during instance start/restart. +- Existing tool config runtime fields can remain available until replaced by profile runtime hints, but reusable file/env launch behavior moves to config profiles. + +## Open Questions + +- Should runtime hints initially include all existing tool config runtime fields, or only env vars/files/mounts with start command and working directory? +- Should resolved config preview be required before first implementation, or can it be shipped after CRUD/start selection? diff --git a/openspec/changes/add-config-profiles/proposal.md b/openspec/changes/add-config-profiles/proposal.md new file mode 100644 index 0000000..0bf0b77 --- /dev/null +++ b/openspec/changes/add-config-profiles/proposal.md @@ -0,0 +1,37 @@ +## Why + +Tool launches need reusable user-owned configuration that can combine editable files, mount targets, environment variables, and runtime hints without forcing every tool start to be configured from scratch. The existing config folder and tool config concepts provide pieces of this, but they do not model a single selectable, composable launch profile with project/tool-aware defaults and override behavior. + +## What Changes + +- Add config profiles as the primary reusable launch configuration model. +- Allow exactly one config profile, or no config profile, to be selected when starting a session. +- Allow profiles to include other profiles in ordered composition, with cycle detection at save and launch resolution. +- Let profiles be portable, tool-specific, project-specific, or project+tool-specific based on optional project and tool references. +- Store profile-owned UTF-8 text files under mount roots, with relative file paths and `ro`/`rw` mount modes. +- Store plain-text environment variables and runtime hints on profiles. +- Resolve profile layers deterministically: included profiles in order, then the selected profile, with later layers overriding earlier layers. +- Select defaults by specificity: project+tool, project, tool, global, then first compatible profile unless a default is explicitly configured. +- Store the selected config profile on started instances so restarts are predictable. +- **BREAKING**: Replace legacy always-active config folder mounting with explicit config profile selection. Legacy compatibility is not required for this change. + +## Capabilities + +### New Capabilities + +- `config-profiles`: User-owned launch profiles that compose files, mounts, env vars, runtime hints, defaults, compatibility, and include relationships. + +### Modified Capabilities + +- `tool-instances`: Session creation/start behavior accepts an optional selected config profile, applies resolved profile output, supports no-profile starts, and persists the selected profile for restart behavior. +- `user-config`: User settings expose profile management/default selection surfaces for launch configuration. +- `tool-config-management`: Legacy config folder behavior is superseded by config profiles for file mounts and reusable launch setup. + +## Impact + +- Backend models and migrations for config profiles, ordered profile includes, and default selection. +- Backend APIs for profile CRUD, include management, default configuration, compatibility filtering, and resolved profile preview. +- Tool instance create/start/restart APIs and Docker compose staging logic to apply resolved profile env vars, mounts, files, and runtime hints. +- Settings UI for a small profile/file editor and default profile management. +- Start session UI to select one compatible config profile or `None` before launching. +- Tests for profile resolution, override ordering, cycle detection, compatibility filtering, defaults, and launch application. diff --git a/openspec/changes/add-config-profiles/specs/config-profiles/spec.md b/openspec/changes/add-config-profiles/specs/config-profiles/spec.md new file mode 100644 index 0000000..2ac4924 --- /dev/null +++ b/openspec/changes/add-config-profiles/specs/config-profiles/spec.md @@ -0,0 +1,118 @@ +## ADDED Requirements + +### Requirement: Profile Ownership And Scope +The system SHALL manage config profiles as user-owned launch configuration records with optional project and tool type references that derive compatibility scope. + +#### Scenario: Create portable profile +- **GIVEN** an authenticated user +- **WHEN** they create a config profile without a project or tool type +- **THEN** the profile is stored for that user +- **AND** the profile is compatible with any project and tool type owned or accessible by that user + +#### Scenario: Create scoped profile +- **GIVEN** an authenticated user with access to a project and a tool type +- **WHEN** they create a config profile with `project_id`, `tool_type_id`, or both +- **THEN** the profile is stored with those references +- **AND** compatibility is derived from the non-null references + +#### Scenario: Reject cross-user references +- **GIVEN** an authenticated user +- **WHEN** they create or update a profile with a project, tool type, include, or default reference they cannot access +- **THEN** the request is rejected + +### Requirement: Profile Content +The system SHALL store profile content as plain environment variables, runtime hints, and one or more mount roots containing UTF-8 text files. + +#### Scenario: Save env vars and runtime hints +- **GIVEN** an authenticated user editing a config profile +- **WHEN** they save plain-text environment variables and runtime hints such as start command, working directory, and port +- **THEN** the system persists those values on the profile +- **AND** returns them through the profile API + +#### Scenario: Save mounted text files +- **GIVEN** an authenticated user editing a config profile +- **WHEN** they add a mount with an absolute `target_path`, mode `ro` or `rw`, and relative UTF-8 text file paths +- **THEN** the system persists the mount and files +- **AND** preserves file content exactly as UTF-8 text + +#### Scenario: Reject unsafe file paths +- **GIVEN** an authenticated user editing a config profile mount +- **WHEN** they submit an absolute file path, an empty relative path, or a relative path containing `..` +- **THEN** the request is rejected + +### Requirement: Ordered Profile Includes +The system SHALL allow a config profile to include other compatible profiles in a deterministic order. + +#### Scenario: Add ordered includes +- **GIVEN** an authenticated user with multiple config profiles +- **WHEN** they configure profile A to include profile B then profile C +- **THEN** the include order is stored +- **AND** resolution processes B before C before A + +#### Scenario: Reject include cycle on save +- **GIVEN** an authenticated user with profiles A and B where A already includes B +- **WHEN** they update B to include A +- **THEN** the request is rejected with a cycle error + +#### Scenario: Guard against cycle during resolution +- **GIVEN** stored profile include data contains a cycle +- **WHEN** the system resolves a selected profile +- **THEN** resolution fails safely without launching a partially resolved configuration + +### Requirement: Profile Resolution +The system SHALL resolve a selected profile by recursively applying included profiles in order and then applying the selected profile itself. + +#### Scenario: Resolve layered env vars +- **GIVEN** profile A includes profile B then profile C +- **AND** B, C, and A define the same environment variable +- **WHEN** profile A is resolved +- **THEN** the value from A wins over C and B +- **AND** the value from C wins over B for keys not set by A + +#### Scenario: Resolve mount file conflicts +- **GIVEN** multiple resolved layers define the same mount `target_path` +- **WHEN** they contain different files under that mount +- **THEN** their file trees are merged +- **AND** later layers replace earlier content for the same relative file path + +#### Scenario: Resolve mount mode conflicts +- **GIVEN** multiple resolved layers define the same mount `target_path` with different modes +- **WHEN** the profile is resolved +- **THEN** the mode from the latest layer wins + +### Requirement: Profile Defaults +The system SHALL select the default compatible profile by explicit default specificity, then by first created compatible profile, then no profile. + +#### Scenario: Choose most specific explicit default +- **GIVEN** a user has explicit default profiles for global, tool, project, and project+tool scopes +- **WHEN** they start a matching project/tool combination +- **THEN** the project+tool default is selected +- **AND** project, tool, and global defaults are used only when no more-specific explicit default matches + +#### Scenario: Fall back to first compatible profile +- **GIVEN** a user has compatible profiles but no explicit matching default +- **WHEN** they start a session for a project/tool combination +- **THEN** the oldest compatible profile is selected by default + +#### Scenario: No compatible profile +- **GIVEN** a user has no compatible profile for a project/tool combination +- **WHEN** they start a session +- **THEN** the default profile selection is `None` + +### Requirement: Profile Compatibility Filtering +The system SHALL list compatible config profiles for a selected project and tool type by default. + +#### Scenario: List compatible profiles +- **GIVEN** an authenticated user has portable, project-specific, tool-specific, and unrelated profiles +- **WHEN** the launch UI requests profiles for a selected project and tool type +- **THEN** the response includes portable profiles and profiles matching that project, tool type, or both +- **AND** excludes unrelated project-specific or tool-specific profiles + +### Requirement: Resolved Profile Preview +The system SHALL expose a resolved profile preview for a selected profile and project/tool context. + +#### Scenario: Preview resolved output +- **GIVEN** an authenticated user selects a compatible config profile +- **WHEN** they request a resolved preview +- **THEN** the response includes the final environment variables, runtime hints, mount targets, mount modes, and relative file paths +- **AND** indicates overridden values where practical diff --git a/openspec/changes/add-config-profiles/specs/tool-config-management/spec.md b/openspec/changes/add-config-profiles/specs/tool-config-management/spec.md new file mode 100644 index 0000000..2fd8d0c --- /dev/null +++ b/openspec/changes/add-config-profiles/specs/tool-config-management/spec.md @@ -0,0 +1,29 @@ +## MODIFIED Requirements + +### Requirement: Tool config supports runtime fields +The system SHALL keep tool config runtime fields available for tool configuration, while reusable launch setup for user-owned files, mounts, and env var bundles SHALL be handled by config profiles. + +#### Scenario: Create config with runtime fields +- **WHEN** user creates a tool config with start_command="npm start", port=3000, working_directory="/app" +- **THEN** the config is saved with all fields populated + +#### Scenario: Environment variables as JSON +- **WHEN** user sets environment_variables to {"NODE_ENV": "production", "API_KEY": "secret"} +- **THEN** the system stores and returns the config with the JSON object preserved +- **AND** reusable per-launch environment bundles are managed through config profiles + +#### Scenario: Volumes as JSON +- **WHEN** user sets volumes to [{"host": "/data", "container": "/app/data", "mode": "rw"}] +- **THEN** the system stores and returns the config with the JSON array preserved +- **AND** user-owned mounted file trees are managed through config profiles + +## REMOVED Requirements + +### Requirement: Active config folders auto-mount during launch +The system SHALL NOT automatically mount all active config folders when starting tool instances. + +#### Scenario: Start instance after config profiles replace folders +- **GIVEN** a user has legacy active config folders +- **WHEN** they start a tool instance without selecting a config profile +- **THEN** those folders are not automatically mounted +- **AND** only selected config profile output is applied for user-owned launch files and mounts diff --git a/openspec/changes/add-config-profiles/specs/tool-instances/spec.md b/openspec/changes/add-config-profiles/specs/tool-instances/spec.md new file mode 100644 index 0000000..7defd3a --- /dev/null +++ b/openspec/changes/add-config-profiles/specs/tool-instances/spec.md @@ -0,0 +1,55 @@ +## MODIFIED Requirements + +### Requirement: Clone mode instance creation +The system SHALL support creating tool instances with a clone mode and an optional selected config profile. + +#### Scenario: Create instance in clone mode +- **GIVEN** an authenticated user with a repository that has an SSH key and remote URL +- **WHEN** they create an instance with `clone_mode: "clone"`, `branch: "main"`, and a compatible config profile selection +- **THEN** the system clones the repository into the instance directory +- **AND** the compose file uses the clone path as `REPO_PATH` +- **AND** the instance record stores `clone_mode="clone"`, `branch="main"`, and the selected config profile ID + +#### Scenario: Create instance in mount mode +- **GIVEN** an authenticated user with a repository +- **WHEN** they create an instance with `clone_mode: "mount"` (or omit the field) +- **THEN** the compose file uses the host repository path as `REPO_PATH` +- **AND** the instance record stores `clone_mode="mount"` + +#### Scenario: Create instance with no config profile +- **GIVEN** an authenticated user creating a tool instance +- **WHEN** they select `None` for config profile +- **THEN** the instance record stores no selected config profile +- **AND** profile-owned env vars, files, mounts, and runtime hints are not applied at start + +#### Scenario: Reject incompatible config profile selection +- **GIVEN** an authenticated user creating a tool instance for a project and tool type +- **WHEN** they select a config profile scoped to a different project or tool type +- **THEN** instance creation or start is rejected + +## ADDED Requirements + +### Requirement: Config profile launch application +The system SHALL apply the selected config profile's resolved output when starting a tool instance. + +#### Scenario: Start instance with selected profile +- **GIVEN** a pending tool instance with a selected compatible config profile +- **WHEN** the instance is started +- **THEN** the system resolves the selected profile +- **AND** writes resolved env vars into the instance environment +- **AND** stages resolved profile files under the instance directory +- **AND** adds Docker bind mounts for resolved profile mounts +- **AND** applies supported runtime hints before starting the container + +#### Scenario: Restart uses stored profile selection +- **GIVEN** an existing instance was started with config profile A +- **AND** the user's default profile later changes to profile B +- **WHEN** the instance is restarted +- **THEN** the system uses stored profile A for restart behavior +- **AND** does not switch to profile B automatically + +#### Scenario: Start fails on profile resolution error +- **GIVEN** an instance references a selected config profile that cannot be resolved +- **WHEN** the instance is started +- **THEN** the start request fails before launching the container +- **AND** the error explains the profile resolution problem diff --git a/openspec/changes/add-config-profiles/specs/user-config/spec.md b/openspec/changes/add-config-profiles/specs/user-config/spec.md new file mode 100644 index 0000000..399588d --- /dev/null +++ b/openspec/changes/add-config-profiles/specs/user-config/spec.md @@ -0,0 +1,35 @@ +## ADDED Requirements + +### Requirement: Config Profile Settings Editor +The system SHALL expose a settings interface for managing the authenticated user's config profiles. + +#### Scenario: Browse config profiles in settings +- **GIVEN** an authenticated user +- **WHEN** they open settings +- **THEN** they can view their config profiles +- **AND** see each profile's name, compatibility scope, default status, and included profiles + +#### Scenario: Edit profile content in settings +- **GIVEN** an authenticated user editing a config profile +- **WHEN** they update env vars, runtime hints, mounts, text files, or include order +- **THEN** the settings UI saves those changes through the config profile API +- **AND** validation errors are shown without losing the user's draft edits + +#### Scenario: Configure profile defaults in settings +- **GIVEN** an authenticated user editing config profiles +- **WHEN** they mark a profile as the default for a global, project, tool, or project+tool context +- **THEN** subsequent launches for matching contexts preselect that profile according to default specificity + +### Requirement: Launch Profile Selection UI +The system SHALL allow the authenticated user to select one compatible config profile or `None` when starting a session. + +#### Scenario: Preselect default compatible profile +- **GIVEN** an authenticated user has a default compatible profile for a selected project and tool type +- **WHEN** they open the start session form +- **THEN** the form preselects that profile +- **AND** also offers `None` as a selectable option + +#### Scenario: Select no profile +- **GIVEN** an authenticated user starts a session +- **WHEN** they choose `None` in the config profile selector +- **THEN** the start request records no selected config profile diff --git a/openspec/changes/add-config-profiles/tasks.md b/openspec/changes/add-config-profiles/tasks.md new file mode 100644 index 0000000..284ef4b --- /dev/null +++ b/openspec/changes/add-config-profiles/tasks.md @@ -0,0 +1,32 @@ +## 1. Sequential Foundation + +- [ ] 1.1 Backend data model and migrations. Add config profile, ordered include, mount/file, default selection, and tool instance selected-profile storage. Remove launch-time reliance on legacy active config folder auto-mounting. Depends on: none. Parallel with: none. + +## 2. Parallel Backend Core After Foundation + +- [ ] 2.1 Profile resolver service. Implement recursive ordered include resolution, deterministic merge rules, save-independent cycle protection, and resolved output structures for env vars, runtime hints, mounts, file trees, and override metadata. Depends on: 1.1. Parallel with: 2.2, 2.3. +- [ ] 2.2 Profile CRUD, validation, compatibility, and default APIs. Implement user-owned CRUD, access checks, path/content validation, ordered include save validation, compatibility-filtered listing, and default profile selection APIs. Depends on: 1.1. Parallel with: 2.1, 2.3. +- [ ] 2.3 Instance API profile selection plumbing. Update instance create/start request and persistence paths to accept one compatible config profile ID or null, reject incompatible selections, and preserve `None`. Depends on: 1.1. Parallel with: 2.1, 2.2. + +## 3. Sequential Backend Integration + +- [ ] 3.1 Resolved profile preview API. Return final env vars, runtime hints, mount targets, mount modes, relative file paths, and practical override indicators. Depends on: 2.1, 2.2. Parallel with: none. +- [ ] 3.2 Launch and restart profile application. Apply resolved profile output during start by writing env vars, staging files, adding Docker bind mounts, applying supported runtime hints, skipping profile output for `None`, and using the stored profile on restart instead of current defaults. Depends on: 2.1, 2.3. Parallel with: none. + +## 4. Parallel Frontend After Backend Contracts + +- [ ] 4.1 Frontend config profile API client and types. Add client methods/types for profile CRUD, includes, defaults, compatible listing, preview, and instance profile selection payloads. Depends on: 2.2, 2.3, 3.1. Parallel with: none. +- [ ] 4.2 Settings profile management UI. Add settings surfaces for browsing profiles, editing env vars/runtime hints/mounts/text files/include order/defaults, and preserving draft edits on validation errors. Depends on: 4.1. Parallel with: 4.3. +- [ ] 4.3 Launch profile selection UI. Update session start UI to load compatible profiles for the selected project/tool, preselect the resolved default, offer `None`, and submit selected profile ID or null. Depends on: 4.1. Parallel with: 4.2. + +## 5. Parallel Test Suites + +- [ ] 5.1 Backend profile API and resolver tests. Cover CRUD, ownership, path validation, compatibility filtering, default precedence, include ordering, save-time cycle rejection, resolution-time cycle protection, and deterministic override behavior. Depends on: 2.1, 2.2, 3.1. Parallel with: 5.2, 5.3. +- [ ] 5.2 Backend instance launch tests. Cover selected profile start/restart behavior, incompatible profile rejection, `None` selection, env var writing, file staging, Docker mount application, runtime hints, and legacy active config folders not auto-mounting. Depends on: 3.2. Parallel with: 5.1, 5.3. +- [ ] 5.3 Frontend profile UI tests. Cover profile settings editing, validation error handling, compatible profile loading, default preselection, `None` selection, and submit payloads. Depends on: 4.2, 4.3. Parallel with: 5.1, 5.2. + +## 6. Sequential Quality Gates + +- [ ] 6.1 Run backend quality gates for touched API/model/services code and fix failures. Depends on: 5.1, 5.2. Parallel with: none. +- [ ] 6.2 Run frontend quality gates for touched settings/session UI code and fix failures. Depends on: 5.3. Parallel with: none. +- [ ] 6.3 Run final OpenSpec status and implementation checklist review. Depends on: 6.1, 6.2. Parallel with: none. diff --git a/openspec/changes/persistent-terminal-sessions/tasks.md b/openspec/changes/persistent-terminal-sessions/tasks.md index 215140b..65ec048 100644 --- a/openspec/changes/persistent-terminal-sessions/tasks.md +++ b/openspec/changes/persistent-terminal-sessions/tasks.md @@ -46,7 +46,7 @@ - [x] 6.1 Add reconnection logic with exponential backoff - [x] 6.2 Handle buffer replay on reconnect (process incoming bytes) - [x] 6.3 Add reset function (send WebSocket message or call API) -- [ ] 6.4 Add heartbeat/ping to detect disconnections faster +- [x] 6.4 Add heartbeat/ping to detect disconnections faster ## 7. Testing @@ -59,6 +59,6 @@ ## 8. Documentation -- [ ] 8.1 Update API documentation with new reset endpoint -- [ ] 8.2 Update user documentation about persistent terminals -- [ ] 8.3 Add troubleshooting guide for terminal issues +- [x] 8.1 Update API documentation with new reset endpoint +- [x] 8.2 Update user documentation about persistent terminals +- [x] 8.3 Add troubleshooting guide for terminal issues