174 lines
5.3 KiB
JavaScript
174 lines
5.3 KiB
JavaScript
/**
|
||
* Channel Components - Separate lanes for different content types
|
||
*
|
||
* CHANNEL SEPARATION:
|
||
* - ChatLane: user + assistant prose only
|
||
* - ToolLane: tool calls, auto-heal, IQ exchange (collapsed by default)
|
||
* - ErrorLane: short summary + expandable details
|
||
*/
|
||
|
||
import React, { useEffect, useState } from 'react';
|
||
import { Box, Text } from 'ink';
|
||
import Spinner from 'ink-spinner';
|
||
import { colors } from '../../tui-theme.mjs';
|
||
import { icon } from '../../icons.mjs';
|
||
import { getCapabilities } from '../../terminal-profile.mjs';
|
||
|
||
const h = React.createElement;
|
||
|
||
/**
|
||
* ToolLane - Collapsed tool/command output
|
||
* Expands on demand, doesn't pollute chat
|
||
*/
|
||
const ToolLane = ({
|
||
name,
|
||
status = 'running', // running, done, failed
|
||
summary = null,
|
||
output = null,
|
||
isExpanded = false,
|
||
onToggle = null,
|
||
width = 80
|
||
}) => {
|
||
const caps = getCapabilities();
|
||
const [expanded, setExpanded] = useState(isExpanded);
|
||
|
||
useEffect(() => {
|
||
setExpanded(isExpanded);
|
||
}, [isExpanded]);
|
||
|
||
const statusConfig = {
|
||
running: { color: colors.accent, icon: null, showSpinner: true },
|
||
done: { color: colors.success, icon: caps.unicodeOK ? '✓' : '+', showSpinner: false },
|
||
failed: { color: colors.error, icon: caps.unicodeOK ? '✗' : 'X', showSpinner: false }
|
||
};
|
||
|
||
const config = statusConfig[status] || statusConfig.running;
|
||
const railChar = caps.unicodeOK ? '│' : '|';
|
||
|
||
// Header line (always shown)
|
||
const header = h(Box, { flexDirection: 'row' },
|
||
// Rail
|
||
h(Text, { color: 'magenta' }, railChar + ' '),
|
||
|
||
// Spinner or icon
|
||
config.showSpinner
|
||
? h(Spinner, { type: 'dots' })
|
||
: h(Text, { color: config.color }, config.icon),
|
||
h(Text, {}, ' '),
|
||
|
||
// Tool name
|
||
h(Text, { color: config.color, bold: true }, name),
|
||
|
||
// Summary (if any)
|
||
summary ? h(Text, { color: colors.muted }, ` – ${summary}`) : null,
|
||
|
||
// Expand hint (if has output)
|
||
output && !expanded ? h(Text, { color: colors.muted, dimColor: true },
|
||
` [${caps.unicodeOK ? '▼' : 'v'} expand]`
|
||
) : null
|
||
);
|
||
|
||
if (!expanded || !output) {
|
||
return header;
|
||
}
|
||
|
||
// Expanded view with output
|
||
return h(Box, { flexDirection: 'column' },
|
||
header,
|
||
h(Box, { paddingLeft: 4, marginTop: 0, marginBottom: 1 },
|
||
h(Text, {
|
||
color: colors.muted,
|
||
dimColor: true,
|
||
wrap: 'wrap'
|
||
}, output.length > 200 ? output.slice(0, 200) + '...' : output)
|
||
)
|
||
);
|
||
};
|
||
|
||
/**
|
||
* ErrorLane - Compact error display
|
||
* Short summary line + expandable details
|
||
*/
|
||
const ErrorLane = ({
|
||
message,
|
||
details = null,
|
||
isExpanded = false,
|
||
width = 80
|
||
}) => {
|
||
const caps = getCapabilities();
|
||
const [expanded, setExpanded] = useState(isExpanded);
|
||
|
||
useEffect(() => {
|
||
setExpanded(isExpanded);
|
||
}, [isExpanded]);
|
||
const railChar = caps.unicodeOK ? '│' : '|';
|
||
const errorIcon = caps.unicodeOK ? '✗' : 'X';
|
||
|
||
// Summary line (always shown)
|
||
const summary = h(Box, { flexDirection: 'row' },
|
||
h(Text, { color: colors.error }, railChar + ' '),
|
||
h(Text, { color: colors.error }, errorIcon + ' '),
|
||
h(Text, { color: colors.error, bold: true }, 'Error: '),
|
||
h(Text, { color: colors.fg, wrap: 'truncate' },
|
||
message.length > 60 ? message.slice(0, 57) + '...' : message
|
||
),
|
||
details && !expanded ? h(Text, { color: colors.muted, dimColor: true },
|
||
` [${caps.unicodeOK ? '▼' : 'v'} details]`
|
||
) : null
|
||
);
|
||
|
||
if (!expanded || !details) {
|
||
return summary;
|
||
}
|
||
|
||
// Expanded with details
|
||
return h(Box, { flexDirection: 'column' },
|
||
summary,
|
||
h(Box, { paddingLeft: 4, marginTop: 0, marginBottom: 1 },
|
||
h(Text, { color: colors.muted, wrap: 'wrap' }, details)
|
||
)
|
||
);
|
||
};
|
||
|
||
/**
|
||
* SystemChip - Single-line system message
|
||
* Minimal, doesn't interrupt conversation flow
|
||
*/
|
||
const SystemChip = ({ message, type = 'info' }) => {
|
||
const caps = getCapabilities();
|
||
const railChar = caps.unicodeOK ? '│' : '|';
|
||
|
||
const typeConfig = {
|
||
info: { color: colors.accent, icon: caps.unicodeOK ? 'ℹ' : 'i' },
|
||
success: { color: colors.success, icon: caps.unicodeOK ? '✓' : '+' },
|
||
warning: { color: colors.warning, icon: caps.unicodeOK ? '⚠' : '!' }
|
||
};
|
||
|
||
const config = typeConfig[type] || typeConfig.info;
|
||
|
||
return h(Box, { flexDirection: 'row' },
|
||
h(Text, { color: config.color, dimColor: true }, railChar + ' '),
|
||
h(Text, { color: config.color, dimColor: true }, config.icon + ' '),
|
||
h(Text, { color: colors.muted, dimColor: true }, message)
|
||
);
|
||
};
|
||
|
||
/**
|
||
* IQExchangeChip - IQ Exchange status (single line)
|
||
*/
|
||
const IQExchangeChip = ({ message, isActive = true }) => {
|
||
const caps = getCapabilities();
|
||
const railChar = caps.unicodeOK ? '│' : '|';
|
||
|
||
return h(Box, { flexDirection: 'row' },
|
||
h(Text, { color: 'magenta' }, railChar + ' '),
|
||
isActive ? h(Spinner, { type: 'dots' }) : null,
|
||
isActive ? h(Text, {}, ' ') : null,
|
||
h(Text, { color: 'magenta', bold: true }, 'IQ Exchange: '),
|
||
h(Text, { color: colors.muted }, message)
|
||
);
|
||
};
|
||
|
||
export { ToolLane, ErrorLane, SystemChip, IQExchangeChip };
|
||
export default { ToolLane, ErrorLane, SystemChip, IQExchangeChip };
|