Files
OpenQode/bin/ui/components/PremiumSidebar.mjs

264 lines
9.9 KiB
JavaScript

/**
* Premium Sidebar Component
*
* DESIGN PRINCIPLES:
* 1. NO nested borders (one-frame rule)
* 2. Three clean sections: Project, Session, Shortcuts
* 3. Headers + subtle dividers (not boxed widgets)
* 4. Consistent typography and alignment
* 5. ASCII-safe icons
*/
import React from 'react';
import { Box, Text } from 'ink';
import Spinner from 'ink-spinner';
import path from 'path';
import { theme, colors, layout } from '../../tui-theme.mjs';
import { icon, roleIcon } from '../../icons.mjs';
import { getCapabilities, PROFILE } from '../../terminal-profile.mjs';
import FileTree from './FileTree.mjs';
const h = React.createElement;
/**
* Section Header (no border, just styled text)
*/
const SectionHeader = ({ title, color = colors.muted }) => {
return h(Text, { color, bold: true }, title);
};
/**
* Divider line (subtle, no heavy borders)
*/
const Divider = ({ width, color = colors.border }) => {
const caps = getCapabilities();
const char = caps.unicodeOK ? '─' : '-';
return h(Text, { color, dimColor: true }, char.repeat(Math.max(1, width)));
};
/**
* Label-Value row (consistent alignment)
*/
const LabelValue = ({ label, value, valueColor = colors.fg }) => {
return h(Box, { flexDirection: 'row' },
h(Text, { color: colors.muted }, `${label}: `),
h(Text, { color: valueColor }, value)
);
};
/**
* Toggle indicator (ON/OFF)
*/
const Toggle = ({ label, value, onColor = 'green' }) => {
return h(Box, { flexDirection: 'row' },
h(Text, { color: colors.muted }, `${label} `),
value
? h(Text, { color: onColor }, 'ON')
: h(Text, { color: colors.muted, dimColor: true }, 'off')
);
};
/**
* Status chip (single line, minimal)
*/
const StatusChip = ({ message, type = 'info', showSpinner = false }) => {
const chipColor = type === 'error' ? colors.error
: type === 'success' ? colors.success
: type === 'warning' ? colors.warning
: colors.accent;
return h(Box, { flexDirection: 'row' },
showSpinner ? h(Text, { color: 'gray', dimColor: true }, '...') : null,
showSpinner ? h(Text, {}, ' ') : null,
h(Text, { color: chipColor, wrap: 'truncate' }, message)
);
};
/**
* Premium Sidebar - Clean 3-section layout
*/
const PremiumSidebar = ({
// Project info
project,
gitBranch,
// Session info
agent,
activeModel,
// Feature toggles
contextEnabled = false,
multiAgentEnabled = false,
exposedThinking = false,
soloMode = false,
autoApprove = false,
// Status indicators
systemStatus = null,
iqStatus = null,
thinkingStats = null,
indexStatus = null,
// Layout
width = 24,
height = 0,
// Explorer
showFileManager = false,
explorerRoot = null,
selectedFiles = new Set(),
onToggleFile = null,
onOpenFile = null,
recentFiles = [],
hotFiles = [],
// Interaction
isFocused = false,
showHint = false,
reduceMotion = true
}) => {
if (width === 0) return null;
const caps = getCapabilities();
const contentWidth = Math.max(10, width - 2);
// Truncate helper
const truncate = (str, len) => {
if (!str) return '';
return str.length > len ? str.slice(0, len - 1) + '…' : str;
};
// Derived values
const projectName = project ? truncate(path.basename(project), contentWidth) : 'None';
const branchName = truncate(gitBranch || 'main', contentWidth);
const agentName = (agent || 'build').toUpperCase();
const modelName = activeModel?.name || 'Not connected';
// Streaming stats
const isStreaming = thinkingStats?.chars > 0;
const explorerHeight = Math.max(8, Math.min(22, (height || 0) - 24));
return h(Box, {
flexDirection: 'column',
width: width,
paddingX: 1,
flexShrink: 0
},
// ═══════════════════════════════════════════════════════════
// BRANDING - Minimal, not animated
// ═══════════════════════════════════════════════════════════
h(Text, { color: colors.accent, bold: true }, 'OpenQode'),
h(Text, { color: colors.muted }, `${agentName} ${icon('branch')} ${branchName}`),
h(Box, { marginTop: 1 }),
// ═══════════════════════════════════════════════════════════
// SECTION 1: PROJECT
// ═══════════════════════════════════════════════════════════
h(SectionHeader, { title: 'PROJECT' }),
h(Divider, { width: contentWidth }),
h(LabelValue, { label: icon('folder'), value: projectName }),
h(LabelValue, { label: icon('branch'), value: branchName }),
// System status (if any)
systemStatus ? h(StatusChip, {
message: systemStatus.message,
type: systemStatus.type
}) : null,
// Index status (if any)
indexStatus ? h(StatusChip, {
message: indexStatus.message,
type: indexStatus.type
}) : null,
// IQ Exchange status (if active)
iqStatus ? h(StatusChip, {
message: iqStatus.message || 'Processing...',
type: 'info',
showSpinner: true
}) : null,
h(Box, { marginTop: 1 }),
// ═══════════════════════════════════════════════════════════
// SECTION 2: SESSION
// ═══════════════════════════════════════════════════════════
h(SectionHeader, { title: 'SESSION' }),
h(Divider, { width: contentWidth }),
h(LabelValue, {
label: icon('model'),
value: truncate(modelName, contentWidth - 3),
valueColor: activeModel?.isFree ? colors.success : colors.accent
}),
// Streaming indicator (only when active)
isStreaming ? h(Box, { flexDirection: 'row' },
reduceMotion ? h(Text, { color: colors.muted, dimColor: true }, '...') : h(Spinner, { type: 'dots' }),
h(Text, { color: colors.muted }, ` ${thinkingStats.chars} chars`)
) : null,
// Feature toggles (compact row)
h(Box, { marginTop: 1, flexDirection: 'column' },
h(Toggle, { label: 'Ctx', value: contextEnabled }),
h(Toggle, { label: 'Multi', value: multiAgentEnabled }),
h(Toggle, { label: 'Think', value: exposedThinking }),
soloMode ? h(Toggle, { label: 'SmartX', value: soloMode, onColor: 'magenta' }) : null,
autoApprove ? h(Toggle, { label: 'Auto', value: autoApprove, onColor: 'yellow' }) : null
),
h(Box, { marginTop: 1 }),
// ═══════════════════════════════════════════════════════════
// SECTION 3: SHORTCUTS
// ═══════════════════════════════════════════════════════════
h(SectionHeader, { title: 'SHORTCUTS' }),
h(Divider, { width: contentWidth }),
h(Text, { color: colors.accent }, '/help'),
h(Text, { color: colors.muted }, '/settings'),
h(Text, { color: colors.muted }, '/theme'),
h(Text, { color: colors.muted, dimColor: true }, 'Ctrl+P commands'),
h(Text, { color: colors.muted, dimColor: true }, 'Ctrl+E explorer'),
h(Text, { color: colors.muted, dimColor: true }, 'Ctrl+R recent'),
h(Text, { color: colors.muted, dimColor: true }, 'Ctrl+H hot'),
// Focus hint
showHint ? h(Box, { marginTop: 1 },
h(Text, { color: colors.muted, dimColor: true }, '[Tab] browse files')
) : null
,
// SECTION 4: EXPLORER (IDE-style file tree)
explorerRoot ? h(Box, { marginTop: 1, flexDirection: 'column' },
h(SectionHeader, { title: 'EXPLORER' }),
h(Divider, { width: contentWidth }),
!showFileManager ? h(Text, { color: colors.muted, dimColor: true, wrap: 'truncate' }, 'Hidden (Ctrl+E or /explorer on)') : null,
showFileManager && recentFiles && recentFiles.length > 0 ? h(Box, { flexDirection: 'column', marginBottom: 1 },
h(Text, { color: colors.muted, dimColor: true }, 'Recent:'),
recentFiles.slice(0, 3).map((f) => h(Text, { key: `recent:${f}`, color: colors.muted, wrap: 'truncate' }, ` ${f}`))
) : null,
showFileManager && hotFiles && hotFiles.length > 0 ? h(Box, { flexDirection: 'column', marginBottom: 1 },
h(Text, { color: colors.muted, dimColor: true }, 'Hot:'),
hotFiles.slice(0, 3).map((f) => h(Text, { key: `hot:${f}`, color: colors.muted, wrap: 'truncate' }, ` ${f}`))
) : null,
showFileManager && h(FileTree, {
rootPath: explorerRoot,
selectedFiles,
onSelect: onToggleFile,
onOpen: onOpenFile,
isActive: Boolean(isFocused),
height: explorerHeight,
width: contentWidth
}),
h(Text, { color: colors.muted, dimColor: true }, '↑↓ navigate • Enter open • Space select')
) : null
);
};
export default PremiumSidebar;
export { PremiumSidebar, SectionHeader, Divider, LabelValue, Toggle, StatusChip };