fix(run,lanes): prompt autocomplete, per-lane Active runs, compact Changes header

- The run form's prompt box offers suggestions again: `/` lists the run
  cwd's project and user commands and skills plus the CLI built-ins
  (claude lanes only), `@` lists files under the cwd; Enter picks while
  the list is open and sends otherwise. The 2f39f4e rewrite had left a
  bare textarea that ignored both.
- Active runs lists only the current lane's runs and external sessions
  (sessions matched to the lane by longest path-boundary cwd), instead of
  every lane's.
- The Changes panel's git line and its actions are one compact row in one
  card, and the mode/range toolbar is one wrapping row.
- The lane card hides itself when it has nothing to show, instead of
  leaving an empty frame where the git row used to be.
This commit is contained in:
2026-09-24 13:30:30 +07:00
parent faa56f5c75
commit 428519335d
15 changed files with 583 additions and 130 deletions
+33 -3
View File
@@ -27,7 +27,7 @@
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Play, AlertCircle } from "lucide-react";
import { api } from "../../lib/api";
@@ -47,6 +47,20 @@ import { TerminalView } from "./TerminalView";
import { RunSetup } from "./RunSetup";
import { ActiveRunsSwitcher } from "./RunHistory";
/** The claude lane a session's cwd belongs to: the longest path-boundary
* prefix, the same rule the server binds hook sessions with. External
* sessions are all claude — Codex emits no hooks. */
function owningLaneId(cwd: string, lanes: Lane[]): number | null {
let best: Lane | null = null;
for (const l of lanes) {
if (l.tool !== "claude") continue;
const root = l.cwd.replace(/\/+$/, "");
if (cwd !== root && !cwd.startsWith(root + "/")) continue;
if (!best || root.length > best.cwd.replace(/\/+$/, "").length) best = l;
}
return best?.id ?? null;
}
export interface LaneConsolePaneProps {
lanes: Lane[];
laneId: number | null;
@@ -97,6 +111,22 @@ export function LaneConsolePane({
const currentLane = laneId !== null ? lanes.find((l) => l.id === laneId) : null;
const laneTool = currentLane?.tool ?? "claude";
// The page polls runs and sessions for EVERY lane; this pane's Active runs
// list is this lane's only. No lane selected keeps the full lists.
const laneRuns = useMemo(
() =>
laneId === null || !activeRuns
? activeRuns
: { ...activeRuns, items: activeRuns.items.filter((r) => r.laneId === laneId) },
[activeRuns, laneId]
);
const laneSessions = useMemo(
() =>
laneId === null
? externalSessions
: (externalSessions ?? []).filter((s) => s.cwd && owningLaneId(s.cwd, lanes) === laneId),
[externalSessions, laneId, lanes]
);
const toolLabel = laneTool === "codex" ? "Codex" : "Claude";
useEffect(() => {
@@ -487,11 +517,11 @@ export function LaneConsolePane({
</p>
</div>
<ActiveRunsSwitcher
activeRuns={activeRuns}
activeRuns={laneRuns}
currentHandleId={handle?.id || null}
onAttach={attachToRun}
runHistory={runHistory}
externalSessions={externalSessions}
externalSessions={laneSessions}
onResumeFromHistory={onResumeFromHistory}
onViewFromHistory={onViewFromHistory}
onKill={killRun}