Skip to Content
Native IntegrationDiagnostics Controller

DiagnosticsController — DPC v0.1

What this is. The mobile control plane for the IDE’s Problems panel — the same panel that lives next to Terminal / Output / Debug Console in VS Code and Cursor. Surfaces every vscode.Diagnostic the language servers + lint extensions emit (TypeScript, ESLint, Pylint, rust-analyzer, etc.) and lets the phone tap a problem to open the file at that line in the IDE.

Scope: observe + reveal only. No Quick Fix / CodeAction execution in v0.1 — that’s a separate doctrine pass with its own protocol contract.

Why this exists

Coding-agent control without seeing the project’s red-squiggle health is half-blind. Knowing “this build has 47 TypeScript errors right now” from your phone — and being able to tap one to see exactly where — is genuinely useful for delegating fix-up work to Claude and tracking whether it landed.

Capability

'diagnostics_observe' (added to SUPPORTED_CAPABILITIES in packages/protocol/src/messages.ts).

Negotiated in the client’s auth / relay_client_auth capabilities array. Without it, the server rejects diagnostics_list / diagnostic_reveal_request with errorCode: 'capability_not_negotiated' and does NOT broadcast diagnostic_event to the client.

This is a read-only observation surface — no destructive action. The PWA negotiates it unconditionally; native clients should follow the same pattern.

Wire shapes

// Severity is an OPEN STRING UNION — clients must degrade gracefully on // unknown future values (LSP authors invent new severities; we render whatever // string arrives without assuming a closed enum). type DiagnosticSeverity = 'error' | 'warning' | 'information' | 'hint' | (string & {}) interface DiagnosticRange { start: { line: number; character: number } end: { line: number; character: number } } interface DiagnosticInfo { id: string // stable opaque id (server-assigned; survives debounce) uri: string // full file URI (file:// or vscode-remote://...) fileName: string // basename — server pre-computed; client just renders range: DiagnosticRange severity: DiagnosticSeverity message: string // truncated to 1024 chars server-side source?: string // 'ts', 'eslint', 'pylint', … — open string code?: string // VS Code's `code` flattened to a flat string createdAt: number // wall-clock when the registry first observed } // Open string union — DPC v0.1 ships 'snapshot' | 'cleared_file', plus future // kinds that may appear as additive extensions. Renderers MUST tolerate // unknown values gracefully. type DiagnosticEventKind = 'snapshot' | 'cleared_file' | (string & {}) // Client → Server interface DiagnosticsListMessage { type: 'diagnostics_list'; requestId?; traceId? } interface DiagnosticRevealRequestMessage { type: 'diagnostic_reveal_request' requestId?; traceId? uri: string range: DiagnosticRange preserveFocus?: boolean } // Server → Client interface DiagnosticsListResultMessage { type: 'diagnostics_list_result' requestId? items: DiagnosticInfo[] // capped at 1000 truncated?: boolean // true if pre-cap totalCount > items.length totalCount: number // pre-cap total seqId: number } interface DiagnosticEventMessage { type: 'diagnostic_event' event: DiagnosticEventKind uri: string items?: DiagnosticInfo[] // present for 'snapshot'; omitted for 'cleared_file' seqId: number traceId?: string } interface DiagnosticRevealAckMessage { type: 'diagnostic_reveal_ack' requestId? uri: string delivered: boolean errorCode?: string // open: 'capability_not_negotiated' | 'reveal_failed' | … error?: string // human-readable fallback seqId: number }

Data flow

┌────────────────┐ onDidChangeDiagnostics ┌──────────────────────┐ │ vscode.languages│ ──────────────────────────► │ DiagnosticsRegistry │ │ (LSPs + lint) │ │ (~500ms debounce) │ └────────────────┘ └──────────┬───────────┘ │ emits │ 'diagnostic_event' ┌──────────────────────┐ │ VSRelayServer │ │ broadcastIfCapable( │ │ 'diagnostics_observe')│ └──────────┬───────────┘ │ via WS / relay ┌──────────────────────┐ │ PWA / native renderer │ │ ProblemsView │ │ (rows grouped by │ │ file, click→reveal)│ └──────────────────────┘

The renderer is “thin”: it converts wire shape → list view, sends diagnostic_reveal_request on tap. No business logic, no filtering, no severity decisions. Everything decisive lives server-side.

Stable VS Code APIs only

DPC v0.1 — like TPC v0.1 — uses stable APIs only. No proposed APIs, no @enabledApiProposals:

APIPurpose
vscode.languages.getDiagnostics()Full snapshot for diagnostics_list
vscode.languages.getDiagnostics(uri)Per-URI snapshot used inside the debounce flush
vscode.languages.onDidChangeDiagnosticsChange feed (debounced)
vscode.window.showTextDocument(uri, { selection })Reveal — same primitive as Terminal reveal

Doctrine rules clients MUST follow

  1. Severity / source / code / event kind are open string unions. Future LSPs invent new severities; future event kinds may be added additively. The renderer must never crash on an unknown value. PWA convention: known severities get coloured chips (ERR red, WARN amber, INFO blue, HINT grey); unknown severities render with a neutral chip and the literal label.
  2. The client renders what the server sends. No client-side filtering. If you want to hide INFO/HINT, that’s a future server-driven primitive — not a hardcoded local switch. Doctrine: app fetches data, server decides which data to send.
  3. No Quick Fix / CodeAction execution. Tapping a problem reveals it. Tapping is NOT a “fix it” surface. Quick Fix is reserved for a future additive protocol message (code_action_list / apply_code_action_request) gated by its own doctrine pass.
  4. uri may be sensitive. Display the fileName (basename) prominently; show the URI in a smaller tone. Don’t log the full URI in client-side telemetry — the server only logs uriLength per its redaction contract.
  5. Diagnostic ids are not stable across server restarts. Use them only for in-session dedup (same logical problem = same id across debounce cycles). Don’t persist them.
  6. Truncation is signalled by the server. If diagnostics_list_result.truncated === true, render a “truncated — totalCount total” badge so the user knows the list isn’t comprehensive.

What clients MUST NOT do

  • ❌ Implement “Fix” buttons that run anything on-device (Apple App Store guidelines forbid downloading + executing code; the protocol forbids it too).
  • ❌ Cache a closed enum of severity strings and crash on anything outside it.
  • ❌ Filter by severity / source / code on the client side. If a filter primitive is needed, it ships as a server-driven severities field on the request.
  • ❌ Persist diagnostics to disk. The slice is ephemeral by design — repopulated on watch / connected / diagnostics_list.
  • ❌ Show diagnostics for files the user hasn’t opened. The server emits everything VS Code knows about; the renderer should show the full Problems list (mirrors how the IDE’s Problems panel works — it’s workspace-wide).

Reference implementation pointers

When you port this to Swift / Kotlin, the dispatcher → store flow maps cleanly to your platform’s state container (@Observable on iOS, StateFlow on Android). The renderer is just a list of file groups; each group is just a list of severity-chip + message rows. Nothing platform-specific lives in the protocol — keep that line clean.