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.
This commit is contained in:
weishu
2026-01-23 16:04:37 +08:00
parent a41d6aa662
commit 9922588c6f
30 changed files with 34 additions and 138 deletions
@@ -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' {
+1 -1
View File
@@ -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';
+1 -7
View File
@@ -1,10 +1,4 @@
export function isObject(value: unknown): value is Record<string, unknown> {
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;
+1 -4
View File
@@ -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<string, unknown> {
return Boolean(value) && typeof value === 'object';
}
function isAskUserQuestionToolName(toolName: string): boolean {
return toolName === 'AskUserQuestion' || toolName === 'ask_user_question';
}
+1 -4
View File
@@ -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<string, unknown> {
return Boolean(value) && typeof value === 'object';
}
function extractRequestedSchema(params: Record<string, unknown>): ElicitRequestedSchema | null {
const raw = params.requestedSchema;
if (!isObject(raw)) return null;
+1 -4
View File
@@ -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<string, unknown> {
return Boolean(value) && typeof value === 'object'
}
function extractEventEnvelope(message: unknown): EventEnvelope | null {
if (!isObject(message)) {
return null
+1 -4
View File
@@ -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<string, unknown> {
return Boolean(value) && typeof value === 'object'
}
function extractTodosFromClaudeOutput(content: Record<string, unknown>): TodoItem[] | null {
if (content.type !== 'output') return null
+1
View File
@@ -1,4 +1,5 @@
export * from './messages'
export * from './modes'
export * from './sessionSummary'
export * from './utils'
export type * from './types'
+2 -4
View File
@@ -1,13 +1,11 @@
import { isObject } from './utils'
type RoleWrappedRecord = {
role: string
content: unknown
meta?: unknown
}
function isObject(value: unknown): value is Record<string, unknown> {
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
@@ -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)
}
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -5
View File
@@ -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<string, NormalizedMessage[]>
}
function isObject(value: unknown): value is Record<string, unknown> {
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<string, unknown>
@@ -124,4 +121,3 @@ export function traceMessages(messages: NormalizedMessage[]): TracedMessage[] {
return results
}
@@ -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<string, unknown> {
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
@@ -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<string, unknown> {
return Boolean(value) && typeof value === 'object'
}
function getInputStringAny(input: unknown, keys: string[]): string | null {
if (!isObject(input)) return null
for (const key of keys) {
+1 -12
View File
@@ -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<string, unknown> {
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]
@@ -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<string, unknown> {
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
}
+1 -4
View File
@@ -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<string, unknown> {
return Boolean(value) && typeof value === 'object'
}
function getInputStringAny(input: unknown, keys: string[]): string | null {
if (!isObject(input)) return null
for (const key of keys) {
@@ -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<string, unknown> {
return Boolean(value) && typeof value === 'object'
}
function parseUnifiedDiff(unifiedDiff: string): { oldText: string; newText: string; fileName?: string } {
const lines = unifiedDiff.split('\n')
const oldLines: string[] = []
@@ -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<string, unknown> {
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) {
</div>
)
}
@@ -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<string, unknown> {
return Boolean(value) && typeof value === 'object'
}
export function EditView(props: ToolViewProps) {
const input = props.block.tool.input
if (!isObject(input)) return null
@@ -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<string, unknown> {
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 <MarkdownRenderer content={plan} />
}
@@ -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<string, unknown> {
return Boolean(value) && typeof value === 'object'
}
type Edit = { old_string: string; new_string: string }
const MAX_COMPACT_EDITS = 3
@@ -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<string, unknown> {
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) {
</div>
)
}
@@ -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<string, unknown> {
return Boolean(value) && typeof value === 'object'
}
export function WriteView(props: ToolViewProps) {
const input = props.block.tool.input
if (!isObject(input)) return null
+1 -12
View File
@@ -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<string, unknown> {
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>(.*?)<\/tool_use_error>/s
const match = message.match(regex)
+1 -4
View File
@@ -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<string, unknown> {
return Boolean(value) && typeof value === 'object'
}
type SSESubscription = {
all?: boolean
sessionId?: string
+1 -10
View File
@@ -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
+1 -4
View File
@@ -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<string, unknown> {
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'