diff --git a/apps/backend/cmd/mock-agent/handler.go b/apps/backend/cmd/mock-agent/handler.go index 0b37e2510a..fa523715d1 100644 --- a/apps/backend/cmd/mock-agent/handler.go +++ b/apps/backend/cmd/mock-agent/handler.go @@ -80,11 +80,31 @@ func handlePrompt(e *emitter, prompt, model string) { emitTodoSequence(e, model) case strings.EqualFold(cmd, "/mermaid"): emitMermaidSequence(e, model) + case strings.EqualFold(cmd, "/markdown"): + emitMarkdownShowcase(e, model) + case strings.EqualFold(cmd, "/sleep") || strings.HasPrefix(strings.ToLower(cmd), "/sleep "): + emitSleep(e, cmd) default: emitRandomResponse(e, cmd, model) } } +// emitSleep sleeps for the requested duration (default 10s) then responds. +// Useful for simulating a slow agent turn without any tool calls. +func emitSleep(e *emitter, cmd string) { + d := 10 * time.Second + parts := strings.Fields(cmd) + if len(parts) >= 2 { + if secs, err := time.ParseDuration(parts[1] + "s"); err == nil && secs > 0 { + d = secs + } else if parsed, err2 := time.ParseDuration(parts[1]); err2 == nil && parsed > 0 { + d = parsed + } + } + time.Sleep(d) + e.text(fmt.Sprintf("Slept for %s.", d)) +} + // emitError emits an error message. func emitError(e *emitter, model string) { randomDelay(model) diff --git a/apps/backend/cmd/mock-agent/main.go b/apps/backend/cmd/mock-agent/main.go index f7cc8508aa..e15dac27c1 100644 --- a/apps/backend/cmd/mock-agent/main.go +++ b/apps/backend/cmd/mock-agent/main.go @@ -264,6 +264,8 @@ func mockAvailableCommands() []acp.AvailableCommand { {Name: "tool:edit", Description: "Emit an edit file tool call"}, {Name: "tool:exec", Description: "Emit a shell exec tool call"}, {Name: "tool:search", Description: "Emit a search tool call"}, + {Name: "markdown", Description: "Render all markdown/text types"}, + {Name: "sleep", Description: "Sleep without output (default 10s)", Input: hint("seconds (e.g. 30)")}, } } diff --git a/apps/backend/cmd/mock-agent/sequences.go b/apps/backend/cmd/mock-agent/sequences.go index eaa03f8a74..fe19ccf061 100644 --- a/apps/backend/cmd/mock-agent/sequences.go +++ b/apps/backend/cmd/mock-agent/sequences.go @@ -215,6 +215,122 @@ func emitMermaidSequence(e *emitter, model string) { "- 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 — 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() diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 9b2a3fc24c..f2ad547aa1 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -715,12 +715,14 @@ margin-bottom: 0.25rem; } -/* Inline code: neutral foreground on a soft muted background. +/* Inline code: neutral foreground on a slightly elevated background with a + subtle border so chips read clearly against the card surface. Linked code (a > code) drops the background and inherits the link color so file references read as links, not as another code chip. */ .markdown-body :not(pre) > code, .markdown-body code:not(pre code) { - background: var(--muted); + background: color-mix(in oklch, var(--foreground) 10%, var(--muted)); + border: 1px solid color-mix(in oklch, var(--foreground) 10%, transparent); color: var(--foreground); padding: 0.1rem 0.375rem; border-radius: 0.25rem; @@ -744,12 +746,13 @@ .markdown-body:not(.markdown-body-user) a > code, .markdown-body:not(.markdown-body-user) a code:not(pre code) { background: transparent; + border: none; color: inherit; } .markdown-body:not(.markdown-body-user) a:hover > code, .markdown-body:not(.markdown-body-user) a:hover code:not(pre code) { - background: var(--muted); + background: color-mix(in oklch, var(--foreground) 10%, var(--muted)); } .markdown-body blockquote {