mirror of
https://github.com/vercel/eve.git
synced 2026-09-20 05:35:39 +08:00
refactor(eve): simplify dev TUI header (#3526)
Signed-off-by: Colton Padden <colton.padden@vercel.com>
This commit is contained in:
@@ -4,7 +4,7 @@ import type { AgentInfoResult } from "#client/index.js";
|
||||
import { stripAnsi } from "#cli/ui/terminal-text.js";
|
||||
import { createTestAgentInfoResult } from "#internal/testing/agent-info-fixture.js";
|
||||
|
||||
import { AGENT_HEADER_TIPS, buildAgentHeader, pickAgentHeaderTip } from "./agent-header.js";
|
||||
import { buildAgentHeader } from "./agent-header.js";
|
||||
import { createTheme } from "./theme.js";
|
||||
|
||||
const INFO = createTestAgentInfoResult({
|
||||
@@ -23,11 +23,13 @@ describe("buildAgentHeader", () => {
|
||||
const titleIndex = plain.findIndex((line) => line.includes("Weather Agent"));
|
||||
|
||||
expect(plain).toHaveLength(1);
|
||||
expect(plain[titleIndex]).toMatch(/^eve v\d+\.\d+\.\d+ +Weather Agent$/u);
|
||||
expect(plain[titleIndex]).toMatch(
|
||||
/^☰eve v\d+\.\d+\.\d+ · Weather Agent · Run \/help for commands$/u,
|
||||
);
|
||||
expect(card).not.toContain("model");
|
||||
expect(card).not.toContain("instructions");
|
||||
expect(card).not.toContain("⣿");
|
||||
expect(lines[0]).toContain(theme.colors.bold("eve"));
|
||||
expect(lines[0]).toContain(theme.colors.bold("☰eve"));
|
||||
});
|
||||
|
||||
it("renders only known fields before agent inspection", () => {
|
||||
@@ -35,27 +37,21 @@ describe("buildAgentHeader", () => {
|
||||
const card = buildAgentHeader({
|
||||
name: "weather-agent",
|
||||
theme,
|
||||
tip: "Use the /help command to see every command.",
|
||||
width: 120,
|
||||
}).join("\n");
|
||||
|
||||
expect(card).toContain("weather-agent");
|
||||
expect(card).toContain("Use the /help command to see every command.");
|
||||
});
|
||||
|
||||
it("renders the /add tip with a blue command", () => {
|
||||
it("uses ASCII separators and wordmark when Unicode is disabled", () => {
|
||||
const theme = createTheme({ color: true, unicode: false });
|
||||
const tip = AGENT_HEADER_TIPS.find((candidate) => candidate.includes("/add"));
|
||||
const lines = buildAgentHeader({ info: INFO, theme, width: 120 });
|
||||
|
||||
expect(tip).toBe("/add to extend your agent · /help for commands");
|
||||
if (tip === undefined) return;
|
||||
|
||||
const line = buildAgentHeader({ info: INFO, theme, width: 120, tip }).find((candidate) =>
|
||||
candidate.includes("/add"),
|
||||
expect(stripAnsi(lines[0] ?? "")).toMatch(
|
||||
/^eve v\d+\.\d+\.\d+ - Weather Agent - Run \/help for commands$/u,
|
||||
);
|
||||
|
||||
expect(stripAnsi(line ?? "")).toContain(tip);
|
||||
expect(line).toContain(theme.colors.blue("/add"));
|
||||
expect(lines[0]).toContain(theme.colors.dim("Weather Agent"));
|
||||
expect(lines[0]).toContain(theme.colors.dim("Run /help for commands"));
|
||||
});
|
||||
|
||||
it("keeps the discovery-diagnostics line when the compiler reported problems", () => {
|
||||
@@ -70,10 +66,3 @@ describe("buildAgentHeader", () => {
|
||||
expect(lines.some((line) => line.includes("2 warnings"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pickAgentHeaderTip", () => {
|
||||
it("maps the random draw across the whole pool", () => {
|
||||
expect(pickAgentHeaderTip(() => 0)).toBe(AGENT_HEADER_TIPS[0]);
|
||||
expect(pickAgentHeaderTip(() => 0.999)).toBe(AGENT_HEADER_TIPS.at(-1));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import type { AgentInfoResult } from "#client/index.js";
|
||||
import { resolveInstalledPackageInfo } from "#internal/application/package.js";
|
||||
import { clipVisible } from "#cli/ui/terminal-text.js";
|
||||
import { isPromptControlCommand } from "./prompt-commands.js";
|
||||
import type { Theme } from "./theme.js";
|
||||
|
||||
export interface AgentHeaderInput {
|
||||
@@ -14,37 +13,22 @@ export interface AgentHeaderInput {
|
||||
theme: Theme;
|
||||
/** Available terminal width. */
|
||||
width: number;
|
||||
/** Message-of-the-day line rendered below the startup card, when present. */
|
||||
tip?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The header's message-of-the-day pool. All entries reference local-only
|
||||
* slash commands, so callers only attach a tip to local `eve dev` sessions.
|
||||
*/
|
||||
export const AGENT_HEADER_TIPS: readonly string[] = [
|
||||
"/add to extend your agent · /help for commands",
|
||||
];
|
||||
|
||||
/** Picks one tip; `random` is a test seam over Math.random. */
|
||||
export function pickAgentHeaderTip(random: () => number = Math.random): string {
|
||||
const index = Math.min(
|
||||
AGENT_HEADER_TIPS.length - 1,
|
||||
Math.floor(random() * AGENT_HEADER_TIPS.length),
|
||||
);
|
||||
return AGENT_HEADER_TIPS[index]!;
|
||||
}
|
||||
|
||||
/** Returns the styled rows of the startup card and optional tip. */
|
||||
/** Returns the styled rows of the startup card. */
|
||||
export function buildAgentHeader(input: AgentHeaderInput): string[] {
|
||||
const { theme, info, width } = input;
|
||||
const c = theme.colors;
|
||||
const version = resolveInstalledPackageInfo().version;
|
||||
const available = Math.max(0, width - 1);
|
||||
const agentName = info?.agent.name ?? input.name;
|
||||
const title = `${c.bold("eve")} ${c.dim(`v${version}`)}${agentName ? ` ${agentName}` : ""}`;
|
||||
const metadata = [
|
||||
...(agentName === undefined ? [] : [c.dim(agentName)]),
|
||||
c.dim("Run /help for commands"),
|
||||
].join(c.dim(` ${theme.glyph.dot} `));
|
||||
const wordmark = theme.unicode ? "☰eve" : "eve";
|
||||
const title = c.bold(wordmark) + c.dim(` v${version} ${theme.glyph.dot} `) + metadata;
|
||||
const lines = [clipVisible(title, available)];
|
||||
if (input.tip) lines.push(renderTip(input.tip, available, theme));
|
||||
|
||||
if (info && (info.diagnostics.discoveryErrors > 0 || info.diagnostics.discoveryWarnings > 0)) {
|
||||
const parts: string[] = [];
|
||||
@@ -70,18 +54,6 @@ export function buildAgentHeader(input: AgentHeaderInput): string[] {
|
||||
return lines;
|
||||
}
|
||||
|
||||
function renderTip(tip: string, width: number, theme: Theme): string {
|
||||
return clipVisible(
|
||||
tip
|
||||
.split(/(\/[a-z:-]+)/u)
|
||||
.map((part) =>
|
||||
isPromptControlCommand(part) ? theme.colors.blue(part) : theme.colors.dim(part),
|
||||
)
|
||||
.join(""),
|
||||
width,
|
||||
);
|
||||
}
|
||||
|
||||
function plural(count: number): string {
|
||||
return count === 1 ? "" : "s";
|
||||
}
|
||||
|
||||
@@ -2068,7 +2068,6 @@ describe("EveTUIRunner initial input", () => {
|
||||
vi.spyOn(client, "info").mockReturnValue(info.promise);
|
||||
const startup = {
|
||||
finish: vi.fn(() => ({ draft: "typed while loading", queuedPrompt: undefined })),
|
||||
headerTip: "Use the /help command to see every command.",
|
||||
};
|
||||
const renderer = fakeRenderer();
|
||||
const runner = new EveTUIRunner({
|
||||
@@ -2096,7 +2095,6 @@ describe("EveTUIRunner initial input", () => {
|
||||
async (result) => {
|
||||
const login = createDeferred<void>();
|
||||
const startup = {
|
||||
headerTip: "/help",
|
||||
finish: vi.fn(() => ({ draft: "still editing", queuedPrompt: "Hello Alice" })),
|
||||
};
|
||||
const handle = vi.fn(async () => {
|
||||
@@ -2137,7 +2135,6 @@ describe("EveTUIRunner initial input", () => {
|
||||
draft: "still editing",
|
||||
queuedPrompt: "first message\n\nsecond message",
|
||||
})),
|
||||
headerTip: "Use the /help command to see every command.",
|
||||
};
|
||||
const renderer = fakeRenderer();
|
||||
const runner = new EveTUIRunner({
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
localFailureHint,
|
||||
} from "./errors.js";
|
||||
|
||||
import { pickAgentHeaderTip } from "./agent-header.js";
|
||||
import { probeAgentInfo } from "#services/dev-client/agent-info-probe.js";
|
||||
import { parseLogDisplayMode } from "./log-display-mode.js";
|
||||
import {
|
||||
@@ -253,8 +252,6 @@ export type AgentTUIAgentHeader = {
|
||||
name: string;
|
||||
serverUrl: string;
|
||||
info?: AgentInfoResult;
|
||||
/** Message-of-the-day line shown below the startup card (local sessions only). */
|
||||
tip?: string;
|
||||
};
|
||||
|
||||
export type AgentTUIRenderer = {
|
||||
@@ -428,7 +425,6 @@ export interface PromptCommandHandler {
|
||||
}
|
||||
|
||||
type TuiStartup = {
|
||||
readonly headerTip: string;
|
||||
finish(): { draft: string; queuedPrompt: string | undefined };
|
||||
};
|
||||
|
||||
@@ -567,12 +563,6 @@ export class EveTUIRunner {
|
||||
*/
|
||||
readonly #vercelStatus?: VercelStatusTracker;
|
||||
readonly #mcpConnectionStatus?: McpConnectionStatusTracker;
|
||||
/**
|
||||
* The header's message-of-the-day, picked once so dev HMR header
|
||||
* refreshes don't re-roll it mid-session. Local sessions only — every
|
||||
* tip references local-only slash commands.
|
||||
*/
|
||||
readonly #headerTip: string;
|
||||
#agentInfo?: AgentInfoResult;
|
||||
/**
|
||||
* approval-id → input-request map populated as `input.requested` events
|
||||
@@ -640,7 +630,6 @@ export class EveTUIRunner {
|
||||
}
|
||||
this.#subagentPump = new SubagentPump(pumpOptions);
|
||||
this.#name = options.name ?? "eve";
|
||||
this.#headerTip = options.startup?.headerTip ?? pickAgentHeaderTip();
|
||||
this.#withExclusiveTerminal = options.withExclusiveTerminal;
|
||||
this.#tools = options.tools ?? "full";
|
||||
this.#reasoning = options.reasoning ?? "full";
|
||||
@@ -756,7 +745,6 @@ export class EveTUIRunner {
|
||||
serverUrl,
|
||||
};
|
||||
if (headerInfo !== undefined) header.info = headerInfo;
|
||||
if (this.#appRoot !== undefined && !this.#onboard) header.tip = this.#headerTip;
|
||||
this.#renderer.renderAgentHeader?.(header);
|
||||
return headerInfo;
|
||||
}
|
||||
|
||||
@@ -203,13 +203,11 @@ describe("TerminalRenderer (inline scrollback)", () => {
|
||||
connected: true,
|
||||
credential: "api-key",
|
||||
}),
|
||||
tip: "Use the /deploy command to deploy your agent.",
|
||||
});
|
||||
renderer.shutdown();
|
||||
|
||||
const snapshot = screen.snapshot();
|
||||
expect(snapshot).toMatch(/eve v\d+\.\d+\.\d+.*Weather Agent/u);
|
||||
expect(snapshot).toContain("Use the /deploy command to deploy your agent.");
|
||||
expect(snapshot).toMatch(/☰eve v\d+\.\d+\.\d+ · Weather Agent · Run \/help for commands/u);
|
||||
expect(snapshot).not.toContain("http://localhost:3000");
|
||||
});
|
||||
|
||||
@@ -238,7 +236,7 @@ describe("TerminalRenderer (inline scrollback)", () => {
|
||||
const snapshot = screen.snapshot();
|
||||
expect(snapshot).toContain("new-model");
|
||||
expect(snapshot).not.toContain("old-model");
|
||||
expect(snapshot.match(/eve v\d/gu)).toHaveLength(1);
|
||||
expect(snapshot.match(/☰eve v\d/gu)).toHaveLength(1);
|
||||
expect(snapshot).toContain("hello");
|
||||
expect(snapshot).toContain("still here");
|
||||
renderer.shutdown();
|
||||
@@ -2613,11 +2611,10 @@ describe("TerminalRenderer (inline scrollback)", () => {
|
||||
|
||||
startupRenderer.beginStartupDraft({
|
||||
initialDraft: "weather",
|
||||
tip: "Use the /help command to see every command.",
|
||||
title: "weather-agent",
|
||||
});
|
||||
expect(screen.snapshot()).toContain("weather-agent");
|
||||
expect(screen.snapshot()).toContain("Use the /help command");
|
||||
expect(screen.snapshot()).toContain("Run /help for commands");
|
||||
expect(screen.snapshot()).not.toContain("model");
|
||||
expect(screen.snapshot()).not.toContain("loading");
|
||||
expect(screen.snapshot()).toContain("Starting agent");
|
||||
@@ -2654,7 +2651,7 @@ describe("TerminalRenderer (inline scrollback)", () => {
|
||||
"defers startup warnings until connection readiness at %i columns",
|
||||
(columns) => {
|
||||
const { renderer, screen } = makeRenderer(columns);
|
||||
renderer.beginStartupDraft({ initialDraft: "Hello Alice", tip: "/help", title: "Agent" });
|
||||
renderer.beginStartupDraft({ initialDraft: "Hello Alice", title: "Agent" });
|
||||
renderer.renderSetupWarning("Model disconnected · /login");
|
||||
expect(screen.snapshot()).not.toContain("Model disconnected");
|
||||
renderer.setStartupPhase("connecting");
|
||||
@@ -2674,7 +2671,7 @@ describe("TerminalRenderer (inline scrollback)", () => {
|
||||
"keeps startup editable across connection work and questions at %i columns",
|
||||
async (columns) => {
|
||||
const { renderer, screen, input } = makeRenderer(columns);
|
||||
renderer.beginStartupDraft({ initialDraft: "Hello", tip: "/help", title: "Agent" });
|
||||
renderer.beginStartupDraft({ initialDraft: "Hello", title: "Agent" });
|
||||
const composerRow = screen
|
||||
.snapshot()
|
||||
.split("\n")
|
||||
@@ -2730,7 +2727,7 @@ describe("TerminalRenderer (inline scrollback)", () => {
|
||||
|
||||
it("restores the startup draft after a masked key question is cancelled", async () => {
|
||||
const { renderer, input, screen } = makeRenderer();
|
||||
renderer.beginStartupDraft({ initialDraft: "My message", tip: "/help", title: "Agent" });
|
||||
renderer.beginStartupDraft({ initialDraft: "My message", title: "Agent" });
|
||||
renderer.setupFlow.begin("Connect a model", "pulse");
|
||||
const answer = renderer.setupFlow.readText({ message: "API key", mask: true });
|
||||
input.type("private-test-key");
|
||||
@@ -2756,7 +2753,6 @@ describe("TerminalRenderer (inline scrollback)", () => {
|
||||
});
|
||||
|
||||
renderer.beginStartupDraft({
|
||||
tip: "Use the /help command to see every command.",
|
||||
title: "weather-agent",
|
||||
});
|
||||
input.ctrlC();
|
||||
@@ -4086,7 +4082,7 @@ describe("TerminalRenderer (inline scrollback)", () => {
|
||||
renderer.renderAgentHeader({ name: "Weather Agent", serverUrl: "http://localhost:3000" });
|
||||
renderer.shutdown();
|
||||
|
||||
expect(countOccurrences(screen.snapshot(), "eve v")).toBe(1);
|
||||
expect(countOccurrences(screen.snapshot(), "☰eve v")).toBe(1);
|
||||
});
|
||||
|
||||
it("reset clears committed transcript rows", () => {
|
||||
|
||||
@@ -302,8 +302,6 @@ export type AgentHeaderOptions = {
|
||||
name: string;
|
||||
serverUrl: string;
|
||||
info?: AgentInfoResult;
|
||||
/** Message-of-the-day line below the startup card (local sessions only). */
|
||||
tip?: string;
|
||||
};
|
||||
|
||||
type DisplayModes = {
|
||||
@@ -414,7 +412,7 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
#startupEditor?: LineState;
|
||||
#startupConsumer?: (key: TerminalKey) => void;
|
||||
#startupStartedAt = 0;
|
||||
#startupHeader?: { readonly name: string; readonly tip: string };
|
||||
#startupHeader?: { readonly name: string };
|
||||
#agentHeaderRendered = false;
|
||||
/** The last committed header body, to skip re-committing an unchanged banner. */
|
||||
#agentHeaderBody?: string;
|
||||
@@ -695,12 +693,12 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
this.#live.flush(this.#renderAgentHeaderRows(), this.#footerRows(this.#width()));
|
||||
}
|
||||
|
||||
beginStartupDraft(options: { initialDraft?: string; tip: string; title: string }): void {
|
||||
beginStartupDraft(options: { initialDraft?: string; title: string }): void {
|
||||
this.#start({ title: options.title });
|
||||
this.#inputActive = true;
|
||||
this.#promptPlaceholderActive = true;
|
||||
this.#startupPhase = "starting";
|
||||
this.#startupHeader = { name: options.title, tip: options.tip };
|
||||
this.#startupHeader = { name: options.title };
|
||||
this.#startupStartedAt = Date.now();
|
||||
let editor = lineOf(stripPromptControlCharacters(options.initialDraft ?? ""));
|
||||
this.#startupEditor = editor;
|
||||
@@ -4237,8 +4235,6 @@ export class TerminalRenderer implements AgentTUIRenderer {
|
||||
width: this.#width(),
|
||||
};
|
||||
if (header?.info !== undefined) input.info = header.info;
|
||||
const tip = header?.tip ?? startup?.tip;
|
||||
if (tip !== undefined) input.tip = tip;
|
||||
return buildAgentHeader(input);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import { createDevDiagnostics, type DevDiagnostics } from "../diagnostics.js";
|
||||
|
||||
import { createPromptCommandHandler } from "./prompt-command-handler.js";
|
||||
import { promptCommandsFor } from "./prompt-commands.js";
|
||||
import { pickAgentHeaderTip } from "./agent-header.js";
|
||||
import { formatRemoteAuthChallengeMessage } from "./remote-auth-result.js";
|
||||
import { probeMcpConnection } from "./mcp-connection-status.js";
|
||||
import { EveTUIRunner, type EveTUIRunnerOptions } from "./runner.js";
|
||||
@@ -53,7 +52,6 @@ export interface RunDevelopmentTuiInput extends TuiDisplayOptions {
|
||||
|
||||
export interface DevelopmentTuiStartup {
|
||||
readonly diagnostics: DevDiagnostics | undefined;
|
||||
readonly headerTip: string;
|
||||
readonly renderer: TerminalRenderer;
|
||||
finish(): { draft: string; queuedPrompt: string | undefined };
|
||||
shutdown(): Promise<void>;
|
||||
@@ -67,7 +65,6 @@ export async function startDevelopmentTuiStartup(
|
||||
},
|
||||
): Promise<DevelopmentTuiStartup> {
|
||||
const diagnostics = await createDevDiagnostics(input.appRoot).catch(() => undefined);
|
||||
const headerTip = pickAgentHeaderTip();
|
||||
const renderer = new TerminalRenderer({
|
||||
...input,
|
||||
diagnostics,
|
||||
@@ -75,12 +72,10 @@ export async function startDevelopmentTuiStartup(
|
||||
});
|
||||
renderer.beginStartupDraft({
|
||||
initialDraft: input.initialInput,
|
||||
tip: headerTip,
|
||||
title: input.name ?? "eve",
|
||||
});
|
||||
return {
|
||||
diagnostics,
|
||||
headerTip,
|
||||
renderer,
|
||||
finish: () => renderer.finishStartupDraft(),
|
||||
async shutdown() {
|
||||
|
||||
Reference in New Issue
Block a user