feat(workspace): the console tab bar is the session view's only header

The status/name/back-arrow row is gone; Tools sits at the tab bar's right
end and the pipeline/feature pickers show only with a template.
LaneConsolePane is reduced to the terminal (no title, no Active runs, no
New run): RunHistory.tsx, api.run.history and the page's active-sessions
poll are removed.
This commit is contained in:
2026-10-03 12:13:11 +07:00
parent 4df944c6aa
commit b7efba8747
17 changed files with 177 additions and 1941 deletions
+29 -20
View File
@@ -7,11 +7,13 @@
* (`POST /api/lanes/:id/start {shell: true}`) or an installed, enabled AI
* tool started bare on its default model, no prompt (a prompt, router or
* worktree is still the workspace home's prompt box) — plus a link to
* Settings → Agents. Four tabs is the server's limit (`MAX_PANE_SLOT`).
* Settings → Agents. Four tabs is the server's limit (`MAX_PANE_SLOT`). The
* bar is the console's only header; `actions` (the workspace's Tools menu)
* sits at its right end.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { Plus, Settings2, SquareTerminal, X } from "lucide-react";
@@ -28,6 +30,7 @@ export function ConsoleTabs({
activeRunId,
onOpen,
onClosed,
actions,
}: {
lane: Lane;
/** The workspace's live runs, in slot order. */
@@ -36,6 +39,8 @@ export function ConsoleTabs({
onOpen: (runId: string) => void;
/** A tab's session was stopped. */
onClosed: (runId: string) => void;
/** Right end of the bar (the workspace's Tools menu). */
actions?: ReactNode;
}) {
const { t } = useTranslation("lanes");
const navigate = useNavigate();
@@ -91,22 +96,23 @@ export function ConsoleTabs({
};
const item =
"flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-fg-secondary hover:bg-surface-4 hover:text-fg-primary";
"flex w-full items-center gap-2.5 rounded-md px-2.5 py-1.5 text-left text-sm text-fg-secondary hover:bg-surface-4 hover:text-fg-primary";
return (
<div className="flex shrink-0 items-center gap-1 border-b border-border">
<div role="tablist" aria-label={t("tabs.label")} className="flex min-w-0 items-end gap-0.5">
<div className="flex h-10 shrink-0 items-stretch rounded-t-lg border border-b-0 border-border bg-surface-2">
<div role="tablist" aria-label={t("tabs.label")} className="flex min-w-0 items-stretch">
{tabs.map((r) => {
const active = r.id === activeRunId;
return (
<div
key={r.id}
className={`group flex min-w-0 max-w-[14rem] items-center rounded-t-md border border-b-0 ${
className={`group relative flex min-w-0 max-w-[15rem] items-center border-r border-border ${
active
? "border-border bg-surface-2 text-fg-primary"
: "border-transparent text-fg-muted hover:bg-surface-2/60 hover:text-fg-secondary"
? "bg-surface-0 text-fg-primary"
: "text-fg-muted hover:bg-surface-3 hover:text-fg-secondary"
}`}
>
{active && <span className="absolute inset-x-0 top-0 h-0.5 bg-accent" />}
<button
type="button"
role="tab"
@@ -114,12 +120,12 @@ export function ConsoleTabs({
data-testid={`console-tab-${r.slot ?? 0}`}
onClick={() => onOpen(r.id)}
title={[r.model, r.promptPreview].filter(Boolean).join(" · ") || undefined}
className="flex min-w-0 items-center gap-1.5 py-1.5 pl-2.5 pr-1 text-xs"
className="flex h-full min-w-0 items-center gap-2 pl-3 pr-1.5 text-sm"
>
{r.tool === "shell" ? (
<SquareTerminal className="h-3.5 w-3.5 shrink-0" />
<SquareTerminal className="h-4 w-4 shrink-0" />
) : (
<AiToolIcon tool={{ id: r.tool, label: label(r) }} className="h-3.5 w-3.5 !p-0" />
<AiToolIcon tool={{ id: r.tool, label: label(r) }} className="h-4 w-4 !p-0" />
)}
<span className="truncate">{label(r)}</span>
</button>
@@ -129,15 +135,17 @@ export function ConsoleTabs({
onClick={() => void close(r.id)}
aria-label={t("tabs.close")}
title={t("tabs.close")}
className="mr-1 shrink-0 rounded p-0.5 opacity-60 hover:bg-surface-4 hover:text-status-danger hover:opacity-100"
className={`mr-2 shrink-0 rounded p-0.5 hover:bg-surface-4 hover:text-status-danger ${
active ? "" : "opacity-0 group-hover:opacity-100 focus:opacity-100"
}`}
>
<X className="h-3 w-3" />
<X className="h-3.5 w-3.5" />
</button>
</div>
);
})}
</div>
<div ref={menuRef} className="relative">
<div ref={menuRef} className="relative flex items-center px-1.5">
<button
type="button"
data-testid="console-tab-new"
@@ -147,7 +155,7 @@ export function ConsoleTabs({
aria-expanded={menuOpen}
aria-label={t("tabs.new")}
title={slot === null ? t("tabs.full") : t("tabs.new")}
className="rounded p-1 text-fg-muted hover:bg-surface-4 hover:text-fg-primary disabled:opacity-40 disabled:hover:bg-transparent"
className="rounded-md p-1.5 text-fg-muted hover:bg-surface-4 hover:text-fg-primary disabled:opacity-40 disabled:hover:bg-transparent"
>
<Plus className="h-4 w-4" />
</button>
@@ -155,7 +163,7 @@ export function ConsoleTabs({
<div
role="menu"
data-testid="console-tab-menu"
className="absolute left-0 top-full z-50 mt-1 w-56 rounded-lg border border-border bg-surface-2 p-1 shadow-lg"
className="absolute left-0 top-full z-50 mt-1 w-64 rounded-xl border border-border bg-surface-2 p-1.5 shadow-lg"
>
<button
type="button"
@@ -166,7 +174,7 @@ export function ConsoleTabs({
<SquareTerminal className="h-4 w-4" />
{t("tabs.newTerminal")}
</button>
<div className="my-1 border-t border-border" />
<div className="my-1.5 border-t border-border" />
{toolIds.map((id) => {
const model = aiTools.byId(id)?.defaultModel;
return (
@@ -191,11 +199,11 @@ export function ConsoleTabs({
</button>
);
})}
<div className="my-1 border-t border-border" />
<div className="my-1.5 border-t border-border" />
<button
type="button"
role="menuitem"
className={item}
className={`${item} !text-fg-muted`}
onClick={() => navigate("/settings/agents")}
>
<Settings2 className="h-4 w-4" />
@@ -205,10 +213,11 @@ export function ConsoleTabs({
)}
</div>
{error && (
<span role="alert" className="ml-2 min-w-0 truncate text-xs text-status-danger">
<span role="alert" className="ml-2 min-w-0 self-center truncate text-xs text-status-danger">
{error}
</span>
)}
{actions && <div className="ml-auto flex items-center pr-2">{actions}</div>}
</div>
);
}