Skip to content

Commit 2a4f7b9

Browse files
committed
feat(chat): refine provider-aware timeline and tool cards
- derive provider usage from the active provider across mixed activity streams - improve mcp tool labels and tooltips with structured server/function names - add inline diff line selection and enable chrome devtools debugging for electron
1 parent 7b3ad22 commit 2a4f7b9

14 files changed

Lines changed: 520 additions & 122 deletions

.mcp.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"mcpServers": {
3+
"chrome-devtools": {
4+
"type": "stdio",
5+
"command": "npx",
6+
"args": ["chrome-devtools-mcp@latest", "--browserUrl", "http://127.0.0.1:9222"],
7+
"env": {}
8+
}
9+
}
10+
}

AGENTS.md

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -131,20 +131,6 @@ ChatView uses **fine-grained Zustand selectors** (one per thread/project ID) ins
131131
- Its volatile dependencies (`activePendingProgress`, `activePendingUserInput`, `composerTerminalContexts`, `composerJiraTaskContexts`) are accessed via **refs** in callbacks, not in the `useCallback` dependency array.
132132
- Fallback empty arrays use **module-level constants** (`EMPTY_TERMINAL_CONTEXT_DRAFTS`, `EMPTY_JIRA_TASK_DRAFTS`) instead of inline `[]`.
133133

134-
### Timeline rendering: NO JS virtualization (`MessagesTimeline.tsx`)
135-
136-
**CRITICAL — DO NOT REINTRODUCE `@tanstack/react-virtual` or any JS virtualizer for the messages timeline.** This has been deliberately removed twice. Upstream uses `useVirtualizer` with absolute positioning + `transform: translateY()`, but it causes persistent message overlap and scroll lag in MarCode because:
137-
138-
- Variable-height messages (markdown, code blocks, images, expandable diffs, quoted contexts) make height estimation fundamentally inaccurate
139-
- Async content (Suspense code highlighting, image loads) changes height after initial measurement
140-
- Expandable/collapsible sections (Show full diff, work groups) change height without virtualizer notification
141-
- `ChatView.tsx` directly manipulates `scrollTop` for interaction anchoring and auto-scroll, which desynchronizes from the virtualizer's internal scroll state
142-
- `SelectionReplyToolbar` wraps every assistant message in extra DOM, adding unmeasured height
143-
144-
**Instead, we use CSS `content-visibility: auto`** with `contain-intrinsic-block-size` hints. All rows render in normal document flow — overlap is physically impossible. The browser natively skips painting offscreen content, giving equivalent performance without the positioning bugs. Height estimates in `timelineHeight.ts` feed into `containIntrinsicBlockSize` for accurate scrollbar sizing.
145-
146-
When merging upstream changes that touch `MessagesTimeline.tsx`, **reject any reintroduction of `useVirtualizer`, `measureElement`, `VirtualItem`, absolute-positioned row containers, or `shouldAdjustScrollPositionOnItemSizeChange`**. Keep the `content-visibility: auto` rendering path.
147-
148134
### Timeline row memoization (`MessagesTimeline.tsx`)
149135

150136
Each timeline row renders through a `memo`'d `TimelineRowContent` component (not an inline function). When adding new row types or modifying row rendering, keep the logic inside `TimelineRowContent` to preserve per-row memoization.
@@ -238,3 +224,29 @@ MarCode maintains a comprehensive regression test suite to protect MarCode-exclu
238224
- **After every upstream merge:** run the full test suite (`bun run test` in each package) and verify all MarCode-exclusive features are preserved. Guard tests in `featureGuards.test.ts` and `workCards.guard.test.ts` will catch deleted/missing features immediately.
239225
- **When implementing new MarCode-exclusive features:** create at least an existence/smoke guard test in the relevant `featureGuards.test.ts`, plus unit tests for any pure logic. Update `FEATURES.md` with the new feature entry.
240226
- **When modifying existing features:** update or extend the corresponding tests. If changing exports, function signatures, or component structure — update the guard tests to match.
227+
228+
## Debugging the Running App: chrome-devtools MCP
229+
230+
MarCode's Electron renderer is wired to the `chrome-devtools` MCP for live debugging and perf work via Chrome DevTools Protocol.
231+
232+
**Wiring (already in place):**
233+
234+
- `apps/desktop/scripts/dev-electron.mjs` spawns Electron with `--remote-debugging-port=9222`, exposing CDP at `http://127.0.0.1:9222`.
235+
- `marcode/.mcp.json` (project-scoped) overrides the global `chrome-devtools` MCP entry to attach via `--browserUrl http://127.0.0.1:9222` instead of spawning a standalone Chrome.
236+
237+
**Prereq:** MarCode dev must be running (`bun run dev`) before MCP tool calls — otherwise `mcp__chrome-devtools__*` errors with no target. Verify with `mcp__chrome-devtools__list_pages`; the renderer URL looks like `http://127.0.0.1:5733/#/{environmentId}/{threadId}`.
238+
239+
**What it's good for:**
240+
241+
- `list_console_messages` / `get_console_message` — inspect renderer console output without manually opening DevTools
242+
- `list_network_requests` / `get_network_request` — observe WebSocket frames and HTTP traffic against the local server (`apps/server` on port from `DEFAULT_DESKTOP_BACKEND_PORT`)
243+
- `evaluate_script` — read Zustand store state, inspect React refs, trigger handlers (`window.__store?.getState()` patterns)
244+
- `take_snapshot` — DOM tree dump for layout/Tailwind cascade debugging (especially the `px-*` vs `pl-*`/`pr-*` v4 pitfall above)
245+
- `take_screenshot` — visual regressions, dark-mode/brand checks
246+
- `performance_start_trace` / `stop_trace` / `analyze_insight` — verify timeline rendering perf and catch regressions in route, composer, sidebar, and timeline interactions
247+
- `take_memory_snapshot` — leak hunting on long sessions / reconnect storms
248+
- `lighthouse_audit` — broader perf passes
249+
250+
**Use it when:** a regression reproduces only in the running app (not in unit/component tests), you need to inspect live store/projection state, or you're profiling timeline/composer/sidebar render paths against a real provider stream.
251+
252+
**Do not use it for:** isolated logic that has unit-test coverage — write a Vitest test instead. Live CDP is for whole-app behavior, not individual functions.

apps/desktop/scripts/dev-electron.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ function startApp() {
6969

7070
const app = spawn(
7171
resolveElectronPath(),
72-
[`--marcode-dev-root=${desktopDir}`, "dist-electron/main.cjs"],
72+
[`--marcode-dev-root=${desktopDir}`, "--remote-debugging-port=9222", "dist-electron/main.cjs"],
7373
{
7474
cwd: desktopDir,
7575
env: childEnv,

apps/web/src/components/ChatView.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,9 +1133,18 @@ export default function ChatView({
11331133
() => deriveLatestContextWindowSnapshot(activeThread?.activities ?? []),
11341134
[activeThread?.activities],
11351135
);
1136-
const activeProviderUsage = useMemo(
1137-
() => deriveLatestProviderUsageSnapshot(activeThread?.activities ?? []),
1138-
[activeThread?.activities],
1136+
const providerUsageActivities = useStore(
1137+
useShallow((state: AppState) => {
1138+
const activities: OrchestrationThreadActivity[] = [];
1139+
for (const thread of selectThreadsAcrossEnvironments(state)) {
1140+
for (const activity of thread.activities) {
1141+
if (activity.kind === "account.rate-limits.updated") {
1142+
activities.push(activity);
1143+
}
1144+
}
1145+
}
1146+
return activities;
1147+
}),
11391148
);
11401149
useEffect(() => {
11411150
setMountedTerminalThreadIds((currentThreadIds) => {
@@ -1337,6 +1346,11 @@ export default function ChatView({
13371346
selectedProviderByThreadId ?? threadProvider ?? "claudeAgent",
13381347
);
13391348
const selectedProvider: ProviderKind = lockedProvider ?? unlockedSelectedProvider;
1349+
const activeProviderUsage = useMemo(
1350+
() =>
1351+
deriveLatestProviderUsageSnapshot(providerUsageActivities, { provider: selectedProvider }),
1352+
[providerUsageActivities, selectedProvider],
1353+
);
13401354
const activeTimelineProvider: ProviderKind =
13411355
activeThread?.session?.provider ?? activeThread?.modelSelection.provider ?? selectedProvider;
13421356
const { modelOptions: composerModelOptions, selectedModel } = useEffectiveComposerModelState({
@@ -1378,13 +1392,23 @@ export default function ChatView({
13781392
const phase = derivePhase(activeThread?.session ?? null);
13791393
const threadActivities = activeThread?.activities ?? EMPTY_ACTIVITIES;
13801394
const timelineThreadActivities = timelineThread?.activities ?? EMPTY_ACTIVITIES;
1395+
const timelineWorkLogTurnId = useMemo(() => {
1396+
const messages = timelineThread?.messages ?? EMPTY_MESSAGES;
1397+
for (let index = messages.length - 1; index >= 0; index -= 1) {
1398+
const message = messages[index];
1399+
if (message?.role === "assistant" && message.turnId) {
1400+
return message.turnId;
1401+
}
1402+
}
1403+
return timelineLatestTurn?.turnId ?? undefined;
1404+
}, [timelineLatestTurn?.turnId, timelineThread?.messages]);
13811405
const workLogEntries = useMemo(
13821406
() =>
1383-
deriveWorkLogEntries(timelineThreadActivities, timelineLatestTurn?.turnId ?? undefined, {
1407+
deriveWorkLogEntries(timelineThreadActivities, timelineWorkLogTurnId, {
13841408
isSessionRunning: phase === "running",
13851409
provider: activeTimelineProvider,
13861410
}),
1387-
[timelineLatestTurn?.turnId, timelineThreadActivities, phase, activeTimelineProvider],
1411+
[timelineWorkLogTurnId, timelineThreadActivities, phase, activeTimelineProvider],
13881412
);
13891413
const timelineLatestTurnHasToolActivity = useMemo(
13901414
() => hasToolActivityForTurn(timelineThreadActivities, timelineLatestTurn?.turnId),

apps/web/src/components/chat/ChatComposer.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -656,8 +656,11 @@ export const ChatComposer = memo(
656656
// Provider usage (rate limits / session %)
657657
// ------------------------------------------------------------------
658658
const activeProviderUsage = useMemo(
659-
() => deriveLatestProviderUsageSnapshot(activeThreadActivities ?? []),
660-
[activeThreadActivities],
659+
() =>
660+
deriveLatestProviderUsageSnapshot(activeThreadActivities ?? [], {
661+
provider: selectedProvider,
662+
}),
663+
[activeThreadActivities, selectedProvider],
661664
);
662665

663666
// ------------------------------------------------------------------

0 commit comments

Comments
 (0)