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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user