refactor: replace any types with proper TypeScript types across gateway client, hooks, and components

- Add GatewayMessage and JsonPayload interfaces to gateway.ts
- Type WebSocket message parsing with GatewayMessage instead of any
- Use ReturnType<typeof setTimeout> for timer refs
- Type chat event payload destructuring explicitly
- Add ChatPayloadMessage interface for extractText()
- Replace any casts in loadSessions/loadHistory with typed assertions
- Add str() helper in ToolCall.tsx for safe unknown→string extraction
- Use Extract<MessageBlock, ...> type guards instead of `as any` casts
- Type CodeBlock children prop properly
This commit is contained in:
Nicolas Varrot
2026-02-11 21:17:44 +00:00
parent d724a8ca0b
commit 693229c14e
6 changed files with 91 additions and 57 deletions

View File

@@ -1,17 +1,21 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { GatewayClient } from '../lib/gateway';
import { GatewayClient, type JsonPayload } from '../lib/gateway';
import { genIdempotencyKey } from '../lib/utils';
import { getStoredCredentials, storeCredentials, clearCredentials } from '../components/LoginScreen';
import type { ChatMessage, MessageBlock, ConnectionStatus, Session } from '../types';
function extractText(message: any): string {
interface ChatPayloadMessage {
content?: string | Array<{ type: string; text?: string }>;
}
function extractText(message: ChatPayloadMessage | undefined): string {
if (!message) return '';
const content = message.content;
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return content
.filter((b: any) => b.type === 'text' && typeof b.text === 'string')
.map((b: any) => b.text)
.filter((b) => b.type === 'text' && typeof b.text === 'string')
.map((b) => b.text as string)
.join('\n');
}
return '';
@@ -72,7 +76,11 @@ export function useGateway() {
}
if (event !== 'chat') return;
const { state, runId, message, errorMessage, sessionKey: evtSession } = payload;
const state = payload.state as string | undefined;
const runId = payload.runId as string;
const message = payload.message as ChatPayloadMessage | undefined;
const errorMessage = payload.errorMessage as string | undefined;
const evtSession = payload.sessionKey as string | undefined;
if (evtSession) {
if (state === 'delta') {
@@ -167,12 +175,12 @@ export function useGateway() {
}
}, [setupClient]);
const handleAgentEvent = useCallback((payload: any) => {
const handleAgentEvent = useCallback((payload: JsonPayload) => {
if (payload?.stream !== 'tool') return;
const data = payload.data ?? {};
const phase = data.phase;
const toolCallId = data.toolCallId;
const name = data.name || 'tool';
const data = (payload.data ?? {}) as Record<string, unknown>;
const phase = data.phase as string | undefined;
const toolCallId = data.toolCallId as string | undefined;
const name = (data.name as string) || 'tool';
if (!toolCallId) return;
setMessages(prev => {
@@ -185,11 +193,12 @@ export function useGateway() {
updated.blocks.push({
type: 'tool_use' as const,
name,
input: data.args,
input: (data.args as Record<string, unknown>) ?? {},
id: toolCallId,
});
} else if (phase === 'result') {
const result = typeof data.result === 'string' ? data.result : JSON.stringify(data.result, null, 2);
const rawResult = data.result;
const result = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult, null, 2);
updated.blocks.push({
type: 'tool_result' as const,
content: result?.slice(0, 500) || '',
@@ -205,15 +214,16 @@ export function useGateway() {
const loadSessions = useCallback(async () => {
try {
const res = await clientRef.current?.send('sessions.list', {});
if (res?.sessions) {
setSessions(res.sessions.map((s: any) => ({
key: s.key || s.sessionKey,
label: s.label || s.key || s.sessionKey,
messageCount: s.messageCount,
totalTokens: s.totalTokens,
contextTokens: s.contextTokens,
inputTokens: s.inputTokens,
outputTokens: s.outputTokens,
const sessionList = res?.sessions as Array<Record<string, unknown>> | undefined;
if (sessionList) {
setSessions(sessionList.map((s) => ({
key: (s.key || s.sessionKey) as string,
label: (s.label || s.key || s.sessionKey) as string,
messageCount: s.messageCount as number | undefined,
totalTokens: s.totalTokens as number | undefined,
contextTokens: s.contextTokens as number | undefined,
inputTokens: s.inputTokens as number | undefined,
outputTokens: s.outputTokens as number | undefined,
})));
}
} catch {}
@@ -222,9 +232,10 @@ export function useGateway() {
const loadHistory = useCallback(async (sessionKey: string) => {
try {
const res = await clientRef.current?.send('chat.history', { sessionKey, limit: 100 });
if (res?.messages) {
const rawMsgs: any[] = res.messages;
const msgs: ChatMessage[] = rawMsgs.map((m: any, i: number) => {
const rawMsgs = res?.messages as Array<Record<string, unknown>> | undefined;
if (rawMsgs) {
/* eslint-disable @typescript-eslint/no-explicit-any -- raw gateway history messages have dynamic shape */
const msgs: ChatMessage[] = rawMsgs.map((m: Record<string, any>, i: number) => {
const blocks: MessageBlock[] = [];
if (m.content) {
if (Array.isArray(m.content)) {
@@ -269,19 +280,20 @@ export function useGateway() {
return {
id: m.id || `hist-${i}`,
role,
content: blocks.filter(b => b.type === 'text').map(b => (b as any).text).join(''),
content: blocks.filter((b): b is Extract<MessageBlock, { type: 'text' }> => b.type === 'text').map(b => b.text).join(''),
timestamp: m.timestamp || Date.now(),
blocks,
};
});
const merged: ChatMessage[] = [];
for (const msg of msgs) {
if ((msg as any).isToolResult && merged.length > 0 && merged[merged.length - 1].role === 'assistant') {
const isToolResult = 'isToolResult' in msg && (msg as ChatMessage & { isToolResult?: boolean }).isToolResult;
if (isToolResult && merged.length > 0 && merged[merged.length - 1].role === 'assistant') {
merged[merged.length - 1] = {
...merged[merged.length - 1],
blocks: [...merged[merged.length - 1].blocks, ...msg.blocks],
};
} else if ((msg as any).isToolResult) {
} else if (isToolResult) {
// skip orphan
} else {
merged.push(msg);