diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx index 291bcab..41a4dfd 100644 --- a/src/components/ChatMessage.tsx +++ b/src/components/ChatMessage.tsx @@ -148,7 +148,7 @@ function renderTextBlocks(blocks: MessageBlock[]) { return getTextBlocks(blocks).map((block, i) => (
- {autoFormatText((block as any).text)} + {autoFormatText((block as Extract).text)}
)); @@ -238,7 +238,7 @@ function CopyButton({ text }: { text: string }) { /** Extract plain text from message blocks for clipboard copy */ function getPlainText(message: ChatMessageType): string { if (message.blocks.length > 0) { - return getTextBlocks(message.blocks).map(b => (b as any).text).join('\n\n'); + return getTextBlocks(message.blocks).map(b => (b as Extract).text).join('\n\n'); } return message.content; } diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx index 9b7aca4..6b4c6e3 100644 --- a/src/components/CodeBlock.tsx +++ b/src/components/CodeBlock.tsx @@ -10,7 +10,7 @@ export function CodeBlock(props: HTMLAttributes) { const handleCopy = useCallback(() => { // Extract text from the nested element - const code = (props.children as any)?.props?.children; + const code = (props.children as React.ReactElement<{ children?: string }> | undefined)?.props?.children; if (typeof code === 'string') { navigator.clipboard.writeText(code).then(() => { setCopied(true); diff --git a/src/components/ToolCall.tsx b/src/components/ToolCall.tsx index acd4739..7af9c7e 100644 --- a/src/components/ToolCall.tsx +++ b/src/components/ToolCall.tsx @@ -122,33 +122,40 @@ export function HighlightedPre({ text, className }: { text: string; className: s return
{text}
; } -function getContextHint(name: string, input: any): string | null { +function str(v: unknown): string | null { + return typeof v === 'string' ? v : null; +} + +function getContextHint(name: string, input: Record | undefined): string | null { if (!input || typeof input !== 'object') return null; switch (name) { case 'exec': - return input.command ? truncate(input.command, 60) : null; + return str(input.command) ? truncate(str(input.command)!, 60) : null; case 'Read': case 'read': case 'Write': case 'write': case 'Edit': case 'edit': - return input.file_path || input.path || null; + return str(input.file_path) || str(input.path) || null; case 'web_search': - return input.query ? truncate(input.query, 50) : null; + return str(input.query) ? truncate(str(input.query)!, 50) : null; case 'web_fetch': - return input.url ? truncate(input.url, 60) : null; + return str(input.url) ? truncate(str(input.url)!, 60) : null; case 'browser': - return input.action || null; - case 'message': - return input.action ? `${input.action}${input.target ? ' → ' + input.target : ''}` : null; + return str(input.action) || null; + case 'message': { + const action = str(input.action); + const target = str(input.target); + return action ? `${action}${target ? ' → ' + target : ''}` : null; + } case 'memory_search': - return input.query ? truncate(input.query, 50) : null; + return str(input.query) ? truncate(str(input.query)!, 50) : null; case 'memory_get': - return input.path || null; + return str(input.path) || null; case 'cron': - return input.action || null; + return str(input.action) || null; case 'sessions_spawn': - return input.task ? truncate(input.task, 50) : null; + return str(input.task) ? truncate(str(input.task)!, 50) : null; case 'image': - return input.prompt ? truncate(input.prompt, 50) : null; + return str(input.prompt) ? truncate(str(input.prompt)!, 50) : null; default: return null; } @@ -184,7 +191,7 @@ function extractImageFromResult(result: string): { src: string; remaining: strin return null; } -export function ToolCall({ name, input, result }: { name: string; input?: any; result?: string }) { +export function ToolCall({ name, input, result }: { name: string; input?: Record; result?: string }) { const t = useT(); const [open, setOpen] = useState(false); const c = getColor(name); diff --git a/src/hooks/useGateway.ts b/src/hooks/useGateway.ts index 9847a1b..c6a5753 100644 --- a/src/hooks/useGateway.ts +++ b/src/hooks/useGateway.ts @@ -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; + 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) ?? {}, 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> | 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> | undefined; + if (rawMsgs) { + /* eslint-disable @typescript-eslint/no-explicit-any -- raw gateway history messages have dynamic shape */ + const msgs: ChatMessage[] = rawMsgs.map((m: Record, 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 => 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); diff --git a/src/lib/gateway.ts b/src/lib/gateway.ts index a152c27..2f6d001 100644 --- a/src/lib/gateway.ts +++ b/src/lib/gateway.ts @@ -1,14 +1,29 @@ import { genId } from './utils'; -export type GatewayEventHandler = (event: string, payload: any) => void; -export type GatewayResponseHandler = (id: string, ok: boolean, payload: any) => void; +/** JSON-safe payload type used for gateway messages. */ +export type JsonPayload = Record; + +export type GatewayEventHandler = (event: string, payload: JsonPayload) => void; +export type GatewayResponseHandler = (id: string, ok: boolean, payload: JsonPayload) => void; + +/** Shape of an incoming WebSocket message from the gateway. */ +interface GatewayMessage { + type: 'event' | 'res'; + // event fields + event?: string; + payload?: JsonPayload; + // response fields + id?: string; + ok?: boolean; + error?: string; +} export class GatewayClient { private ws: WebSocket | null = null; - private pendingRequests = new Map void; reject: (e: any) => void }>(); + private pendingRequests = new Map void; reject: (e: unknown) => void }>(); private eventHandlers: GatewayEventHandler[] = []; private _onStatus: (s: 'disconnected' | 'connecting' | 'connected') => void = () => {}; - private reconnectTimer: any = null; + private reconnectTimer: ReturnType | null = null; private connected = false; private autoReconnect = true; @@ -44,22 +59,22 @@ export class GatewayClient { this.ws.onopen = () => { console.log('[GW] WS open'); }; this.ws.onmessage = (ev) => { - let msg: any; - try { msg = JSON.parse(ev.data); } catch { console.log('[GW] parse error', ev.data); return; } + let msg: GatewayMessage; + try { msg = JSON.parse(ev.data as string) as GatewayMessage; } catch { console.log('[GW] parse error', ev.data); return; } console.log('[GW] msg:', msg.type, msg.event || msg.id || '', msg.ok); if (msg.type === 'event') { if (msg.event === 'connect.challenge') { this.handleChallenge(); } else { - for (const h of this.eventHandlers) h(msg.event, msg.payload); + for (const h of this.eventHandlers) h(msg.event ?? '', msg.payload ?? {}); } - } else if (msg.type === 'res') { + } else if (msg.type === 'res' && msg.id) { const pending = this.pendingRequests.get(msg.id); if (pending) { this.pendingRequests.delete(msg.id); - if (msg.ok) pending.resolve(msg.payload); - else pending.reject(msg.payload || msg.error); + if (msg.ok) pending.resolve(msg.payload ?? {}); + else pending.reject(msg.payload ?? msg.error ?? 'unknown error'); } } }; @@ -118,7 +133,7 @@ export class GatewayClient { this._onStatus('disconnected'); } - request(id: string, method: string, params: any): Promise { + request(id: string, method: string, params: JsonPayload): Promise { return new Promise((resolve, reject) => { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { return reject(new Error('not connected')); @@ -134,7 +149,7 @@ export class GatewayClient { }); } - async send(method: string, params: any): Promise { + async send(method: string, params: JsonPayload): Promise { const id = genId('req'); return this.request(id, method, params); } diff --git a/src/types/index.ts b/src/types/index.ts index b97a9f1..dcf4400 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -11,7 +11,7 @@ export interface ChatMessage { export type MessageBlock = | { type: 'text'; text: string } | { type: 'thinking'; text: string } - | { type: 'tool_use'; name: string; input: any; id?: string } + | { type: 'tool_use'; name: string; input: Record; id?: string } | { type: 'tool_result'; content: string; toolUseId?: string; name?: string } | { type: 'image'; mediaType: string; data?: string; url?: string };