Files
stack/packages/mosaic/src/tui/components/sidebar.tsx
Jarvis c6fc090c98
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
feat(mosaic): merge @mosaic/cli into @mosaic/mosaic
@mosaic/mosaic is now the single package providing both:
- 'mosaic' binary (CLI: yolo, coord, prdy, tui, gateway, etc.)
- 'mosaic-wizard' binary (installation wizard)

Changes:
- Move packages/cli/src/* into packages/mosaic/src/
- Convert dynamic @mosaic/mosaic imports to static relative imports
- Add CLI deps (ink, react, socket.io-client, @mosaic/config) to mosaic
- Add jsx: react-jsx to mosaic's tsconfig
- Exclude packages/cli from workspace (pnpm-workspace.yaml)
- Update install.sh to install @mosaic/mosaic instead of @mosaic/cli
- Bump version to 0.0.17

This eliminates the circular dependency between @mosaic/cli and
@mosaic/mosaic that was blocking the build graph.
2026-04-04 20:07:27 -05:00

144 lines
3.9 KiB
TypeScript

import React from 'react';
import { Box, Text, useInput } from 'ink';
import type { ConversationSummary } from '../hooks/use-conversations.js';
export interface SidebarProps {
conversations: ConversationSummary[];
activeConversationId: string | undefined;
selectedIndex: number;
onSelectIndex: (index: number) => void;
onSwitchConversation: (id: string) => void;
onDeleteConversation: (id: string) => void;
loading: boolean;
focused: boolean;
width: number;
}
function formatRelativeTime(dateStr: string): string {
const date = new Date(dateStr);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
const hh = String(date.getHours()).padStart(2, '0');
const mm = String(date.getMinutes()).padStart(2, '0');
return `${hh}:${mm}`;
}
if (diffDays < 7) {
return `${diffDays}d ago`;
}
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
const mon = months[date.getMonth()];
const dd = String(date.getDate()).padStart(2, '0');
return `${mon} ${dd}`;
}
function truncate(text: string, maxLen: number): string {
if (text.length <= maxLen) return text;
return text.slice(0, maxLen - 1) + '…';
}
export function Sidebar({
conversations,
activeConversationId,
selectedIndex,
onSelectIndex,
onSwitchConversation,
onDeleteConversation,
loading,
focused,
width,
}: SidebarProps) {
useInput(
(_input, key) => {
if (key.upArrow) {
onSelectIndex(Math.max(0, selectedIndex - 1));
}
if (key.downArrow) {
onSelectIndex(Math.min(conversations.length - 1, selectedIndex + 1));
}
if (key.return) {
const conv = conversations[selectedIndex];
if (conv) {
onSwitchConversation(conv.id);
}
}
if (_input === 'd') {
const conv = conversations[selectedIndex];
if (conv) {
onDeleteConversation(conv.id);
}
}
},
{ isActive: focused },
);
const borderColor = focused ? 'cyan' : 'gray';
// Available width for content inside border + padding
const innerWidth = width - 4; // 2 border + 2 padding
return (
<Box
flexDirection="column"
width={width}
borderStyle="single"
borderColor={borderColor}
paddingX={1}
>
<Text bold color="cyan">
Conversations
</Text>
<Box marginTop={0} flexDirection="column" flexGrow={1}>
{loading && conversations.length === 0 ? (
<Text dimColor>Loading</Text>
) : conversations.length === 0 ? (
<Text dimColor>No conversations</Text>
) : (
conversations.map((conv, idx) => {
const isActive = conv.id === activeConversationId;
const isSelected = idx === selectedIndex && focused;
const marker = isActive ? '● ' : ' ';
const time = formatRelativeTime(conv.updatedAt);
const title = conv.title ?? 'Untitled';
// marker(2) + title + space(1) + time
const maxTitleLen = Math.max(4, innerWidth - marker.length - time.length - 1);
const displayTitle = truncate(title, maxTitleLen);
return (
<Box key={conv.id}>
<Text
inverse={isSelected}
color={isActive ? 'cyan' : undefined}
dimColor={!isActive && !isSelected}
>
{marker}
{displayTitle}
{' '.repeat(
Math.max(0, innerWidth - marker.length - displayTitle.length - time.length),
)}
{time}
</Text>
</Box>
);
})
)}
</Box>
{focused && <Text dimColor> navigate enter switch d delete</Text>}
</Box>
);
}