Files
twenty/packages/twenty-front/src/modules/ai/components/TerminalOutput.tsx
T
Raphaël Bosi 8034c7725f Reorganize twenty-ui into best-practice component domains and per-component folders (#21745)
Reorganizes `twenty-ui`'s component organization to follow how the best
UI libraries (MUI, Mantine, Base UI, Polaris) structure their source,
now that the package has stabilized.

**Taxonomy** — dissolves the meaningless `components/` junk-drawer and
the 107-file `display/` mega-category. New domains/subpaths:
`data-display`, `typography`, `icon`, `surfaces`; `feedback` and
`layout` absorb the rest (banners/callout/info + placeholders →
feedback; modal/card → surfaces; motion + separators → layout).

**Per-component layout** — every component is now
`<domain>/<ComponentName>/<ComponentName>.tsx` with colocated
styles/stories/types, `internal/` for private helpers and `parts/` for
re-exported compound sub-parts. The redundant inner `/components/` is
gone. `icon` and `json-visualizer` are kept as cohesive subsystems.

**Also:** adds a tree-shakeable root barrel (`import { Button } from
'twenty-ui'`), the generator now owns `individual-entry.ts`, and a real
barrel-leak bug is fixed (private `internals/` parts were leaking into
the public API).

Consumer imports (~1.2k files) and the `twenty-sdk` UI aggregator were
updated by codemod. The change is **export-neutral** except 16
intentionally-removed private internals symbols (all verified
unconsumed). Gates green: typecheck, lint, build, size-limit, storybook.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21745?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-18 10:31:29 +02:00

182 lines
5.0 KiB
TypeScript

import { MONOSPACE_FONT_FAMILY } from '@/ui/theme/constants/MonospaceFontFamily';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { IconCopy, IconTerminal } from 'twenty-ui/icon';
import { LightIconButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
const StyledContainer = styled.div`
border-radius: ${themeCssVariables.border.radius.sm};
display: flex;
flex-direction: column;
overflow: hidden;
`;
const StyledHeader = styled.div`
align-items: center;
background: ${themeCssVariables.background.tertiary};
border-bottom: 1px solid ${themeCssVariables.border.color.medium};
display: flex;
gap: ${themeCssVariables.spacing[2]};
justify-content: space-between;
padding: ${themeCssVariables.spacing[1]} ${themeCssVariables.spacing[2]};
`;
const StyledHeaderLeft = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.secondary};
display: flex;
font-size: ${themeCssVariables.font.size.sm};
gap: ${themeCssVariables.spacing[1]};
`;
const StyledTabContainer = styled.div`
display: flex;
gap: ${themeCssVariables.spacing[1]};
`;
const StyledTab = styled.button<{ isActive: boolean; hasError?: boolean }>`
background: ${({ isActive }) =>
isActive ? themeCssVariables.background.secondary : 'transparent'};
border: none;
border-radius: ${themeCssVariables.border.radius.xs};
color: ${({ isActive, hasError }) =>
hasError
? themeCssVariables.color.red
: isActive
? themeCssVariables.font.color.primary
: themeCssVariables.font.color.tertiary};
cursor: pointer;
font-size: ${themeCssVariables.font.size.xs};
font-weight: ${({ isActive }) =>
isActive
? themeCssVariables.font.weight.medium
: themeCssVariables.font.weight.regular};
padding: ${themeCssVariables.spacing['0.5']} ${themeCssVariables.spacing[1]};
&:hover {
background: ${themeCssVariables.background.secondary};
color: ${({ hasError }) =>
hasError
? themeCssVariables.color.red
: themeCssVariables.font.color.primary};
}
`;
const StyledOutputArea = styled.div<{ isError?: boolean }>`
background: ${themeCssVariables.background.tertiary};
color: ${({ isError }) =>
isError
? themeCssVariables.color.red
: themeCssVariables.font.color.primary};
font-family: ${MONOSPACE_FONT_FAMILY};
font-size: ${themeCssVariables.font.size.sm};
line-height: 1.5;
max-height: 300px;
min-height: 100px;
overflow-y: auto;
padding: ${themeCssVariables.spacing[2]};
white-space: pre-wrap;
word-break: break-word;
`;
const StyledEmptyMessage = styled.div`
color: ${themeCssVariables.font.color.tertiary};
font-style: italic;
`;
const StyledCursor = styled.span`
animation: blink 1s step-end infinite;
background: ${themeCssVariables.font.color.primary};
display: inline-block;
height: 1em;
margin-left: 2px;
vertical-align: text-bottom;
width: 8px;
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
`;
type TabType = 'stdout' | 'stderr';
type TerminalOutputProps = {
stdout: string;
stderr: string;
isRunning?: boolean;
};
export const TerminalOutput = ({
stdout,
stderr,
isRunning = false,
}: TerminalOutputProps) => {
const { t } = useLingui();
const { copyToClipboard } = useCopyToClipboard();
const hasStderr = stderr.length > 0;
const hasStdout = stdout.length > 0;
const defaultTab: TabType = hasStderr && !hasStdout ? 'stderr' : 'stdout';
const [userSelectedTab, setUserSelectedTab] = useState<TabType | null>(null);
const activeTab = userSelectedTab ?? defaultTab;
const currentOutput = activeTab === 'stdout' ? stdout : stderr;
return (
<StyledContainer>
<StyledHeader>
<StyledHeaderLeft>
<IconTerminal size={14} />
{t`Output`}
</StyledHeaderLeft>
<StyledTabContainer>
<StyledTab
isActive={activeTab === 'stdout'}
onClick={() => setUserSelectedTab('stdout')}
>
stdout
</StyledTab>
{hasStderr && (
<StyledTab
isActive={activeTab === 'stderr'}
hasError
onClick={() => setUserSelectedTab('stderr')}
>
stderr
</StyledTab>
)}
<LightIconButton
Icon={IconCopy}
onClick={() => copyToClipboard(currentOutput)}
title={t`Copy output`}
size="small"
accent="tertiary"
/>
</StyledTabContainer>
</StyledHeader>
<StyledOutputArea isError={activeTab === 'stderr'}>
{currentOutput ? (
<>
{currentOutput}
{isRunning && <StyledCursor />}
</>
) : (
<StyledEmptyMessage>
{isRunning ? t`Waiting for output...` : t`No output`}
</StyledEmptyMessage>
)}
</StyledOutputArea>
</StyledContainer>
);
};