refactor: extract shared message parsing helpers into lib/messageExtract
- Move extractText and extractThinking from useGateway and useSecondarySession into shared lib - Add comprehensive unit tests (13 tests) for both functions - Eliminate code duplication between the two hooks
This commit is contained in:
@@ -5,35 +5,9 @@ import { getStoredCredentials, storeCredentials, clearCredentials, type AuthMode
|
||||
import { getOrCreateDeviceIdentity } from '../lib/deviceIdentity';
|
||||
import { isSystemEvent } from '../lib/systemEvent';
|
||||
import { getCachedMessages, setCachedMessages, mergeWithCache } from '../lib/messageCache';
|
||||
import { extractText, extractThinking, type ChatPayloadMessage } from '../lib/messageExtract';
|
||||
import type { ChatMessage, MessageBlock, ConnectionStatus, Session, AgentIdentity } from '../types';
|
||||
|
||||
interface ChatPayloadMessage {
|
||||
content?: string | Array<{ type: string; text?: string; thinking?: 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) => b.type === 'text' && typeof b.text === 'string')
|
||||
.map((b) => b.text as string)
|
||||
.join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractThinking(message: ChatPayloadMessage | undefined): string {
|
||||
if (!message) return '';
|
||||
const content = message.content;
|
||||
if (!Array.isArray(content)) return '';
|
||||
return content
|
||||
.filter((b) => b.type === 'thinking')
|
||||
.map((b) => b.thinking || b.text || '')
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export function useGateway() {
|
||||
const clientRef = useRef<GatewayClient | null>(null);
|
||||
const [status, setStatus] = useState<ConnectionStatus>('disconnected');
|
||||
|
||||
@@ -2,33 +2,8 @@ import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import type { GatewayClient, JsonPayload } from '../lib/gateway';
|
||||
import type { ChatMessage, MessageBlock } from '../types';
|
||||
import { isSystemEvent } from '../lib/systemEvent';
|
||||
|
||||
interface ChatPayloadMessage {
|
||||
content?: string | Array<{ type: string; text?: string; thinking?: 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) => b.type === 'text' && typeof b.text === 'string')
|
||||
.map((b) => b.text as string)
|
||||
.join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function extractThinking(message: ChatPayloadMessage | undefined): string {
|
||||
if (!message) return '';
|
||||
const content = message.content;
|
||||
if (!Array.isArray(content)) return '';
|
||||
return content
|
||||
.filter((b) => b.type === 'thinking')
|
||||
.map((b) => b.thinking || b.text || '')
|
||||
.join('\n');
|
||||
}
|
||||
import { extractText, extractThinking } from '../lib/messageExtract';
|
||||
import type { ChatPayloadMessage } from '../lib/messageExtract';
|
||||
|
||||
/**
|
||||
* Hook to manage a secondary session for split view.
|
||||
|
||||
90
src/lib/__tests__/messageExtract.test.ts
Normal file
90
src/lib/__tests__/messageExtract.test.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { extractText, extractThinking } from '../messageExtract';
|
||||
|
||||
describe('extractText', () => {
|
||||
it('returns empty string for undefined', () => {
|
||||
expect(extractText(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string for empty message', () => {
|
||||
expect(extractText({})).toBe('');
|
||||
});
|
||||
|
||||
it('returns string content directly', () => {
|
||||
expect(extractText({ content: 'hello world' })).toBe('hello world');
|
||||
});
|
||||
|
||||
it('extracts text blocks from array content', () => {
|
||||
expect(extractText({
|
||||
content: [
|
||||
{ type: 'text', text: 'hello' },
|
||||
{ type: 'thinking', thinking: 'hmm' },
|
||||
{ type: 'text', text: 'world' },
|
||||
],
|
||||
})).toBe('hello\nworld');
|
||||
});
|
||||
|
||||
it('ignores non-text blocks', () => {
|
||||
expect(extractText({
|
||||
content: [
|
||||
{ type: 'thinking', text: 'reasoning' },
|
||||
{ type: 'image', text: 'img' },
|
||||
],
|
||||
})).toBe('');
|
||||
});
|
||||
|
||||
it('skips text blocks with undefined text', () => {
|
||||
expect(extractText({
|
||||
content: [
|
||||
{ type: 'text' },
|
||||
{ type: 'text', text: 'ok' },
|
||||
],
|
||||
})).toBe('ok');
|
||||
});
|
||||
|
||||
it('returns empty string for non-string non-array content', () => {
|
||||
expect(extractText({ content: 42 as unknown as string })).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractThinking', () => {
|
||||
it('returns empty string for undefined', () => {
|
||||
expect(extractThinking(undefined)).toBe('');
|
||||
});
|
||||
|
||||
it('returns empty string for string content', () => {
|
||||
expect(extractThinking({ content: 'hello' })).toBe('');
|
||||
});
|
||||
|
||||
it('extracts thinking blocks using thinking field', () => {
|
||||
expect(extractThinking({
|
||||
content: [
|
||||
{ type: 'text', text: 'hello' },
|
||||
{ type: 'thinking', thinking: 'let me think' },
|
||||
],
|
||||
})).toBe('let me think');
|
||||
});
|
||||
|
||||
it('falls back to text field when thinking is absent', () => {
|
||||
expect(extractThinking({
|
||||
content: [
|
||||
{ type: 'thinking', text: 'fallback' },
|
||||
],
|
||||
})).toBe('fallback');
|
||||
});
|
||||
|
||||
it('joins multiple thinking blocks', () => {
|
||||
expect(extractThinking({
|
||||
content: [
|
||||
{ type: 'thinking', thinking: 'step 1' },
|
||||
{ type: 'thinking', thinking: 'step 2' },
|
||||
],
|
||||
})).toBe('step 1\nstep 2');
|
||||
});
|
||||
|
||||
it('returns empty string for thinking block with no content', () => {
|
||||
expect(extractThinking({
|
||||
content: [{ type: 'thinking' }],
|
||||
})).toBe('');
|
||||
});
|
||||
});
|
||||
32
src/lib/messageExtract.ts
Normal file
32
src/lib/messageExtract.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Shared helpers for extracting text and thinking content from gateway chat messages.
|
||||
*/
|
||||
|
||||
export interface ChatPayloadMessage {
|
||||
content?: string | Array<{ type: string; text?: string; thinking?: string }>;
|
||||
}
|
||||
|
||||
/** Extract all text blocks from a gateway chat message, joined by newline. */
|
||||
export 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) => b.type === 'text' && typeof b.text === 'string')
|
||||
.map((b) => b.text as string)
|
||||
.join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Extract all thinking blocks from a gateway chat message, joined by newline. */
|
||||
export function extractThinking(message: ChatPayloadMessage | undefined): string {
|
||||
if (!message) return '';
|
||||
const content = message.content;
|
||||
if (!Array.isArray(content)) return '';
|
||||
return content
|
||||
.filter((b) => b.type === 'thinking')
|
||||
.map((b) => b.thinking || b.text || '')
|
||||
.join('\n');
|
||||
}
|
||||
Reference in New Issue
Block a user