import * as fs from "node:fs/promises";
import * as path from "node:path";
import type { ToolName, ToolRequestId } from "@magenta/core";
import { expect, it } from "vitest";
import { withDriver } from "../test/preamble.ts";
import { pollUntil } from "../utils/async.ts";

it("compact flow: user initiates @compact, spawns compact thread, compacts and continues", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Build up some conversation history
    await driver.inputMagentaText("What is 2+2?");
    await driver.send();

    const request1 = await driver.mockAnthropic.awaitPendingStream({
      message: "initial request",
    });
    request1.respond({
      stopReason: "end_turn",
      text: "2+2 equals 4.",
      toolRequests: [],
    });

    await driver.inputMagentaText("What about 3+3?");
    await driver.send();

    const request2 = await driver.mockAnthropic.awaitPendingStream({
      message: "followup request",
    });
    request2.respond({
      stopReason: "end_turn",
      text: "3+3 equals 6.",
      toolRequests: [],
    });

    const originalThread = driver.magenta.chat.getActiveThread();
    const originalThreadId = originalThread.id;

    // Wait for second response to be fully processed
    await pollUntil(() => {
      if (originalThread.getMessages().length >= 4) return true;
      throw new Error("waiting for messages");
    });

    // User initiates compact with a next prompt
    await driver.inputMagentaText("@compact Now help me with multiplication");
    await driver.send();

    // The compact flow should:
    // 1. Render the thread to markdown
    // 2. Write it to a temp file
    // 3. Spawn a compact subagent thread

    // Wait for the thread to enter compacting mode
    await pollUntil(
      () => {
        if (originalThread.core.state.mode.type !== "compacting")
          throw new Error(
            `expected compacting mode but got ${originalThread.core.state.mode.type}`,
          );
      },
      { timeout: 2000, message: "thread should enter compacting mode" },
    );

    // The compact subagent should receive a stream
    const compactSubagentStream = await driver.mockAnthropic.awaitPendingStream(
      {
        message: "compact subagent stream",
      },
    );

    // Verify the compact subagent uses the fast model
    expect(compactSubagentStream.params.model).toBe("mock-fast");
    // Verify the compact subagent received the file contents in its user message
    const subagentMessages = compactSubagentStream.getProviderMessages();
    const userMsg = subagentMessages.find((m) => m.role === "user");
    expect(userMsg).toBeDefined();
    const textContent = userMsg!.content
      .filter(
        (c): c is Extract<typeof c, { type: "text" }> => c.type === "text",
      )
      .map((c) => c.text)
      .join("");
    // The subagent should see the rendered thread content
    expect(textContent).toContain("2+2 equals 4");
    expect(textContent).toContain("3+3 equals 6");
    // The subagent should see the user's next prompt for prioritizing retention
    expect(textContent).toContain("Now help me with multiplication");

    // Have the compact subagent use the EDL tool to edit /summary.md in memory
    const edlScript = `file \`/summary.md\`\nselect bof-eof\nreplace <<COMPACT_SUMMARY\n# Summary\nUser asked basic arithmetic: 2+2=4, 3+3=6\nCOMPACT_SUMMARY`;

    compactSubagentStream.respond({
      stopReason: "tool_use",
      text: "I'll compact this conversation.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: "edl_1" as ToolRequestId,
            toolName: "edl" as ToolName,
            input: { script: edlScript },
          },
        },
      ],
    });

    // EDL tool auto-executes (no permission needed for /tmp/magenta/ files)
    // After EDL completes, the compact subagent gets a continuation stream
    const afterEdlStream = await driver.mockAnthropic.awaitPendingStream({
      message: "compact subagent after EDL",
    });

    // Verify the EDL tool result was successful
    const afterEdlMessages = afterEdlStream.getProviderMessages();
    const toolResultMsg = afterEdlMessages.find(
      (m) =>
        m.role === "user" && m.content.some((c) => c.type === "tool_result"),
    );
    expect(toolResultMsg).toBeDefined();
    const toolResult = toolResultMsg!.content.find(
      (c) => c.type === "tool_result",
    );
    if (toolResult?.type === "tool_result") {
      expect(toolResult.result.status).toBe("ok");
    }

    afterEdlStream.respond({
      stopReason: "end_turn",
      text: "I have compacted the conversation.",
      toolRequests: [],
    });

    // After the compact subagent stops, the parent thread should:
    // 1. Read back the temp file as the summary
    // 2. Call agent.compact() to replace messages with the summary
    // 3. Auto-respond with the next prompt

    // Wait for the continuation stream on the parent thread
    const afterCompactStream = await driver.mockAnthropic.awaitPendingStream({
      message: "after compact continuation",
    });

    // Verify the compacted thread has reduced messages
    const afterCompactMessages = afterCompactStream.getProviderMessages();

    // After compaction, messages should be minimal:
    // The summary from the temp file + the user's next prompt
    const hasNextPrompt = afterCompactMessages.some(
      (m) =>
        m.role === "user" &&
        m.content.some(
          (c) =>
            c.type === "text" &&
            c.text.includes("Now help me with multiplication"),
        ),
    );
    expect(hasNextPrompt).toBe(true);

    // The original conversation details should be gone (replaced by summary)
    const allText = afterCompactMessages
      .flatMap((m) =>
        m.content
          .filter(
            (c): c is Extract<typeof c, { type: "text" }> => c.type === "text",
          )
          .map((c) => c.text),
      )
      .join("");

    // The EDL-edited summary content should be present in the compacted thread
    expect(allText).toContain("User asked basic arithmetic");
    // Original conversation exchanges should be gone
    expect(allText).not.toContain("What is 2+2?");
    expect(allText).not.toContain("What about 3+3?");

    // Respond to the continuation
    afterCompactStream.respond({
      stopReason: "end_turn",
      text: "Sure! What multiplication would you like help with?",
      toolRequests: [],
    });

    // We should still be on the same thread (compact doesn't create a new root thread)
    expect(driver.magenta.chat.getActiveThread().id).toBe(originalThreadId);

    await driver.assertDisplayBufferContains(
      "What multiplication would you like help with?",
    );
  });
});

it("compact flow without continuation: @compact with no next prompt", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Build up conversation history
    await driver.inputMagentaText("Hello");
    await driver.send();

    const stream1 = await driver.mockAnthropic.awaitPendingStream({
      message: "initial request",
    });
    stream1.respond({
      stopReason: "end_turn",
      text: "Hi there!",
      toolRequests: [],
    });

    const thread = driver.magenta.chat.getActiveThread();

    // User initiates compact with no next prompt
    await driver.inputMagentaText("@compact");
    await driver.send();

    // Wait for compacting mode
    await pollUntil(
      () => {
        if (thread.core.state.mode.type !== "compacting")
          throw new Error(
            `expected compacting mode but got ${thread.core.state.mode.type}`,
          );
      },
      { timeout: 2000, message: "thread should enter compacting mode" },
    );

    // Compact subagent receives its stream
    const compactStream = await driver.mockAnthropic.awaitPendingStream({
      message: "compact subagent stream",
    });

    // The compact subagent must write to /summary.md via EDL
    const edlScript = `file \`/summary.md\`\nselect bof-eof\nreplace <<COMPACT_SUMMARY\n# Summary\nGreeting conversation: user said hello, assistant responded.\nCOMPACT_SUMMARY`;

    compactStream.respond({
      stopReason: "tool_use",
      text: "I'll compact this conversation.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: "edl_1" as ToolRequestId,
            toolName: "edl" as ToolName,
            input: { script: edlScript },
          },
        },
      ],
    });

    const afterEdlStream = await driver.mockAnthropic.awaitPendingStream({
      message: "compact subagent after EDL",
    });
    afterEdlStream.respond({
      stopReason: "end_turn",
      text: "Done compacting.",
      toolRequests: [],
    });

    // Without a next prompt, the thread sends "Please continue from where you left off."
    const afterCompactStream = await driver.mockAnthropic.awaitPendingStream({
      message: "after compact continuation",
    });
    afterCompactStream.respond({
      stopReason: "end_turn",
      text: "Ready to continue!",
      toolRequests: [],
    });

    await pollUntil(
      () => {
        const agentStatus = thread.agent.getState().status;
        if (agentStatus.type !== "stopped")
          throw new Error(`expected stopped but got ${agentStatus.type}`);
        if (agentStatus.stopReason !== "end_turn")
          throw new Error(
            `expected end_turn but got ${agentStatus.stopReason}`,
          );
      },
      { timeout: 2000, message: "thread should stop after compaction" },
    );

    // Verify messages have been compacted - fresh agent with summary + continuation
    const messages = thread.getMessages();
    expect(messages.length).toBeLessThanOrEqual(4);
  });
});

it("compact flow does not process @file commands in subagent or summary", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Build up conversation history that mentions @file
    await driver.inputMagentaText("Tell me about @file:poem.txt usage");
    await driver.send();

    const request1 = await driver.mockAnthropic.awaitPendingStream({
      message: "initial request",
    });
    request1.respond({
      stopReason: "end_turn",
      text: "The @file:poem.txt command adds a file to context.",
      toolRequests: [],
    });

    const thread = driver.magenta.chat.getActiveThread();

    // Compact with a nextPrompt that contains @file:poem.txt
    await driver.inputMagentaText(
      "@compact Now read @file:poem.txt and summarize",
    );
    await driver.send();

    await pollUntil(
      () => {
        if (thread.core.state.mode.type !== "compacting")
          throw new Error(
            `expected compacting mode but got ${thread.core.state.mode.type}`,
          );
      },
      { timeout: 2000, message: "thread should enter compacting mode" },
    );

    // 1. Verify the compact subagent does NOT expand @file commands.
    //    The subagent's user message should contain the raw markdown text
    //    including literal "@file:poem.txt" strings, without extra content blocks
    //    from file expansion.
    const compactStream = await driver.mockAnthropic.awaitPendingStream({
      message: "compact subagent stream",
    });

    const subagentMessages = compactStream.getProviderMessages();
    const subagentUserMsg = subagentMessages.find((m) => m.role === "user");
    expect(subagentUserMsg).toBeDefined();

    // The compact subagent should have exactly one text content block (the instructions)
    // If @file were processed, there would be additional content blocks for file contents
    const textBlocks = subagentUserMsg!.content.filter(
      (c) => c.type === "text",
    );
    expect(textBlocks).toHaveLength(1);

    // The raw text should contain the literal @file:poem.txt from the conversation
    const subagentText = textBlocks[0].text;
    expect(subagentText).toContain("@file:poem.txt");

    // Have the compact subagent edit /summary.md with a summary that also contains @file
    const edlScript = `file \`/summary.md\`\nselect bof-eof\nreplace <<COMPACT_SUMMARY\n# Summary\nUser discussed @file:poem.txt usage. Assistant explained the command.\nCOMPACT_SUMMARY`;

    compactStream.respond({
      stopReason: "tool_use",
      text: "Compacting.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: "edl_1" as ToolRequestId,
            toolName: "edl" as ToolName,
            input: { script: edlScript },
          },
        },
      ],
    });

    const afterEdlStream = await driver.mockAnthropic.awaitPendingStream({
      message: "compact subagent after EDL",
    });
    afterEdlStream.respond({
      stopReason: "end_turn",
      text: "Done compacting.",
      toolRequests: [],
    });

    // 2. Verify that after compaction:
    //    - The summary is sent as a raw user message (no command processing)
    //    - The nextPrompt goes through sendMessage, so @file:poem.txt IS expanded
    const afterCompactStream = await driver.mockAnthropic.awaitPendingStream({
      message: "after compact continuation",
    });

    const afterCompactMessages = afterCompactStream.getProviderMessages();

    // Should have two user messages:
    // 1. The raw summary (appendUserMessage)
    // 2. The nextPrompt processed through sendMessage (@file expanded)
    const userMessages = afterCompactMessages.filter((m) => m.role === "user");
    expect(userMessages).toHaveLength(2);

    // First user message: raw summary (no command processing)
    const summaryContentTypes = userMessages[0].content.map((c) => c.type);
    expect(summaryContentTypes).toEqual(["text"]);
    const summaryText = (
      userMessages[0].content[0] as Extract<
        (typeof userMessages)[0]["content"][0],
        { type: "text" }
      >
    ).text;
    expect(summaryText).toContain("<conversation-summary>");
    expect(summaryText).toContain("@file:poem.txt");

    // Second user message: nextPrompt processed through sendMessage
    // Should have context update from @file expansion + text + system_reminder
    const promptContentTypes = userMessages[1].content.map((c) => c.type);
    expect(promptContentTypes).toContain("text");

    const promptText = userMessages[1].content
      .filter(
        (
          c,
        ): c is Extract<
          (typeof userMessages)[1]["content"][0],
          { type: "text" }
        > => c.type === "text",
      )
      .map((c) => c.text)
      .join("\n");
    expect(promptText).toContain("Now read @file:poem.txt and summarize");

    // The @file:poem.txt was processed before compaction (adding poem.txt to
    // context), but since the summary doesn't mention poem.txt, it is not
    // retained after compaction.
    const contextFiles = Object.keys(thread.contextManager.files);
    expect(contextFiles.some((f) => f.includes("poem.txt"))).toBe(false);

    afterCompactStream.respond({
      stopReason: "end_turn",
      text: "Ready to continue.",
      toolRequests: [],
    });

    await driver.assertDisplayBufferContains("Ready to continue.");
  });
});
it("forks a thread with @compact to clone and compact in one step", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Build up some conversation history
    await driver.inputMagentaText("What is 2+2?");
    await driver.send();

    const request1 = await driver.mockAnthropic.awaitPendingStream({
      message: "initial request",
    });
    request1.respond({
      stopReason: "end_turn",
      text: "2+2 equals 4.",
      toolRequests: [],
    });

    await driver.inputMagentaText("What about 3+3?");
    await driver.send();

    const request2 = await driver.mockAnthropic.awaitPendingStream({
      message: "followup request",
    });
    request2.respond({
      stopReason: "end_turn",
      text: "3+3 equals 6.",
      toolRequests: [],
    });

    const originalThreadId = driver.magenta.chat.state.activeThreadId;

    // Fork by pressing F on the most recent assistant message, then send
    // @compact + new prompt on the forked thread.
    await driver.pressOnDisplayMessage("3+3 equals 6.", "F");

    await pollUntil(() => {
      if (driver.magenta.chat.state.activeThreadId === originalThreadId) {
        throw new Error("Still on original thread");
      }
    });

    await driver.inputMagentaText("@compact Now help me with multiplication");
    await driver.send();

    // The forked thread detects @compact and spawns a compact subagent
    const compactSubagentStream = await driver.mockAnthropic.awaitPendingStream(
      {
        message: "compact subagent in forked thread",
      },
    );

    // Verify the compact subagent sees the original conversation content
    const subagentMessages = compactSubagentStream.getProviderMessages();
    const userMsg = subagentMessages.find((m) => m.role === "user");
    expect(userMsg).toBeDefined();
    const textContent = userMsg!.content
      .filter(
        (c): c is Extract<typeof c, { type: "text" }> => c.type === "text",
      )
      .map((c) => c.text)
      .join("");
    expect(textContent).toContain("2+2 equals 4");

    // Use real EDL tool to edit /summary.md in memory
    const edlScript2 = `file \`/summary.md\`\nselect bof-eof\nreplace <<COMPACT_SUMMARY\n# Summary\nArithmetic conversation: 2+2=4, 3+3=6\nCOMPACT_SUMMARY`;

    compactSubagentStream.respond({
      stopReason: "tool_use",
      text: "I'll compact this conversation.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: "edl_1" as ToolRequestId,
            toolName: "edl" as ToolName,
            input: { script: edlScript2 },
          },
        },
      ],
    });

    // EDL tool auto-executes, then compact subagent finishes
    const afterEdlStream2 = await driver.mockAnthropic.awaitPendingStream({
      message: "compact subagent after EDL in forked thread",
    });

    afterEdlStream2.respond({
      stopReason: "end_turn",
      text: "Compacted the arithmetic conversation.",
      toolRequests: [],
    });

    // After compact completes, the forked thread should continue with the next prompt
    const afterCompactStream = await driver.mockAnthropic.awaitPendingStream({
      message: "after compact in forked thread",
    });

    afterCompactStream.respond({
      stopReason: "end_turn",
      text: "Sure! What multiplication would you like help with?",
      toolRequests: [],
    });

    // Verify we're on the new forked thread (not the original)
    const newThread = driver.magenta.chat.getActiveThread();
    expect(newThread.id).not.toBe(originalThreadId);

    await driver.assertDisplayBufferContains(
      "What multiplication would you like help with?",
    );
  });
});

it("auto-compact triggers when inputTokenCount exceeds 80% of context window", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Build up some conversation history
    await driver.inputMagentaText("What is 2+2?");
    await driver.send();

    const request1 = await driver.mockAnthropic.awaitPendingStream({
      message: "initial request",
    });
    request1.respond({
      stopReason: "end_turn",
      text: "2+2 equals 4.",
      toolRequests: [],
    });

    // Set the mock token count to exceed 80% of 200K context window (160K)
    driver.mockAnthropic.mockClient.mockInputTokenCount = 170_000;

    // Wait for countTokensPostFlight to run and update inputTokenCount
    await pollUntil(
      () => {
        const tokenCount = driver.magenta.chat
          .getActiveThread()
          .agent.getState().inputTokenCount;
        if (tokenCount === undefined || tokenCount < 160_000) {
          throw new Error(
            `expected inputTokenCount >= 160000 but got ${tokenCount}`,
          );
        }
      },
      { timeout: 2000, message: "inputTokenCount should be populated" },
    );

    const originalThread = driver.magenta.chat.getActiveThread();

    // Send another message - this should trigger auto-compact instead of normal send
    await driver.inputMagentaText("Now help me with multiplication");
    await driver.send();

    // The thread should enter compacting mode automatically
    await pollUntil(
      () => {
        if (originalThread.core.state.mode.type !== "compacting")
          throw new Error(
            `expected compacting mode but got ${originalThread.core.state.mode.type}`,
          );
      },
      { timeout: 2000, message: "thread should auto-compact" },
    );

    // Verify the nextPrompt was preserved
    if (originalThread.core.state.mode.type !== "compacting")
      throw new Error("expected compacting");
    expect(originalThread.core.compactionController?.nextPrompt).toBe(
      "Now help me with multiplication",
    );

    // Complete the compact subagent flow
    const compactSubagentStream = await driver.mockAnthropic.awaitPendingStream(
      {
        message: "compact subagent stream",
      },
    );

    const subagentMessages = compactSubagentStream.getProviderMessages();
    const userMsg = subagentMessages.find((m) => m.role === "user");
    expect(userMsg).toBeDefined();
    const textContent = userMsg!.content
      .filter(
        (c): c is Extract<typeof c, { type: "text" }> => c.type === "text",
      )
      .map((c) => c.text)
      .join("");
    expect(textContent).toContain("2+2 equals 4");

    const edlScript = `file \`/summary.md\`\nselect bof-eof\nreplace <<COMPACT_SUMMARY\n# Summary\nUser asked basic arithmetic: 2+2=4\nCOMPACT_SUMMARY`;

    compactSubagentStream.respond({
      stopReason: "tool_use",
      text: "I'll compact this conversation.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: "edl_1" as ToolRequestId,
            toolName: "edl" as ToolName,
            input: { script: edlScript },
          },
        },
      ],
    });

    const afterEdlStream = await driver.mockAnthropic.awaitPendingStream({
      message: "compact subagent after EDL",
    });

    afterEdlStream.respond({
      stopReason: "end_turn",
      text: "I have compacted the conversation.",
      toolRequests: [],
    });

    // Reset mock token count so the post-compact conversation doesn't re-trigger
    driver.mockAnthropic.mockClient.mockInputTokenCount = 1000;

    // After compact, the parent thread should resume with the next prompt
    const afterCompactStream = await driver.mockAnthropic.awaitPendingStream({
      message: "after compact continuation",
    });

    const afterCompactMessages = afterCompactStream.getProviderMessages();
    const hasNextPrompt = afterCompactMessages.some(
      (m) =>
        m.role === "user" &&
        m.content.some(
          (c) =>
            c.type === "text" &&
            c.text.includes("Now help me with multiplication"),
        ),
    );
    expect(hasNextPrompt).toBe(true);

    // The summary should be present
    const allText = afterCompactMessages
      .flatMap((m) =>
        m.content
          .filter(
            (c): c is Extract<typeof c, { type: "text" }> => c.type === "text",
          )
          .map((c) => c.text),
      )
      .join("");
    expect(allText).toContain("User asked basic arithmetic");

    afterCompactStream.respond({
      stopReason: "end_turn",
      text: "Sure! What multiplication would you like help with?",
      toolRequests: [],
    });

    await driver.assertDisplayBufferContains(
      "What multiplication would you like help with?",
    );
  });
});

it("compaction history records steps from multi-chunk compaction", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Build up a very large conversation to produce multiple chunks.
    // TARGET_CHUNK_TOKENS=25000, CHARS_PER_TOKEN=4 → targetChunkChars=100000
    // We need >100K chars total for 2+ chunks.
    const longText = "x".repeat(60_000);

    await driver.inputMagentaText("First question");
    await driver.send();

    const r1 = await driver.mockAnthropic.awaitPendingStream({
      message: "r1",
    });
    r1.respond({
      stopReason: "end_turn",
      text: `Answer 1: ${longText}`,
      toolRequests: [],
    });

    await driver.inputMagentaText("Second question");
    await driver.send();

    const r2 = await driver.mockAnthropic.awaitPendingStream({
      message: "r2",
    });
    r2.respond({
      stopReason: "end_turn",
      text: `Answer 2: ${longText}`,
      toolRequests: [],
    });

    const thread = driver.magenta.chat.getActiveThread();
    expect(thread.core.state.compactionHistory).toHaveLength(0);

    // Trigger compaction
    await driver.inputMagentaText("@compact Continue with next task");
    await driver.send();

    await pollUntil(
      () => {
        if (thread.core.state.mode.type !== "compacting")
          throw new Error(
            `expected compacting mode but got ${thread.core.state.mode.type}`,
          );
      },
      { timeout: 2000, message: "thread should enter compacting mode" },
    );

    // Verify we got multiple chunks
    if (thread.core.state.mode.type !== "compacting")
      throw new Error("not compacting");
    expect(
      thread.core.compactionController!.chunks.length,
    ).toBeGreaterThanOrEqual(2);
    const totalChunks = thread.core.compactionController!.chunks.length;

    // === Process chunk 1 ===
    const chunk1Stream = await driver.mockAnthropic.awaitPendingStream({
      message: "compact chunk 1",
    });

    // Verify chunk 1 prompt contains the chunk content
    const chunk1Messages = chunk1Stream.getProviderMessages();
    const chunk1UserMsg = chunk1Messages.find((m) => m.role === "user");
    expect(chunk1UserMsg).toBeDefined();
    const chunk1Text = chunk1UserMsg!.content
      .filter(
        (c): c is Extract<typeof c, { type: "text" }> => c.type === "text",
      )
      .map((c) => c.text)
      .join("");
    expect(chunk1Text).toContain("chunk 1 of");

    const edlScript1 = `file \`/summary.md\`\nselect bof-eof\nreplace <<COMPACT_SUMMARY\n# Summary\nFirst chunk processed: user asked two questions about large texts.\nCOMPACT_SUMMARY`;

    chunk1Stream.respond({
      stopReason: "tool_use",
      text: "Processing chunk 1.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: "edl_chunk1" as ToolRequestId,
            toolName: "edl" as ToolName,
            input: { script: edlScript1 },
          },
        },
      ],
    });

    const afterEdl1 = await driver.mockAnthropic.awaitPendingStream({
      message: "compact after EDL chunk 1",
    });
    afterEdl1.respond({
      stopReason: "end_turn",
      text: "Chunk 1 summarized.",
      toolRequests: [],
    });

    // === Process chunk 2 ===
    const chunk2Stream = await driver.mockAnthropic.awaitPendingStream({
      message: "compact chunk 2",
    });

    // Verify chunk 2 prompt references the existing summary
    const chunk2Messages = chunk2Stream.getProviderMessages();
    const chunk2UserMsg = chunk2Messages.find((m) => m.role === "user");
    expect(chunk2UserMsg).toBeDefined();
    const chunk2Text = chunk2UserMsg!.content
      .filter(
        (c): c is Extract<typeof c, { type: "text" }> => c.type === "text",
      )
      .map((c) => c.text)
      .join("");
    expect(chunk2Text).toContain("chunk 2 of");

    const edlScript2 = `file \`/summary.md\`\nselect bof-eof\nreplace <<COMPACT_SUMMARY\n# Summary\nUser asked two questions. Both answers were very long.\nCOMPACT_SUMMARY`;

    chunk2Stream.respond({
      stopReason: "tool_use",
      text: "Processing chunk 2.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: "edl_chunk2" as ToolRequestId,
            toolName: "edl" as ToolName,
            input: { script: edlScript2 },
          },
        },
      ],
    });

    const afterEdl2 = await driver.mockAnthropic.awaitPendingStream({
      message: "compact after EDL chunk 2",
    });
    afterEdl2.respond({
      stopReason: "end_turn",
      text: "Chunk 2 summarized.",
      toolRequests: [],
    });

    // If there are more chunks, process them the same way
    // For safety, drain any remaining chunks
    for (let i = 2; i < totalChunks; i++) {
      const extraStream = await driver.mockAnthropic.awaitPendingStream({
        message: `compact chunk ${i + 1}`,
      });
      const edlExtra = `file \`/summary.md\`\nselect bof-eof\nreplace <<COMPACT_SUMMARY\n# Summary\nUser asked two questions. Both answers were very long.\nCOMPACT_SUMMARY`;
      extraStream.respond({
        stopReason: "tool_use",
        text: `Processing chunk ${i + 1}.`,
        toolRequests: [
          {
            status: "ok",
            value: {
              id: `edl_chunk${i + 1}` as ToolRequestId,
              toolName: "edl" as ToolName,
              input: { script: edlExtra },
            },
          },
        ],
      });
      const afterEdlExtra = await driver.mockAnthropic.awaitPendingStream({
        message: `compact after EDL chunk ${i + 1}`,
      });
      afterEdlExtra.respond({
        stopReason: "end_turn",
        text: `Chunk ${i + 1} summarized.`,
        toolRequests: [],
      });
    }

    // After all chunks, the parent thread should resume
    const afterCompactStream = await driver.mockAnthropic.awaitPendingStream({
      message: "after compact continuation",
    });
    afterCompactStream.respond({
      stopReason: "end_turn",
      text: "Ready for the next task!",
      toolRequests: [],
    });

    await driver.assertDisplayBufferContains("Ready for the next task!");

    // === Verify compaction history ===
    expect(thread.core.state.compactionHistory).toHaveLength(1);
    const record = thread.core.state.compactionHistory[0];
    expect(record.steps).toHaveLength(totalChunks);
    expect(record.finalSummary).toBeDefined();
    expect(record.finalSummary).toContain("Summary");

    // Each step should have the correct chunk index and messages from its agent
    for (let i = 0; i < totalChunks; i++) {
      const step = record.steps[i];
      expect(step.chunkIndex).toBe(i);
      expect(step.totalChunks).toBe(totalChunks);
      // Each step should have messages (at least user + assistant exchanges)
      expect(step.messages.length).toBeGreaterThanOrEqual(2);
      // The assistant should have produced text and tool_use
      const assistantMsgs = step.messages.filter((m) => m.role === "assistant");
      expect(assistantMsgs.length).toBeGreaterThanOrEqual(1);
    }

    // Verify the compaction history view is renderable in the display
    await driver.assertDisplayBufferContains("📦 [Compaction 1");
  });
});

it("auto-compact does not trigger on compact threads", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Build conversation and trigger manual @compact
    await driver.inputMagentaText("Hello");
    await driver.send();

    const stream1 = await driver.mockAnthropic.awaitPendingStream({
      message: "initial",
    });
    stream1.respond({
      stopReason: "end_turn",
      text: "Hi there!",
      toolRequests: [],
    });

    // Set extremely high token count to make sure it would trigger on a normal thread
    driver.mockAnthropic.mockClient.mockInputTokenCount = 190_000;

    // Wait for token count to propagate
    await pollUntil(
      () => {
        const tokenCount = driver.magenta.chat
          .getActiveThread()
          .agent.getState().inputTokenCount;
        if (tokenCount === undefined || tokenCount < 160_000) {
          throw new Error(`expected high token count but got ${tokenCount}`);
        }
      },
      { timeout: 2000, message: "inputTokenCount should be populated" },
    );

    // Trigger manual compact
    await driver.inputMagentaText("@compact Continue please");
    await driver.send();

    const originalThread = driver.magenta.chat.getActiveThread();

    await pollUntil(
      () => {
        if (originalThread.core.state.mode.type !== "compacting")
          throw new Error("expected compacting");
      },
      { timeout: 2000, message: "thread should enter compacting mode" },
    );

    // The compact subagent should proceed normally (not self-compact)
    const compactStream = await driver.mockAnthropic.awaitPendingStream({
      message: "compact subagent",
    });

    // The compact subagent should have received a stream (meaning it did NOT
    // auto-compact itself, which would have blocked it)
    expect(compactStream).toBeDefined();

    // Verify a compact thread was spawned
    // Verify the thread is in compacting mode with an internal compact agent
    expect(originalThread.core.state.mode.type).toBe("compacting");

    // Clean up: respond to the compact subagent

    const edlScript = `file \`/summary.md\`\nselect bof-eof\nreplace <<COMPACT_SUMMARY\n# Summary\nHello conversation\nCOMPACT_SUMMARY`;

    compactStream.respond({
      stopReason: "tool_use",
      text: "Compacting.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: "edl_1" as ToolRequestId,
            toolName: "edl" as ToolName,
            input: { script: edlScript },
          },
        },
      ],
    });

    const afterEdlStream = await driver.mockAnthropic.awaitPendingStream({
      message: "compact subagent after EDL",
    });
    afterEdlStream.respond({
      stopReason: "end_turn",
      text: "Done compacting.",
      toolRequests: [],
    });

    // Reset token count for the resumed conversation
    driver.mockAnthropic.mockClient.mockInputTokenCount = 1000;

    const afterCompactStream = await driver.mockAnthropic.awaitPendingStream({
      message: "after compact",
    });
    afterCompactStream.respond({
      stopReason: "end_turn",
      text: "Continuing!",
      toolRequests: [],
    });

    await driver.assertDisplayBufferContains("Continuing!");
  });
});

it("compact drops all context files after compaction", async () => {
  await withDriver(
    {
      setupFiles: async (tmpDir) => {
        const fs = await import("node:fs/promises");
        const path = await import("node:path");
        await fs.writeFile(
          path.join(tmpDir, "context-file.ts"),
          "export const x = 1;",
        );
      },
    },
    async (driver) => {
      await driver.showSidebar();

      // Add a context file
      await driver.inputMagentaText("@file:context-file.ts Hello");
      await driver.send();

      const request1 = await driver.mockAnthropic.awaitPendingStream({
        message: "initial request",
      });
      request1.respond({
        stopReason: "end_turn",
        text: "I see the file.",
        toolRequests: [],
      });

      const thread = driver.magenta.chat.getActiveThread();

      await pollUntil(() => {
        const files = Object.keys(thread.contextManager.files);
        if (files.length < 1) throw new Error("expected 1 file");
      });

      expect(Object.keys(thread.contextManager.files)).toHaveLength(1);

      // Trigger compaction
      await driver.inputMagentaText("@compact Continue working");
      await driver.send();

      await pollUntil(
        () => {
          if (thread.core.state.mode.type !== "compacting")
            throw new Error("expected compacting");
        },
        { timeout: 2000 },
      );

      const compactStream = await driver.mockAnthropic.awaitPendingStream({
        message: "compact subagent",
      });

      const edlScript = [
        "file `/summary.md`",
        "select bof-eof",
        "replace <<COMPACT_SUMMARY",
        "# Summary",
        "User added context-file.ts and asked hello.",
        "COMPACT_SUMMARY",
      ].join("\n");

      compactStream.respond({
        stopReason: "tool_use",
        text: "Compacting.",
        toolRequests: [
          {
            status: "ok",
            value: {
              id: "edl_1" as ToolRequestId,
              toolName: "edl" as ToolName,
              input: { script: edlScript },
            },
          },
        ],
      });

      const afterEdlStream = await driver.mockAnthropic.awaitPendingStream({
        message: "after EDL",
      });
      afterEdlStream.respond({
        stopReason: "end_turn",
        text: "Done.",
        toolRequests: [],
      });

      const afterCompactStream = await driver.mockAnthropic.awaitPendingStream({
        message: "after compact",
      });
      afterCompactStream.respond({
        stopReason: "end_turn",
        text: "Starting fresh!",
        toolRequests: [],
      });

      await driver.assertDisplayBufferContains("Starting fresh!");

      // Context files should always be empty after compaction
      expect(Object.keys(thread.contextManager.files)).toHaveLength(0);
    },
  );
});

it("compaction drops active reminders (transient reads and context-file blocks)", async () => {
  await withDriver(
    {
      setupFiles: async (tmpDir) => {
        await fs.writeFile(
          path.join(tmpDir, "ctx.md"),
          "# Ctx\n\n<system_reminder>\ncontext cat reminder\n</system_reminder>\n",
        );
        await fs.writeFile(
          path.join(tmpDir, "transient.md"),
          "# Transient\n\n<system_reminder>\ntransient cat reminder\n</system_reminder>\n",
        );
      },
    },
    async (driver) => {
      await driver.showSidebar();
      await driver.magenta.command("context-files './ctx.md'");

      await driver.inputMagentaText("Use a skill");
      await driver.send();

      const request = await driver.mockAnthropic.awaitPendingStream();
      request.respond({
        stopReason: "tool_use",
        text: "reading the transient skill",
        toolRequests: [
          {
            status: "ok",
            value: {
              id: "tool_1" as ToolRequestId,
              toolName: "get_file" as ToolName,
              input: { filePath: "./transient.md" },
            },
          },
        ],
        usage: { inputTokens: 100, outputTokens: 5000 },
      });

      const autoRespond = await driver.mockAnthropic.awaitPendingStream();
      autoRespond.respond({
        stopReason: "end_turn",
        text: "done reading",
        toolRequests: [],
      });

      await driver.inputMagentaText("@compact continue please");
      await driver.send();

      const compactSubagentStream =
        await driver.mockAnthropic.awaitPendingStream({
          message: "compact subagent stream",
        });
      const edlScript = `file \`/summary.md\`\nselect bof-eof\nreplace <<COMPACT_SUMMARY\n# Summary\nread skills\nCOMPACT_SUMMARY`;
      compactSubagentStream.respond({
        stopReason: "tool_use",
        text: "compacting",
        toolRequests: [
          {
            status: "ok",
            value: {
              id: "edl_1" as ToolRequestId,
              toolName: "edl" as ToolName,
              input: { script: edlScript },
            },
          },
        ],
      });

      const afterEdlStream = await driver.mockAnthropic.awaitPendingStream({
        message: "compact subagent after EDL",
      });
      afterEdlStream.respond({
        stopReason: "end_turn",
        text: "compacted",
        toolRequests: [],
      });

      const afterCompactStream = await driver.mockAnthropic.awaitPendingStream({
        message: "after compact continuation",
      });
      const lastMessage =
        afterCompactStream.messages[afterCompactStream.messages.length - 1];
      if (typeof lastMessage.content === "string") {
        throw new Error("Expected array content");
      }
      const reminder = lastMessage.content.find(
        (c) => c.type === "text" && c.text.includes("<system-reminder>"),
      );
      expect(reminder).toBeDefined();
      if (reminder && reminder.type === "text") {
        // Compaction rebuilds an empty ContextManager and clears the transient
        // read set, so neither reminder survives.
        expect(reminder.text).not.toContain("context cat reminder");
        expect(reminder.text).not.toContain("transient cat reminder");
      }

      afterCompactStream.respond({
        stopReason: "end_turn",
        text: "all set",
        toolRequests: [],
      });
    },
  );
});
