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
24 changes: 24 additions & 0 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2401,6 +2401,30 @@ describe("adeRpcServer", () => {
expect(response.structuredContent.contextRef?.path).toBeNull();
});

it("launches default Codex spawn_agent sessions with supported sandbox flags", async () => {
const fixture = createRuntime();
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-spawn-codex-bin-"));
createFakePathExecutable(binDir, "codex");
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });

const response = await withEnv({ PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`, SHELL: "/bin/sh" }, async () => {
await initialize(handler, { role: "orchestrator" });
return await callTool(handler, "spawn_agent", {
laneId: "lane-1",
provider: "codex",
permissionMode: "default",
prompt: "Check the Codex launch flags",
});
});

expect(response?.isError).toBeUndefined();
const createCall = fixture.runtime.ptyService.create.mock.calls[0]?.[0] as { args?: string[]; startupCommand?: string };
expect(createCall.args).toEqual(expect.arrayContaining(["--sandbox", "workspace-write", "--ask-for-approval", "on-request"]));
expect(createCall.args).not.toContain("--full-auto");
expect(createCall.startupCommand).toContain("--sandbox workspace-write --ask-for-approval on-request");
expect(createCall.startupCommand).not.toContain("--full-auto");
});

it("routes start_cli_session through shared provider launch helpers", async () => {
const fixture = createRuntime();
fixture.runtime.sessionService.get.mockReturnValue({
Expand Down
4 changes: 2 additions & 2 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6905,8 +6905,8 @@ async function runTool(args: {
commandArgs.push("--dangerously-bypass-approvals-and-sandbox");
commandPreviewParts.push("--dangerously-bypass-approvals-and-sandbox");
} else if (permissionMode === "default") {
commandArgs.push("--full-auto");
commandPreviewParts.push("--full-auto");
commandArgs.push("--sandbox", "workspace-write", "--ask-for-approval", "on-request");
commandPreviewParts.push("--sandbox", "workspace-write", "--ask-for-approval", "on-request");
} else if (permissionMode === "config-toml") {
// No explicit Codex permission flags; let the host config.toml decide.
} else if (permissionMode === "plan") {
Expand Down
99 changes: 90 additions & 9 deletions apps/ade-cli/src/tuiClient/__tests__/ChatView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { render } from "ink-testing-library";
import {
ChatView,
computeChatScrollMaxOffset,
renderChatSelectableRowTexts,
renderChatTranscriptPlainText,
selectedTextFromVisibleChatRows,
selectedTextFromChatRows,
} from "../components/ChatView";
import { buildSubagentTranscriptEvents } from "../subagentPane";
import {
Expand Down Expand Up @@ -59,12 +60,39 @@ function transcriptLines(frame: string): string[] {

describe("ChatView", () => {
it("copies only the selected chat row columns", () => {
expect(selectedTextFromVisibleChatRows(
expect(selectedTextFromChatRows(
["alpha bravo", "charlie delta", "echo"],
{ startRow: 0, startColumn: 6, endRow: 1, endColumn: 6 },
)).toBe("bravo\ncharlie");
});

it("preserves selected leading and trailing whitespace", () => {
expect(selectedTextFromChatRows(
[" const value = 1; ", " return value; "],
{ startRow: 0, startColumn: 0, endRow: 1, endColumn: 19 },
)).toBe(" const value = 1; \n return value; ");
});

it("copies selected absolute transcript rows outside the visible viewport", () => {
const events = Array.from({ length: 12 }, (_, index): AgentChatEventEnvelope => ({
sessionId: "s1",
timestamp: `2026-01-01T12:00:${String(index).padStart(2, "0")}.000Z`,
sequence: index + 1,
event: { type: "text", text: `selectable row ${index + 1}` },
}));
const rows = renderChatSelectableRowTexts({
events,
notices: [],
activeSession: session,
width: 80,
});

expect(rows.join("\n")).toContain("selectable row 1");
expect(rows.join("\n")).toContain("selectable row 12");
expect(selectedTextFromChatRows(rows, { startRow: 0, startColumn: 0, endRow: rows.length - 1, endColumn: 200 }))
.toContain("selectable row 12");
});

it("renders a bordered hero card with the ADE wordmark when the chat is empty", () => {
const frame = renderEvents([]);
// Hero card uses a bordered box
Expand Down Expand Up @@ -99,7 +127,7 @@ describe("ChatView", () => {
expect(frame).not.toContain("type to chat");
});

it("shows a model working indicator while a turn is active before text arrives", () => {
it("shows an active-turn wait state before runtime events arrive", () => {
const frame = renderEvents([
{
sessionId: "s1",
Expand All @@ -116,10 +144,59 @@ describe("ChatView", () => {
], { streaming: true, width: 80 });

expect(frame).toContain("check status");
expect(frame).toContain("model working");
expect(frame).toContain("active turn · waiting for runtime events");
});

it("keeps the model working indicator visible while active text is streaming", () => {
it("shows the active-turn wait state after historical assistant output", () => {
const events: AgentChatEventEnvelope[] = [
{
sessionId: "s1",
timestamp: "2026-01-01T12:00:00.000Z",
sequence: 1,
event: { type: "user_message", text: "first turn", turnId: "turn-1" },
},
{
sessionId: "s1",
timestamp: "2026-01-01T12:00:01.000Z",
sequence: 2,
event: { type: "text", text: "first answer", turnId: "turn-1" },
},
{
sessionId: "s1",
timestamp: "2026-01-01T12:00:02.000Z",
sequence: 3,
event: { type: "done", status: "completed", turnId: "turn-1" },
},
{
sessionId: "s1",
timestamp: "2026-01-01T12:00:03.000Z",
sequence: 4,
event: { type: "user_message", text: "second turn", turnId: "turn-2" },
},
{
sessionId: "s1",
timestamp: "2026-01-01T12:00:04.000Z",
sequence: 5,
event: { type: "status", turnStatus: "started", turnId: "turn-2" },
},
];
const frame = renderEvents(events, { streaming: true, width: 80 });
const maxOffset = computeChatScrollMaxOffset({
events,
notices: [],
activeSession: session,
streaming: true,
maxRows: 3,
width: 80,
});

expect(frame).toContain("first answer");
expect(frame).toContain("second turn");
expect(frame).toContain("active turn · waiting for runtime events");
expect(maxOffset).toBeGreaterThan(0);
});

it("does not add a generic working indicator while active text is streaming", () => {
const frame = renderEvents([
{
sessionId: "s1",
Expand All @@ -130,7 +207,8 @@ describe("ChatView", () => {
], { streaming: true, width: 80 });

expect(frame).toContain("I found the issue.");
expect(frame).toContain("model working");
expect(frame).not.toContain("model working");
expect(frame).not.toContain("waiting for runtime events");
});

it("shows interrupted state where the working indicator normally appears", () => {
Expand All @@ -146,6 +224,7 @@ describe("ChatView", () => {
expect(frame).toContain("stop this");
expect(frame).toContain("Interrupted · chat to continue");
expect(frame).not.toContain("model working");
expect(frame).not.toContain("waiting for runtime events");
});

it("renders context compaction as an explicit active state", () => {
Expand All @@ -160,6 +239,8 @@ describe("ChatView", () => {

expect(frame).toContain("compacting context");
expect(frame).toContain("auto");
expect(frame).not.toContain("model working");
expect(frame).not.toContain("waiting for runtime events");
});

it("renders queued steer messages as staged instead of normal sent bubbles", () => {
Expand Down Expand Up @@ -632,9 +713,9 @@ describe("ChatView", () => {
.map((entry) => JSON.stringify(entry.event))
.join("\n");
const frame = renderEvents(transcriptEvents, { width: 100, maxRows: 40 });
expect(frame).toContain("Viewing subagent: Explore renderer");
expect(frame).toContain("Leave the agents pane to return to the main chat.");
expect(transcriptBody).toContain("Subagent started: Explore renderer");
expect(frame).toContain("Viewing agent transcript.");
expect(frame).toContain("Select Main chat in Chat Info to return.");
expect(transcriptBody).toContain("Started.");
expect(frame).toContain("read_file");
expect(frame).toContain("src/child.ts");
expect(transcriptBody).toContain("found the renderer path");
Expand Down
11 changes: 6 additions & 5 deletions apps/ade-cli/src/tuiClient/__tests__/HeaderFooter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe("FooterControls", () => {
expect(frame).toContain("full-auto");
});

it("renders the resting hint strip with lanes/info/subagents/cmds/help", () => {
it("renders the resting hint strip with lanes/pane/chat-info/cmds/help", () => {
const result = render(
<FooterControls
provider="codex"
Expand All @@ -107,9 +107,10 @@ describe("FooterControls", () => {
expect(frame).toContain("^o");
expect(frame).toContain("lanes");
expect(frame).toContain("^p");
expect(frame).toContain("info");
expect(frame).toContain("pane");
expect(frame).toContain("^a");
expect(frame).toContain("subagents");
expect(frame).toContain("chat info");
expect(frame).not.toContain("subagents");
expect(frame).toContain("cmds");
expect(frame).toContain("help");
});
Expand Down Expand Up @@ -187,7 +188,7 @@ describe("FooterControls", () => {
expect(frame).toContain("acceptEdits");
});

it("renders the subagents button when visible and counts agents", () => {
it("renders the chat info button when visible and counts agents", () => {
const result = render(
<FooterControls
provider="claude"
Expand All @@ -199,7 +200,7 @@ describe("FooterControls", () => {
);
const frame = stripAnsi(result.lastFrame() ?? "");

expect(frame).toContain("2 subagents");
expect(frame).toContain("chat info · 2");
});

it("renders the approval prompt hints when an approval is active", () => {
Expand Down
Loading
Loading