264 lines
9.9 KiB
JavaScript
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 };
|