feat(run,ui): make the lane console usable — terminal focus, live-exit recovery, auto permission mode, model aliases
Terminal (issue #33): - `convertEol` is gone from xterm. It rewrote every bare LF as CR+LF, so a tmux pane that scrolled (tmux emits a bare LF and keeps drawing at the column it was on) had the rest of that draw land at column 0, stamping a line's first characters into the left margin of the rows below — the torn left edge that only showed up once content scrolled. - The attach carries the client's real size (`?cols=&rows=`, `sizeFromQuery` in `server/websocket.js`, bounded because it reaches a TIOCSWINSZ ioctl). `attach()` spawned `tmux attach-session` at a hardcoded 80x24 on every attach, and tmux sizes the window to the newest client, so `claude` redrew its whole UI at 80 columns until the first `resize` message landed. - The terminal takes the keyboard on mount, so typing reaches tmux without clicking the pane first. - A pane that ends (double Ctrl+C, `exit`, a crash) reports itself through a new `onExit` prop and `LaneConsolePane` runs `newRun()`, so the console returns to the setup form instead of leaving a dead pane behind a "New run" button. Pane environment: - `server/lib/desktop-env.js` forwards the graphical session's DISPLAY / WAYLAND_DISPLAY / XAUTHORITY / XDG_RUNTIME_DIR (from `systemctl --user show-environment`, this process's own values winning) into the pane. The server normally runs as a systemd user service with none of them, so `claude` there could not read the clipboard and answered "no image found in clipboard" on every image paste. Run setup: - `auto` is a permission mode and is now the default everywhere — allowlists, the `--permission-mode` fallback, the select, and the pane's initial state. - The model picker sends the CLI's aliases (`fable`/`opus`/`sonnet`/`haiku`) instead of pinned ids. `claude --model` resolves an alias to the latest model itself, so a release like Fable 5.1 needs no edit here; pricing is unaffected because it keys off the resolved id the transcript reports. - **Create/update readme file** moved from the Add-lane modal into the lane's Tools menu: creating a lane and writing its docs are separate decisions, and the second is worth re-running long after the first. - The Workspace's no-lane state is a real banner; the lane strip (add tile included) renders only once a lane exists. Refs #32, #33. Tests: npm run test:server (1828 pass), client vitest (558 pass).
This commit is contained in:
+28
-4
@@ -82,6 +82,23 @@ function initWebSocket(server) {
|
||||
let ptyWss = null;
|
||||
const PTY_PATH_RE = /^\/ws-pty\/(agenthub-lane-\d+)$/;
|
||||
|
||||
/**
|
||||
* The attaching terminal's own size, off the attach URL's query. tmux resizes
|
||||
* the window to whichever client attached most recently, so a hardcoded 80×24
|
||||
* here is not a harmless default: the pane's program redraws its whole UI at
|
||||
* 80 columns and only gets the real width back when the client's first
|
||||
* `resize` message lands, leaving 80-column wreckage in the scrollback that
|
||||
* shows up as torn left-margin fragments when the user scrolls back
|
||||
* (issue #33). Bounded, because this number reaches a `TIOCSWINSZ` ioctl.
|
||||
*/
|
||||
function sizeFromQuery(params) {
|
||||
const read = (name, fallback) => {
|
||||
const parsed = Number.parseInt(params.get(name) ?? "", 10);
|
||||
return Number.isInteger(parsed) && parsed >= 2 && parsed <= 1000 ? parsed : fallback;
|
||||
};
|
||||
return { cols: read("cols", 80), rows: read("rows", 24) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Second WebSocket server, dedicated to the terminal-run PTY transport
|
||||
* (`/ws-pty/:runId`). Kept separate from the `/ws` JSON-broadcast path so
|
||||
@@ -100,9 +117,9 @@ function initPtyWebSocket(server) {
|
||||
const { attach } = require("./lib/pty-attach");
|
||||
ptyWss = new WebSocketServer({ noServer: true, maxPayload: 1024 * 1024 });
|
||||
|
||||
ptyWss.on("connection", (ws, runId) => {
|
||||
ptyWss.on("connection", (ws, runId, size) => {
|
||||
try {
|
||||
attach(ws, runId, { cols: 80, rows: 24 });
|
||||
attach(ws, runId, size || { cols: 80, rows: 24 });
|
||||
} catch (err) {
|
||||
try {
|
||||
ws.close(1008, err.message);
|
||||
@@ -133,7 +150,7 @@ function initPtyWebSocket(server) {
|
||||
return;
|
||||
}
|
||||
ptyWss.handleUpgrade(req, socket, head, (ws) => {
|
||||
ptyWss.emit("connection", ws, match[1]);
|
||||
ptyWss.emit("connection", ws, match[1], sizeFromQuery(url.searchParams));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,4 +220,11 @@ function closeWebSocket() {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { initWebSocket, initPtyWebSocket, broadcast, getConnectionCount, closeWebSocket };
|
||||
module.exports = {
|
||||
initWebSocket,
|
||||
initPtyWebSocket,
|
||||
broadcast,
|
||||
getConnectionCount,
|
||||
closeWebSocket,
|
||||
sizeFromQuery,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user