package main import ( "fmt" "strings" acp "github.com/coder/acp-go-sdk" ) // scriptState holds mutable state shared across script commands and sequence emitters. type scriptState struct { toolCallCounter int lastToolID string // monitorTools maps user-supplied Monitor taskID -> generated toolCallID, // so e2e:monitor_event and e2e:monitor_end can reference an earlier // e2e:monitor_start by name without the script needing to track IDs. monitorTools map[string]acp.ToolCallId } // state is the process-wide script state. Use resetState() in tests // to get a clean starting point instead of touching fields directly. var state = &scriptState{monitorTools: map[string]acp.ToolCallId{}} func resetState() { state = &scriptState{monitorTools: map[string]acp.ToolCallId{}} } func nextToolID() acp.ToolCallId { state.toolCallCounter++ id := fmt.Sprintf("mock_tool_%04d", state.toolCallCounter) state.lastToolID = id return acp.ToolCallId(id) } // --- Atomic emitters --- // emitThinking emits a thinking block with random delay. func emitThinking(e *emitter, model string) { randomDelay(model) e.thought("Analyzing the request and considering the best approach...") } // emitReadFile emits a Read tool call using a real workspace file. func emitReadFile(e *emitter, model string) { toolID := nextToolID() f := randomFile() snippet := readFileSnippet(f.absPath, 30) randomDelay(model) e.startTool(toolID, "Read "+f.relPath, acp.ToolKindRead, map[string]any{"file_path": f.absPath}, acp.ToolCallLocation{Path: f.absPath}) randomDelay(model) e.completeTool(toolID, map[string]any{"content": snippet}) } // emitEditFile emits an Edit tool call with permission request. func emitEditFile(e *emitter, model string) { toolID := nextToolID() f := randomFile() oldStr, newStr := pickEditableFragment(f.absPath) randomDelay(model) input := map[string]any{ "file_path": f.absPath, "old_string": oldStr, "new_string": newStr, } e.startTool(toolID, "Edit "+f.relPath, acp.ToolKindEdit, input, acp.ToolCallLocation{Path: f.absPath}) if e.requestPermission(toolID, "Edit "+f.relPath, acp.ToolKindEdit, input) { e.completeTool(toolID, map[string]any{toolKeyResult: "File edited successfully: " + f.absPath}) } else { e.completeTool(toolID, map[string]any{toolKeyResult: "Permission denied for Edit"}) e.text("Permission denied for Edit, skipping.") } } // emitShellExec emits a Bash tool call with permission request. func emitShellExec(e *emitter, model string) { toolID := nextToolID() randomDelay(model) input := map[string]any{ "command": "go test ./...", "description": "Run all tests", } e.startTool(toolID, "Run tests", acp.ToolKindExecute, input) if e.requestPermission(toolID, "Run tests", acp.ToolKindExecute, input) { e.completeTool(toolID, map[string]any{"output": "ok \tgithub.com/example/project\t0.042s\nPASS"}) } else { e.completeTool(toolID, map[string]any{"output": "Permission denied"}) e.text("Permission denied for Bash, skipping.") } } // emitCodeSearch emits a Grep tool call using real workspace file paths. func emitCodeSearch(e *emitter, model string) { toolID := nextToolID() randomDelay(model) searchPatterns := []string{"func ", "import ", "TODO", "return ", toolKeyError, "type "} pattern := searchPatterns[state.toolCallCounter%len(searchPatterns)] f := randomFile() e.startTool(toolID, "Search for \""+pattern+"\"", acp.ToolKindSearch, map[string]any{"pattern": pattern, "path": f.absPath}) randomDelay(model) paths := randomFilePaths(3) var results []string for i, p := range paths { results = append(results, fmt.Sprintf("%s:%d:%s found here", p, (i+1)*10, strings.TrimSpace(pattern))) } e.completeTool(toolID, map[string]any{"matches": strings.Join(results, "\n")}) } // emitSubagent emits a claude-style subagent (Task) tool call plus a sibling // search tool call. func emitSubagent(e *emitter, model string) { taskToolID := nextToolID() randomDelay(model) e.startSubagentTool(taskToolID, "Explore codebase", "Find all files and summarize the project structure", "general-purpose") randomDelay(model) e.thought("Exploring the project structure...") randomDelay(model) paths := randomFilePaths(5) e.text(fmt.Sprintf("Found %d files. The project structure looks well-organized.", len(paths))) randomDelay(model) // Sibling glob tool call (subagents do not nest over ACP) childToolID := nextToolID() e.startTool(childToolID, "Glob **/*", acp.ToolKindSearch, map[string]any{"pattern": "**/*"}) randomDelay(model) e.completeTool(childToolID, map[string]any{"files": strings.Join(paths, "\n")}) randomDelay(model) e.text("Project structure analysis complete.") randomDelay(model) e.completeSubagentTool(taskToolID, fmt.Sprintf("Subagent completed: Found %d files across the project.", len(paths)), subagentResult{ agentID: "agent_dev_0001", subagentType: "general-purpose", durationMs: 1850, totalTokens: 7421, toolUseCount: 1, }) } // emitTodo emits a TodoWrite tool call. func emitTodo(e *emitter, model string) { toolID := nextToolID() randomDelay(model) e.startTool(toolID, "Update todo list", acp.ToolKindOther, map[string]any{ "todos": []map[string]any{ {"id": "1", "content": "Review code changes", "status": "in_progress"}, {"id": "2", "content": "Run tests", "status": "pending"}, {"id": "3", "content": "Update documentation", "status": "pending"}, }, }) randomDelay(model) e.completeTool(toolID, map[string]any{toolKeyResult: "Todo list updated: 3 items (1 in progress, 2 pending)"}) } // emitMermaidSequence emits a rich markdown message containing mermaid diagrams. func emitMermaidSequence(e *emitter, model string) { emitThinking(e, model) randomDelay(model) e.text("Here's an overview of the system architecture with diagrams:\n\n" + "## System Flow\n\n" + "The following flowchart shows the request processing pipeline:\n\n" + "```mermaid\n" + "flowchart TD\n" + " A([Client Request]) --> B{Auth Check}\n" + " B -->|Valid| C[API Gateway]\n" + " B -->|Invalid| D[401 Unauthorized]\n" + " C --> E[Load Balancer]\n" + " E --> F[Service A]\n" + " E --> G[Service B]\n" + " F --> H[(Database)]\n" + " G --> H\n" + " H --> I[Response Builder]\n" + " I --> J([Client Response])\n" + "```\n\n" + "## Sequence Diagram\n\n" + "Here's how the authentication flow works between components:\n\n" + "```mermaid\n" + "sequenceDiagram\n" + " participant U as User\n" + " participant FE as Frontend\n" + " participant API as API Server\n" + " participant DB as Database\n" + " U->>FE: Login Request\n" + " FE->>API: POST /auth/login\n" + " API->>DB: Verify Credentials\n" + " DB-->>API: User Record\n" + " API-->>FE: JWT Token\n" + " FE-->>U: Redirect to Dashboard\n" + "```\n\n" + "### Key Points\n\n" + "- All requests go through the **API Gateway** for rate limiting\n" + "- Authentication uses **JWT tokens** with a 24h expiry\n" + "- Services communicate via `gRPC` internally\n" + "- Database connections use a **connection pool** (max 50)\n") } // emitMarkdownShowcase emits a rich multi-message demonstration of every // rendered text/markdown type: headings, paragraphs (short and long), inline // code, fenced code blocks, lists, tables, blockquotes, bold/italic, and hr. func emitMarkdownShowcase(e *emitter, model string) { randomDelay(model) e.text(markdownHeadingsMsg()) randomDelay(model) e.text(markdownLongParaMsg()) randomDelay(model) e.text(markdownCodeBlocksMsg()) randomDelay(model) e.text(markdownListsMsg()) randomDelay(model) e.text(markdownTableBlockquoteMsg()) } func markdownHeadingsMsg() string { return "# Heading 1\n\n" + "## Heading 2\n\n" + "### Heading 3\n\n" + "Short paragraph. Inline code looks like `const x = 42` in the middle of a sentence.\n\n" + "Another short one: call `os.Exit(1)` to terminate, or use `fmt.Errorf(\"wrap: %w\", err)` for wrapping errors." } func markdownLongParaMsg() string { return "## Long Paragraph\n\n" + "This is a deliberately long paragraph to test line wrapping and reading comfort at various chat panel widths. " + "When a user writes a prompt that requires careful reasoning, the agent first decomposes the request into sub-problems, " + "then resolves each one in dependency order before synthesising a final answer. " + "The approach mirrors how a senior engineer would tackle an unfamiliar codebase: read the entry points, " + "trace the data flow, identify the invariants that must hold, and only then start making changes. " + "Getting this sequence right matters because changes made without understanding the invariants tend to " + "introduce subtle regressions that only surface under production load. This is exactly the kind of bug that is " + "hardest to diagnose after the fact.\n\n" + "A second long paragraph follows immediately to check spacing between blocks. " + "The spacing should feel consistent with single-paragraph messages, neither too tight nor too loose. " + "Inter-paragraph spacing is controlled by `margin-top` on `.markdown-body p + p`, " + "which currently sits at `0.5em`: enough breathing room without pushing content off-screen on short viewports." } func markdownCodeBlocksMsg() string { return "## Code Blocks\n\n" + "TypeScript:\n\n" + "```typescript\n" + "interface Session {\n" + " id: string;\n" + " createdAt: Date;\n" + " messages: Message[];\n" + "}\n\n" + "async function fetchSession(id: string): Promise {\n" + " const res = await fetch(`/api/sessions/${id}`);\n" + " if (!res.ok) throw new Error(`HTTP ${res.status}`);\n" + " return res.json();\n" + "}\n" + "```\n\n" + "Go:\n\n" + "```go\n" + "func (s *Service) CreateTask(ctx context.Context, req CreateTaskRequest) (*Task, error) {\n" + "\tif req.Title == \"\" {\n" + "\t\treturn nil, fmt.Errorf(\"title is required\")\n" + "\t}\n" + "\ttask := &Task{\n" + "\t\tID: uuid.New().String(),\n" + "\t\tTitle: req.Title,\n" + "\t\tCreatedAt: time.Now(),\n" + "\t}\n" + "\treturn s.repo.Save(ctx, task)\n" + "}\n" + "```\n\n" + "Shell:\n\n" + "```bash\n" + "# Build and run tests\n" + "make -C apps/backend build && \\\n" + " go test ./... -race -count=1 | tee /tmp/test.log\n" + "```" } func markdownListsMsg() string { return "## Lists\n\n" + "Unordered:\n\n" + "- First item with `inline code` in it\n" + "- Second item: **bold text** and *italic text* inline\n" + "- Third item with a longer description: this tests wrapping inside list items at narrow panel widths, " + "which is a common layout scenario in split-pane editors\n" + " - Nested item A\n" + " - Nested item B\n\n" + "Ordered:\n\n" + "1. Read the spec\n" + "2. Write a failing test\n" + "3. Implement the feature\n" + "4. Refactor until the code is clean\n" + "5. Open a PR and request review" } func markdownTableBlockquoteMsg() string { return "## Table\n\n" + "| Command | Description | Example |\n" + "|---------|-------------|----------|\n" + "| `/slow` | Slow response | `/slow 10s` |\n" + "| `/thinking` | Reasoning blocks | `/thinking` |\n" + "| `/mermaid` | Mermaid diagram | `/mermaid` |\n" + "| `/markdown` | This showcase | `/markdown` |\n\n" + "---\n\n" + "## Blockquote\n\n" + "> Programs must be written for people to read, and only incidentally for machines to execute.\n" + "> Harold Abelson\n\n" + "> A second blockquote paragraph to verify spacing between consecutive quote blocks " + "and to ensure the left-border styling extends for the full height of wrapped lines.\n\n" + "---\n\n" + "## Inline Styles\n\n" + "**Bold**, *italic*, ~~strikethrough~~, and `inline code` all in one sentence. " + "A path like `apps/web/app/globals.css` mid-sentence, then more prose after it. " + "And a longer inline snippet: `color-mix(in oklch, var(--foreground) 10%, var(--muted))` " + "should wrap cleanly without breaking the surrounding text flow." } // emitWebFetch emits a WebFetch tool call. func emitWebFetch(e *emitter, model string) { toolID := nextToolID() randomDelay(model) e.startTool(toolID, "Fetch API docs", acp.ToolKindFetch, map[string]any{ "url": "https://example.com/api/docs", clarificationPromptKey: "Extract the API endpoints and their descriptions", }) randomDelay(model) e.completeTool(toolID, map[string]any{ "content": "API Documentation:\n- GET /api/v1/users - List all users\n- POST /api/v1/users - Create a new user\n- GET /api/v1/users/:id - Get user by ID\n- PUT /api/v1/users/:id - Update user\n- DELETE /api/v1/users/:id - Delete user", }) }