Release v1.01 Enhanced: Vi Control, TUI Gen5, Core Stability
This commit is contained in:
294
bin/ui/components/PremiumMessage.mjs
Normal file
294
bin/ui/components/PremiumMessage.mjs
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Premium Message Component
|
||||
*
|
||||
* DESIGN PRINCIPLES:
|
||||
* 1. Single rail-based layout for ALL roles (user, assistant, system, tool, error)
|
||||
* 2. NO message borders - uses left rail + role label line + body
|
||||
* 3. Max readable line width (clamped)
|
||||
* 4. Width-aware wrapping
|
||||
* 5. ASCII-safe icons
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import Spinner from 'ink-spinner';
|
||||
import { colors, layout } from '../../tui-theme.mjs';
|
||||
import { icon, roleIcon } from '../../icons.mjs';
|
||||
import { getCapabilities } from '../../terminal-profile.mjs';
|
||||
|
||||
const h = React.createElement;
|
||||
|
||||
/**
|
||||
* Rail character for message left border
|
||||
*/
|
||||
const getRailChar = (isStreaming = false) => {
|
||||
const caps = getCapabilities();
|
||||
if (!caps.unicodeOK) return '|';
|
||||
return isStreaming ? '┃' : '│';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get rail color by role
|
||||
*/
|
||||
const getRailColor = (role) => {
|
||||
const roleColors = {
|
||||
user: colors.rail.user,
|
||||
assistant: colors.rail.assistant,
|
||||
system: colors.rail.system,
|
||||
tool: colors.rail.tool,
|
||||
error: colors.rail.error,
|
||||
thinking: colors.rail.thinking
|
||||
};
|
||||
return roleColors[role] || colors.muted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Role label (first line of message)
|
||||
*/
|
||||
const RoleLabel = ({ role, isStreaming = false, timestamp = null }) => {
|
||||
const caps = getCapabilities();
|
||||
const labels = {
|
||||
user: 'You',
|
||||
assistant: 'Assistant',
|
||||
system: 'System',
|
||||
tool: 'Tool',
|
||||
error: 'Error',
|
||||
thinking: 'Thinking'
|
||||
};
|
||||
|
||||
const label = labels[role] || role;
|
||||
const labelIcon = roleIcon(role);
|
||||
const color = getRailColor(role);
|
||||
|
||||
return h(Box, { flexDirection: 'row' },
|
||||
h(Text, { color, bold: true }, `${labelIcon} ${label}`),
|
||||
isStreaming ? h(Box, { marginLeft: 1 },
|
||||
h(Spinner, { type: 'dots' }),
|
||||
h(Text, { color: colors.muted }, ' generating...')
|
||||
) : null,
|
||||
timestamp ? h(Text, { color: colors.muted, dimColor: true, marginLeft: 1 }, timestamp) : null
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Message body with proper wrapping
|
||||
*/
|
||||
const MessageBody = ({ content, width, color = colors.fg }) => {
|
||||
// Clamp width for readability
|
||||
const maxWidth = Math.min(width, layout.transcript.maxLineWidth);
|
||||
|
||||
return h(Box, { flexDirection: 'column', width: maxWidth },
|
||||
h(Text, { color, wrap: 'wrap' }, content)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Status Chip for short system/tool messages
|
||||
* Single-line, minimal interruption
|
||||
*/
|
||||
const StatusChip = ({ message, type = 'info', showSpinner = false }) => {
|
||||
const chipColors = {
|
||||
info: colors.accent,
|
||||
success: colors.success,
|
||||
warning: colors.warning,
|
||||
error: colors.error
|
||||
};
|
||||
const chipColor = chipColors[type] || colors.muted;
|
||||
|
||||
return h(Box, { flexDirection: 'row', marginY: 0 },
|
||||
h(Text, { color: colors.muted }, getRailChar()),
|
||||
h(Text, {}, ' '),
|
||||
showSpinner ? h(Spinner, { type: 'dots' }) : null,
|
||||
showSpinner ? h(Text, {}, ' ') : null,
|
||||
h(Text, { color: chipColor }, message)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Premium Message - Unified rail-based layout
|
||||
*/
|
||||
const PremiumMessage = ({
|
||||
role = 'assistant',
|
||||
content = '',
|
||||
isStreaming = false,
|
||||
timestamp = null,
|
||||
width = 80,
|
||||
// For tool messages
|
||||
toolName = null,
|
||||
isCollapsed = false,
|
||||
onToggle = null,
|
||||
// For status chips (short messages)
|
||||
isChip = false,
|
||||
chipType = 'info'
|
||||
}) => {
|
||||
// Short status messages use chip style
|
||||
if (isChip || (role === 'system' && content.length < 60)) {
|
||||
return h(StatusChip, {
|
||||
message: content,
|
||||
type: chipType,
|
||||
showSpinner: isStreaming
|
||||
});
|
||||
}
|
||||
|
||||
const railColor = getRailColor(role);
|
||||
const railChar = getRailChar(isStreaming);
|
||||
|
||||
// Calculate body width (minus rail + spacing)
|
||||
const bodyWidth = Math.max(20, width - 4);
|
||||
|
||||
return h(Box, {
|
||||
flexDirection: 'row',
|
||||
marginY: role === 'user' ? 1 : 0
|
||||
},
|
||||
// Left Rail
|
||||
h(Box, { width: 2, flexShrink: 0 },
|
||||
h(Text, { color: railColor }, railChar)
|
||||
),
|
||||
|
||||
// Content area
|
||||
h(Box, { flexDirection: 'column', flexGrow: 1, width: bodyWidth },
|
||||
// Role label line
|
||||
h(RoleLabel, {
|
||||
role,
|
||||
isStreaming,
|
||||
timestamp
|
||||
}),
|
||||
|
||||
// Tool name (if applicable)
|
||||
toolName ? h(Text, { color: colors.muted, dimColor: true },
|
||||
`${icon('tool')} ${toolName}`
|
||||
) : null,
|
||||
|
||||
// Message body
|
||||
h(MessageBody, {
|
||||
content,
|
||||
width: bodyWidth,
|
||||
color: role === 'error' ? colors.error : colors.fg
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Thinking Block - Collapsible intent trace
|
||||
*/
|
||||
const ThinkingBlock = ({
|
||||
lines = [],
|
||||
isThinking = false,
|
||||
showFull = false,
|
||||
width = 80
|
||||
}) => {
|
||||
const visibleLines = showFull ? lines : lines.slice(-3);
|
||||
const hiddenCount = Math.max(0, lines.length - visibleLines.length);
|
||||
|
||||
if (lines.length === 0 && !isThinking) return null;
|
||||
|
||||
const railChar = getRailChar(isThinking);
|
||||
const railColor = getRailColor('thinking');
|
||||
|
||||
return h(Box, {
|
||||
flexDirection: 'row',
|
||||
marginY: 0
|
||||
},
|
||||
// Left Rail
|
||||
h(Box, { width: 2, flexShrink: 0 },
|
||||
h(Text, { color: railColor, dimColor: true }, railChar)
|
||||
),
|
||||
|
||||
// Content
|
||||
h(Box, { flexDirection: 'column' },
|
||||
// Header
|
||||
h(Box, { flexDirection: 'row' },
|
||||
isThinking ? h(Spinner, { type: 'dots' }) : null,
|
||||
isThinking ? h(Text, {}, ' ') : null,
|
||||
h(Text, { color: colors.muted, dimColor: true },
|
||||
isThinking ? 'thinking...' : 'thought'
|
||||
)
|
||||
),
|
||||
|
||||
// Visible lines
|
||||
...visibleLines.map((line, i) =>
|
||||
h(Text, {
|
||||
key: i,
|
||||
color: colors.muted,
|
||||
dimColor: true,
|
||||
wrap: 'truncate'
|
||||
}, ` ${line.slice(0, width - 6)}`)
|
||||
),
|
||||
|
||||
// Hidden count
|
||||
hiddenCount > 0 ? h(Text, {
|
||||
color: colors.muted,
|
||||
dimColor: true
|
||||
}, ` +${hiddenCount} more`) : null
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool Call Card - Collapsed by default
|
||||
*/
|
||||
const ToolCard = ({
|
||||
name,
|
||||
status = 'running', // running, done, failed
|
||||
output = '',
|
||||
isExpanded = false,
|
||||
onToggle = null,
|
||||
width = 80
|
||||
}) => {
|
||||
const statusColors = {
|
||||
running: colors.accent,
|
||||
done: colors.success,
|
||||
failed: colors.error
|
||||
};
|
||||
const statusIcons = {
|
||||
running: icon('running'),
|
||||
done: icon('done'),
|
||||
failed: icon('failed')
|
||||
};
|
||||
|
||||
const railColor = colors.rail.tool;
|
||||
const railChar = getRailChar(status === 'running');
|
||||
|
||||
return h(Box, {
|
||||
flexDirection: 'row',
|
||||
marginY: 0
|
||||
},
|
||||
// Left Rail
|
||||
h(Box, { width: 2, flexShrink: 0 },
|
||||
h(Text, { color: railColor }, railChar)
|
||||
),
|
||||
|
||||
// Content
|
||||
h(Box, { flexDirection: 'column' },
|
||||
// Header line
|
||||
h(Box, { flexDirection: 'row' },
|
||||
status === 'running' ? h(Spinner, { type: 'dots' }) : null,
|
||||
status === 'running' ? h(Text, {}, ' ') : null,
|
||||
h(Text, { color: statusColors[status] },
|
||||
`${statusIcons[status]} ${name}`
|
||||
),
|
||||
!isExpanded && output ? h(Text, { color: colors.muted, dimColor: true },
|
||||
` [${icon('expand')} expand]`
|
||||
) : null
|
||||
),
|
||||
|
||||
// Expanded output
|
||||
isExpanded && output ? h(Box, { marginTop: 0, paddingLeft: 2 },
|
||||
h(Text, { color: colors.muted, wrap: 'wrap' }, output)
|
||||
) : null
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default PremiumMessage;
|
||||
export {
|
||||
PremiumMessage,
|
||||
StatusChip,
|
||||
ThinkingBlock,
|
||||
ToolCard,
|
||||
RoleLabel,
|
||||
MessageBody,
|
||||
getRailColor,
|
||||
getRailChar
|
||||
};
|
||||
Reference in New Issue
Block a user