fix(web): keep markdown table headers readable on narrow content (#1122)

This commit is contained in:
Carlos Florêncio
2026-05-30 13:38:21 +01:00
committed by GitHub
parent 72441fb69a
commit 43977ca570
5 changed files with 86 additions and 107 deletions
-107
View File
@@ -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 <file>`
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
+1
View File
@@ -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:<name>` scenarios vs inline `e2e:...` scripts; recipe for adding a scenario to reproduce agent-output rendering bugs locally
+41
View File
@@ -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:<name>`**
`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<Name>(e *emitter)` function.
Friendly aliases live in `handler.go` next to the dispatcher: `/ask-single`, `/ask-multiple`, `/crash`, `/todo`, `/mermaid`, `/markdown`, `/sleep [n]`, `/tool:<name>`, `/subagent`, `/subtask`.
**2. Inline scripts — `e2e:<directive>(...)`**
`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:<tool>({...})`. 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 `"<your-name>": scenario<YourName>` to `scenarioRegistry` in `scenarios.go`.
2. Implement `scenario<YourName>(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:<your-name>` 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.
+19
View File
@@ -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")
}
+25
View File
@@ -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 {