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:
@@ -14,8 +14,6 @@
|
||||
* the route answers before the actual `git worktree add` runs, so this
|
||||
* component polls `GET /api/lanes/:id` until that finishes before running the
|
||||
* auto-setup calls against a cwd that must actually exist on disk first.
|
||||
* An optional "Create/update readme file" checkbox starts the lane's terminal
|
||||
* run right after creation with `/sg-all:sg-md-writing` as its first message.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -96,7 +94,6 @@ export function AddLaneModal({
|
||||
const [branches, setBranches] = useState<string[] | null>(null);
|
||||
const [base, setBase] = useState("");
|
||||
const [pipeline, setPipeline] = useState("default");
|
||||
const [writeDocs, setWriteDocs] = useState(false);
|
||||
// "native" is the built-in profile, not a null state — see server/lib/ai-routers.js.
|
||||
const [router, setRouter] = useState("native");
|
||||
const [routers, setRouters] = useState<AiRouter[]>([]);
|
||||
@@ -122,7 +119,6 @@ export function AddLaneModal({
|
||||
setBranches(null);
|
||||
setBase("");
|
||||
setPipeline("default");
|
||||
setWriteDocs(false);
|
||||
setRouter("native");
|
||||
setRoutersLoaded(false);
|
||||
setTool("claude");
|
||||
@@ -208,14 +204,6 @@ export function AddLaneModal({
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [mode, sourceRepo, t]);
|
||||
|
||||
// The checkbox below runs the docs skill by opening the lane's own terminal
|
||||
// run with the slash command as its first message — a skill only exists
|
||||
// inside a CLI session, so there is nothing server-side to call instead.
|
||||
// Claude-only: codex has no slash commands (same reason LaneConsolePane
|
||||
// prefills prose for a codex lane).
|
||||
const startDocsRun = (laneId: number, cwd: string) =>
|
||||
api.run.start({ laneId, cwd, initialPrompt: "/sg-all:sg-md-writing" });
|
||||
|
||||
// Native is the built-in profile, so this is one call either way — there is
|
||||
// no "no router" branch to keep in step with the real one.
|
||||
const applyRouter = (laneId: number) => api.lanes.routerApply(laneId, router);
|
||||
@@ -242,19 +230,14 @@ export function AddLaneModal({
|
||||
// Worktree mode below already works this way.
|
||||
onAdded(result.lane);
|
||||
|
||||
// Router first: a session that booted on the vendor's API would keep
|
||||
// talking to it for its whole life.
|
||||
const [routerOutcome, docsOutcome] = await Promise.allSettled([
|
||||
applyRouter(result.lane.id),
|
||||
writeDocs && tool === "claude"
|
||||
? startDocsRun(result.lane.id, result.lane.cwd)
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
const failed = [routerOutcome, docsOutcome].find((o) => o.status === "rejected");
|
||||
if (failed && failed.status === "rejected") {
|
||||
// A session that booted on the vendor's API would keep talking to it
|
||||
// for its whole life, so the router is applied before anything runs.
|
||||
try {
|
||||
await applyRouter(result.lane.id);
|
||||
} catch (routerErr) {
|
||||
// The lane exists and is already listed — name what did not finish
|
||||
// instead of closing on a half-set-up lane.
|
||||
setError(failed.reason instanceof Error ? failed.reason.message : String(failed.reason));
|
||||
setError(routerErr instanceof Error ? routerErr.message : String(routerErr));
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
@@ -312,10 +295,6 @@ export function AddLaneModal({
|
||||
const anyFailed = Object.values(outcome).includes("failed");
|
||||
setSetupResult(outcome);
|
||||
|
||||
if (writeDocs && tool === "claude") {
|
||||
await startDocsRun(result.lane.id, provisionedLane.cwd);
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.info("[add-lane] auto-setup result:", {
|
||||
profile:
|
||||
@@ -555,39 +534,6 @@ export function AddLaneModal({
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
{tool === "claude" && (
|
||||
<label
|
||||
// No tick box: the card itself lights up when selected, the same
|
||||
// treatment LaneStripCard uses, which makes a second indicator of
|
||||
// the same state redundant. The input stays and only goes visually
|
||||
// hidden — it is still what a keyboard toggles and what a screen
|
||||
// reader announces — so the card carries the focus ring for it.
|
||||
className={`block cursor-pointer rounded-lg border p-3 shadow-sm transition-colors focus-within:ring-2 focus-within:ring-accent/50 ${
|
||||
writeDocs
|
||||
? "border-accent bg-accent/10"
|
||||
: "border-border bg-surface-2 hover:border-border-light hover:bg-surface-3"
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
// The wrapping label also holds the hint, so name the input
|
||||
// itself — otherwise its accessible name is label + hint.
|
||||
aria-label={t("addLaneWriteDocsLabel")}
|
||||
checked={writeDocs}
|
||||
onChange={(e) => setWriteDocs(e.target.checked)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<span
|
||||
className={`block text-xs font-medium ${writeDocs ? "text-accent" : "text-fg-secondary"}`}
|
||||
>
|
||||
{t("addLaneWriteDocsLabel")}
|
||||
</span>
|
||||
<span className="mt-1 block text-[10px] text-fg-muted">
|
||||
{t("addLaneWriteDocsHint")}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
|
||||
{mode === "worktree" && branches && (
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-fg-secondary" htmlFor="add-lane-base">
|
||||
|
||||
Reference in New Issue
Block a user