Skip to content
Open
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
8 changes: 8 additions & 0 deletions packages/happy-app/sources/app/(app)/session/[id]/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,14 @@ function SessionInfoContent({ session }: { session: Session }) {
showChevron={false}
/>
)}
{session.metadata.startedBy && (
<Item
title={t('sessionInfo.startedBy')}
detail={session.metadata.startedBy === 'daemon' ? t('sessionInfo.startedByDaemon') : t('sessionInfo.startedByTerminal')}
icon={<Ionicons name={session.metadata.startedBy === 'daemon' ? 'cloud-outline' : 'terminal-outline'} size={29} color="#5856D6" />}
showChevron={false}
/>
)}
{session.metadata.happyHomeDir && (
<Item
title={t('sessionInfo.happyHome')}
Expand Down
49 changes: 49 additions & 0 deletions packages/happy-app/sources/sync/storageTypes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, it, expect } from 'vitest';
import { MetadataSchema } from './storageTypes';

describe('MetadataSchema', () => {
const baseMetadata = {
path: '/home/user/project',
host: 'my-host',
};

describe('startedBy field', () => {
it('accepts daemon as startedBy value', () => {
const result = MetadataSchema.safeParse({
...baseMetadata,
startedBy: 'daemon',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.startedBy).toBe('daemon');
}
});

it('accepts terminal as startedBy value', () => {
const result = MetadataSchema.safeParse({
...baseMetadata,
startedBy: 'terminal',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.startedBy).toBe('terminal');
}
});

it('accepts metadata without startedBy (optional)', () => {
const result = MetadataSchema.safeParse(baseMetadata);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.startedBy).toBeUndefined();
}
});

it('rejects invalid startedBy values', () => {
const result = MetadataSchema.safeParse({
...baseMetadata,
startedBy: 'unknown',
});
expect(result.success).toBe(false);
});
});
});
1 change: 1 addition & 0 deletions packages/happy-app/sources/sync/storageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const MetadataSchema = z.object({
homeDir: z.string().optional(), // User's home directory on the machine
happyHomeDir: z.string().optional(), // Happy configuration directory
hostPid: z.number().optional(), // Process ID of the session
startedBy: z.enum(['daemon', 'terminal']).optional(), // How the session was started
flavor: z.string().nullish(), // Session flavor/variant identifier
sandbox: z.any().nullish(), // Sandbox config metadata from CLI (or null when disabled)
dangerouslySkipPermissions: z.boolean().nullish(), // Claude --dangerously-skip-permissions mode (or null when unknown)
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-app/sources/text/_default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ export const en = {
path: 'Path',
operatingSystem: 'Operating System',
processId: 'Process ID',
startedBy: 'Started By',
startedByDaemon: 'Daemon',
startedByTerminal: 'Terminal',
happyHome: 'Happy Home',
copyMetadata: 'Copy Metadata',
agentState: 'Agent State',
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-app/sources/text/translations/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ export const ca: TranslationStructure = {
path: 'Camí',
operatingSystem: 'Sistema operatiu',
processId: 'ID del procés',
startedBy: 'Iniciat per',
startedByDaemon: 'Daemon',
startedByTerminal: 'Terminal',
happyHome: 'Directori de Happy',
copyMetadata: 'Copia les metadades',
agentState: 'Estat de l\'agent',
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-app/sources/text/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@ export const en: TranslationStructure = {
path: 'Path',
operatingSystem: 'Operating System',
processId: 'Process ID',
startedBy: 'Started By',
startedByDaemon: 'Daemon',
startedByTerminal: 'Terminal',
happyHome: 'Happy Home',
copyMetadata: 'Copy Metadata',
agentState: 'Agent State',
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-app/sources/text/translations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ export const es: TranslationStructure = {
path: 'Ruta',
operatingSystem: 'Sistema operativo',
processId: 'ID del proceso',
startedBy: 'Iniciado por',
startedByDaemon: 'Daemon',
startedByTerminal: 'Terminal',
happyHome: 'Directorio de Happy',
copyMetadata: 'Copiar metadatos',
agentState: 'Estado del agente',
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-app/sources/text/translations/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,9 @@ export const it: TranslationStructure = {
path: 'Percorso',
operatingSystem: 'Sistema operativo',
processId: 'ID processo',
startedBy: 'Avviato da',
startedByDaemon: 'Daemon',
startedByTerminal: 'Terminale',
happyHome: 'Happy Home',
copyMetadata: 'Copia metadati',
agentState: 'Stato agente',
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-app/sources/text/translations/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,9 @@ export const ja: TranslationStructure = {
path: 'パス',
operatingSystem: 'オペレーティングシステム',
processId: 'プロセスID',
startedBy: '起動元',
startedByDaemon: 'デーモン',
startedByTerminal: 'ターミナル',
happyHome: 'Happy Home',
copyMetadata: 'メタデータをコピー',
agentState: 'エージェント状態',
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-app/sources/text/translations/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,9 @@ export const pl: TranslationStructure = {
path: 'Ścieżka',
operatingSystem: 'System operacyjny',
processId: 'ID procesu',
startedBy: 'Uruchomiono przez',
startedByDaemon: 'Demon',
startedByTerminal: 'Terminal',
happyHome: 'Katalog domowy Happy',
copyMetadata: 'Kopiuj metadane',
agentState: 'Stan agenta',
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-app/sources/text/translations/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,9 @@ export const pt: TranslationStructure = {
path: 'Caminho',
operatingSystem: 'Sistema operacional',
processId: 'ID do processo',
startedBy: 'Iniciado por',
startedByDaemon: 'Daemon',
startedByTerminal: 'Terminal',
happyHome: 'Diretório Happy',
copyMetadata: 'Copiar metadados',
agentState: 'Estado do agente',
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-app/sources/text/translations/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@ export const ru: TranslationStructure = {
path: 'Путь',
operatingSystem: 'Операционная система',
processId: 'ID процесса',
startedBy: 'Запущено',
startedByDaemon: 'Демон',
startedByTerminal: 'Терминал',
happyHome: 'Домашний каталог Happy',
copyMetadata: 'Копировать метаданные',
agentState: 'Состояние агента',
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-app/sources/text/translations/zh-Hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,9 @@ export const zhHans: TranslationStructure = {
path: '路径',
operatingSystem: '操作系统',
processId: '进程 ID',
startedBy: '启动方式',
startedByDaemon: '守护进程',
startedByTerminal: '终端',
happyHome: 'Happy 主目录',
copyMetadata: '复制元数据',
agentState: 'Agent 状态',
Expand Down
3 changes: 3 additions & 0 deletions packages/happy-app/sources/text/translations/zh-Hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,9 @@ export const zhHant: TranslationStructure = {
path: '路徑',
operatingSystem: '作業系統',
processId: '處理程序 ID',
startedBy: '啟動方式',
startedByDaemon: '守護程序',
startedByTerminal: '終端機',
happyHome: 'Happy 主目錄',
copyMetadata: '複製中繼資料',
agentState: 'Agent 狀態',
Expand Down
95 changes: 95 additions & 0 deletions packages/happy-app/sources/utils/sessionUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, it, expect, vi } from 'vitest';
import { getSessionSubtitle, formatPathRelativeToHome, getSessionName } from './sessionUtils';
import { Session } from '@/sync/storageTypes';

// Mock @/text to return key-based values for deterministic testing
vi.mock('@/text', () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'status.unknown': 'Unknown',
'sessionInfo.startedByDaemon': 'Daemon',
'sessionInfo.startedByTerminal': 'Terminal',
};
return translations[key] || key;
}
}));

function createSession(overrides: Partial<Session> = {}): Session {
return {
id: 'test-session-id',
seq: 1,
createdAt: Date.now(),
updatedAt: Date.now(),
active: true,
activeAt: Date.now(),
presence: 'online',
thinking: false,
thinkingAt: 0,
metadata: {
path: '/home/user/projects/my-app',
host: 'localhost',
homeDir: '/home/user',
},
agentState: null,
messages: [],
permissionMode: 'default',
...overrides,
} as Session;
}

describe('sessionUtils', () => {
describe('getSessionSubtitle', () => {
it('returns path relative to home for terminal sessions', () => {
const session = createSession();
expect(getSessionSubtitle(session)).toBe('~/projects/my-app');
});

it('appends daemon label when session was started by daemon', () => {
const session = createSession({
metadata: {
path: '/home/user/projects/my-app',
host: 'localhost',
homeDir: '/home/user',
startedBy: 'daemon',
},
} as Partial<Session>);
expect(getSessionSubtitle(session)).toBe('~/projects/my-app · daemon');
});

it('does not append label for terminal sessions', () => {
const session = createSession({
metadata: {
path: '/home/user/projects/my-app',
host: 'localhost',
homeDir: '/home/user',
startedBy: 'terminal',
},
} as Partial<Session>);
expect(getSessionSubtitle(session)).toBe('~/projects/my-app');
});

it('does not append label when startedBy is not set', () => {
const session = createSession();
expect(getSessionSubtitle(session)).not.toContain('·');
});

it('returns Unknown when metadata is missing', () => {
const session = createSession({ metadata: null } as Partial<Session>);
expect(getSessionSubtitle(session)).toBe('Unknown');
});
});

describe('formatPathRelativeToHome', () => {
it('replaces home dir with ~', () => {
expect(formatPathRelativeToHome('/home/user/projects', '/home/user')).toBe('~/projects');
});

it('returns full path when no homeDir', () => {
expect(formatPathRelativeToHome('/home/user/projects')).toBe('/home/user/projects');
});

it('returns ~ for exact home dir match', () => {
expect(formatPathRelativeToHome('/home/user', '/home/user')).toBe('~');
});
});
});
6 changes: 5 additions & 1 deletion packages/happy-app/sources/utils/sessionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,11 @@ export function formatPathRelativeToHome(path: string, homeDir?: string): string
*/
export function getSessionSubtitle(session: Session): string {
if (session.metadata) {
return formatPathRelativeToHome(session.metadata.path, session.metadata.homeDir);
const path = formatPathRelativeToHome(session.metadata.path, session.metadata.homeDir);
if (session.metadata.startedBy === 'daemon') {
return `${path} · ${t('sessionInfo.startedByDaemon').toLowerCase()}`;
}
return path;
}
return t('status.unknown');
}
Expand Down