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.Diagnosticthe 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:
| API | Purpose |
|---|---|
vscode.languages.getDiagnostics() | Full snapshot for diagnostics_list |
vscode.languages.getDiagnostics(uri) | Per-URI snapshot used inside the debounce flush |
vscode.languages.onDidChangeDiagnostics | Change feed (debounced) |
vscode.window.showTextDocument(uri, { selection }) | Reveal — same primitive as Terminal reveal |
Doctrine rules clients MUST follow
- 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.
- 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.
- 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. urimay be sensitive. Display thefileName(basename) prominently; show the URI in a smaller tone. Don’t log the full URI in client-side telemetry — the server only logsuriLengthper its redaction contract.- 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.
- 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
severitiesfield 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
- Wire shapes:
packages/protocol/src/messages.ts→ search for “DPC v0.1”. - Server-side registry:
packages/extension/src/diagnostics-registry.ts— debouncedonDidChangeDiagnostics, stable-idsha256(uri|line|col|severity|message), message truncation,cleared_filetransitions. - MessageHandler:
packages/extension/src/message-handler.ts—diagnostics_list+diagnostic_reveal_requestcases (capability gate + registry dispatch). - Server broadcast:
packages/extension/src/ws-server.ts—broadcastIfCapable('diagnostics_observe', …)subscription. - Logger redaction:
packages/extension/src/logger.ts— full URIs dropped, onlyuriLength+severityHistogramretained. - PWA dispatcher:
packages/web/src/core/dispatcher.ts— 3 new cases (diagnostics_list_result,diagnostic_event,diagnostic_reveal_ack). - PWA store actions:
packages/web/src/store/index.ts—setDiagnostics,applyDiagnosticEvent,applyDiagnosticRevealAck,revealDiagnostic. - PWA component:
packages/web/src/components/ProblemsView.tsx— row layout, severity chips, file grouping, reveal action.
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.