A local development dashboard that lets you switch between Chat, Terminal, and Editor in the browser while working with codex app-server and files in your workspace.
- Chat: integrates with
codex app-serverfor thread creation, messaging, streaming output, launch permission options, approval responses, and tool user-input responses - Terminal: create PTY sessions, send input, resize, and reconnect
- Editor: browse and edit files under
WORKSPACE_ROOTwith basic conflict detection
- Monorepo: pnpm workspaces
- Frontend: React + Vite + TypeScript
- Server: Node.js + TypeScript + Hono + ws + node-pty
- Node.js 20+
- pnpm
codexCLI available (codex app-servermust run)
pnpm install.env is automatically loaded from either server/.env or the repository root .env.
Example:
PORT=4877
BIND_HOST=127.0.0.1
WORKSPACE_ROOT=/absolute/path/to/workspace
APP_SERVER_COMMAND=codex
APP_SERVER_ARGS=app-server
APP_SERVER_CWD=/absolute/path/to/workspace
SYNC_HOST=main-pc
SYNC_ALLOWED_ROOTS=["/home/sandyman/projects","/home/sandyman/worktrees"]
SYNC_WORKSPACE_ROOT=/srv/codex-dashboard/workspacesPORT(default:4877): API server portBIND_HOST(default:127.0.0.1): bind host (0.0.0.0to expose on LAN)WORKSPACE_ROOT(optional, recommended): root path for Terminal / Editor / Chatcwdrestrictions (absolute path)APP_SERVER_COMMAND(default:codex): app-server commandAPP_SERVER_ARGS(default:app-server): app-server arguments (space-separated)APP_SERVER_CWD(optional): working directory for the app-server child process (absolute path)APP_SERVER_REQUEST_TIMEOUT_MS(default:120000): app-server RPC timeoutSYNC_HOST(optional): main PC SSH host name used by Import / ExportSYNC_SSH_USER(optional): SSH user override for syncSYNC_SSH_PORT(default:22): SSH port for syncSYNC_ALLOWED_ROOTS(optional): JSON array or path-delimited absolute roots allowed on the main PCSYNC_WORKSPACE_ROOT(optional): server-side workspace storage root for syncSYNC_STATUS_TIMEOUT_MS(default:5000): timeout for SSH status probeSYNC_PREVIEW_TTL_MS(default:300000): preview token expiry in msTERMINAL_IDLE_TIMEOUT_MS(default:600000): terminal idle timeout in msEDITOR_MAX_FILE_SIZE_BYTES(default:1048576): max file read size in EditorEDITOR_MAX_SAVE_BYTES(default:1048576): max file save size in Editor
Run both frontend and server from the repository root:
pnpm devRun with portless using the current branch name:
pnpm dev:pldev:pl uses a sanitized branch base (e.g. fix/foo -> fix-foo) and starts:
portless "<base>-api" pnpm -C server dev
PORTLESS_BASE="<base>" portless "<base>-web" pnpm -C frontend dev- Frontend:
http://localhost:4873 - Server API:
http://localhost:4877 - Vite proxies
/apiand/wsto the server. - Frontend dev host uses
FRONTEND_HOSTwhen provided (fallback:127.0.0.1). - Frontend dev port uses
FRONTEND_PORTwhen provided (fallback:4873). - When
PORTLESS_BASEis set, Vite proxies/apiand/wstohttp(s)://<PORTLESS_BASE>-api.localhost:1355.
Run separately:
pnpm -C server dev
pnpm -C frontend devpnpm build
pnpm startpnpm build generates frontend/dist, and pnpm start (server) serves static assets.
- Health:
GET /api/health - Chats:
GET/POST /api/chats,GET /api/chat-options,GET /api/chats/:id,PATCH /api/chats/:id/options - Chat actions:
POST /api/chats/:id/messages,POST /api/chats/:id/interrupt,POST /api/chats/:id/approvals/:itemId,POST /api/chats/:id/user-input/:itemId - Editor:
GET /api/editor/tree,GET/PUT /api/editor/file - Terminals:
GET/POST /api/terminals,POST /api/terminals/:id/write,POST /api/terminals/:id/resize - Sync:
GET /api/sync/status,GET /api/sync/workspaces,POST /api/sync/import/preview,POST /api/sync/import,POST /api/sync/export/preview,POST /api/sync/export,GET /api/sync/jobs/:jobId,GET /api/sync/jobs/:jobId/error - Chat WS:
/ws/chats/:threadId - Terminal WS:
/ws/terminals/:terminalId
.
├── frontend/ # React + Vite UI
└── server/ # Hono API / WS / PTY / app-server bridge
GET /api/chat-options returns:
- model choices (
models) cwdchoices (workspaceRoot,cwdChoices)- approval/sandbox choices (
approvalPolicies,sandboxModes) - defaults (
defaultApprovalPolicy,defaultSandboxMode)
When creating or updating chats, you can set:
approvalPolicy(untrusted/on-failure/on-request/never)sandboxMode(read-only/workspace-write/danger-full-access)
The server normalizes known aliases (for example onRequest, workspaceWrite) to canonical values above.
When app-server asks for approval (item/commandExecution/requestApproval or item/fileChange/requestApproval):
- Dashboard emits WS event
approval_requested - UI shows a
Yes / Nocard inline in Chat view - UI calls
POST /api/chats/:id/approvals/:itemIdwith{ "decision": "accept" | "decline" } - Dashboard replies to app-server with the same request id and emits
approval_resolved
When app-server requests tool user input (item/tool/requestUserInput):
- Dashboard emits WS event
user_input_requested - UI renders question cards and submits answers
- UI calls
POST /api/chats/:id/user-input/:itemIdwith:
{
"answers": {
"question_id": { "answers": ["selected or typed value"] }
}
}- Dashboard replies to app-server with the same request id and emits
user_input_resolved
item/tool/call is currently rejected explicitly as unsupported by this dashboard.
skill/requestApproval is also rejected explicitly as unsupported by this dashboard.
initializesends capabilities withexperimentalApi: trueandoptOutNotificationMethodsfor known noisy notifications.- JSON-RPC error
-32001(Server overloaded; retry later.) is treated as retryable with exponential backoff + jitter. turn/diff/updated,thread/status/changed, anditem/commandExecution/terminalInteractionare handled as log-only stream events (app_server_event) so the stream remains healthy even when UI rendering is not implemented.