feat(run): rewrite routes for the tmux backend, drop stdin-message endpoint
This commit is contained in:
+30
-64
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* @file run.js
|
||||
* @description HTTP routes for the dashboard's Run feature. Spawns and
|
||||
* supervises `claude` processes (headless one-shot or multi-turn
|
||||
* conversation), streams structured envelopes to the client over the
|
||||
* existing WebSocket, and exposes a tiny CRUD-ish surface for run management.
|
||||
* @description HTTP routes for the dashboard's terminal-run feature. Starts,
|
||||
* resumes, kills, and lists tmux-backed `claude` sessions (one per lane),
|
||||
* streamed to the client over a dedicated WebSocket path (see
|
||||
* server/websocket.js `/ws-pty/:runId`) rather than this REST surface.
|
||||
*
|
||||
* Security model:
|
||||
* - Local-first dashboard. The dashboard server is expected to bind to
|
||||
@@ -22,7 +22,8 @@
|
||||
const { Router } = require("express");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const runs = require("../lib/run-spawner");
|
||||
const runs = require("../lib/pty-run");
|
||||
const tmux = require("../lib/tmux");
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -96,11 +97,7 @@ function sanitiseCwd(input) {
|
||||
const ALLOWED_PERMISSION_MODES = new Set(["acceptEdits", "default", "plan", "bypassPermissions"]);
|
||||
|
||||
router.get("/", (_req, res) => {
|
||||
res.json({
|
||||
items: runs.listRuns(),
|
||||
maxConcurrent: runs.getMaxConcurrent(),
|
||||
activeCount: runs.liveCount(),
|
||||
});
|
||||
res.json({ items: runs.listRuns() });
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -125,12 +122,7 @@ router.get("/history", (req, res) => {
|
||||
limit: Number.isFinite(limit) ? limit : 50,
|
||||
laneId: Number.isFinite(laneId) ? laneId : null,
|
||||
});
|
||||
// Cross-reference with live handles so the UI can mark which history
|
||||
// entries are still attached / running.
|
||||
const liveIds = new Set();
|
||||
for (const h of runs.listRuns()) {
|
||||
if (h.id && (h.status === "running" || h.status === "spawning")) liveIds.add(h.id);
|
||||
}
|
||||
const liveIds = new Set(runs.listRuns().map((h) => h.id));
|
||||
res.json({
|
||||
items: items.map((it) => ({ ...it, isLive: liveIds.has(it.id) })),
|
||||
});
|
||||
@@ -260,23 +252,15 @@ router.get("/binary", (_req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/tmux", (_req, res) => {
|
||||
res.json({ available: tmux.isTmuxAvailable() });
|
||||
});
|
||||
|
||||
router.post("/", (req, res) => {
|
||||
const body = req.body || {};
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt : "";
|
||||
const mode = body.mode === "headless" ? "headless" : "conversation";
|
||||
const model = typeof body.model === "string" && body.model ? body.model : null;
|
||||
const resumeSessionId =
|
||||
typeof body.resumeSessionId === "string" && body.resumeSessionId ? body.resumeSessionId : null;
|
||||
const effort = typeof body.effort === "string" && body.effort ? body.effort : null;
|
||||
const permissionMode =
|
||||
typeof body.permissionMode === "string" && ALLOWED_PERMISSION_MODES.has(body.permissionMode)
|
||||
? body.permissionMode
|
||||
: "acceptEdits";
|
||||
// Resuming a conversation can spawn with an empty prompt — claude waits
|
||||
// on stdin until the user types a follow-up. Headless and fresh
|
||||
// conversation runs still need a prompt to do anything.
|
||||
if (!prompt.trim() && !(mode === "conversation" && resumeSessionId)) {
|
||||
return res.status(400).json({ error: { code: "EBADPROMPT", message: "prompt is required" } });
|
||||
const laneId = Number.parseInt(String(body.laneId ?? ""), 10);
|
||||
if (!Number.isInteger(laneId)) {
|
||||
return res.status(400).json({ error: { code: "EBADLANE", message: "laneId is required" } });
|
||||
}
|
||||
let cwd;
|
||||
try {
|
||||
@@ -286,22 +270,22 @@ router.post("/", (req, res) => {
|
||||
}
|
||||
try {
|
||||
const handle = runs.spawnRun({
|
||||
prompt,
|
||||
mode,
|
||||
laneId,
|
||||
cwd,
|
||||
model,
|
||||
permissionMode,
|
||||
resumeSessionId,
|
||||
effort,
|
||||
model: typeof body.model === "string" && body.model ? body.model : null,
|
||||
permissionMode:
|
||||
typeof body.permissionMode === "string" && ALLOWED_PERMISSION_MODES.has(body.permissionMode)
|
||||
? body.permissionMode
|
||||
: "acceptEdits",
|
||||
effort: typeof body.effort === "string" && body.effort ? body.effort : null,
|
||||
resumeSessionId:
|
||||
typeof body.resumeSessionId === "string" && body.resumeSessionId
|
||||
? body.resumeSessionId
|
||||
: null,
|
||||
initialPrompt: typeof body.initialPrompt === "string" ? body.initialPrompt : "",
|
||||
});
|
||||
return res.status(201).json(runs.getRun(handle.id));
|
||||
return res.status(201).json(handle);
|
||||
} catch (err) {
|
||||
if (err.code === "ECONCURRENCY") {
|
||||
return res.status(429).json({
|
||||
error: { code: err.code, message: err.message },
|
||||
running: err.running || [],
|
||||
});
|
||||
}
|
||||
if (err.code && err.code.startsWith("E")) {
|
||||
return res.status(400).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
@@ -309,27 +293,9 @@ router.post("/", (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/:id/message", (req, res) => {
|
||||
const body = req.body || {};
|
||||
const text = typeof body.text === "string" ? body.text : "";
|
||||
if (!text) {
|
||||
return res.status(400).json({ error: { code: "EBADINPUT", message: "text is required" } });
|
||||
}
|
||||
try {
|
||||
const result = runs.sendInput(req.params.id, text);
|
||||
return res.json(result);
|
||||
} catch (err) {
|
||||
const status = err.code === "ENOTFOUND" ? 404 : 400;
|
||||
return res.status(status).json({ error: { code: err.code, message: err.message } });
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/:id", (req, res) => {
|
||||
// ?envelopes=1 includes the in-memory envelope history so the UI can
|
||||
// re-attach to an active run started elsewhere and see what it missed.
|
||||
const includeEnvelopes = req.query.envelopes === "1";
|
||||
const handle = runs.getRun(req.params.id, { includeEnvelopes });
|
||||
if (!handle) {
|
||||
const handle = runs.getRun(req.params.id);
|
||||
if (!handle || handle.status === "gone") {
|
||||
return res.status(404).json({ error: { code: "ENOTFOUND", message: "run not found" } });
|
||||
}
|
||||
return res.json(handle);
|
||||
|
||||
Reference in New Issue
Block a user