fix(workspace): a new tab shows in its workspace; a row opens its tabs
A tab opened bare from + had no session row until its first hook, and the sidebar re-read its live panes only when a workspace's status or session moved, which a tab in slots 1-3 never does: the new tab showed nowhere. The sidebar now lists a live AI tab whose session has no row yet as "New session" straight from the run (its session id is known from the start) and follows the Workspace page's own run-list poll (LIVE_RUNS_EVENT). A sidebar workspace row now opens the workspace's live tabs on the pane it last focused, and its home only when it has none; a Home button at the left end of the tab bar reaches the home of a workspace whose tabs keep running.
This commit is contained in:
@@ -8,7 +8,9 @@
|
||||
* renames it and its hover button archives it (`PATCH {name}` / `{archived}`,
|
||||
* which deletes nothing). A session with a LIVE pane (a `GET /api/run` handle
|
||||
* carrying its session id) opens that tmux pane on the Workspace page
|
||||
* (`?lane=<id>&run=<runId>`, its tab in the session view; Ctrl/Cmd+click opens
|
||||
* (`?lane=<id>&run=<runId>`, its tab in the session view; a live tab whose
|
||||
* session has no row yet — a bare tab before its first prompt — is listed as
|
||||
* **New session** from the run itself; Ctrl/Cmd+click opens
|
||||
* it in a split beside the focused pane) — attaching, never starting or
|
||||
* resuming anything;
|
||||
* any other session opens in its workspace too (`?lane=<id>&session=<id>`,
|
||||
@@ -200,9 +202,17 @@ export function WorkspaceSessionList({
|
||||
const [archiving, setArchiving] = useState<Session | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
// This workspace's live AI tabs. A tab started bare (no prompt) has its
|
||||
// session id from the start, but no session row until a hook creates one.
|
||||
const laneRuns = liveRuns.filter(
|
||||
(r) => r.laneId === lane.id && r.status === "running" && r.tool !== "shell"
|
||||
);
|
||||
// The lane's own session/status/needs-you move on every hook, which is when
|
||||
// a session can appear or change state — re-read then, not on a timer.
|
||||
const refreshKey = `${lane.session_id}:${lane.status}:${lane.needs_action}:${version}`;
|
||||
// a session can appear or change state, and a tab opening or getting its
|
||||
// session is too — re-read then, not on a timer.
|
||||
const refreshKey = `${lane.session_id}:${lane.status}:${lane.needs_action}:${version}:${laneRuns
|
||||
.map((r) => `${r.id}=${r.sessionId ?? ""}`)
|
||||
.join(",")}`;
|
||||
const own = useSessionPage({ lane: lane.id, archived: false, limit: SESSION_LIMIT }, refreshKey);
|
||||
const sessions = own?.sessions ?? (own === null ? null : []);
|
||||
|
||||
@@ -211,6 +221,13 @@ export function WorkspaceSessionList({
|
||||
(s) => !q || (s.name ?? "").toLowerCase().includes(q) || s.id.startsWith(q)
|
||||
);
|
||||
const runFor = (s: Session) => liveRunFor(lane, liveRuns, s.id);
|
||||
// A live tab whose session has no row yet: shown as a New session at the
|
||||
// top, like Claude Desktop, until its first hook brings the real row. Not
|
||||
// during a search — it has no name to match.
|
||||
const fresh =
|
||||
sessions === null || q
|
||||
? []
|
||||
: laneRuns.filter((r) => !r.sessionId || !sessions.some((s) => s.id === r.sessionId));
|
||||
const count = sessions === null ? null : shown.length;
|
||||
useEffect(() => {
|
||||
if (count !== null) onMatches?.(count);
|
||||
@@ -270,7 +287,7 @@ export function WorkspaceSessionList({
|
||||
{error}
|
||||
</p>
|
||||
);
|
||||
if (sessions !== null && shown.length === 0) {
|
||||
if (sessions !== null && shown.length === 0 && fresh.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<p className="py-1 pl-8 text-[11px] text-fg-muted">{t("tree.noSessions")}</p>
|
||||
@@ -282,6 +299,34 @@ export function WorkspaceSessionList({
|
||||
return (
|
||||
<>
|
||||
<ul data-testid={`workspace-sessions-${lane.id}`} className="space-y-px">
|
||||
{fresh.map((r) => (
|
||||
<li key={r.id}>
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`workspace-session-new-${r.id}`}
|
||||
data-live="true"
|
||||
onClick={(e) =>
|
||||
navigate(
|
||||
`/workspace?lane=${lane.id}&run=${encodeURIComponent(r.id)}`,
|
||||
e.ctrlKey || e.metaKey ? { state: { split: true } } : undefined
|
||||
)
|
||||
}
|
||||
title={`${t("tree.newSession")}\n${t("tree.splitHint")}`}
|
||||
className={`flex w-full items-center gap-2 rounded-md py-1 pl-7 pr-2 text-left text-[12px] ${
|
||||
r.id === selectedRunId
|
||||
? "bg-accent/10 text-fg-primary"
|
||||
: "text-fg-secondary hover:bg-surface-3 hover:text-fg-primary"
|
||||
}`}
|
||||
>
|
||||
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-status-success" />
|
||||
<span className="truncate italic">{t("tree.newSession")}</span>
|
||||
<AiToolIcon
|
||||
tool={{ id: r.tool, label: tools.label(r.tool) }}
|
||||
className="ml-auto h-3.5 w-3.5 shrink-0 !p-0 opacity-70"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{shown.map((s) => {
|
||||
const run = runFor(s);
|
||||
const selected = run ? run.id === selectedRunId : s.id === selectedSessionId;
|
||||
|
||||
Reference in New Issue
Block a user