Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/tui/THEMES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# TUI themes

DataFoundry separates theme presets from semantic color usage:

- `src/ui/themes/types.ts` defines the semantic token contract.
- `src/ui/themes/presets.ts` contains complete theme presets.
- `src/ui/themes/theme-manager.ts` selects the active preset.
- `src/ui/theme.ts` exposes dynamic semantic accessors to components.

Components should use semantic groups instead of literal colors:

- `inkColors`: compatibility layer for the main canvas, structure, text, and status.
- `selectionColors`: slash completion, session picker, resource picker, and output picker.
- `tuiTheme`: full semantic token tree for new components.

The default preset is `mist-dark`. To switch at startup:

```bash
datafoundry-tui --theme legacy-dark
DATAFOUNDRY_TUI_THEME=legacy-dark datafoundry-tui
```

To add a style, define one complete `TuiThemePreset` in `presets.ts` and append it
to `builtInThemes`. Components consuming semantic tokens do not need to change.
11 changes: 11 additions & 0 deletions apps/tui/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { store } from "./state/store.js";
import { installTerminalRedrawOptimizer } from "./terminal-redraw-optimizer.js";
import { withAlternateScreen } from "./terminal-screen.js";
import { App } from "./ui/App.js";
import { themeManager } from "./ui/themes/theme-manager.js";

const args = process.argv.slice(2);
const STARTUP_PREFLIGHT_TIMEOUT_MS = 1200;
Expand All @@ -28,6 +29,8 @@ Options:
(default: backend run-defaults; demo: api-duckdb-demo)
--agent <name> Agent name
(default: dataFoundry)
--theme <name> TUI color theme
(mist-dark or legacy-dark; env: DATAFOUNDRY_TUI_THEME)
--resume [sessionId] Resume the latest server session, or a specific session
--demo Show mock messages and use a local mock stream
--help, -h Show this help message
Expand All @@ -36,6 +39,7 @@ Examples:
datafoundry-tui
datafoundry-tui --runtime-url http://localhost:8787/api/copilotkit
datafoundry-tui --datasource-id my-database
datafoundry-tui --theme mist-dark
datafoundry-tui --resume
datafoundry-tui --resume thread-001
datafoundry-tui --demo
Expand All @@ -60,6 +64,13 @@ function getOptionalArg(name: string): string | undefined {
return next && !next.startsWith("-") ? next : undefined;
}

const requestedTheme = getOptionalArg("--theme") ?? process.env.DATAFOUNDRY_TUI_THEME;
if (requestedTheme && !themeManager.setActiveTheme(requestedTheme)) {
const availableThemes = themeManager.getAvailableThemes().map((theme) => theme.name).join(", ");
console.error(`Unknown TUI theme "${requestedTheme}". Available themes: ${availableThemes}.`);
process.exit(1);
}

function resolveResumeRequest(): { enabled: boolean; sessionId?: string | undefined } | undefined {
if (args.includes("--resume")) {
return {
Expand Down
61 changes: 42 additions & 19 deletions apps/tui/src/ui/OutputsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
isMarkdownArtifact,
} from './ArtifactCard.js';
import { textWidth, truncateToWidth } from './text-width.js';
import { inkColors } from './theme.js';
import { inkColors, selectionColors } from './theme.js';

interface OutputsViewProps {
artifacts: DataArtifact[];
Expand Down Expand Up @@ -210,17 +210,17 @@ export const OutputsView: React.FC<OutputsViewProps> = ({
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
{artifacts.length === 0 ? (
<Box flexDirection="column" paddingY={2}>
<Text dimColor wrap="truncate-end">
<Text color={selectionColors.disabled} wrap="truncate-end">
{truncate('暂无产出。', contentWidth)}
</Text>
<Text dimColor wrap="truncate-end">
<Text color={selectionColors.description} wrap="truncate-end">
{truncate('Agent 生成 SQL 结果、图表、报告或文件后会显示在这里。', contentWidth)}
</Text>
</Box>
) : (
<Box flexDirection="column" flexGrow={1} overflow="hidden">
<Box marginBottom={1}>
<Text dimColor wrap="truncate-end">
<Text color={selectionColors.description} wrap="truncate-end">
{truncate('最新产出排在前面。上下选择,Enter 查看详情。', contentWidth)}
</Text>
</Box>
Expand All @@ -230,7 +230,7 @@ export const OutputsView: React.FC<OutputsViewProps> = ({
const isFirst = index === 0;
const isLast = index === visibleArtifacts.length - 1;
const prefix = selected
? '> '
? ' '
: isFirst && showScrollUp
? '^ '
: isLast && showScrollDown
Expand All @@ -250,25 +250,47 @@ export const OutputsView: React.FC<OutputsViewProps> = ({
key={artifact.id}
flexDirection="column"
marginBottom={isLast ? 0 : 1}
backgroundColor={selected
? selectionColors.selectedBackground
: selectionColors.background}
>
<Box>
<Text color={selected ? inkColors.accent : inkColors.text} bold={selected}>
<Text color={selected ? selectionColors.accent : selectionColors.disabled}>
{prefix}
</Text>
<Text color={selected ? inkColors.accent : inkColors.text} bold={selected} wrap="truncate-end">
<Text
color={selected ? selectionColors.selectedTitle : selectionColors.title}
bold={selected}
wrap="truncate-end"
>
{title}
</Text>
<Text dimColor wrap="truncate-end">
<Text
color={selected
? selectionColors.selectedDescription
: selectionColors.description}
wrap="truncate-end"
>
{truncate(typeLabel, Math.max(1, titleWidth - textWidth(title)))}
</Text>
</Box>
<Box paddingLeft={2}>
<Text dimColor wrap="truncate-end">
<Text
color={selected
? selectionColors.selectedDescription
: selectionColors.description}
wrap="truncate-end"
>
{truncate(artifact.summary, summaryWidth)}
</Text>
</Box>
<Box paddingLeft={2}>
<Text dimColor wrap="truncate-end">
<Text
color={selected
? selectionColors.selectedDescription
: selectionColors.disabled}
wrap="truncate-end"
>
{truncate(artifactMetadata(artifact, events), metadataWidth)}
</Text>
</Box>
Expand Down Expand Up @@ -333,7 +355,7 @@ export const OutputsSidebar: React.FC<{
</Box>

<Box>
<Text color="gray">{'-'.repeat(separatorWidth)}</Text>
<Text color={inkColors.border}>{'-'.repeat(separatorWidth)}</Text>
</Box>

<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
Expand Down Expand Up @@ -392,7 +414,7 @@ export const OutputsSidebar: React.FC<{
</Box>

<Box>
<Text color="gray">{'-'.repeat(separatorWidth)}</Text>
<Text color={inkColors.border}>{'-'.repeat(separatorWidth)}</Text>
</Box>

<Box paddingX={1}>
Expand Down Expand Up @@ -425,7 +447,7 @@ const OutputsDetailView: React.FC<{
<Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
<Box>
<Text dimColor>#{index + 1} </Text>
<Text color={inkColors.accent} wrap="truncate-end">
<Text color={selectionColors.accent} wrap="truncate-end">
{truncate(sourceLabel(artifact, events), Math.max(1, contentWidth - 4))}
</Text>
</Box>
Expand Down Expand Up @@ -658,20 +680,21 @@ export const OutputsScreen: React.FC<OutputsScreenProps> = ({
<Box
flexDirection="column"
borderStyle="single"
borderColor={inkColors.border}
borderColor={selectionColors.border}
backgroundColor={selectionColors.background}
width={panelWidth}
height={panelHeight}
overflow="hidden"
>
<Box paddingX={1}>
<Text bold color={inkColors.accent} wrap="truncate-end">{headerTitle}</Text>
<Text dimColor wrap="truncate-end">
<Text bold color={selectionColors.heading} wrap="truncate-end">{headerTitle}</Text>
<Text color={selectionColors.description} wrap="truncate-end">
{truncate(headerSuffix, Math.max(1, contentWidth - textWidth(headerTitle)))}
</Text>
</Box>

<Box>
<Text color="gray">{'-'.repeat(separatorWidth)}</Text>
<Text color={selectionColors.border}>{'-'.repeat(separatorWidth)}</Text>
</Box>

<Box flexDirection="column" flexGrow={1} overflow="hidden">
Expand All @@ -698,11 +721,11 @@ export const OutputsScreen: React.FC<OutputsScreenProps> = ({
</Box>

<Box>
<Text color="gray">{'-'.repeat(separatorWidth)}</Text>
<Text color={selectionColors.border}>{'-'.repeat(separatorWidth)}</Text>
</Box>

<Box paddingX={1}>
<Text dimColor wrap="truncate-end">
<Text color={selectionColors.disabled} wrap="truncate-end">
{truncate(footerText, contentWidth)}
</Text>
</Box>
Expand Down
57 changes: 42 additions & 15 deletions apps/tui/src/ui/ResourcePicker.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Box, Text, useInput } from 'ink';
import { inkColors } from './theme.js';
import { inkColors, selectionColors } from './theme.js';

export interface ResourcePickerItem {
id: string;
Expand Down Expand Up @@ -133,18 +133,19 @@ export const ResourcePicker: React.FC<ResourcePickerProps> = ({
<Box
flexDirection="column"
borderStyle="single"
borderColor={inkColors.border}
borderColor={selectionColors.border}
backgroundColor={selectionColors.background}
width={panelWidth}
height={panelHeight}
overflow={fullscreen ? 'hidden' : undefined}
paddingX={1}
paddingY={1}
marginX={fullscreen ? 0 : 1}
>
<Text bold color={inkColors.accent}>{title}</Text>
<Text bold color={selectionColors.heading}>{title}</Text>
<Box>
<Text dimColor>Type to search: </Text>
<Text>{query}</Text>
<Text color={selectionColors.description}>Type to search: </Text>
<Text color={selectionColors.title}>{query}</Text>
<Text inverse> </Text>
</Box>

Expand All @@ -162,13 +163,13 @@ export const ResourcePicker: React.FC<ResourcePickerProps> = ({
overflow={fullscreen ? 'hidden' : undefined}
>
{loading ? (
<Text dimColor>Loading...</Text>
<Text color={selectionColors.description}>Loading...</Text>
) : error ? (
<Text color={inkColors.error}>{error}</Text>
) : items.length === 0 ? (
<Text dimColor>{emptyMessage}</Text>
<Text color={selectionColors.disabled}>{emptyMessage}</Text>
) : filteredItems.length === 0 ? (
<Text dimColor>No items match "{query}".</Text>
<Text color={selectionColors.disabled}>No items match "{query}".</Text>
) : (
visibleItems.map((item, index) => {
const absoluteIndex = windowStart + index;
Expand All @@ -177,21 +178,45 @@ export const ResourcePicker: React.FC<ResourcePickerProps> = ({
const titleText = truncate(item.name || item.id, titleMaxWidth);
const idText = truncate(item.id, idMaxWidth);
const detailText = item.detail ? truncate(item.detail, detailMaxWidth) : '';
const itemColor = selected ? inkColors.accent : item.enabled === false ? inkColors.muted : inkColors.text;
const itemColor = selected
? selectionColors.selectedTitle
: item.enabled === false
? selectionColors.disabled
: selectionColors.title;

return (
<Box key={item.id} flexDirection="column">
<Box
key={item.id}
flexDirection="column"
backgroundColor={selected
? selectionColors.selectedBackground
: selectionColors.background}
>
<Box>
<Text color={selected ? inkColors.accent : inkColors.text}>{selected ? '>' : ' '} </Text>
<Text dimColor>{stateMarker} </Text>
<Text color={selected ? selectionColors.accent : selectionColors.disabled}>
{selected ? '› ' : ' '}
</Text>
<Text color={item.active ? selectionColors.accent : selectionColors.disabled}>
{stateMarker}{' '}
</Text>
<Text color={itemColor} bold={selected}>
{titleText}
</Text>
<Text dimColor> ({idText})</Text>
<Text
color={selected
? selectionColors.selectedDescription
: selectionColors.description}
>
{' '}({idText})
</Text>
</Box>
{(item.description || detailText) ? (
<Box paddingLeft={4}>
<Text dimColor>
<Text
color={selected
? selectionColors.selectedDescription
: selectionColors.description}
>
{truncate(item.description ?? detailText, descriptionMaxWidth)}
{item.description && detailText ? ` - ${detailText}` : ''}
</Text>
Expand All @@ -204,7 +229,9 @@ export const ResourcePicker: React.FC<ResourcePickerProps> = ({
</Box>

<Box marginTop={1}>
<Text dimColor>Up/Down Navigate - Enter Select - Esc Cancel - * active, + enabled</Text>
<Text color={selectionColors.disabled}>
Up/Down Navigate - Enter Select - Esc Cancel - * active, + enabled
</Text>
</Box>
</Box>
);
Expand Down
Loading