diff --git a/src/components/ui/task-list/TaskList.test.tsx b/src/components/ui/task-list/TaskList.test.tsx
index 91531d4f7..c66ddf9da 100644
--- a/src/components/ui/task-list/TaskList.test.tsx
+++ b/src/components/ui/task-list/TaskList.test.tsx
@@ -55,6 +55,29 @@ describe("TaskList", () => {
expect(frame).toContain("│ three");
});
+ test("keeps a long title on one line without losing its glyph", () => {
+ const instance = render(<>>);
+ Object.defineProperty(instance.stdout, "columns", { configurable: true, value: 40 });
+ // Left to wrap, the title flex-shrinks the glyph column away entirely.
+ instance.rerender(
+ ,
+ );
+
+ const lines = (instance.lastFrame() ?? "").split("\n").filter((line) => line.trim());
+ expect(lines).toHaveLength(1);
+ expect(lines[0]).toStartWith("✓ Updating project spec file");
+ expect(lines[0]).toContain("…");
+ instance.unmount();
+ });
+
test("truncates tail lines to the terminal width", () => {
const instance = render(<>>);
Object.defineProperty(instance.stdout, "columns", { configurable: true, value: 24 });
diff --git a/src/components/ui/task-list/TaskList.tsx b/src/components/ui/task-list/TaskList.tsx
index db956c594..946d7fcda 100644
--- a/src/components/ui/task-list/TaskList.tsx
+++ b/src/components/ui/task-list/TaskList.tsx
@@ -40,19 +40,23 @@ export const TaskList: React.FC = ({
// `||`, not `??`: a pty can report 0 columns, which would truncate every
// tail line to nothing. Match Ink's own layout fallback of 80.
const columns = stdout?.columns || 80;
+ // One line per step: a title left to wrap flex-shrinks the 1-char glyph
+ // column to nothing, so the ✓/✕/spinner disappears. The glyph and its
+ // trailing space take 2 of the row's columns.
+ const title = (task: Task) => cliTruncate(task.title, Math.max(columns - 2, 3));
return (
{tasks.map((task, index) => (
{task.state === "running" ? (
-
+
) : (
{task.state === "done" ? "✓" : "✕"}
- {task.title}
+ {title(task)}
)}
{task.state !== "done" &&
diff --git a/src/core/project/manager.test.ts b/src/core/project/manager.test.ts
index 674b84be7..bb785d22f 100644
--- a/src/core/project/manager.test.ts
+++ b/src/core/project/manager.test.ts
@@ -279,6 +279,29 @@ describe("FsProjectManager.create", () => {
]);
});
+ test("streams subprocess output as line-buffered output events", async () => {
+ await inTempDirectory();
+ const subject = new FsProjectManager({
+ logger: createSilentLogger(),
+ // The chunk boundary splits a line, so a chunk-per-event bridge would
+ // leak the fragments "vulnerabilit" / "ies".
+ runner: async (_command, { onOutput }) => {
+ onOutput?.("added 12 packages\nfound 0 vulnerabilit");
+ onOutput?.("ies\n");
+ },
+ checkTool: async () => {},
+ identity: new TestIdentityClient(),
+ });
+
+ const { events } = await runCreate(subject, {
+ name: "example",
+ scaffoldRuntimeInput: AGENT_PYTHON,
+ });
+
+ expect(events).toContainEqual({ type: "output", line: "added 12 packages" });
+ expect(events).toContainEqual({ type: "output", line: "found 0 vulnerabilities" });
+ });
+
test("skipInstall skips npm install and uv sync", async () => {
const directory = await inTempDirectory();
const { manager: subject, commands } = manager();
diff --git a/src/handlers/project/create/index.ts b/src/handlers/project/create/index.ts
index 4b23a9bca..5f36f73c2 100644
--- a/src/handlers/project/create/index.ts
+++ b/src/handlers/project/create/index.ts
@@ -25,7 +25,8 @@ import {
type HarnessModelProvider,
} from "../../../projectSchemas/harness";
import { InputValidationError } from "../../../errors";
-import { parseJsonFlag } from "../../utils";
+import { JsonRendererKey } from "../../../tui";
+import { parseJsonFlag, renderJsonError } from "../../utils";
import { DEFAULT_HARNESS_MODEL } from "../add/harness";
import type { CoreBedrockAgentImporter } from "../../../core/project/bedrockAgentImport";
import { importScaffoldRuntimeInput, resolveImportBedrockAgentInput } from "../importBedrockAgent";
@@ -196,6 +197,7 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) =
flag("skip-git", "skip initializing a git repository", z.boolean().default(false)),
],
handle: async (ctx, flags) => {
+ const jsonOutput = ctx.require(JsonKey);
const name = flags["name"];
if (name === undefined) {
throw new InputValidationError("required option '--name ' not specified");
@@ -305,13 +307,20 @@ export const createCreateProjectHandler = (config: CreateProjectHandlerConfig) =
// Same driver as build and deploy: a live step list in a TTY, and the previous plain
// line-per-step output when stderr is not a TTY or --json wants no ANSI on it.
- await runWithProgress(config.projectManager.create(createInput), {
- io: config.io,
- interactive: ctx.require(JsonKey) ? false : undefined,
- });
+ try {
+ await runWithProgress(config.projectManager.create(createInput), {
+ io: config.io,
+ interactive: jsonOutput ? false : undefined,
+ });
+ } catch (error) {
+ if (jsonOutput) renderJsonError(ctx, error);
+ throw error;
+ }
- config.io.stderr.write(`Created project '${name}' in ./${name}\n`);
+ const message = `Created project '${name}' in ./${name}`;
+ config.io.stderr.write(`${message}\n`);
config.io.stderr.write(`To deploy it: cd ${name} && agentcore project deploy\n`);
+ if (jsonOutput) ctx.require(JsonRendererKey).renderJson({ message });
},
});
diff --git a/src/handlers/project/project.test.ts b/src/handlers/project/project.test.ts
index 9a1a1ee61..c7c136514 100644
--- a/src/handlers/project/project.test.ts
+++ b/src/handlers/project/project.test.ts
@@ -387,6 +387,40 @@ describe("project create", () => {
expect(io.stderr()).toContain("Syncing Python dependencies with uv");
expect(io.stderr()).toContain("Initializing git repository");
expect(io.stderr()).toContain("Created project 'MyAgent' in ./MyAgent");
+ expect(io.stdout()).toBe("");
+ });
+
+ test("renders the success message as JSON with --json", async () => {
+ await inTempDirectory();
+ const { io } = await run([
+ "create",
+ "--name",
+ "MyAgent",
+ "--skip-install",
+ "--skip-git",
+ "--json",
+ ]);
+
+ expect(JSON.parse(io.stdout())).toEqual({ message: "Created project 'MyAgent' in ./MyAgent" });
+ });
+
+ test("renders a create failure as JSON without changing the thrown error", async () => {
+ await inTempDirectory();
+ await run(["create", "--name", "MyAgent", "--skip-install", "--skip-git"]);
+
+ // The run helper builds io after routing, so drive the failing route with
+ // its own io to read what the command wrote before rejecting.
+ const io = testIO();
+ const root = createRootHandler(new TestCoreClient(), {
+ io: io.io,
+ globalConfigAccessor: new TestGlobalConfigAccessor(),
+ logger: createSilentLogger(),
+ });
+ const args = ["create", "--name", "MyAgent", "--skip-install", "--skip-git", "--json"];
+
+ await expect(root.route(["node", "agentcore", "project", ...args])).rejects.toThrow(/MyAgent/);
+
+ expect(JSON.parse(io.stdout())).toEqual({ error: expect.stringContaining("MyAgent") });
});
test("--skip-install and --skip-git run no commands", async () => {