From 9922588c6f7f4660f02e72f8353c83cf3051c4e2 Mon Sep 17 00:00:00 2001 From: weishu Date: Fri, 23 Jan 2026 12:59:22 +0800 Subject: [PATCH] refactor: consolidate utility functions into shared package Move duplicate isObject, asString, asNumber, and safeStringify functions from multiple modules into a centralized shared/src/utils.ts module and update imports across cli, server, and web packages. This eliminates code duplication and improves maintainability. --- cli/src/agent/backends/acp/AcpMessageHandler.ts | 3 ++- cli/src/agent/backends/acp/AcpSdkBackend.ts | 2 +- cli/src/agent/utils.ts | 8 +------- cli/src/claude/utils/permissionHandler.ts | 5 +---- cli/src/codex/codexMcpClient.ts | 5 +---- server/src/notifications/eventParsing.ts | 5 +---- server/src/sync/todos.ts | 5 +---- shared/src/index.ts | 1 + shared/src/messages.ts | 6 ++---- .../chat/normalizeUtils.ts => shared/src/utils.ts | 3 ++- web/src/chat/normalize.ts | 2 +- web/src/chat/normalizeAgent.ts | 2 +- web/src/chat/normalizeUser.ts | 2 +- web/src/chat/tracer.ts | 6 +----- .../AssistantChat/messages/ToolMessage.tsx | 15 +-------------- web/src/components/ToolCard/PermissionFooter.tsx | 5 +---- web/src/components/ToolCard/ToolCard.tsx | 13 +------------ web/src/components/ToolCard/askUserQuestion.ts | 7 ++----- web/src/components/ToolCard/knownTools.tsx | 5 +---- .../components/ToolCard/views/CodexDiffView.tsx | 5 +---- .../components/ToolCard/views/CodexPatchView.tsx | 6 +----- web/src/components/ToolCard/views/EditView.tsx | 5 +---- .../ToolCard/views/ExitPlanModeView.tsx | 6 +----- .../components/ToolCard/views/MultiEditView.tsx | 5 +---- .../components/ToolCard/views/TodoWriteView.tsx | 6 +----- web/src/components/ToolCard/views/WriteView.tsx | 5 +---- web/src/components/ToolCard/views/_results.tsx | 13 +------------ web/src/hooks/useSSE.ts | 5 +---- web/src/lib/assistant-runtime.ts | 11 +---------- web/src/realtime/hooks/contextFormatters.ts | 5 +---- 30 files changed, 34 insertions(+), 138 deletions(-) rename web/src/chat/normalizeUtils.ts => shared/src/utils.ts (80%) diff --git a/cli/src/agent/backends/acp/AcpMessageHandler.ts b/cli/src/agent/backends/acp/AcpMessageHandler.ts index 01e68c5d..0f744da2 100644 --- a/cli/src/agent/backends/acp/AcpMessageHandler.ts +++ b/cli/src/agent/backends/acp/AcpMessageHandler.ts @@ -1,5 +1,6 @@ import type { AgentMessage, PlanItem } from '@/agent/types'; -import { asString, deriveToolName, isObject } from '@/agent/utils'; +import { asString, isObject } from '@hapi/protocol'; +import { deriveToolName } from '@/agent/utils'; import { ACP_SESSION_UPDATE_TYPES } from './constants'; function normalizeStatus(status: unknown): 'pending' | 'in_progress' | 'completed' | 'failed' { diff --git a/cli/src/agent/backends/acp/AcpSdkBackend.ts b/cli/src/agent/backends/acp/AcpSdkBackend.ts index b64a15af..973596ab 100644 --- a/cli/src/agent/backends/acp/AcpSdkBackend.ts +++ b/cli/src/agent/backends/acp/AcpSdkBackend.ts @@ -1,5 +1,5 @@ import type { AgentBackend, AgentMessage, AgentSessionConfig, PermissionRequest, PermissionResponse, PromptContent } from '@/agent/types'; -import { asString, isObject } from '@/agent/utils'; +import { asString, isObject } from '@hapi/protocol'; import { AcpStdioTransport, type AcpStderrError } from './AcpStdioTransport'; import { AcpMessageHandler } from './AcpMessageHandler'; import { logger } from '@/ui/logger'; diff --git a/cli/src/agent/utils.ts b/cli/src/agent/utils.ts index 80f88c74..4f9830fb 100644 --- a/cli/src/agent/utils.ts +++ b/cli/src/agent/utils.ts @@ -1,10 +1,4 @@ -export function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object'; -} - -export function asString(value: unknown): string | null { - return typeof value === 'string' ? value : null; -} +import { isObject } from '@hapi/protocol'; export function deriveToolName(input: { title?: string | null; diff --git a/cli/src/claude/utils/permissionHandler.ts b/cli/src/claude/utils/permissionHandler.ts index 5815cedd..623a86ce 100644 --- a/cli/src/claude/utils/permissionHandler.ts +++ b/cli/src/claude/utils/permissionHandler.ts @@ -15,6 +15,7 @@ import { getToolName } from "./getToolName"; import { EnhancedMode, PermissionMode } from "../loop"; import { getToolDescriptor } from "./getToolDescriptor"; import { delay } from "@/utils/time"; +import { isObject } from "@hapi/protocol"; import { BasePermissionHandler, type PendingPermissionRequest, @@ -33,10 +34,6 @@ interface PermissionResponse { const PLAN_EXIT_MODES: PermissionMode[] = ['default', 'acceptEdits', 'bypassPermissions']; -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object'; -} - function isAskUserQuestionToolName(toolName: string): boolean { return toolName === 'AskUserQuestion' || toolName === 'ask_user_question'; } diff --git a/cli/src/codex/codexMcpClient.ts b/cli/src/codex/codexMcpClient.ts index 8352704a..7c82ab74 100644 --- a/cli/src/codex/codexMcpClient.ts +++ b/cli/src/codex/codexMcpClient.ts @@ -4,6 +4,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { isObject } from '@hapi/protocol'; import { logger } from '@/ui/logger'; import { isProcessAlive, killProcess } from '@/utils/process'; import type { CodexSessionConfig, CodexToolResponse } from './types'; @@ -20,10 +21,6 @@ type ElicitRequestedSchema = { required?: string[]; }; -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object'; -} - function extractRequestedSchema(params: Record): ElicitRequestedSchema | null { const raw = params.requestedSchema; if (!isObject(raw)) return null; diff --git a/server/src/notifications/eventParsing.ts b/server/src/notifications/eventParsing.ts index 504b42a2..ac615520 100644 --- a/server/src/notifications/eventParsing.ts +++ b/server/src/notifications/eventParsing.ts @@ -1,3 +1,4 @@ +import { isObject } from '@hapi/protocol' import type { SyncEvent } from '../sync/syncEngine' type EventEnvelope = { @@ -5,10 +6,6 @@ type EventEnvelope = { data?: unknown } -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - function extractEventEnvelope(message: unknown): EventEnvelope | null { if (!isObject(message)) { return null diff --git a/server/src/sync/todos.ts b/server/src/sync/todos.ts index 7fdc0900..a95c5db9 100644 --- a/server/src/sync/todos.ts +++ b/server/src/sync/todos.ts @@ -1,3 +1,4 @@ +import { isObject } from '@hapi/protocol' import { unwrapRoleWrappedRecordEnvelope } from '@hapi/protocol/messages' import { TodoItemSchema, TodosSchema } from '@hapi/protocol/schemas' import type { TodoItem } from '@hapi/protocol/types' @@ -5,10 +6,6 @@ import type { TodoItem } from '@hapi/protocol/types' export { TodoItemSchema, TodosSchema } export type { TodoItem } -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - function extractTodosFromClaudeOutput(content: Record): TodoItem[] | null { if (content.type !== 'output') return null diff --git a/shared/src/index.ts b/shared/src/index.ts index 55d6004a..93984422 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -1,4 +1,5 @@ export * from './messages' export * from './modes' export * from './sessionSummary' +export * from './utils' export type * from './types' diff --git a/shared/src/messages.ts b/shared/src/messages.ts index 09e569aa..12d20dc9 100644 --- a/shared/src/messages.ts +++ b/shared/src/messages.ts @@ -1,13 +1,11 @@ +import { isObject } from './utils' + type RoleWrappedRecord = { role: string content: unknown meta?: unknown } -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - export function isRoleWrappedRecord(value: unknown): value is RoleWrappedRecord { if (!isObject(value)) return false return typeof value.role === 'string' && 'content' in value diff --git a/web/src/chat/normalizeUtils.ts b/shared/src/utils.ts similarity index 80% rename from web/src/chat/normalizeUtils.ts rename to shared/src/utils.ts index be1cf589..39f20b85 100644 --- a/web/src/chat/normalizeUtils.ts +++ b/shared/src/utils.ts @@ -13,7 +13,8 @@ export function asNumber(value: unknown): number | null { export function safeStringify(value: unknown): string { if (typeof value === 'string') return value try { - return JSON.stringify(value, null, 2) + const stringified = JSON.stringify(value, null, 2) + return typeof stringified === 'string' ? stringified : String(value) } catch { return String(value) } diff --git a/web/src/chat/normalize.ts b/web/src/chat/normalize.ts index 9b44d459..0215dac8 100644 --- a/web/src/chat/normalize.ts +++ b/web/src/chat/normalize.ts @@ -1,7 +1,7 @@ import { unwrapRoleWrappedRecordEnvelope } from '@hapi/protocol/messages' +import { safeStringify } from '@hapi/protocol' import type { DecryptedMessage } from '@/types/api' import type { NormalizedMessage } from '@/chat/types' -import { safeStringify } from '@/chat/normalizeUtils' import { isCodexContent, isSkippableAgentContent, normalizeAgentRecord } from '@/chat/normalizeAgent' import { normalizeUserRecord } from '@/chat/normalizeUser' diff --git a/web/src/chat/normalizeAgent.ts b/web/src/chat/normalizeAgent.ts index 81301f6f..5fe7a8fc 100644 --- a/web/src/chat/normalizeAgent.ts +++ b/web/src/chat/normalizeAgent.ts @@ -1,5 +1,5 @@ import type { AgentEvent, NormalizedAgentContent, NormalizedMessage, ToolResultPermission } from '@/chat/types' -import { asNumber, asString, isObject } from '@/chat/normalizeUtils' +import { asNumber, asString, isObject } from '@hapi/protocol' function normalizeToolResultPermissions(value: unknown): ToolResultPermission | undefined { if (!isObject(value)) return undefined diff --git a/web/src/chat/normalizeUser.ts b/web/src/chat/normalizeUser.ts index 023fa7a6..3785c8f6 100644 --- a/web/src/chat/normalizeUser.ts +++ b/web/src/chat/normalizeUser.ts @@ -1,6 +1,6 @@ import type { NormalizedMessage } from '@/chat/types' import type { AttachmentMetadata } from '@/types/api' -import { isObject } from '@/chat/normalizeUtils' +import { isObject } from '@hapi/protocol' function parseAttachments(raw: unknown): AttachmentMetadata[] | undefined { if (!Array.isArray(raw)) return undefined diff --git a/web/src/chat/tracer.ts b/web/src/chat/tracer.ts index 411fbb41..db3981c8 100644 --- a/web/src/chat/tracer.ts +++ b/web/src/chat/tracer.ts @@ -1,4 +1,5 @@ import type { NormalizedMessage } from '@/chat/types' +import { isObject } from '@hapi/protocol' export type TracedMessage = NormalizedMessage & { sidechainId?: string @@ -10,10 +11,6 @@ type TracerState = { orphanMessages: Map } -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - function getMessageUuid(message: NormalizedMessage): string | null { if (message.role === 'agent' && message.content.length > 0) { const first = message.content[0] as unknown as Record @@ -124,4 +121,3 @@ export function traceMessages(messages: NormalizedMessage[]): TracedMessage[] { return results } - diff --git a/web/src/components/AssistantChat/messages/ToolMessage.tsx b/web/src/components/AssistantChat/messages/ToolMessage.tsx index e1804c41..ca1c1f61 100644 --- a/web/src/components/AssistantChat/messages/ToolMessage.tsx +++ b/web/src/components/AssistantChat/messages/ToolMessage.tsx @@ -1,6 +1,7 @@ import type { ToolCallMessagePartProps } from '@assistant-ui/react' import type { ChatBlock } from '@/chat/types' import type { ToolCallBlock } from '@/chat/types' +import { isObject, safeStringify } from '@hapi/protocol' import { getEventPresentation } from '@/chat/presentation' import { CodeBlock } from '@/components/CodeBlock' import { MarkdownRenderer } from '@/components/MarkdownRenderer' @@ -10,20 +11,6 @@ import { ToolCard } from '@/components/ToolCard/ToolCard' import { useHappyChatContext } from '@/components/AssistantChat/context' import { CliOutputBlock } from '@/components/CliOutputBlock' -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - -function safeStringify(value: unknown): string { - if (typeof value === 'string') return value - try { - const stringified = JSON.stringify(value, null, 2) - return typeof stringified === 'string' ? stringified : String(value) - } catch { - return String(value) - } -} - function isToolCallBlock(value: unknown): value is ToolCallBlock { if (!isObject(value)) return false if (value.kind !== 'tool-call') return false diff --git a/web/src/components/ToolCard/PermissionFooter.tsx b/web/src/components/ToolCard/PermissionFooter.tsx index e40bec87..348a6ed2 100644 --- a/web/src/components/ToolCard/PermissionFooter.tsx +++ b/web/src/components/ToolCard/PermissionFooter.tsx @@ -2,14 +2,11 @@ import { useMemo, useState } from 'react' import type { ApiClient } from '@/api/client' import type { SessionMetadataSummary } from '@/types/api' import type { ChatToolCall, ToolPermission } from '@/chat/types' +import { isObject } from '@hapi/protocol' import { usePlatform } from '@/hooks/usePlatform' import { Spinner } from '@/components/Spinner' import { useTranslation } from '@/lib/use-translation' -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - function getInputStringAny(input: unknown, keys: string[]): string | null { if (!isObject(input)) return null for (const key of keys) { diff --git a/web/src/components/ToolCard/ToolCard.tsx b/web/src/components/ToolCard/ToolCard.tsx index 2c336393..196e4cba 100644 --- a/web/src/components/ToolCard/ToolCard.tsx +++ b/web/src/components/ToolCard/ToolCard.tsx @@ -2,6 +2,7 @@ import type { ToolCallBlock } from '@/chat/types' import type { ApiClient } from '@/api/client' import type { SessionMetadataSummary } from '@/types/api' import { memo, useEffect, useMemo, useState, type ReactNode } from 'react' +import { isObject, safeStringify } from '@hapi/protocol' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { CodeBlock } from '@/components/CodeBlock' import { MarkdownRenderer } from '@/components/MarkdownRenderer' @@ -17,18 +18,6 @@ import { usePointerFocusRing } from '@/hooks/usePointerFocusRing' import { cn } from '@/lib/utils' import { useTranslation } from '@/lib/use-translation' -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - -function safeStringify(value: unknown): string { - try { - return JSON.stringify(value, null, 2) - } catch { - return String(value) - } -} - function getInputString(input: unknown, key: string): string | null { if (!isObject(input)) return null const value = input[key] diff --git a/web/src/components/ToolCard/askUserQuestion.ts b/web/src/components/ToolCard/askUserQuestion.ts index e2957cdb..95940637 100644 --- a/web/src/components/ToolCard/askUserQuestion.ts +++ b/web/src/components/ToolCard/askUserQuestion.ts @@ -1,3 +1,5 @@ +import { isObject } from '@hapi/protocol' + export type AskUserQuestionOption = { label: string description: string | null @@ -15,10 +17,6 @@ export type AskUserQuestionQuestionInfo = { question: string | null } -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - export function isAskUserQuestionToolName(toolName: string): boolean { return toolName === 'AskUserQuestion' || toolName === 'ask_user_question' } @@ -77,4 +75,3 @@ export function extractAskUserQuestionQuestionsInfo(input: unknown): AskUserQues } return questions } - diff --git a/web/src/components/ToolCard/knownTools.tsx b/web/src/components/ToolCard/knownTools.tsx index 8d07e5d7..a4559086 100644 --- a/web/src/components/ToolCard/knownTools.tsx +++ b/web/src/components/ToolCard/knownTools.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from 'react' import type { SessionMetadataSummary } from '@/types/api' +import { isObject } from '@hapi/protocol' import { BulbIcon, ClipboardIcon, EyeIcon, FileDiffIcon, GlobeIcon, PuzzleIcon, QuestionIcon, RocketIcon, SearchIcon, TerminalIcon, WrenchIcon } from '@/components/ToolCard/icons' import { basename, resolveDisplayPath } from '@/utils/path' @@ -13,10 +14,6 @@ export type ToolPresentation = { minimal: boolean } -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - function getInputStringAny(input: unknown, keys: string[]): string | null { if (!isObject(input)) return null for (const key of keys) { diff --git a/web/src/components/ToolCard/views/CodexDiffView.tsx b/web/src/components/ToolCard/views/CodexDiffView.tsx index 017782bc..615fc381 100644 --- a/web/src/components/ToolCard/views/CodexDiffView.tsx +++ b/web/src/components/ToolCard/views/CodexDiffView.tsx @@ -1,10 +1,7 @@ import type { ToolViewProps } from '@/components/ToolCard/views/_all' +import { isObject } from '@hapi/protocol' import { DiffView } from '@/components/DiffView' -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - function parseUnifiedDiff(unifiedDiff: string): { oldText: string; newText: string; fileName?: string } { const lines = unifiedDiff.split('\n') const oldLines: string[] = [] diff --git a/web/src/components/ToolCard/views/CodexPatchView.tsx b/web/src/components/ToolCard/views/CodexPatchView.tsx index 37ea26e1..e330d6a7 100644 --- a/web/src/components/ToolCard/views/CodexPatchView.tsx +++ b/web/src/components/ToolCard/views/CodexPatchView.tsx @@ -1,10 +1,7 @@ import type { ToolViewProps } from '@/components/ToolCard/views/_all' +import { isObject } from '@hapi/protocol' import { basename, resolveDisplayPath } from '@/utils/path' -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - export function CodexPatchView(props: ToolViewProps) { const input = props.block.tool.input if (!isObject(input) || !isObject(input.changes)) return null @@ -25,4 +22,3 @@ export function CodexPatchView(props: ToolViewProps) { ) } - diff --git a/web/src/components/ToolCard/views/EditView.tsx b/web/src/components/ToolCard/views/EditView.tsx index da84a497..6a706917 100644 --- a/web/src/components/ToolCard/views/EditView.tsx +++ b/web/src/components/ToolCard/views/EditView.tsx @@ -1,10 +1,7 @@ import type { ToolViewProps } from '@/components/ToolCard/views/_all' +import { isObject } from '@hapi/protocol' import { DiffView } from '@/components/DiffView' -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - export function EditView(props: ToolViewProps) { const input = props.block.tool.input if (!isObject(input)) return null diff --git a/web/src/components/ToolCard/views/ExitPlanModeView.tsx b/web/src/components/ToolCard/views/ExitPlanModeView.tsx index 4e82f1aa..96f1aed1 100644 --- a/web/src/components/ToolCard/views/ExitPlanModeView.tsx +++ b/web/src/components/ToolCard/views/ExitPlanModeView.tsx @@ -1,10 +1,7 @@ import type { ToolViewProps } from '@/components/ToolCard/views/_all' +import { isObject } from '@hapi/protocol' import { MarkdownRenderer } from '@/components/MarkdownRenderer' -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - export function ExitPlanModeView(props: ToolViewProps) { const input = props.block.tool.input if (!isObject(input)) return null @@ -12,4 +9,3 @@ export function ExitPlanModeView(props: ToolViewProps) { if (!plan) return null return } - diff --git a/web/src/components/ToolCard/views/MultiEditView.tsx b/web/src/components/ToolCard/views/MultiEditView.tsx index 4464bae5..d3dc0920 100644 --- a/web/src/components/ToolCard/views/MultiEditView.tsx +++ b/web/src/components/ToolCard/views/MultiEditView.tsx @@ -1,10 +1,7 @@ import type { ToolViewProps } from '@/components/ToolCard/views/_all' +import { isObject } from '@hapi/protocol' import { DiffView } from '@/components/DiffView' -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - type Edit = { old_string: string; new_string: string } const MAX_COMPACT_EDITS = 3 diff --git a/web/src/components/ToolCard/views/TodoWriteView.tsx b/web/src/components/ToolCard/views/TodoWriteView.tsx index 987f66a7..4367eba9 100644 --- a/web/src/components/ToolCard/views/TodoWriteView.tsx +++ b/web/src/components/ToolCard/views/TodoWriteView.tsx @@ -1,4 +1,5 @@ import type { ToolViewProps } from '@/components/ToolCard/views/_all' +import { isObject } from '@hapi/protocol' type TodoItem = { id?: string @@ -7,10 +8,6 @@ type TodoItem = { priority?: 'high' | 'medium' | 'low' } -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - function extractTodos(input: unknown, result: unknown): TodoItem[] { const todosFromInput = isObject(input) && Array.isArray(input.todos) ? input.todos.filter(isObject) @@ -63,4 +60,3 @@ export function TodoWriteView(props: ToolViewProps) { ) } - diff --git a/web/src/components/ToolCard/views/WriteView.tsx b/web/src/components/ToolCard/views/WriteView.tsx index f7cd6e54..af4629a9 100644 --- a/web/src/components/ToolCard/views/WriteView.tsx +++ b/web/src/components/ToolCard/views/WriteView.tsx @@ -1,10 +1,7 @@ import type { ToolViewProps } from '@/components/ToolCard/views/_all' +import { isObject } from '@hapi/protocol' import { DiffView } from '@/components/DiffView' -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - export function WriteView(props: ToolViewProps) { const input = props.block.tool.input if (!isObject(input)) return null diff --git a/web/src/components/ToolCard/views/_results.tsx b/web/src/components/ToolCard/views/_results.tsx index c2e552a6..fe24212d 100644 --- a/web/src/components/ToolCard/views/_results.tsx +++ b/web/src/components/ToolCard/views/_results.tsx @@ -1,20 +1,9 @@ import type { ToolViewComponent, ToolViewProps } from '@/components/ToolCard/views/_all' +import { isObject, safeStringify } from '@hapi/protocol' import { CodeBlock } from '@/components/CodeBlock' import { MarkdownRenderer } from '@/components/MarkdownRenderer' import { basename, resolveDisplayPath } from '@/utils/path' -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - -function safeStringify(value: unknown): string { - try { - return JSON.stringify(value, null, 2) - } catch { - return String(value) - } -} - function parseToolUseError(message: string): { isToolUseError: boolean; errorMessage: string | null } { const regex = /(.*?)<\/tool_use_error>/s const match = message.match(regex) diff --git a/web/src/hooks/useSSE.ts b/web/src/hooks/useSSE.ts index c131a1a5..36f06944 100644 --- a/web/src/hooks/useSSE.ts +++ b/web/src/hooks/useSSE.ts @@ -1,13 +1,10 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useQueryClient } from '@tanstack/react-query' +import { isObject } from '@hapi/protocol' import type { SyncEvent } from '@/types/api' import { queryKeys } from '@/lib/query-keys' import { clearMessageWindow, ingestIncomingMessages } from '@/lib/message-window-store' -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - type SSESubscription = { all?: boolean sessionId?: string diff --git a/web/src/lib/assistant-runtime.ts b/web/src/lib/assistant-runtime.ts index 532cb5fe..3425cea0 100644 --- a/web/src/lib/assistant-runtime.ts +++ b/web/src/lib/assistant-runtime.ts @@ -1,21 +1,12 @@ import { useCallback, useMemo } from 'react' import type { AppendMessage, AttachmentAdapter, ThreadMessageLike } from '@assistant-ui/react' import { useExternalMessageConverter, useExternalStoreRuntime } from '@assistant-ui/react' +import { safeStringify } from '@hapi/protocol' import { renderEventLabel } from '@/chat/presentation' import type { ChatBlock, CliOutputBlock } from '@/chat/types' import type { AgentEvent, ToolCallBlock } from '@/chat/types' import type { AttachmentMetadata, MessageStatus as HappyMessageStatus, Session } from '@/types/api' -function safeStringify(value: unknown): string { - if (typeof value === 'string') return value - try { - const stringified = JSON.stringify(value, null, 2) - return typeof stringified === 'string' ? stringified : String(value) - } catch { - return String(value) - } -} - export type HappyChatMessageMetadata = { kind: 'user' | 'assistant' | 'tool' | 'event' | 'cli-output' status?: HappyMessageStatus diff --git a/web/src/realtime/hooks/contextFormatters.ts b/web/src/realtime/hooks/contextFormatters.ts index fd20f1e2..a6dba4bb 100644 --- a/web/src/realtime/hooks/contextFormatters.ts +++ b/web/src/realtime/hooks/contextFormatters.ts @@ -1,4 +1,5 @@ import { unwrapRoleWrappedRecordEnvelope } from '@hapi/protocol/messages' +import { isObject } from '@hapi/protocol' import type { DecryptedMessage, Session } from '@/types/api' import { VOICE_CONFIG } from '../voiceConfig' @@ -22,10 +23,6 @@ function isContentArray(content: unknown): content is ContentItem[] { return Array.isArray(content) } -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - function normalizeRole(role: string | null | undefined): NormalizedRole | null { if (role === 'agent' || role === 'assistant') return 'assistant' if (role === 'user') return 'user'