PR-Agent flagged that trace child rows rendered an extra stage-hint
cell while the header grid template only had 7 columns. Result: every
expanded child shifted module/message/latency/request rightward, and
the stage chip overlapped its neighbour in the trace timeline.
Add a dedicated 72px "Stage" column to the header grid template and
to every row variant. Standalone leaves and trace heads render an
empty span at the same width; trace children with stage hints render
the chip in that slot. Column edges now match across all row types.
Refs #1138, #1151
The first click on a trace's chevron did nothing visible when the
trace's child was the currently-selected entry. Root cause: `toggle`
only checked the `expanded` set; auto-expand kept re-injecting the
requestId so the row stayed open even after the user added it to
`expanded` (and oscillating subsequent clicks).
Replace the single `expanded` set with a tristate driven by two
sets — `userExpanded` (explicitly opened) and `userCollapsed`
(explicitly closed). Auto-expand only fires when the id is in
neither, so a click on an auto-expanded chevron now writes the
collapse intent to `userCollapsed` and the row collapses on the
first click.
Refs #1138, #1151
PR-Agent flagged that the previous derive-trace-groups split entries
into trace and leaf buckets *before* coalescing leaves. That meant two
identical no-requestId entries separated by other rows in the real
stream would still merge into a single `×N` row, hiding signal.
Walk `entries` in input order with single-pass coalescing: a leaf
only extends the previous leaf when no other entry (trace child or
otherwise) appeared between them. Trace children still gather across
interleaving since they're identified by `requestId`. Group children
get sorted ts-ascending for trace-timeline display, with the group's
positional ts pinned to the oldest child for the reverse-chrono
display sort.
Refs #1138, #1151
PR-Agent flagged that `React.MouseEvent` referenced an unimported
`React` namespace; in stricter TypeScript builds this would fail
typecheck. Import the type alias from `react` directly.
Refs #1138, #1151
PR-Agent re-review flagged that `copyText()` swallowed clipboard
errors but the row's `setJustCopied(true)` always fired regardless,
so users in insecure contexts (or with denied clipboard permission)
saw a fake success indicator while nothing was actually copied.
Make `copyText()` return a boolean and gate the "Copied" UI state
on it. When the clipboard write fails, the button stays in its idle
state and the user can retry rather than being misled.
Refs #1138, #1151
PR-Agent re-review flagged that adjacent trace rows from different
services with the same event/message/stage/level/module would still
merge because `source` was missing from the coalesce key. A request
that fans out across multiple services would lose the second service's
row behind a `× 2` badge.
Add `source` to the trace child coalesce key so each participating
service stays visible in the trace timeline.
Refs #1138, #1151
PR-Agent flagged that the row's copy-requestId widget was a
`<span role="button" tabIndex={-1}>` nested inside a row `<button>`.
Two issues:
1. Nested interactive elements inside a `<button>` is invalid HTML.
2. `tabIndex={-1}` made the copy widget unreachable via keyboard, so
the new affordance was effectively mouse-only.
Convert the row's outer element from `<button>` to `<div role="row"
tabIndex={0}>` with `onKeyDown` for Enter/Space → select. Now the row
itself is keyboard-focusable and the copy widget can be a real
`<button>` with focus-visible styling — keyboard users can Tab to it
and Enter to copy the requestId.
Refs #1138, #1151
PR-Agent flagged that the leaf and trace-child coalesce keys collapsed
on `(event, module, level, requestId, source)` which merges adjacent
entries that share that tuple but report different content (e.g.
`User logged in: alice` vs `User logged in: bob` would appear as a
single `× 2` row, hiding bob).
Add `message` to both coalesce keys so distinct content stays
distinct. Skip `latencyMs` and `metadata` — those drift per request
even on truly redundant polls and would defeat the dedup the feature
exists for.
Refs #1138, #1151
Trace rows pushed the TIME column ~30px right because the expand chevron
was an inline flex child while leaf rows had nothing in that position.
Result: every column drifted right whenever a trace row was visible.
Reserve a 16px slot at the start of every row (header + leaf + trace).
Leaf rows render an empty span; trace rows render the chevron centered
inside the slot. All columns now share the same x-coordinates regardless
of row type.
Refs #1138
Light-theme captures from Playwright MCP against `bun run dev` with
`?mock=logs` deterministic fixture and real backend (no mock):
1. `01-after-overview-mock.png` — LOGS.STREAM marker, Live activity
title, ENTRIES/TRACES/ERRORS stat strip, populated detail panel
2. `02-after-real-backend-clean.png` — real backend with default filter
active: 0 entries because all are dashboard internals; no noise
3. `03-hide-internals-toggle.png` — advanced filters showing the new
labelled toggle with red callout
4. `04-internals-shown-with-coalesce.png` — toggle off: dashboard
internals visible but a 149-stage trace coalesces to 3 rows; detail
panel shows full requestId UUID + module (C1 fix proven)
5. `05-after-mobile.png` — 390x844: header marker + title + stat strip
wrap, expanded trace renders cleanly
Asset directory will be renamed from PLACEHOLDER_PR to actual PR number;
HEAD SHA placeholder pinned after final commit.
Refs #1138, #1141, #1142
On real backend, the logs page was drowning in dashboard self-polling
noise — every `/api/logs` poll emitted a 149-stage `web-server:http`
trace, burying real provider activity.
- New workspace state `hideDashboardInternals` defaults ON. Entries
whose `source` matches `/^web-server:/i` are filtered client-side
before they reach the list.
- Toggle exposed in advanced filters as a labelled checkbox/switch with
a one-line description, so users can opt in to see internals when
debugging the dashboard itself.
- `clearAdvancedFilters` resets the toggle back to ON to keep the
default signal-clean experience.
- Header stat strip (entries / traces / errors) wired through the shell.
Backend-side `web-server:*` instrumentation stays intact (still useful
for ops debugging) — just hidden from the user-facing log feed by
default.
Refs #1138, #1141, #1142
Logs page was visually disconnected from the rest of the dashboard. Health
uses ornamental `HEALTH.ATTENTIONREQUIRED` markers, Home uses `LIVE
Account Monitor` + stat cards. Logs shipped with a 14px h1 + clinical
table that felt like a different product.
- Header now opens with a `LOGS.STREAM` mono-uppercase marker matching
the dashboard's `HEALTH.X` style, plus a 16px "Live activity" title
and a status pill.
- Below the header, a stat strip mirrors the home page's monitor layout:
ENTRIES / TRACES / ERRORS counters, errors highlighted red when >0.
- Trace row + child row font sizes lifted from 11px to 12-13px;
request-id column widened to 112px to match the standalone-row table.
- Stage-hint fallback derived from event names so the trace timeline
still renders meaningful chips when backend entries lack an explicit
`stage` field (e.g. dashboard self-polling).
- Intra-trace coalesce: identical consecutive child rows collapse to a
single row with `× N` badge so a 149-stage self-poll trace renders as
3 rows of signal instead of 149 rows of noise.
Refs #1138, #1141, #1142
Both surfaces now consume `getDisplayModule / getDisplayStage /
getDisplayRequestId / getDisplayLatency` from `utils.ts`, replacing the
previous divergence where the list row fell back to `source` while the
detail panel showed em-dash placeholders for the same entry.
Also widens the request-id column in the row table from 88px to 112px
and adds a hover-revealed clipboard icon for one-click copy of the full
requestId. List header bumped to 12px sans-serif uppercase + 9h height
for legibility.
Refs #1138, #1141, #1142
Rename .github/pr-assets/PLACEHOLDER_PR -> .github/pr-assets/1146 and
substitute {HEAD_SHA_PLACEHOLDER} with the current HEAD so commit-pinned
raw GitHub URLs in the HTML report resolve correctly.
The pinned SHA in this commit is the parent commit; once this commit is
pushed, the PR body references will resolve via the htmlpreview.github.io
proxy against the parent SHA. Reviewers see frozen-in-time evidence.
Refs #1142, #1138
Self-contained HTML report + 7 light-theme PNG captures with DOM-overlay
red callouts (#ef4444, 3px, 8px pad). Captured via Playwright MCP against
local 'bun run dev' instance with ?mock=logs deterministic 150-entry
fixture.
Surfaces:
1. Overview - default state, callouts on Live pill + ? button
2. Trace grouping - expanded vs collapsed states
3. Live-tail paused (Resume tail button)
4. Advanced filters expanded
5. Empty state - filtered-to-nothing + active filter chip
6. Keyboard shortcuts dialog
7. Mobile responsive (390x844)
Targeted-mode evidence; the 'before' UI is captured in the parent issue's
contributor screenshot. Asset directory will be renamed from PLACEHOLDER_PR
to actual PR number; HTML and asset URLs use {HEAD_SHA_PLACEHOLDER}
tokens that the maintainer pins after final commit.
Refs #1142, #1138
Three component tests under jsdom: skeleton render, search filter wiring,
row-click -> detail-panel-header flow. react-virtuoso is mocked at module
level (vi.mock) so jsdom can render rows without layout measurements.
Production behavior unchanged.
Refs #1142, #1138
docs/logging-contract.md defines the canonical LogEntry shape, the 8
lifecycle stages, requestId propagation rules, redaction policy, and the
backend ordering guarantee (per-requestId monotonic ts at emit time) that
the dashboard logs UI consumes.
CLAUDE.md updated: Node.js 14+ -> 18+ (AsyncLocalStorage is more stable on
18+ across timer / microtask / dynamic-import boundaries).
Refs #1141, #1138
Verifies that a single requestId propagates across at least 3 lifecycle
stages emitted from different modules, and that ALS context survives across
async boundaries (setImmediate, microtask, promise.then). Registered in
test:slow bucket per validate:ci-parity gate.
Refs #1141, #1138
Each target adapter (claude, codex, droid) now emits dispatch on child
spawn and respond on child exit with latencyMs. instance-manager emits
lifecycle events for instance create / start / stop so multi-instance flows
are traceable per requestId.
Refs #1141, #1138
Emit auth-stage events around OAuth round-trips and profile lookups so auth
flows are traceable end-to-end. Doctor command emits dispatch stages around
each health-check phase for clearer diagnostic logs.
Refs #1141, #1138
Wrap proxy server entry edge in withRequestContext so every inbound request
gets a requestId (reused from x-ccs-request-id header when valid UUID-ish,
freshly minted otherwise). messages-route emits 7 stages: intake / auth /
transform / route / dispatch / upstream / respond, each with latencyMs and
structured error metadata on failure.
CCS CLI entry (ccs.ts) wraps main() in runWithRequestId and emits
cli.command.start / complete / failed stages so command lifecycle is
correlatable end-to-end.
Refs #1141, #1138
- Stop cleanup interval when all auth sessions are removed (unregister,
cancel, cancelAll) so the Node event loop isn't held alive.
- getActiveSessionForProvider now returns the most recent session by
startedAt instead of the first map entry.
- HTTPS tunnel includes port in Host header when non-443.
forceValidChars() now ensures leading char is letter or underscore
(catches dot, hyphen, colon, slash — not just digits). registerTools()
always registers in mapping so collisions are detected regardless of
tool registration order.
sanitizeToolName() fallback now forces valid characters (replaces
unsupported chars with underscores, ensures leading letter/underscore)
instead of only truncating. registerTools() disambiguates colliding
sanitized names with numeric suffixes so restoreToolUse() always maps
back to the correct original.
- base-config-loader.ts moved from src/cliproxy/ to src/cliproxy/config/,
so __dirname relative path needs one more .. to reach config/ dir
- Update mock.module paths for proxy-detector and routing-strategy
in non-colocated test files
- Add src/**/__tests__/** to tsconfig.json exclude list
- Add ignores pattern for __tests__ in eslint.config.mjs
- Fix ui/src/lib/api-client.ts import path for provider-entitlement-types
- Remove stale build artifacts from ui/src/lib/ and tests/mocks/