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
5 changes: 5 additions & 0 deletions android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>
Binary file modified android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions android/app/src/main/res/values/colors.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>
7 changes: 7 additions & 0 deletions assets/branding/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ Esta pasta guarda os assets oficiais da identidade **Forty-Two AI**.
## Asset atual

- `forty-two-ai-logo.webp` — versão otimizada para apresentação no repositório.
- `forty-two-ai-playstore-icon.png` — ícone canônico de distribuição Android em 512 × 512 px.

Os ícones de launcher instalados no Android derivam do pacote oficial gerado em
1º de setembro de 2026. Em Android 8.0 ou superior, o aplicativo usa ícone
adaptativo com fundo preto; nas versões anteriores, usa os PNGs legados por
densidade. O ícone redondo mantém a mesma arte e deixa o launcher aplicar sua
máscara nativa.

O desenho deve ser preservado sem recolorações arbitrárias, deformação, cortes ou simplificações que comprometam o símbolo, o lettering e a composição metálica original.

Expand Down
Binary file added assets/branding/forty-two-ai-playstore-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 18 additions & 2 deletions docs/forty-two/PROJECT_CANON.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Forty-Two AI — Project Canon

**Last continuity checkpoint:** 2026-08-18 00:38 BRT (UTC-03:00)
**Last continuity checkpoint:** 2026-09-02 07:07 BRT (UTC-03:00)

## Canonical project

Expand Down Expand Up @@ -38,7 +38,7 @@ The current master artifacts are tracked by SHA-256 so they remain identifiable
- logo/wordmark master, 1254×1254 PNG: `b49a78296320da607b2ea61eb17ce14ef33c5fa518085b18a1ff9d0488729738`
- splash master, 1586×992 PNG: `07537b6d6abce8e5d0643fbb00c4139705e384e0aa7b6f4fda12f053594ae786`
- startup intro MP4, 688×752 H.264/AAC, ~12.04 s: `6ba07d556b4026824d333c8f24ad1f35b4f48ac1388f319231961f9756493419`
- Android launcher asset bundle: `6a9d522241cbd5dd18a30f45e8bb3bf43ca8fac40731831c550dd40331a85153`
- Android launcher asset bundle, added to Project Files as `Forty Two AI icons.zip`: `2786a44e36b0572e42041a980d25e85a0bede6f8bfa7f67f9970762847e9c4ca`

## What belongs in Git

Expand Down Expand Up @@ -70,6 +70,22 @@ Current preserved reference-only artifacts:

Forty-Two AI is local-first and chat-first. The current acceleration target is the Blackview MEGA 3 / Mali-G57 MC2. CPU fallback must remain valid. Hardware capability detection must never be presented as proof that an inference backend is actually active: requested backend, effective backend and effective offload must be reported separately.

## Active implementation state

- Canonical `main` observed at `3a311bd36469b2584ae518537b45339bdbbc550a`.
- Draft PR #1, branch `runtime/orchestrator-v1`, remains the isolated runtime-orchestrator foundation; its observed head is `c41e1438440291b8ee34e3a6dd58ee8f814be767`.
- Draft PR #2, branch `ui/forty-two-chat-shell-v1`, is the active UI integration line. The current slice starts from `9cf72644b189965fdec333d93a00607c3fe811c5` and installs the official Android launcher assets, adaptive-icon definitions, Play Store derivative and direct quick-panel access to the existing model/Pal picker and generation parameters.
- The UI slice does not modify the Kotlin/JNI bridge or claim Vulkan execution. Runtime integration remains separate until the branches are reconciled deliberately and effective-backend telemetry is verified on device.

### Verification at this checkpoint

- TypeScript compilation: passed.
- Repository ESLint: passed with 0 errors and 7 pre-existing warnings in unrelated files.
- Localization and bundled-font validation: passed.
- `ChatHeader` and `HeaderRight` callback-routing tests: 13 passed.
- Android launcher dimensions, PNG readability, XML parsing and resource-variant inventory: passed.
- Local Gradle resource processing: not completed because the isolated environment could not reach `services.gradle.org` to download Gradle 9.0.0; repository CI is the authoritative next build check.

## Chronology rule

Human-readable project checkpoints and continuity documents should include an absolute **date + local time + timezone**. Git commit timestamps remain authoritative for code history. This avoids ambiguity when long chats freeze, resume later or are reconstructed from Project Files.
Expand Down
19 changes: 13 additions & 6 deletions src/components/ChatHeader/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {observer} from 'mobx-react';
import {createStyles} from './styles';
import {HeaderRight} from '../HeaderRight';
import {ChatHeaderTitle} from '../ChatHeaderTitle';
import {EnterpriseQuickPanel} from '../EnterpriseQuickPanel';
import {FortyTwoQuickPanel} from '../FortyTwoQuickPanel';
import {
useSafeAreaFrame,
useSafeAreaInsets,
Expand All @@ -15,7 +15,13 @@ import {useTheme} from '../../hooks';
import {chatSessionStore} from '../../store';
import {HeaderLeft} from '../HeaderLeft';

export const ChatHeader: React.FC = observer(() => {
interface Props {
onOpenModelPalPicker?: () => void;
onOpenGenerationSettings?: () => void;
}

export const ChatHeader = observer((props: Props) => {
const {onOpenModelPalPicker, onOpenGenerationSettings} = props;
const theme = useTheme();

const insets = useSafeAreaInsets();
Expand All @@ -24,12 +30,10 @@ export const ChatHeader: React.FC = observer(() => {
// On models with Dynamic Island the status bar height is smaller than the safe area top inset.
// https://github.com/react-navigation/react-navigation/blob/e4815c538536ddccf4207b87bf3e2f1603dedd84/packages/elements/src/Header/Header.tsx#L52
// NOTE: in v7, this is fixed and getDefaultHeaderHeight returns the correct height.

const hasDynamicIsland = Platform.OS === 'ios' && insets.top > 50;
const statusBarHeight = hasDynamicIsland ? insets.top - 5 : insets.top;

const headerHeight = getDefaultHeaderHeight(layout, false, statusBarHeight);

const styles = createStyles({theme, insets, headerHeight});

const headerStyle = chatSessionStore?.shouldShowHeaderDivider
Expand All @@ -43,8 +47,11 @@ export const ChatHeader: React.FC = observer(() => {
<ChatHeaderTitle />
</View>
<View style={styles.rightSection}>
<EnterpriseQuickPanel />
<HeaderRight />
<FortyTwoQuickPanel
onOpenModelPalPicker={onOpenModelPalPicker}
onOpenGenerationSettings={onOpenGenerationSettings}
/>
<HeaderRight onOpenGenerationSettings={onOpenGenerationSettings} />
</View>
</View>
);
Expand Down
44 changes: 34 additions & 10 deletions src/components/ChatHeader/__tests__/ChatHeader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ jest.mock('../../HeaderLeft', () => ({
},
}));

const mockHeaderRight = jest.fn((_props: object) => {
const {View} = require('react-native');
return <View testID="header-right" />;
});

jest.mock('../../HeaderRight', () => ({
HeaderRight: () => {
const {View} = require('react-native');
return <View testID="header-right" />;
},
HeaderRight: (props: object) => mockHeaderRight(props),
}));

jest.mock('../../ChatHeaderTitle', () => ({
Expand All @@ -24,11 +26,13 @@ jest.mock('../../ChatHeaderTitle', () => ({
},
}));

jest.mock('../../EnterpriseQuickPanel', () => ({
EnterpriseQuickPanel: () => {
const {View} = require('react-native');
return <View testID="enterprise-quick-panel" />;
},
const mockFortyTwoQuickPanel = jest.fn((_props: object) => {
const {View} = require('react-native');
return <View testID="forty-two-quick-panel" />;
});

jest.mock('../../FortyTwoQuickPanel', () => ({
FortyTwoQuickPanel: (props: object) => mockFortyTwoQuickPanel(props),
}));

// Create a mock store object
Expand All @@ -55,7 +59,27 @@ describe('ChatHeader', () => {
expect(getByTestId('header-left')).toBeTruthy();
expect(getByTestId('header-right')).toBeTruthy();
expect(getByTestId('chat-header-title')).toBeTruthy();
expect(getByTestId('enterprise-quick-panel')).toBeTruthy();
expect(getByTestId('forty-two-quick-panel')).toBeTruthy();
});

it('forwards quick-control callbacks to the Forty-Two panel', () => {
const onOpenModelPalPicker = jest.fn();
const onOpenGenerationSettings = jest.fn();

render(
<ChatHeader
onOpenModelPalPicker={onOpenModelPalPicker}
onOpenGenerationSettings={onOpenGenerationSettings}
/>,
);

expect(mockFortyTwoQuickPanel).toHaveBeenLastCalledWith({
onOpenModelPalPicker,
onOpenGenerationSettings,
});
expect(mockHeaderRight).toHaveBeenLastCalledWith({
onOpenGenerationSettings,
});
});

it('applies correct styles when header divider should not be shown', () => {
Expand Down
14 changes: 10 additions & 4 deletions src/components/ChatHeader/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const createStyles = ({
container: {
height: headerHeight,
paddingTop: insets.top,
paddingHorizontal: 8,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
Expand All @@ -35,18 +36,23 @@ export const createStyles = ({
flexShrink: 0,
},
menuIcon: {
height: 40,
width: 40,
height: 44,
width: 44,
justifyContent: 'center',
alignItems: 'center',
},
headerWithoutDivider: {
elevation: 0,
shadowOpacity: 0,
borderBottomWidth: 0,
backgroundColor: theme.colors.background,
borderBottomColor: 'transparent',
backgroundColor: theme.colors.surface,
},
headerWithDivider: {
backgroundColor: theme.colors.background,
elevation: 0,
shadowOpacity: 0,
borderBottomWidth: 1,
borderBottomColor: theme.colors.outline,
backgroundColor: theme.colors.surface,
},
});
25 changes: 19 additions & 6 deletions src/components/ChatHeaderTitle/ChatHeaderTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ const backendLabel = {
idle: 'pronto para carregar',
remote: 'remoto',
cpu: 'CPU',
gpu: 'GPU',
hybrid: 'CPU + GPU',
unverified: 'acelerador não verificado',
gpu: 'Vulkan',
hybrid: 'CPU + Vulkan',
unverified: 'aceleração não verificada',
} as const;

export const ChatHeaderTitle: React.FC = observer(() => {
Expand All @@ -38,17 +38,30 @@ export const ChatHeaderTitle: React.FC = observer(() => {
const defaultTitle = l10n.components.chatHeaderTitle.defaultTitle;
const title = activePal?.name || activeSession?.title || defaultTitle;
const modelLabel = activeModel?.name ?? 'Nenhum modelo carregado';
const executionLabel = backendLabel[enterpriseRuntimeStore.effectiveBackend];
const effectiveBackend = enterpriseRuntimeStore.effectiveBackend;
const executionLabel = backendLabel[effectiveBackend];

const statusColor =
effectiveBackend === 'gpu' || effectiveBackend === 'hybrid'
? theme.colors.tertiary
: effectiveBackend === 'cpu'
? theme.colors.secondary
: effectiveBackend === 'unverified'
? theme.colors.error
: theme.colors.outlineVariant;

return (
<View style={styles.container}>
<Text numberOfLines={1} variant="titleSmall" style={styles.title}>
{title}
</Text>
<View style={styles.subtitleRow}>
<View style={styles.onlineDot} />
<View style={[styles.statusDot, {backgroundColor: statusColor}]} />
<Text numberOfLines={1} variant="bodySmall" style={styles.subtitle}>
{modelLabel}
</Text>
<Text numberOfLines={1} variant="bodySmall" style={styles.subtitle}>
{modelLabel} · {executionLabel}
· {executionLabel}
</Text>
</View>
</View>
Expand Down
3 changes: 1 addition & 2 deletions src/components/ChatHeaderTitle/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ export const createStyles = (theme: Theme) =>
gap: 6,
minWidth: 0,
},
onlineDot: {
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: theme.colors.primary,
},
subtitle: {
flexShrink: 1,
Expand Down
Loading
Loading