diff --git a/e2e/cast/README.md b/e2e/cast/README.md new file mode 100644 index 0000000..fef5f71 --- /dev/null +++ b/e2e/cast/README.md @@ -0,0 +1,70 @@ +# Cast-Based E2E Testing + +This directory contains a cast-based end-to-end (E2E) test for the AgentAPI project. The framework simulates realistic agent interactions by replaying asciicast v2 recordings. + +## TL;DR + +```shell +go test ./e2e/cast +``` + +## How it Works + +The testing framework (`cast_test.go`) does the following: +- Starts the AgentAPI server with a fake agent (`cmd/cast_agent.go`). +- The fake agent replays a `.cast` file (asciicast v2 format), writing terminal output to stdout and validating stdin against the recorded input events. +- The testing framework sends messages to the fake agent via the AgentAPI and validates the responses. + +## Adding or Updating Fixtures + +### Step 1: Record with asciinema + +The recording captures both agentapi's output (what it sends to the agent) and the agent's responses. +Use `asciinema` to record agentapi wrapping the agent. Below is an example for Claude: + +```shell +# Set terminal to minimum size Claude supports (80 columns) +stty cols 80 rows 1000 + +# Build agentapi (or use an existing known-good version) +agentapi server -t claude -- asciinema rec --stdin testdata/claude.cast --command 'echo hello | claude' + +# Then interact with Claude via AgentAPI (either web UI or API): +# 1. Wait for Claude to respond to the initial prompt +# 2. Type a test message (e.g., "This is just a test.") +# 3. Wait for Claude's reply +# 4. Press Ctrl+C to exit +``` + +**Important notes:** +- The terminal dimensions (80x1000) must match the `--term-width` and `--term-height` flags in the test (see `defaultCmdFn` in `cast_test.go`). +- Recording agentapi (not Claude directly) captures the exact byte sequences that agentapi sends, including bracketed paste mode escape sequences. +- The `--stdin` flag captures your input, which the test uses to validate that agentapi sends the correct bytes. +- To overwrite an existing fixture, add `--overwrite` to the `asciinema` invocation. + +### Step 2: Create the sidecar script file + +Create a matching sidecar file `testdata/my-fixture.txt` that lists the expected conversation in order, one entry per line: + +``` +user hello +agent Hello! How can I help you today? +user This is just a test. +agent Got it! Let me know if you need anything. +``` + +Each line is ``. Valid roles are `agent` or `user`. + +**Flow explanation:** +- Line 1: The initial prompt passed to the agent at startup +- Line 2: The agent's reply to the initial prompt +- Line 3: The test message sent via the AgentAPI +- Line 4: The agent's reply to the test message + +Note: The AgentAPI merges the startup sequence (welcome screen + initial prompt + initial reply) into the first agent message. The test expects 3 messages from the API, even though the script file has 4 entries. + +### Step 3: Review and update the test + +> **Caution:** Review the recording before committing. Remove or redact any API keys, tokens, or other sensitive data that may appear in the terminal output (stdout events), recorded keystrokes (`"i"` events from `--stdin`), or environment variables captured in the cast header. + +To use a new fixture, update the `castFile` and `scriptFile` constants in `cast_test.go` to reference the new fixture files, then update the assertions in `TestE2E` to match the new conversation. diff --git a/e2e/cast/cast_test.go b/e2e/cast/cast_test.go new file mode 100644 index 0000000..65ade1a --- /dev/null +++ b/e2e/cast/cast_test.go @@ -0,0 +1,259 @@ +package cast_test + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + agentapisdk "github.com/coder/agentapi-sdk-go" + "github.com/stretchr/testify/require" +) + +const ( + castTestTimeout = 30 * time.Second + castOperationTimeout = 10 * time.Second + castHealthCheckTimeout = 10 * time.Second + castFile = "testdata/claude.cast" + scriptFile = "testdata/claude.txt" +) + +type scriptEntry struct { + Role string // "user" or "agent" + Message string +} + +func loadScript(t testing.TB, path string) []scriptEntry { + t.Helper() + data, err := os.ReadFile(path) + require.NoError(t, err) + var entries []scriptEntry + sc := bufio.NewScanner(bytes.NewReader(data)) + for sc.Scan() { + line := sc.Text() + if line == "" { + continue + } + role, msg, ok := strings.Cut(line, "\t") + require.True(t, ok, "malformed script line: %q", line) + require.Contains(t, []string{"user", "agent"}, role, + "unexpected role %q in script line: %q", role, line) + entries = append(entries, scriptEntry{Role: role, Message: msg}) + } + require.NoError(t, sc.Err()) + return entries +} + +func TestE2E(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + script := loadScript(t, scriptFile) + require.GreaterOrEqual(t, len(script), 4, "claude.txt must have at least 4 entries") + require.Equal(t, "user", script[0].Role) // initial prompt (startup) + require.Equal(t, "agent", script[1].Role) // reply to initial prompt + require.Equal(t, "user", script[2].Role) // test message sent via API + require.Equal(t, "agent", script[3].Role) // reply to test message + // Note: The API merges the startup sequence (initial prompt + initial reply) + // into the first agent message, so we expect 3 messages from the API, not 4. + initialPromptReply := script[1].Message + userMessage := script[2].Message + agentReply := script[3].Message + + ctx, cancel := context.WithTimeout(context.Background(), castTestTimeout) + defer cancel() + + apiClient := setup(ctx, t) + + // Agent should be running while processing the initial greeting. + statusResp, err := apiClient.GetStatus(ctx) + require.NoError(t, err) + require.Equal(t, agentapisdk.StatusRunning, statusResp.Status) + + require.NoError(t, waitAgentAPIStable(ctx, t, apiClient, castOperationTimeout, "initial stable")) + + _, err = apiClient.PostMessage(ctx, agentapisdk.PostMessageParams{ + Content: userMessage, + Type: agentapisdk.MessageTypeUser, + }) + require.NoError(t, err, "failed to send message") + + // Agent should be running while processing the reply. + statusResp, err = apiClient.GetStatus(ctx) + require.NoError(t, err) + require.Equal(t, agentapisdk.StatusRunning, statusResp.Status) + + require.NoError(t, waitAgentAPIStable(ctx, t, apiClient, castOperationTimeout, "post message")) + + msgResp, err := apiClient.GetMessages(ctx) + require.NoError(t, err, "failed to get messages") + require.Len(t, msgResp.Messages, 3) + // First message is agent greeting (contains startup screen + initial prompt reply) + require.Contains(t, msgResp.Messages[0].Content, initialPromptReply) + require.Contains(t, msgResp.Messages[1].Content, userMessage) + require.Contains(t, msgResp.Messages[2].Content, agentReply) +} + +func defaultCmdFn(ctx context.Context, t testing.TB, serverPort int, binaryPath, cwd string) (string, []string) { + // Terminal dimensions must match the cast file (80x1000) for correct ANSI escape sequence positioning. + return binaryPath, []string{"server", fmt.Sprintf("--port=%d", serverPort), "--term-width=80", "--term-height=1000", "--", "go", "run", filepath.Join(cwd, "cmd", "cast_agent.go"), castFile} +} + +func setup(ctx context.Context, t testing.TB) *agentapisdk.Client { + t.Helper() + + binaryPath := os.Getenv("AGENTAPI_BINARY_PATH") + if binaryPath == "" { + cwd, err := os.Getwd() + require.NoError(t, err, "Failed to get current working directory") + // We're in e2e/cast, so go up two levels to reach the repo root + binaryPath = filepath.Join(cwd, "..", "..", "out", "agentapi") + t.Logf("Building binary at %s", binaryPath) + buildCmd := exec.CommandContext(ctx, "go", "build", "-o", binaryPath, ".") + buildCmd.Dir = filepath.Join(cwd, "..", "..") + t.Logf("run: %s", buildCmd.String()) + require.NoError(t, buildCmd.Run(), "Failed to build binary") + } + + serverPort, err := getFreePort() + require.NoError(t, err, "Failed to get free port for server") + + cwd, err := os.Getwd() + require.NoError(t, err, "Failed to get current working directory") + + bin, args := defaultCmdFn(ctx, t, serverPort, binaryPath, cwd) + t.Logf("Running command: %s %s", bin, strings.Join(args, " ")) + cmd := exec.CommandContext(ctx, bin, args...) + + stdout, err := cmd.StdoutPipe() + require.NoError(t, err, "Failed to create stdout pipe") + + stderr, err := cmd.StderrPipe() + require.NoError(t, err, "Failed to create stderr pipe") + + err = cmd.Start() + require.NoError(t, err, "Failed to start agentapi server") + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + logOutput(t, "SERVER-STDOUT", stdout) + }() + + go func() { + defer wg.Done() + logOutput(t, "SERVER-STDERR", stderr) + }() + + t.Cleanup(func() { + if cmd.Process != nil { + _ = cmd.Process.Kill() + _ = cmd.Wait() + } + wg.Wait() + }) + + serverURL := fmt.Sprintf("http://localhost:%d", serverPort) + require.NoError(t, waitForServer(ctx, t, serverURL, castHealthCheckTimeout), "Server not ready") + apiClient, err := agentapisdk.NewClient(serverURL) + require.NoError(t, err, "Failed to create agentapi SDK client") + + return apiClient +} + +func logOutput(t testing.TB, prefix string, r io.Reader) { + t.Helper() + scanner := bufio.NewScanner(r) + for scanner.Scan() { + t.Logf("[%s] %s", prefix, scanner.Text()) + } +} + +func waitForServer(ctx context.Context, t testing.TB, url string, timeout time.Duration) error { + t.Helper() + client := &http.Client{Timeout: time.Second} + healthCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-healthCtx.Done(): + require.Failf(t, "failed to start server", "server at %s not ready within timeout: %w", url, healthCtx.Err()) + case <-ticker.C: + resp, err := client.Get(url) + if err == nil { + _ = resp.Body.Close() + return nil + } + t.Logf("Server not ready yet: %s", err) + } + } +} + +func waitAgentAPIStable(ctx context.Context, t testing.TB, apiClient *agentapisdk.Client, waitFor time.Duration, msg string) error { + t.Helper() + waitCtx, waitCancel := context.WithTimeout(ctx, waitFor) + defer waitCancel() + + start := time.Now() + var currStatus agentapisdk.AgentStatus + var lastMessage string + defer func() { + elapsed := time.Since(start) + t.Logf("%s: agent API status: %s (elapsed: %s)", msg, currStatus, elapsed.Round(100*time.Millisecond)) + if t.Failed() && lastMessage != "" { + fmt.Fprintf(os.Stderr, "\n=== Last agent message ===\n%s\n=== End last agent message ===\n", lastMessage) + } + }() + evts, errs, err := apiClient.SubscribeEvents(ctx) + require.NoError(t, err, "failed to subscribe to events") + for { + select { + case <-waitCtx.Done(): + return waitCtx.Err() + case evt := <-evts: + if esc, ok := evt.(agentapisdk.EventStatusChange); ok { + currStatus = esc.Status + if currStatus == agentapisdk.StatusStable { + return nil + } + } else if emc, ok := evt.(agentapisdk.EventMessageUpdate); ok { + lastMessage = emc.Message + t.Logf("Got message event: id=%d role=%s len=%d", emc.Id, emc.Role, len(emc.Message)) + } + case err := <-errs: + return fmt.Errorf("read events: %w", err) + } + } +} + +func getFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer func() { _ = l.Close() }() + + return l.Addr().(*net.TCPAddr).Port, nil +} diff --git a/e2e/cast/cmd/cast_agent.go b/e2e/cast/cmd/cast_agent.go new file mode 100644 index 0000000..d740d19 --- /dev/null +++ b/e2e/cast/cmd/cast_agent.go @@ -0,0 +1,123 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "os" + "os/signal" + "regexp" + "strings" + + "github.com/acarl005/stripansi" +) + +func main() { + if len(os.Args) != 2 { + fmt.Println("Usage: cast_agent ") + os.Exit(1) + } + + go func() { + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, os.Interrupt) + <-sigCh + os.Exit(0) + }() + + f, err := os.Open(os.Args[1]) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open cast file: %v\n", err) + os.Exit(1) + } + defer f.Close() + + stdinReader := bufio.NewReader(os.Stdin) + fileScanner := bufio.NewScanner(f) + + // Skip the header line. + fileScanner.Scan() + + var inputBuf strings.Builder + + for fileScanner.Scan() { + line := fileScanner.Text() + var event [3]json.RawMessage + if err := json.Unmarshal([]byte(line), &event); err != nil { + fmt.Fprintf(os.Stderr, "failed to parse event: %v\n", err) + os.Exit(1) + } + + var eventType string + if err := json.Unmarshal(event[1], &eventType); err != nil { + fmt.Fprintf(os.Stderr, "failed to parse event type: %v\n", err) + os.Exit(1) + } + + var data string + if err := json.Unmarshal(event[2], &data); err != nil { + fmt.Fprintf(os.Stderr, "failed to parse event data: %v\n", err) + os.Exit(1) + } + + switch eventType { + case "o": + fmt.Print(data) + case "i": + switch data { + case "\r": + // Consume the Enter keystroke, then reset the accumulated buffer. + if _, err := stdinReader.ReadByte(); err != nil && err != io.EOF { + fmt.Fprintf(os.Stderr, "failed to read stdin: %v\n", err) + os.Exit(1) + } + inputBuf.Reset() + case "\x03": + // Ctrl-C marks end of interactive session; block indefinitely. + // This ensures the agent displays the last reply and remains stable + // rather than continuing to replay exit sequences. + <-make(chan struct{}) + default: + // Block until agentapi writes this input, then validate byte-by-byte. + expected := []byte(data) + for _, exp := range expected { + b, err := stdinReader.ReadByte() + if err != nil && err != io.EOF { + fmt.Fprintf(os.Stderr, "failed to read stdin: %v\n", err) + os.Exit(1) + } + if b != exp { + fmt.Fprintf(os.Stderr, "input mismatch: expected 0x%02x, got 0x%02x\n", exp, b) + os.Exit(1) + } + } + inputBuf.WriteString(data) + } + } + } + + if err := fileScanner.Err(); err != nil { + fmt.Fprintf(os.Stderr, "error reading cast file: %v\n", err) + os.Exit(1) + } + + // Block until interrupted. + <-make(chan struct{}) +} + +func cleanTerminalInput(input string) string { + input = stripansi.Strip(input) + + bracketedPasteRe := regexp.MustCompile(`\x1b\[\d+~`) + input = bracketedPasteRe.ReplaceAllString(input, "") + + backspaceRe := regexp.MustCompile(`.\x08`) + input = backspaceRe.ReplaceAllString(input, "") + + input = strings.ReplaceAll(input, "\x08", "") + input = strings.ReplaceAll(input, "\x7f", "") + input = strings.ReplaceAll(input, "\x1b", "") + + return strings.TrimSpace(input) +} diff --git a/e2e/cast/loadscript_test.go b/e2e/cast/loadscript_test.go new file mode 100644 index 0000000..1521f8e --- /dev/null +++ b/e2e/cast/loadscript_test.go @@ -0,0 +1,55 @@ +package cast_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoadScript(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + f := filepath.Join(t.TempDir(), "script.txt") + err := os.WriteFile(f, []byte("agent\tHello!\nuser\tHi there\nagent\tHow can I help?\n"), 0o600) + require.NoError(t, err) + + entries := loadScript(t, f) + require.Len(t, entries, 3) + require.Equal(t, scriptEntry{Role: "agent", Message: "Hello!"}, entries[0]) + require.Equal(t, scriptEntry{Role: "user", Message: "Hi there"}, entries[1]) + require.Equal(t, scriptEntry{Role: "agent", Message: "How can I help?"}, entries[2]) + }) + + t.Run("empty file", func(t *testing.T) { + f := filepath.Join(t.TempDir(), "empty.txt") + err := os.WriteFile(f, []byte(""), 0o600) + require.NoError(t, err) + + entries := loadScript(t, f) + require.Empty(t, entries) + }) + + t.Run("blank lines are skipped", func(t *testing.T) { + f := filepath.Join(t.TempDir(), "blanks.txt") + err := os.WriteFile(f, []byte("\nagent\tGreeting\n\nuser\tQuestion\n\n"), 0o600) + require.NoError(t, err) + + entries := loadScript(t, f) + require.Len(t, entries, 2) + require.Equal(t, "agent", entries[0].Role) + require.Equal(t, "user", entries[1].Role) + }) + + t.Run("tab in message body is preserved", func(t *testing.T) { + f := filepath.Join(t.TempDir(), "tabmsg.txt") + // The message itself contains a tab character after the first one. + err := os.WriteFile(f, []byte("user\thello\tworld\n"), 0o600) + require.NoError(t, err) + + entries := loadScript(t, f) + require.Len(t, entries, 1) + require.Equal(t, "user", entries[0].Role) + require.Equal(t, "hello\tworld", entries[0].Message) + }) +} diff --git a/e2e/cast/testdata/claude.cast b/e2e/cast/testdata/claude.cast new file mode 100644 index 0000000..231fe35 --- /dev/null +++ b/e2e/cast/testdata/claude.cast @@ -0,0 +1,112 @@ +{"version": 2, "width": 80, "height": 1000, "timestamp": 1771590373, "env": {"SHELL": "/bin/bash", "TERM": "vt100"}} +[0.499981, "o", "\u001b[?2026h\r\r\n\u001b[31m╭───\u001b[1CClaude\u001b[1CCode\u001b[1C\u001b[37mv2.1.49\u001b[1C\u001b[31m──────────────────────────────────────────────────────╮\u001b[39m\r\r\n\u001b[31m│\u001b[36C\u001b[2m│\u001b[1C\u001b[1mTips\u001b[1Cfor\u001b[1Cgetting\u001b[1Cstarted\u001b[16C\u001b[22m│\u001b[39m\r\r\n\u001b[31m│\u001b[12C\u001b[39m\u001b[1mWelcome\u001b[1Cback!\u001b[11C\u001b[2m\u001b[31m│\u001b[1C\u001b[39m\u001b[22mRun\u001b[1C/init\u001b[1Cto\u001b[1Ccreate\u001b[1Ca\u001b[1CCLAUDE.md\u001b[1Cfile\u001b[1Cw…\u001b[1C\u001b[31m│\u001b[39m\r\r\n\u001b[31m│\u001b[36C\u001b[2m│\u001b[1C───────────────────────────────────────\u001b[1C\u001b[22m│\u001b[39m\r\r\n\u001b[31m│\u001b[36C\u001b[2m│\u001b[1C\u001b[1mRecent\u001b[1Cactivity\u001b[25C\u001b[22m│\u001b[39m\r\r\n\u001b[31m│\u001b[18C\u001b[97m✻\u001b[17C\u001b[2m\u001b[31m│\u001b[1C\u001b[22m\u001b[37mNo\u001b[1Crecent\u001b[1Cactivity\u001b[22C\u001b[31m│\u001b[39m\r\r\n\u001b[31m│\u001b[18C\u001b[37m|\u001b[17C\u001b[2m\u001b[31m│\u001b[41C\u001b[22m│\u001b[39m\r\r\n\u001b[31m│\u001b[17C\u001b[97m▟█▙\u001b[16C\u001b[2m\u001b[31m│\u001b[41C\u001b[22m│\u001b[39m\r\r\n\u001b[31m│\u001b[15C▐\u001b[40m▛███▜\u001b[49m▌\u001b[14C\u001b[2m│\u001b[41C\u001b[22m│\u001b[39m\r\r\n\u001b[31m│\u001b[14C▝▜\u001b[40"] +[0.50088, "o", "m█████\u001b[49m▛▘\u001b[13C\u001b[2m│\u001b[41C\u001b[22m│\u001b[39m\r\r\n\u001b[31m│\u001b[16C▘▘\u001b[1C▝▝\u001b[15C\u001b[2m│\u001b[41C\u001b[22m│\u001b[39m\r\r\n\u001b[31m│\u001b[36C\u001b[2m│\u001b[41C\u001b[22m│\u001b[39m\r\r\n\u001b[31m│\u001b[3C\u001b[37mSonnet\u001b[1C4.6\u001b[1C·\u001b[1CAPI\u001b[1CUsage\u001b[1CBilling\u001b[3C\u001b[2m\u001b[31m│\u001b[41C\u001b[22m│\u001b[39m\r\r\n\u001b[31m│\u001b[8C\u001b[37m~/src/coder/agentapi\u001b[8C\u001b[2m\u001b[31m│\u001b[41C\u001b[22m│\u001b[39m\r\r\n\u001b[31m╰──────────────────────────────────────────────────────────────────────────────╯\u001b[39m\r\r\n\r\r\n\u001b[2C\u001b[37m/model\u001b[1Cto\u001b[1Ctry\u001b[1COpus\u001b[1C4.6\u001b[39m\r\r\n\r\r\n\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\u001b[39m\u001b[22m\r\r\n❯ \u001b[7mT\u001b[27m\u001b[2mry\u001b[1C\"write\u001b[1Ca\u001b[1Ctest\u001b[1Cfor\u001b[1Cserver.go\"\u001b[22m\r\r\n\u001b[2m\u001b[37m───────────"] +[0.500942, "o", "─────────────────────────────────────────────────────────────────────\u001b[39m\u001b[22m\r\r\n\u001b[2C\u001b[37m?\u001b[1Cfor\u001b[1Cshortcuts\u001b[39m\r\r\n\u001b[?2026l\u001b[?2004h\u001b[?1004h\u001b[?25l"] +[0.502434, "o", "\u001b]0;✳ Claude Code\u0007"] +[0.503342, "o", "\u001b]0;✳ Claude Code\u0007"] +[0.514646, "o", "\u001b[?2026h\r\u001b[19C\u001b[17A \r\u001b[19C\u001b[1B\u001b[97m✻\r\u001b[18C\u001b[1B\u001b[39m \u001b[37m|\u001b[39m \r\u001b[15C\u001b[1B \u001b[97m▟█▙\u001b[39m \r\u001b[15C\u001b[1B\u001b[31m ▐\u001b[40m▛\u001b[3C▜\u001b[49m▌\u001b[39m \r\u001b[15C\u001b[1B\u001b[31m▝▜\u001b[40m█████\u001b[49m▛▘\r\u001b[15C\u001b[1B ▘▘ ▝▝ \r\u001b[2C\u001b[8B\u001b[39m\u001b[7m \u001b[27m \r\r\n\r\n\r\n\u001b[?2026l"] +[0.554224, "o", "\u001b[?2026h\r\u001b[4A\u001b[31m·\u001b[39m \u001b[31mHarmonizing… \u001b[39m \r\u001b[1B \r\u001b[2B\u001b[37m❯ \u001b[39m\u001b[7m \u001b[27m \r\r\n\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\u001b[39m\u001b[22m\r\r\n\u001b[2C\u001b[37mesc\u001b[1Cto\u001b[1Cinterrupt\u001b[39m\r\r\n\u001b[?2026l"] +[0.554446, "o", "\u001b]0;✳ Claude Code\u0007"] +[0.655226, "o", "\u001b[?2026h\r\u001b[2C\u001b[1A \u001b[93mNative installation exists but ~/.local/bin is not in your PATH. Run:\u001b[39m\r\r\n\r\r\n\u001b[3C\u001b[93mecho\u001b[1C'export\u001b[1CPATH=\"$HOME/.local/bin:$PATH\"'\u001b[1C>>\u001b[1C~/.bashrc\u001b[1C&&\u001b[1Csource\u001b[1C~/.bashrc\u001b[39m\r\r\n\u001b[?2026l"] +[0.702641, "o", "\u001b[?2026h\r\u001b[8A\u001b[40m\u001b[30m❯ \u001b[97mhello \r\u001b[1B \r\u001b[1B\u001b[39m\u001b[49m \r\u001b[1B\u001b[31m·\u001b[39m \u001b[31mHarmonizing… \r\u001b[1B\u001b[39m \r\u001b[1B\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\r\u001b[1B\u001b[22m❯ \u001b[39m\u001b[7m \r\u001b[1B\u001b[27m\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\u001b[39m\u001b[22m\r\r\n\u001b[3C\u001b[93mNative\u001b[1Cinstallation\u001b[1Cexists\u001b[1Cbut\u001b[1C~/.local/bi"] +[0.70277, "o", "n\u001b[1Cis\u001b[1Cnot\u001b[1Cin\u001b[1Cyour\u001b[1CPATH.\u001b[1CRun:\u001b[39m\r\r\n\r\r\n\u001b[3C\u001b[93mecho\u001b[1C'export\u001b[1CPATH=\"$HOME/.local/bin:$PATH\"'\u001b[1C>>\u001b[1C~/.bashrc\u001b[1C&&\u001b[1Csource\u001b[1C~/.bashrc\u001b[39m\r\r\n\u001b[?2026l"] +[0.71905, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✢\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[0.808334, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m*\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[0.91435, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✶\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.017899, "o", "\u001b[?2026h\r\u001b[2C\u001b[8A\u001b[93mH\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.075191, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✻\u001b[2C\u001b[93ma\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.122816, "o", "\u001b[?2026h\r\u001b[4C\u001b[8A\u001b[93mr\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.17376, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✽\u001b[1CH\u001b[2C\u001b[93mm\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.23048, "o", "\u001b[?2026h\r\u001b[3C\u001b[8A\u001b[31ma\u001b[2C\u001b[93mo\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.279932, "o", "\u001b[?2026h\r\u001b[4C\u001b[8A\u001b[31mr\u001b[2C\u001b[93mn\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.331009, "o", "\u001b[?2026h\r\u001b[5C\u001b[8A\u001b[31mm\u001b[2C\u001b[93mi\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.39101, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✻\u001b[5Con\u001b[1C\u001b[93mzi\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.439822, "o", "\u001b[?2026h\r\u001b[8C\u001b[8A\u001b[31mi\u001b[2C\u001b[93mn\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.491272, "o", "\u001b[?2026h\r\u001b[9C\u001b[8A\u001b[31mz\u001b[2C\u001b[93mg\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.519281, "o", "\u001b]0;⠐ Claude Code\u0007"] +[1.545751, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✶\u001b[9Ci\u001b[2C\u001b[93m…\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.598697, "o", "\u001b[?2026h\r\u001b[11C\u001b[8A\u001b[31mn\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.651702, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m*\u001b[11Cg\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.708031, "o", "\u001b[?2026h\r\u001b[13C\u001b[8A\u001b[31m…\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.758385, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✢\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[1.866317, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m·\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.130769, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✢\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.233371, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m*\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.3438, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✶\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.480248, "o", "\u001b]0;⠂ Claude Code\u0007"] +[2.504945, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✻\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.608146, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✽\u001b[1C\u001b[93mH\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.663681, "o", "\u001b[?2026h\r\u001b[3C\u001b[8A\u001b[93ma\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.714104, "o", "\u001b[?2026h\r\u001b[4C\u001b[8A\u001b[93mr\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.767531, "o", "\u001b[?2026h\r\u001b[2C\u001b[8A\u001b[31mH\u001b[2C\u001b[93mm\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.818026, "o", "\u001b[?2026h\r\u001b[3C\u001b[8A\u001b[31ma\u001b[2C\u001b[93mo\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.868096, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✻\u001b[3Cr\u001b[2C\u001b[93mn\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.923735, "o", "\u001b[?2026h\r\u001b[5C\u001b[8A\u001b[31mm\u001b[2C\u001b[93mi\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[2.973172, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✶\u001b[5Co\u001b[2C\u001b[93mz\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[3.024772, "o", "\u001b[?2026h\r\u001b[7C\u001b[8A\u001b[31mn\u001b[2C\u001b[93mi\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[3.08074, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m*\u001b[7Ci\u001b[2C\u001b[93mn\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[3.131097, "o", "\u001b[?2026h\r\u001b[9C\u001b[8A\u001b[31mz\u001b[2C\u001b[93mg\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[3.18233, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✢\u001b[9Ci\u001b[2C\u001b[93m…\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[3.237054, "o", "\u001b[?2026h\r\u001b[11C\u001b[8A\u001b[31mn\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[3.292731, "o", "\u001b[?2026h\r\u001b[12C\u001b[8A\u001b[31mg…\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[3.342919, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m·\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[3.441252, "o", "\u001b]0;⠐ Claude Code\u0007"] +[3.551918, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✢\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[3.708965, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m*\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[3.818043, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✶\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[3.926071, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✻\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[4.026451, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✽\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[4.246063, "o", "\u001b[?2026h\r\u001b[5C\u001b[8A\u001b[93mmon\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[4.299204, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✻\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[4.355999, "o", "\u001b[?2026h\r\u001b[4C\u001b[8A\u001b[93mr\u001b[2C\u001b[31mn\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[4.402203, "o", "\u001b]0;⠂ Claude Code\u0007"] +[4.40562, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m✶\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[4.439559, "o", "\u001b[?2026h\r\u001b[8A\u001b[97m●\u001b[1C\u001b[39mHello! How can\u001b[1CI\u001b[1Chelp\u001b[1Cyou\u001b[1Ctoday?\r\u001b[2B\u001b[31m✶\u001b[39m \u001b[31mHa\u001b[93mrmo\u001b[31mnizing… \u001b[39m \r\u001b[1B \r\u001b[2B\u001b[37m❯ \u001b[39m\u001b[7m \u001b[27m \r\u001b[1B\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\r\u001b[3C\u001b[1B\u001b[22m\u001b[93mNative installation exists but ~/.local/bin\u001b[1Cis\u001b[1Cnot in your PATH. Run:\u001b[39m \r\r\n\r\r\n\u001b[3C\u001b[93mecho\u001b[1C'export\u001b[1CPATH=\"$HOME/.local/bin:$PATH\"'\u001b[1C>>\u001b[1C~/.bashrc\u001b[1C&&\u001b[1Csource\u001b[1C~/.bashrc\u001b[39m\r\r\n\u001b[?2026l"] +[4.520331, "o", "\u001b[?2026h\r\u001b[8A\u001b[31m*\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[4.547836, "o", "\u001b[?2026h\r\u001b[2C\u001b[8A\u001b[33mHarmonizing…\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[4.567992, "o", "\u001b[?2026h\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[1A\r\u001b[6A\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\r\u001b[1B\u001b[39m\u001b[22m❯ \u001b[7m \r\u001b[2B\u001b[27m \u001b[93mNative installation exists but ~/.local/bin is not in your PATH. Run:\r\u001b[1B\u001b[39m \r\u001b[3C\u001b[1B\u001b[93mecho 'export PATH=\"$HOME/.local/bin:$PATH\"'\u001b[1C>>\u001b[1C~/.bashrc && source ~/.bashrc\r\u001b[1B\u001b[39m \r\u001b[1B \r\u001b[1A\u001b[?2026l"] +[4.568395, "o", "\u001b]0;✳ Claude Code\u0007"] +[4.568606, "o", "\u001b]0;✳ Claude Code\u0007"] +[8.65582, "o", "\u001b[?2026h\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[1A\r\u001b[2C\u001b[1A\u001b[37m? for shortcuts\u001b[39m \r\u001b[1B \r\u001b[1B \r\u001b[1A\u001b[?2026l"] +[12.175677, "i", "x\b\u001b[200~This is just a test.\u001b[201~"] +[12.180599, "o", "\u001b[?2026h\u001b[2K\u001b[G\u001b[1A\r\u001b[2C\u001b[2Ax\u001b[7m \r\u001b[2B\u001b[27m \r\u001b[?2026l"] +[12.200164, "o", "\u001b[?2026h\u001b[2C\u001b[37mPasting\u001b[1Ctext…\u001b[39m\r\r\n\u001b[?2026l"] +[12.287743, "o", "\u001b[?2026h\r\u001b[2C\u001b[3AThis\u001b[1Cis\u001b[1Cjust\u001b[1Ca\u001b[1Ctest.\u001b[7m \u001b[27m\r\r\n\r\n\r\n\u001b[?2026l"] +[12.303796, "o", "\u001b[?2026h\u001b[2K\u001b[G\u001b[1A \r\u001b[?2026l"] +[14.334743, "i", "\r"] +[14.35569, "o", "\u001b[?2026h\r\u001b[3A\u001b[31m✻\u001b[39m \u001b[33mRazzle-dazzling…\u001b[31m \u001b[39m \r\u001b[1B ⎿  \u001b[37mTip: Use /memory to view and manage Claude memory\r\u001b[1B\u001b[39m \r\r\n\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\u001b[39m\u001b[22m\r\r\n\u001b[37m❯ \u001b[39m\u001b[7m \u001b[27m\r\r\n\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\u001b[39m\u001b[22m\r\r\n\u001b[2C\u001b[37mesc\u001b[1Cto\u001b[1Cinterrupt\u001b[39m\r\r\n\u001b[?2026l"] +[14.355912, "o", "\u001b]0;✳ Claude Code\u0007"] +[14.383801, "o", "\u001b[?2026h\r\u001b[2C\u001b[7A\u001b[31mRazzle-dazzli\u001b[93mng…\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[14.409189, "o", "\u001b[?2026h\r\u001b[7A\u001b[40m\u001b[30m❯ \u001b[97mThis is just a test. \r\u001b[2C\u001b[1B\u001b[39m\u001b[49m \u001b[1C \r\u001b[1B\u001b[31m✻\u001b[1CRazzle-dazzli\u001b[93mng…\u001b[31m \r\u001b[1B\u001b[39m ⎿  \u001b[37mTip: Use /memory to view and manage Claude memory\u001b[39m \r\u001b[1B \r\u001b[2B\u001b[37m❯ \u001b[39m\u001b[7m \u001b[27m \r\r\n\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\u001b[39m\u001b[22m\r\r\n\u001b[2C\u001b[37mesc\u001b[1Cto\u001b[1Cinterrupt\u001b[39m\r\r\n\u001b[?2026l"] +[14.416464, "o", "\u001b[?2026h\r\u001b[15C\u001b[7A\u001b[31mn\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[14.471671, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m✶\u001b[15Cg\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[14.520331, "o", "\u001b[?2026h\r\u001b[17C\u001b[7A\u001b[31m…\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[14.631253, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m*\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[14.732256, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m✢\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[14.842505, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m·\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.097342, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m✢\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.205335, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m*\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.307332, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m✶\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.318166, "o", "\u001b]0;⠐ Claude Code\u0007"] +[15.41391, "o", "\u001b[?2026h\r\u001b[2C\u001b[7A\u001b[93mR\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.464335, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m✻\u001b[2C\u001b[93ma\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.51605, "o", "\u001b[?2026h\r\u001b[4C\u001b[7A\u001b[93mz\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.574806, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m✽\u001b[1CR\u001b[2C\u001b[93mz\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.627079, "o", "\u001b[?2026h\r\u001b[3C\u001b[7A\u001b[31ma\u001b[2C\u001b[93ml\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.677917, "o", "\u001b[?2026h\r\u001b[4C\u001b[7A\u001b[31mz\u001b[2C\u001b[93me\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.729024, "o", "\u001b[?2026h\r\u001b[5C\u001b[7A\u001b[31mz\u001b[2C\u001b[93m-\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.781594, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m✻\u001b[5Cl\u001b[2C\u001b[93md\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.833063, "o", "\u001b[?2026h\r\u001b[7C\u001b[7A\u001b[31me\u001b[2C\u001b[93ma\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.884251, "o", "\u001b[?2026h\r\u001b[8C\u001b[7A\u001b[31m-\u001b[2C\u001b[93mz\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.940731, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m✶\u001b[8Cda\u001b[1C\u001b[93mzl\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[15.990075, "o", "\u001b[?2026h\r\u001b[11C\u001b[7A\u001b[31mz\u001b[2C\u001b[93mi\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[16.041306, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m*\u001b[11Cz\u001b[2C\u001b[93mn\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[16.094007, "o", "\u001b[?2026h\r\u001b[13C\u001b[7A\u001b[31mlin\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[16.155877, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m✢\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[16.267831, "o", "\u001b[?2026h\r\u001b[7A\u001b[31m·\u001b[39m\r\r\n\r\n\r\n\r\n\r\n\r\n\r\n\u001b[?2026l"] +[16.279255, "o", "\u001b]0;⠂ Claude Code\u0007"] +[16.42499, "o", "\u001b[?2026h\r\u001b[7A\u001b[97m●\u001b[1C\u001b[39mGot it! Let me know\u001b[1Cif\u001b[1Cyou\u001b[1Cneed\u001b[1Canything.\r\u001b[2C\u001b[1B \u001b[1C \r\u001b[1B\u001b[31m·\u001b[1CRazzle-dazzling… \r\u001b[1B\u001b[39m ⎿  \u001b[37mTip: Use /memory to view and manage Claude memory\u001b[39m \r\u001b[1B \r\u001b[2B\u001b[37m❯ \u001b[39m\u001b[7m \u001b[27m \r\r\n\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\u001b[39m\u001b[22m\r\r\n\u001b[2C\u001b[37mesc\u001b[1Cto\u001b[1Cinterrupt\u001b[39m\r\r\n\u001b[?2026l"] +[16.465844, "o", "\u001b[?2026h\u001b[2K\u001b[1A\u001b[2K\u001b[1A\u001b[2K\u001b[G\u001b[1A\r\u001b[4A\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\r\u001b[1B\u001b[39m\u001b[22m❯ \u001b[7m \u001b[1C\u001b[27m \r\u001b[1B\u001b[2m\u001b[37m────────────────────────────────────────────────────────────────────────────────\r\u001b[1B\u001b[39m\u001b[22m \u001b[37m? for shortcuts\u001b[39m \r\u001b[1B \r\u001b[1B \r\u001b[1B \r\u001b[2A\u001b[?2026l"] +[16.466036, "o", "\u001b]0;✳ Claude Code\u0007"] +[16.466216, "o", "\u001b]0;✳ Claude Code\u0007"] +[19.078841, "o", "\u001b[?2026h\r\u001b[2C\u001b[3A\u001b[7mw\u001b[27m\u001b[2mhat's in this repo?\u001b[22m\r\r\n\r\n\r\n\u001b[?2026l"] diff --git a/e2e/cast/testdata/claude.txt b/e2e/cast/testdata/claude.txt new file mode 100644 index 0000000..ef8d840 --- /dev/null +++ b/e2e/cast/testdata/claude.txt @@ -0,0 +1,4 @@ +user hello +agent Hello! How can I help you today? +user This is just a test. +agent Got it! Let me know if you need anything.