233 lines
5.4 KiB
JavaScript
233 lines
5.4 KiB
JavaScript
/**
|
|
* Part Model - Structured message parts
|
|
*
|
|
* Based on sst/opencode message architecture
|
|
* Credit: https://github.com/sst/opencode
|
|
*
|
|
* Normalizes all content into typed parts for consistent rendering
|
|
*/
|
|
|
|
// Part types enum
|
|
export const PART_TYPES = {
|
|
TEXT: 'text', // User/assistant prose
|
|
REASONING: 'reasoning', // Thinking/intent trace
|
|
TOOL_CALL: 'tool_call', // Tool invocation
|
|
TOOL_RESULT: 'tool_result', // Tool output
|
|
FILE_SNIPPET: 'file_snippet', // Code file content
|
|
DIFF: 'diff', // Diff view
|
|
TODO: 'todo', // Todo/checklist
|
|
SYSTEM: 'system', // System message
|
|
ERROR: 'error' // Error message
|
|
};
|
|
|
|
/**
|
|
* Create a text part (user/assistant prose)
|
|
*/
|
|
export function createTextPart(content, role = 'assistant') {
|
|
return {
|
|
type: PART_TYPES.TEXT,
|
|
role,
|
|
content,
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a reasoning part (thinking/intent trace)
|
|
*/
|
|
export function createReasoningPart(intent, next, why = null, steps = []) {
|
|
return {
|
|
type: PART_TYPES.REASONING,
|
|
intent,
|
|
next,
|
|
why,
|
|
steps,
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a tool call part
|
|
*/
|
|
export function createToolCallPart(toolName, args = {}, status = 'running') {
|
|
return {
|
|
type: PART_TYPES.TOOL_CALL,
|
|
toolName,
|
|
args,
|
|
status, // 'running' | 'done' | 'failed'
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a tool result part
|
|
*/
|
|
export function createToolResultPart(toolName, output, summary = null, exitCode = 0) {
|
|
return {
|
|
type: PART_TYPES.TOOL_RESULT,
|
|
toolName,
|
|
output,
|
|
summary,
|
|
exitCode,
|
|
isError: exitCode !== 0,
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a file snippet part
|
|
*/
|
|
export function createFileSnippetPart(filename, content, language = null, startLine = 1) {
|
|
const lines = content.split('\n').length;
|
|
return {
|
|
type: PART_TYPES.FILE_SNIPPET,
|
|
filename,
|
|
content,
|
|
language: language || detectLanguage(filename),
|
|
startLine,
|
|
lineCount: lines,
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a diff part
|
|
*/
|
|
export function createDiffPart(filename, diff, mode = 'unified') {
|
|
return {
|
|
type: PART_TYPES.DIFF,
|
|
filename,
|
|
diff,
|
|
mode, // 'unified' | 'split'
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a todo part
|
|
*/
|
|
export function createTodoPart(items) {
|
|
return {
|
|
type: PART_TYPES.TODO,
|
|
items: items.map(item => ({
|
|
text: item.text,
|
|
status: item.status || 'pending', // 'pending' | 'in_progress' | 'done'
|
|
checked: item.status === 'done'
|
|
})),
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a system part
|
|
*/
|
|
export function createSystemPart(content, subtype = 'info') {
|
|
return {
|
|
type: PART_TYPES.SYSTEM,
|
|
content,
|
|
subtype, // 'info' | 'warning' | 'success'
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create an error part
|
|
*/
|
|
export function createErrorPart(message, details = null, stack = null) {
|
|
return {
|
|
type: PART_TYPES.ERROR,
|
|
message,
|
|
details,
|
|
stack,
|
|
timestamp: Date.now()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Detect language from filename
|
|
*/
|
|
function detectLanguage(filename) {
|
|
if (!filename) return null;
|
|
const ext = filename.split('.').pop()?.toLowerCase();
|
|
const langMap = {
|
|
js: 'javascript', mjs: 'javascript', cjs: 'javascript',
|
|
ts: 'typescript', tsx: 'typescript',
|
|
py: 'python',
|
|
rb: 'ruby',
|
|
go: 'go',
|
|
rs: 'rust',
|
|
java: 'java',
|
|
c: 'c', cpp: 'cpp', h: 'c',
|
|
cs: 'csharp',
|
|
php: 'php',
|
|
sh: 'bash', bash: 'bash',
|
|
ps1: 'powershell',
|
|
json: 'json',
|
|
yaml: 'yaml', yml: 'yaml',
|
|
md: 'markdown',
|
|
html: 'html',
|
|
css: 'css',
|
|
sql: 'sql'
|
|
};
|
|
return langMap[ext] || null;
|
|
}
|
|
|
|
/**
|
|
* Parse raw message content into parts
|
|
* Extracts code blocks, tool calls, etc.
|
|
*/
|
|
export function parseContentToParts(content, role = 'assistant') {
|
|
const parts = [];
|
|
|
|
// Simple parsing - extract code blocks
|
|
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
|
|
let lastIndex = 0;
|
|
let match;
|
|
|
|
while ((match = codeBlockRegex.exec(content)) !== null) {
|
|
// Text before code block
|
|
if (match.index > lastIndex) {
|
|
const text = content.slice(lastIndex, match.index).trim();
|
|
if (text) {
|
|
parts.push(createTextPart(text, role));
|
|
}
|
|
}
|
|
|
|
// Code block
|
|
const lang = match[1] || null;
|
|
const code = match[2];
|
|
parts.push(createFileSnippetPart(null, code, lang));
|
|
|
|
lastIndex = match.index + match[0].length;
|
|
}
|
|
|
|
// Remaining text after last code block
|
|
if (lastIndex < content.length) {
|
|
const text = content.slice(lastIndex).trim();
|
|
if (text) {
|
|
parts.push(createTextPart(text, role));
|
|
}
|
|
}
|
|
|
|
// If no parts created, treat entire content as text
|
|
if (parts.length === 0) {
|
|
parts.push(createTextPart(content, role));
|
|
}
|
|
|
|
return parts;
|
|
}
|
|
|
|
export default {
|
|
PART_TYPES,
|
|
createTextPart,
|
|
createReasoningPart,
|
|
createToolCallPart,
|
|
createToolResultPart,
|
|
createFileSnippetPart,
|
|
createDiffPart,
|
|
createTodoPart,
|
|
createSystemPart,
|
|
createErrorPart,
|
|
parseContentToParts
|
|
};
|