Skip to content
Closed
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
44 changes: 44 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.PHONY: up down server web cli cli-stop logs clean

up: ## Start all containerized services (server + web)
docker compose up -d --build

down: ## Stop all services
docker compose down

server: ## Start only the backend server
docker compose up -d --build server

web: ## Start only the web UI
docker compose up -d --build web

seed: ## Authenticate CLI against local server (no mobile app needed)
HAPPY_SERVER_URL=http://localhost:3005 node scripts/seed-cli-auth.mjs

machine-info: ## Report host machine info (ROOT=/path required)
HAPPY_SERVER_URL=http://localhost:3005 HAPPY_WORKSPACE_ROOT=$(ROOT) node scripts/report-machine-info.mjs

cli: ## Start happy-cli daemon (ROOT=/path/to/workspace required)
cd packages/happy-cli && HAPPY_SERVER_URL=http://localhost:3005 \
HAPPY_WORKSPACE_ROOT=$(ROOT) node bin/happy.mjs daemon start

cli-stop: ## Stop happy-cli daemon
cd packages/happy-cli && node bin/happy.mjs daemon stop

cli-build: ## Build the happy CLI (required before daemon start)
yarn workspace happy build

logs: ## Tail logs from all containers
docker compose logs -f

logs-server: ## Tail server logs only
docker compose logs -f server

logs-web: ## Tail web UI logs only
docker compose logs -f web

clean: ## Stop containers and remove volumes
docker compose down -v

help: ## Show available targets
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}'
18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
services:
server:
build:
context: .
dockerfile: Dockerfile
ports:
- "3005:3005"
volumes:
- happy-data:/data
environment:
- HANDY_MASTER_SECRET=happy-dev-secret
working_dir: /repo/packages/happy-server
command: >
sh -c "../../node_modules/.bin/tsx sources/standalone.ts migrate &&
exec ../../node_modules/.bin/tsx sources/standalone.ts serve"

volumes:
happy-data:
6 changes: 6 additions & 0 deletions packages/happy-app/sources/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ export default function RootLayout() {
headerBackTitle: t('common.cancel'),
}}
/>
<Stack.Screen
name="tasks/index"
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="text-selection"
options={{
Expand Down
10 changes: 10 additions & 0 deletions packages/happy-app/sources/app/(app)/tasks/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as React from 'react';
import { View } from 'react-native';
import { Redirect } from 'expo-router';

/**
* Non-web platforms redirect to home - task manager is web-only.
*/
export default React.memo(function TasksRedirect() {
return <Redirect href="/" />;
});
85 changes: 85 additions & 0 deletions packages/happy-app/sources/app/(app)/tasks/index.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as React from 'react';
import { View, Text, TouchableOpacity, ScrollView, TextInput, Pressable } from 'react-native';
import { StyleSheet, useUnistyles } from 'react-native-unistyles';
import { useAuth } from '@/auth/AuthContext';
import { useTaskManagerStore, useTaskManagerActions } from '@/hooks/useTaskManager';
import { TaskDetailView } from '@/components/web/TaskDetailView';
import { TaskManagerSidebar } from '@/components/web/TaskManagerSidebar';
import { t } from '@/text';

export default React.memo(function TaskManagerScreen() {
const { isAuthenticated } = useAuth();
const { loadProjects, loadAgents, loadMachines } = useTaskManagerActions();
const selectedTaskId = useTaskManagerStore(s => s.selectedTaskId);

React.useEffect(() => {
if (!isAuthenticated) return;
loadProjects();
loadAgents();
loadMachines();
const interval = setInterval(loadMachines, 30000);
return () => clearInterval(interval);
}, [isAuthenticated]);

if (!isAuthenticated) {
return (
<View style={styles.loading}>
<Text style={styles.loadingText}>{t('status.connecting')}</Text>
</View>
);
}

return (
<View style={styles.container}>
<View style={styles.sidebar}>
<TaskManagerSidebar />
</View>
<View style={styles.main}>
{selectedTaskId ? (
<TaskDetailView taskId={selectedTaskId} />
) : (
<View style={styles.empty}>
<Text style={styles.emptyText}>{t('taskManager.selectTask')}</Text>
</View>
)}
</View>
</View>
);
});

const styles = StyleSheet.create((theme) => ({
container: {
flex: 1,
flexDirection: 'row',
backgroundColor: theme.colors.groupped.background,
},
sidebar: {
width: 320,
borderRightWidth: 1,
borderRightColor: theme.colors.divider,
backgroundColor: theme.colors.surface,
},
main: {
flex: 1,
backgroundColor: theme.colors.surface,
},
empty: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
emptyText: {
fontSize: 14,
color: theme.colors.textSecondary,
},
loading: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: theme.colors.groupped.background,
},
loadingText: {
fontSize: 14,
color: theme.colors.textSecondary,
},
}));
9 changes: 7 additions & 2 deletions packages/happy-app/sources/components/MainView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,11 @@ const TAB_TITLES = {
sessions: 'tabs.sessions',
inbox: 'tabs.inbox',
settings: 'tabs.settings',
tasks: 'tabs.tasks',
} as const;

// Active tabs
type ActiveTabType = 'sessions' | 'inbox' | 'settings';
type ActiveTabType = 'sessions' | 'inbox' | 'settings' | 'tasks';

// Header title component with connection status
const HeaderTitle = React.memo(({ activeTab }: { activeTab: ActiveTabType }) => {
Expand Down Expand Up @@ -240,8 +241,12 @@ export const MainView = React.memo(({ variant }: MainViewProps) => {
}, [router]);

const handleTabPress = React.useCallback((tab: TabType) => {
if (tab === 'tasks') {
router.push('/tasks');
return;
}
setActiveTab(tab);
}, []);
}, [router]);

// Regular phone mode with tabs - define this before any conditional returns
const renderTabContent = React.useCallback(() => {
Expand Down
13 changes: 8 additions & 5 deletions packages/happy-app/sources/components/TabBar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { View, Pressable, Text } from 'react-native';
import { View, Pressable, Text, Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { StyleSheet, useUnistyles } from 'react-native-unistyles';
import { Image } from 'expo-image';
Expand All @@ -8,7 +8,7 @@ import { Typography } from '@/constants/Typography';
import { layout } from '@/components/layout';
import { useInboxHasContent } from '@/hooks/useInboxHasContent';

export type TabType = 'inbox' | 'sessions' | 'settings';
export type TabType = 'inbox' | 'sessions' | 'settings' | 'tasks';

interface TabBarProps {
activeTab: TabType;
Expand Down Expand Up @@ -86,12 +86,15 @@ export const TabBar = React.memo(({ activeTab, onTabPress, inboxBadgeCount = 0 }
const inboxHasContent = useInboxHasContent();

const tabs: { key: TabType; icon: any; label: string }[] = React.useMemo(() => {
// NOTE: Zen tab removed - the feature never got to a useful state
return [
const base: { key: TabType; icon: any; label: string }[] = [
{ key: 'inbox', icon: require('@/assets/images/brutalist/Brutalism-27.png'), label: t('tabs.inbox') },
{ key: 'sessions', icon: require('@/assets/images/brutalist/Brutalism-15.png'), label: t('tabs.sessions') },
{ key: 'settings', icon: require('@/assets/images/brutalist/Brutalism-9.png'), label: t('tabs.settings') },
];
if (Platform.OS === 'web') {
base.push({ key: 'tasks', icon: require('@/assets/images/brutalist/Brutalism-15.png'), label: t('tabs.tasks') });
}
base.push({ key: 'settings', icon: require('@/assets/images/brutalist/Brutalism-9.png'), label: t('tabs.settings') });
return base;
}, []);

return (
Expand Down
Loading