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
+58 -43
View File
@@ -13,6 +13,7 @@
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next";
import { GitBranch } from "lucide-react";
import { LaneGitBar } from "./LaneGitBar";
import { api } from "../../lib/api";
import { keepIfSame } from "../../lib/keepIfSame";
@@ -230,50 +231,64 @@ export function LaneGitHeader({
useEffect(() => () => window.clearTimeout(filesCloseTimer.current), []);
if (!git?.available) return null;
return (
<div data-testid="lane-git-header">
{/* One line, because every part of it is short and the panel is
narrow: branch, head, subject, and the uncommitted counts read as a
sentence rather than a four-row block. */}
<div className="mb-2">
<div
data-testid="lane-git"
className="flex items-baseline gap-2 truncate font-mono text-[11px] text-fg-secondary"
title={git.subject}
>
<span className="shrink-0">⑂ {git.branch}</span>
<span className="shrink-0 text-fg-muted">{git.head}</span>
<span className="truncate text-fg-muted">{git.subject}</span>
{(git.dirty > 0 || git.untracked > 0) && (
<button
type="button"
ref={setCountsAnchor}
data-testid="lane-git-counts"
aria-expanded={filesOpen}
onMouseEnter={openFiles}
onMouseLeave={closeFiles}
// Focus and click open it too: hover is not a gesture a
// keyboard or a touchscreen has.
onFocus={openFiles}
onBlur={closeFiles}
onClick={() => setFilesOpen((v) => !v)}
className="shrink-0 cursor-help text-status-warning/80 underline decoration-dotted underline-offset-2"
>
{t("git.uncommitted", { dirty: git.dirty, untracked: git.untracked })}
</button>
)}
</div>
{filesOpen && git.files?.length > 0 && (
<LaneGitFilesPanel
files={git.files}
hidden={git.dirty + git.untracked - git.files.length}
anchor={countsAnchor}
onPointerEnter={openFiles}
onPointerLeave={closeFiles}
/>
)}
const lead = (
<>
{/* One row with the actions: branch first and strongest, then the head
and its subject, which is what truncates when the panel is narrow
(the whole of it stays in the tooltip). */}
<div
data-testid="lane-git"
className="flex min-w-0 flex-1 items-baseline gap-1.5 font-mono text-[11px]"
title={`${git.head} ${git.subject}`}
>
<span className="flex shrink-0 items-center gap-1 self-center font-semibold text-fg-primary">
<GitBranch className="h-3 w-3 text-fg-muted" />
{git.branch}
</span>
<span className="shrink-0 text-fg-muted">{git.head}</span>
<span className="min-w-0 truncate text-fg-muted">{git.subject}</span>
</div>
<LaneGitBar laneId={laneId} git={git} onChanged={onChanged} onNewLane={onNewLane} />
{(git.dirty > 0 || git.untracked > 0) && (
<button
type="button"
ref={setCountsAnchor}
data-testid="lane-git-counts"
aria-expanded={filesOpen}
onMouseEnter={openFiles}
onMouseLeave={closeFiles}
// Focus and click open it too: hover is not a gesture a
// keyboard or a touchscreen has.
onFocus={openFiles}
onBlur={closeFiles}
onClick={() => setFilesOpen((v) => !v)}
className="shrink-0 cursor-help rounded-full border border-status-warning/40 bg-status-warning/10 px-2 py-px text-[10.5px] text-status-warning"
>
{t("git.uncommitted", { dirty: git.dirty, untracked: git.untracked })}
</button>
)}
</>
);
return (
<div
data-testid="lane-git-header"
className="mb-2 rounded-md border border-border bg-surface-2 px-2 py-1"
>
<LaneGitBar
laneId={laneId}
git={git}
onChanged={onChanged}
onNewLane={onNewLane}
lead={lead}
/>
{filesOpen && git.files?.length > 0 && (
<LaneGitFilesPanel
files={git.files}
hidden={git.dirty + git.untracked - git.files.length}
anchor={countsAnchor}
onPointerEnter={openFiles}
onPointerLeave={closeFiles}
/>
)}
</div>
);
}