Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/components/ui/spinner/Spinner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ export const Spinner: React.FC<SpinnerProps> = ({

return (
<Box>
<Text color={theme.colors.primary}>{frames[frame]}</Text>
{/* Keep the frame column when the label is wider than the row. */}
<Box flexShrink={0}>
<Text color={theme.colors.primary}>{frames[frame]}</Text>
</Box>
{label ? <Text color={theme.colors.text}> {label}</Text> : null}
</Box>
);
Expand Down
21 changes: 21 additions & 0 deletions src/components/ui/task-list/TaskList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,25 @@ describe("TaskList", () => {
expect(tailLine).toContain("…");
instance.unmount();
});

// A title with no break opportunity (a Windows temp path) is wider than the
// row; Ink's default shrink used to give the glyph column to the title.
test("keeps the glyph and spinner when an unbreakable title exceeds the width", () => {
const title = "Reading 'C:\\Users\\Admin\\AppData\\Local\\Temp\\agentcore-x\\agentcore.json'";
const instance = render(<></>);
Object.defineProperty(instance.stdout, "columns", { configurable: true, value: 40 });
instance.rerender(
<TaskList
tasks={[
{ title, state: "done", tail: [] },
{ title, state: "running", tail: [] },
]}
/>,
);

const lines = (instance.lastFrame() ?? "").split("\n");
expect(lines[0]).toMatch(/^✓ Reading/);
expect(lines.some((line) => /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] Reading/.test(line))).toBe(true);
instance.unmount();
});
});
9 changes: 6 additions & 3 deletions src/components/ui/task-list/TaskList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ export const TaskList: React.FC<TaskListProps> = ({
<Spinner label={task.title} theme={theme} />
) : (
<Box>
<Text color={task.state === "done" ? theme.colors.success : theme.colors.error}>
{task.state === "done" ? glyphs.done : glyphs.failed}
</Text>
{/* Keep the glyph column when the title is wider than the row. */}
<Box flexShrink={0}>
<Text color={task.state === "done" ? theme.colors.success : theme.colors.error}>
{task.state === "done" ? glyphs.done : glyphs.failed}
</Text>
</Box>
<Text color={theme.colors.text}> {task.title}</Text>
</Box>
)}
Expand Down
40 changes: 38 additions & 2 deletions src/handlers/project/add/memory/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,17 @@ afterEach(async () => {
);
});

async function run(args: string[], opts?: { core?: TestCoreClient }) {
const io = testIO();
// Ink writes cursor/erase sequences around each frame; the TTY assertions
// below care about frame text, not terminal control. Built without a
// control-char literal so lint stays quiet.
const ANSI_SEQUENCE = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-9;?]*[A-Za-z]`, "g");

function stripAnsi(text: string): string {
return text.replace(ANSI_SEQUENCE, "");
}

async function run(args: string[], opts?: { core?: TestCoreClient; isTTY?: boolean }) {
const io = testIO({ isTTY: opts?.isTTY });
const core = opts?.core ?? new TestCoreClient();
const root = createRootHandler(core, {
io: io.io,
Expand Down Expand Up @@ -66,6 +75,33 @@ describe("project add memory", () => {
expect(io.stderr()).not.toContain("added memory");
});

// The same progress driver create, build, and deploy use: a TTY gets the live
// step list with every step marked done, and the success line follows it.
test("renders a live step list on a TTY", async () => {
await inProject();
const { io } = await run(["add", "memory", "--name", "customer_memory"], { isTTY: true });

const frames = stripAnsi(io.stderr());
expect(frames).toContain("✓ Reading project spec file");
expect(frames).toContain("✓ Updating project spec file");
expect(frames).toContain("added memory 'customer_memory' to 'TestProject'");
expect(io.stdout()).toBe("");
});

test("--json on a TTY keeps the plain step lines so no ANSI reaches stderr", async () => {
await inProject();
const { io } = await run(["add", "memory", "--name", "customer_memory", "--json"], {
isTTY: true,
});

expect(io.stderr()).not.toContain(String.fromCharCode(0x1b));
expect(io.stderr()).toContain("Reading project spec file");
expect(JSON.parse(io.stdout())).toMatchObject({
operation: "add",
resource: { type: "memory", name: "customer_memory" },
});
});

/** Verify the flag -> agentcore.json memories[] entry for each flag. */
test.each<[string, string[], Record<string, unknown>]>([
[
Expand Down
8 changes: 5 additions & 3 deletions src/handlers/project/add/shared.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Context } from "../../../router";
import { runWithProgress } from "../../../tui/progress";
import { JsonKey } from "../../keys";
import { renderResult } from "../../utils";
import {
projectMutationResource,
Expand All @@ -23,11 +24,12 @@ export async function addProjectResource(
humanSuccessMessage: string,
options: AddProjectResourceResultOptions = {},
): Promise<Project> {
// Same driver as create, build, and deploy: a live step list in a TTY, and
// plain line-per-step output when stderr is not a TTY or --json wants no ANSI
// on it.
const updatedProject = await runWithProgress(config.projectManager.addResource(project, input), {
io: config.io,
// Project add commands historically print plain progress lines even on a
// TTY. Keep that behavior while still collecting the generator result.
interactive: false,
interactive: ctx.require(JsonKey) ? false : undefined,
});

renderResult<ProjectMutationResult>(
Expand Down
Loading