| name | cross-surface-changes |
| description | Coordinating changes that span multiple surfaces (Python backend, VS Code extension, React webview). Use when implementing features that require changes across the stack, updating APIs, or modifying message protocols. |
Cross-Surface Development for RoboView
Surface Overview
RoboView has three connected surfaces:
┌─────────────────────────────────────────────────────────────────┐
│ VS Code │
│ ┌──────────────────────┐ ┌──────────────────────────────┐ │
│ │ Extension (TS) │◄──►│ Webview (React) │ │
│ │ │ │ │ │
│ │ - Commands │ │ - Dashboard │ │
│ │ - Services │ │ - Keyword Usage Panel │ │
│ │ - Webview Panel │ │ - Robocop Panel │ │
│ └──────────┬───────────┘ └──────────────────────────────┘ │
│ │ │
│ │ HTTP (axios) │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Backend (Python) │ │
│ │ │ │
│ │ - FastAPI Server │ │
│ │ - Services │ │
│ │ - Registries │ │
│ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Communication Protocols
Backend ↔ Extension (HTTP/REST)
Extension Backend
│ │
│ GET /system/health │
│──────────────────────────►│
│ 200 { status: "ok" } │
│◄──────────────────────────│
│ │
│ POST /system/initialize │
│ { project_path: "..." } │
│──────────────────────────►│
│ 200 { status: "init" } │
│◄──────────────────────────│
│ │
│ GET /overview/kpis │
│──────────────────────────►│
│ 200 { kpis: {...} } │
│◄──────────────────────────│
Extension ↔ Webview (postMessage)
Webview Extension
│ │
│ { command: 'getKPIs' } │
│──────────────────────────►│
│ │ (Extension fetches from backend)
│ { command: 'kpisData', │
│ data: {...} } │
│◄──────────────────────────│
│ │
│ { command: 'openFile', │
│ data: { path, line }} │
│──────────────────────────►│
│ │ (Extension opens file in editor)
Adding a New Feature Across Surfaces
Example: Adding "Get Keyword Details" Feature
Step 1: Backend Schema & Endpoint
class KeywordDetails(BaseModel):
keyword_id: UUID
name: str
documentation: str | None
code: str
source: str
line_number: int
called_keywords: list[str]
usage_count: int
class KeywordDetailsResponse(BaseModel):
keyword_id: str
name: str
documentation: str | None
code: str
source: str
line_number: int
called_keywords: list[str]
usage_count: int
@classmethod
def from_domain(cls, domain: KeywordDetails) -> "KeywordDetailsResponse":
return cls(
keyword_id=str(domain.keyword_id),
)
@router.get("/{keyword_id}", response_model=KeywordDetailsResponse)
async def get_keyword_details(request: Request, keyword_id: str):
service = request.app.state.keyword_usage_service
details = service.get_keyword_details(keyword_id)
if not details:
raise HTTPException(404, "Keyword not found")
return KeywordDetailsResponse.from_domain(details)
Step 2: Extension Backend Client
interface KeywordDetails {
keyword_id: string;
name: string;
documentation: string | null;
code: string;
source: string;
line_number: number;
called_keywords: string[];
usage_count: number;
}
async getKeywordDetails(keywordId: string): Promise<KeywordDetails> {
const response = await this.client.get<KeywordDetails>(
`/keyword-usage/${keywordId}`
);
return response.data;
}
Step 3: Extension Message Handler
private async handleMessage(message: WebviewMessage): Promise<void> {
switch (message.command) {
case 'getKeywordDetails':
const details = await this.backendManager.getKeywordDetails(
message.data.keywordId
);
this._panel.webview.postMessage({
command: 'keywordDetailsData',
data: details,
});
break;
}
}
Step 4: Webview Types & Hooks
export interface KeywordDetails {
keyword_id: string;
name: string;
documentation: string | null;
code: string;
source: string;
line_number: number;
called_keywords: string[];
usage_count: number;
}
export function useKeywordDetails(keywordId: string | null) {
const [details, setDetails] = useState<KeywordDetails | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!keywordId) return;
setLoading(true);
vscode.postMessage({ command: 'getKeywordDetails', data: { keywordId } });
}, [keywordId]);
useMessageListener((message) => {
if (message.command === 'keywordDetailsData') {
setDetails(message.data as KeywordDetails);
setLoading(false);
}
});
return { details, loading };
}
Step 5: Webview Component
export function KeywordDetailsPanel({ keywordId }: { keywordId: string | null }) {
const { details, loading } = useKeywordDetails(keywordId);
if (loading) return <Skeleton />;
if (!details) return <EmptyState message="Select a keyword" />;
return (
<Card>
<CardHeader>
<CardTitle>{details.name}</CardTitle>
</CardHeader>
<CardContent>
<p>{details.documentation}</p>
<CodeBlock code={details.code} />
</CardContent>
</Card>
);
}
Change Coordination Checklist
When Changing Backend Schemas
When Changing API Endpoints
When Changing Message Protocol
Validation Sequence
Execute validations in order:
cd packages/roboview
pyright
pytest tests/utest/ -v
cd vscode-integration
npm run check-types
npm run lint
cd vscode-integration/webview-ui
npm run typecheck
npm run build
Common Pitfalls
Type Drift
Problem: Python schema and TypeScript types get out of sync.
Solution: Always update both in the same PR. Consider generating TS types from OpenAPI.
Message Protocol Mismatch
Problem: Webview sends command extension doesn't handle.
Solution: Define message types in one place, import in both extension and webview.
Null/Undefined Handling
Problem: Python returns None, TypeScript expects undefined.
Solution: Explicitly handle null in TypeScript types, use | null not | undefined.
CORS Issues
Problem: Webview can't reach backend directly.
Solution: All backend communication must go through extension. Webview only uses postMessage.