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) => (
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 };