From 43977ca570625c65bb589829bc09af6fae7966bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sat, 30 May 2026 13:38:21 +0100 Subject: [PATCH] fix(web): keep markdown table headers readable on narrow content (#1122) --- .agents/skills/pull-upstream/SKILL.md | 107 ----------------------- apps/backend/AGENTS.md | 1 + apps/backend/cmd/mock-agent/AGENTS.md | 41 +++++++++ apps/backend/cmd/mock-agent/scenarios.go | 19 ++++ apps/web/app/globals.css | 25 ++++++ 5 files changed, 86 insertions(+), 107 deletions(-) delete mode 100644 .agents/skills/pull-upstream/SKILL.md create mode 100644 apps/backend/cmd/mock-agent/AGENTS.md diff --git a/.agents/skills/pull-upstream/SKILL.md b/.agents/skills/pull-upstream/SKILL.md deleted file mode 100644 index 8c21e38392..0000000000 --- a/.agents/skills/pull-upstream/SKILL.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -name: pull-upstream -description: Pull latest changes from the public kandev repo, rebase onto them, and fix any conflicts or issues. ---- - -# Pull Upstream - -Pull latest changes from the public kandev repo, rebase onto them, and fix any conflicts or issues. - -## Context - -This repo (`kdlbs/kandev-orchestrate`) is a private fork of `kdlbs/kandev` (public). - -``` -origin → git@github.com:kdlbs/kandev-orchestrate.git (private, our code) -upstream → git@github.com:kdlbs/kandev.git (public, community changes) -``` - -The public repo's `main` receives bug fixes, features, and community PRs independently. - -## Steps - -### 1. Fetch upstream - -```bash -git fetch upstream -``` - -### 2. Check what's new - -```bash -git log --oneline main..upstream/main -``` - -If empty, nothing to do. Report and stop. - -### 3. Update main - -Fast-forward `main` to match upstream, then push: - -```bash -git checkout main -git merge --ff-only upstream/main -git push origin main -``` - -### 4. Rebase feature branch - -Switch back to the feature branch and rebase onto updated main: - -```bash -git checkout feature/orchestrate -git rebase main -``` - -### 5. Handle conflicts - -If the rebase has conflicts: - -1. For each conflicting file, read both sides and resolve correctly -2. Our office feature code takes priority over upstream when the same area is modified -3. Upstream changes to shared infrastructure (agent lifecycle, MCP, task system, workflow engine) should be preserved — integrate both sides -4. After resolving each file: `git add ` -5. Continue: `git rebase --continue` -6. Repeat until rebase completes - -Common conflict zones: -- `cmd/kandev/main.go` — we wire office services here -- `cmd/kandev/helpers.go` — we register office routes here -- `internal/mcp/server/server.go` — we added ModeOffice -- `internal/mcp/handlers/handlers.go` — we added office MCP tools -- `apps/web/lib/state/` — we added office store slices -- `docs/specs/INDEX.md` — both sides add spec entries -- `CLAUDE.md` — may have parallel edits - -### 6. Verify - -Run the full verification pipeline. Use subshells so the second command -doesn't resolve `cd apps` relative to the first command's cwd (which -would look for a non-existent `apps/backend/apps/`): - -```bash -(cd apps/backend && go build ./... && make test && make lint) -(cd apps && pnpm --filter @kandev/web lint) -``` - -Fix any issues: -- **Import conflicts**: upstream may have renamed/moved packages (e.g. `agentctl/server/mcp` → `mcp/server`) -- **Interface changes**: upstream may have changed interfaces we implement -- **Test failures**: upstream tests may depend on state we changed -- **Lint errors**: upstream may have added stricter rules - -### 7. Push - -Force-push the rebased feature branch: - -```bash -git push --force-with-lease -``` - -### 8. Report - -Summarize: -- How many upstream commits were pulled -- Any conflicts resolved and how -- Any fixes applied post-rebase -- Build/test/lint status diff --git a/apps/backend/AGENTS.md b/apps/backend/AGENTS.md index 9eada585c3..ec116b6126 100644 --- a/apps/backend/AGENTS.md +++ b/apps/backend/AGENTS.md @@ -216,3 +216,4 @@ When you hit a limit, extract a helper function. Prefer composition over growing - `internal/agentctl/AGENTS.md` — agentctl server route groups, adapter model, ACP protocol - `internal/agentctl/server/api/AGENTS.md` — reverse-proxy body rewriting (`Accept-Encoding`), iframe-blocking header stripping - `internal/integrations/AGENTS.md` — playbook for adding a new third-party integration (Jira/Linear pattern) +- `cmd/mock-agent/AGENTS.md` — predefined `/e2e:` scenarios vs inline `e2e:...` scripts; recipe for adding a scenario to reproduce agent-output rendering bugs locally diff --git a/apps/backend/cmd/mock-agent/AGENTS.md b/apps/backend/cmd/mock-agent/AGENTS.md new file mode 100644 index 0000000000..a486d9fe2b --- /dev/null +++ b/apps/backend/cmd/mock-agent/AGENTS.md @@ -0,0 +1,41 @@ +# mock-agent — scenario library for reproducing agent output locally + +Scoped guidance for `apps/backend/cmd/mock-agent/`. The mock agent satisfies the same ACP surface a real agent does, so the rest of the stack (orchestrator, lifecycle manager, agentctl, web UI) sees it as an ordinary agent. It runs in dev/e2e mode behind `KANDEV_MOCK_AGENT=true` (set automatically by the `dev` and `e2e` profiles — see `profiles.yaml`). + +## Two ways to drive it + +**1. Predefined scenarios — `/e2e:`** + +`handler.go` routes any prompt starting with `/e2e:` to `emitPredefinedScenario(e, name)`, which looks `name` up in `scenarioRegistry` (`scenarios.go`). The registry is the single source of truth for scenario names — to add a scenario, add one map entry and one `scenario(e *emitter)` function. + +Friendly aliases live in `handler.go` next to the dispatcher: `/ask-single`, `/ask-multiple`, `/crash`, `/todo`, `/mermaid`, `/markdown`, `/sleep [n]`, `/tool:`, `/subagent`, `/subtask`. + +**2. Inline scripts — `e2e:(...)`** + +`script.go` parses prompts starting with `e2e:` (no leading slash) as multi-line scripts. Supported directives include `e2e:message(...)`, `e2e:thinking(...)`, `e2e:tool_use("Name", {...})`, `e2e:tool_result(...)`, `e2e:delay(ms)`, and the MCP-flavoured `e2e:mcp:kandev:({...})`. Comments start with `#`, blank lines are ignored. Tests live in `script_test.go`. + +Use predefined scenarios for anything you want to trigger from the slash menu or replay across sessions; use inline scripts for ad-hoc shapes inside a single E2E spec or manual session. + +## Adding a scenario to reproduce a UI rendering bug + +When a real-agent message renders poorly (markdown table, tool card, diff panel, plan summary, etc.), the cheapest reliable repro is a mock scenario that emits a representative payload. Pattern: + +1. Add `"": scenario` to `scenarioRegistry` in `scenarios.go`. +2. Implement `scenario(e *emitter)` using the helpers in `emitter.go` — `e.text`, `e.thought`, `e.startTool` / `e.completeTool`, `e.plan`, etc. Keep delays modest (`fixedDelay(100)`) so manual repro feels live but tests stay fast. +3. Trigger from any task in dev/mock mode by typing `/e2e:` as the prompt. + +This avoids burning real-agent tokens, gives you a deterministic payload to iterate against, and the scenario stays around as a permanent regression hook — anyone can re-trigger it after the fix. + +## Emitter helpers worth knowing + +`emitter.go` wraps the ACP `sessionUpdater` with helpers that shield scenarios from SDK plumbing: + +- `e.text(s)` / `e.thought(s)` — plain agent message vs reasoning channel. +- `e.startTool(id, title, kind, input, locs...)` / `e.completeTool(id, output)` — paired tool-call lifecycle. +- `e.plan(entries)` — ACP plan updates. +- `e.startMonitorTool(id, taskID, command)` / `e.emitMonitorEvent(taskID, body)` / `e.endMonitorTool(id)` — reproduces the two-frame Monitor wire pattern the kandev ACP adapter recognises. Use these instead of building raw `SessionNotification`s when you need to test the Monitor recogniser path; they embed the correct `_meta.claudeCode.toolName=Monitor` shape. +- `e.requestPermission(...)` — interactive permission flow for scenarios that need an Allow/Reject decision. + +## Tests + +`mock_agent_test.go` exercises the top-level handler dispatch. `script_test.go` covers the inline-script parser end-to-end. New scenarios don't typically need a unit test — the assertion is that the scenario name is registered in `scenarioRegistry` and the payload looks right when triggered manually. If a scenario contains conditional logic worth pinning, add a focused test that constructs an emitter with a fake `sessionUpdater` and asserts on the emitted notifications. diff --git a/apps/backend/cmd/mock-agent/scenarios.go b/apps/backend/cmd/mock-agent/scenarios.go index df0ccf34eb..13bfccb9fb 100644 --- a/apps/backend/cmd/mock-agent/scenarios.go +++ b/apps/backend/cmd/mock-agent/scenarios.go @@ -37,6 +37,7 @@ var scenarioRegistry = map[string]func(e *emitter){ "kandev-mcp-permission": scenarioKandevMCPPermission, "review-cumulative-setup": scenarioReviewCumulativeSetup, "symlink-file-setup": scenarioSymlinkFileSetup, + "markdown-table": scenarioMarkdownTable, } // emitPredefinedScenario dispatches to a named e2e scenario. @@ -813,3 +814,21 @@ func makeGitRunner(wd string) func(args ...string) error { func contextWithTimeout(seconds int) (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), time.Duration(seconds)*time.Second) } + +// scenarioMarkdownTable: emits a dense label/value markdown table modeled on a +// real agent bug-report summary. The table has narrow header labels in column 1 +// and long inline-code identifiers in column 2 — the shape that previously +// caused header words to wrap character-by-character ("Failin g test") because +// the value column starved the label column. +func scenarioMarkdownTable(e *emitter) { + fixedDelay(100) + e.text("Pushed.\n\n" + + "## Summary\n\n" + + "| | |\n" + + "|---|---|\n" + + "| **Failing test** | `TestHandleAgentBootReady_DrainsOrphanedQueuedMessage/already_WAITING_FOR_INPUT_(boot_raced_persistResumeState)` |\n" + + "| **Symptom** | `session.State = \"RUNNING\", want WAITING_FOR_INPUT` |\n" + + "| **Root cause** | Pre-existing race, not introduced by this PR. `handleAgentBootReady` synchronously flips state to `WAITING_FOR_INPUT` then spawns a goroutine that calls `PromptTask` → flips state to `RUNNING`. The test asserted on `WAITING_FOR_INPUT` immediately after the handler returned — on faster CI scheduling the goroutine wins the race. The kandev-ci container apparently schedules tighter than the github-hosted ubuntu, so it loses where the previous env got lucky. |\n" + + "| **Fix** | Cherry-picked `b8d06ea8 test(backend): fix race in TestHandleAgentBootReady_DrainsOrphanedQueuedMessage` from `feature/subtask-with-repo-se-vhz` (a parallel branch that already addressed this). The test now accepts either `WAITING_FOR_INPUT` or `RUNNING` — both prove the boot-ready flip landed and rule out the original `STARTING + queue still full` regression. |\n" + + "| **Local verification** | `go test -race -count=5` → 5/5 PASS. |\n") +} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index bbccac3feb..f9395fd408 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -775,8 +775,13 @@ .markdown-body table { border-collapse: collapse; + /* width:100% fills the container and pins the table to it, so it can never + shrink-to-fit and grow into the surrounding overflow-x-auto scroll + container (which is what produced a horizontal scrollbar under width:auto). + Long values wrap instead of overflowing. */ width: 100%; margin: 0.75rem 0; + table-layout: auto; } .markdown-body th, @@ -784,11 +789,31 @@ border: 1px solid var(--border); padding: 0.25rem 0.5rem; font-size: 13px; + vertical-align: top; +} + +.markdown-body td { + overflow-wrap: anywhere; } .markdown-body th { background: var(--muted); font-weight: 600; + white-space: nowrap; +} + +/* Label/value tables (common in agent output) put short keys in the first + column. Keep that column on one line and sized to its content so a long + value cell can't starve it down to a few characters and wrap "Failing" + into "Failin g". */ +.markdown-body td:first-child { + white-space: nowrap; + width: 1%; +} + +.markdown-body td:not(:first-child) code { + white-space: normal; + overflow-wrap: anywhere; } .markdown-body thead {