diff --git a/cli/src/claude/utils/chatVisibility.ts b/cli/src/claude/utils/chatVisibility.ts index 2515d69f..d03178f4 100644 --- a/cli/src/claude/utils/chatVisibility.ts +++ b/cli/src/claude/utils/chatVisibility.ts @@ -1,16 +1,6 @@ import type { RawJSONLines } from '@/claude/types' - -const VISIBLE_SYSTEM_SUBTYPES = new Set([ - 'api_error', - 'turn_duration', - 'microcompact_boundary', - 'compact_boundary' -]) +import { isClaudeChatVisibleMessage as isSharedClaudeChatVisibleMessage } from '@hapi/protocol/messages' export function isClaudeChatVisibleMessage(message: Pick & { subtype?: string }): boolean { - if (message.type !== 'system') { - return true - } - - return typeof message.subtype === 'string' && VISIBLE_SYSTEM_SUBTYPES.has(message.subtype) + return isSharedClaudeChatVisibleMessage(message) } diff --git a/shared/src/messages.ts b/shared/src/messages.ts index 12d20dc9..c97fb59b 100644 --- a/shared/src/messages.ts +++ b/shared/src/messages.ts @@ -6,6 +6,13 @@ type RoleWrappedRecord = { meta?: unknown } +const VISIBLE_CLAUDE_SYSTEM_SUBTYPES = new Set([ + 'api_error', + 'turn_duration', + 'microcompact_boundary', + 'compact_boundary' +]) + export function isRoleWrappedRecord(value: unknown): value is RoleWrappedRecord { if (!isObject(value)) return false return typeof value.role === 'string' && 'content' in value @@ -27,4 +34,16 @@ export function unwrapRoleWrappedRecordEnvelope(value: unknown): RoleWrappedReco return null } +export function isClaudeChatVisibleSystemSubtype(subtype: unknown): subtype is string { + return typeof subtype === 'string' && VISIBLE_CLAUDE_SYSTEM_SUBTYPES.has(subtype) +} + +export function isClaudeChatVisibleMessage(message: { type: unknown; subtype?: unknown }): boolean { + if (message.type !== 'system') { + return true + } + + return isClaudeChatVisibleSystemSubtype(message.subtype) +} + export type { RoleWrappedRecord } diff --git a/web/src/chat/normalizeAgent.ts b/web/src/chat/normalizeAgent.ts index 7e3bd810..4096e758 100644 --- a/web/src/chat/normalizeAgent.ts +++ b/web/src/chat/normalizeAgent.ts @@ -1,5 +1,6 @@ import type { AgentEvent, NormalizedAgentContent, NormalizedMessage, ToolResultPermission } from '@/chat/types' import { AGENT_MESSAGE_PAYLOAD_TYPE, asNumber, asString, isObject } from '@hapi/protocol' +import { isClaudeChatVisibleMessage } from '@hapi/protocol/messages' function normalizeToolResultPermissions(value: unknown): ToolResultPermission | undefined { if (!isObject(value)) return undefined @@ -175,7 +176,8 @@ export function isSkippableAgentContent(content: unknown): boolean { if (!isObject(content) || content.type !== 'output') return false const data = isObject(content.data) ? content.data : null if (!data) return false - return Boolean(data.isMeta) || Boolean(data.isCompactSummary) + if (Boolean(data.isMeta) || Boolean(data.isCompactSummary)) return true + return !isClaudeChatVisibleMessage({ type: data.type, subtype: data.subtype }) } export function isCodexContent(content: unknown): boolean { @@ -198,6 +200,7 @@ export function normalizeAgentRecord( // Skip meta/compact-summary messages (parity with hapi-app) if (data.isMeta) return null if (data.isCompactSummary) return null + if (!isClaudeChatVisibleMessage({ type: data.type, subtype: data.subtype })) return null if (data.type === 'assistant') { return normalizeAssistantOutput(messageId, localId, createdAt, data, meta)