diff --git a/packages/happy-app/sources/app/(app)/session/[id]/info.tsx b/packages/happy-app/sources/app/(app)/session/[id]/info.tsx
index 9bedf2212..0eb8d5716 100644
--- a/packages/happy-app/sources/app/(app)/session/[id]/info.tsx
+++ b/packages/happy-app/sources/app/(app)/session/[id]/info.tsx
@@ -404,6 +404,14 @@ function SessionInfoContent({ session }: { session: Session }) {
showChevron={false}
/>
)}
+ {session.metadata.startedBy && (
+ }
+ showChevron={false}
+ />
+ )}
{session.metadata.happyHomeDir && (
- {
+ 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);
+ });
+ });
+});
diff --git a/packages/happy-app/sources/sync/storageTypes.ts b/packages/happy-app/sources/sync/storageTypes.ts
index 643f75445..fa634125a 100644
--- a/packages/happy-app/sources/sync/storageTypes.ts
+++ b/packages/happy-app/sources/sync/storageTypes.ts
@@ -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)
diff --git a/packages/happy-app/sources/text/_default.ts b/packages/happy-app/sources/text/_default.ts
index c3a0d673a..35c8cdfee 100644
--- a/packages/happy-app/sources/text/_default.ts
+++ b/packages/happy-app/sources/text/_default.ts
@@ -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',
diff --git a/packages/happy-app/sources/text/translations/ca.ts b/packages/happy-app/sources/text/translations/ca.ts
index 76d928500..ed431386e 100644
--- a/packages/happy-app/sources/text/translations/ca.ts
+++ b/packages/happy-app/sources/text/translations/ca.ts
@@ -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',
diff --git a/packages/happy-app/sources/text/translations/en.ts b/packages/happy-app/sources/text/translations/en.ts
index b86aa4af6..d481d679d 100644
--- a/packages/happy-app/sources/text/translations/en.ts
+++ b/packages/happy-app/sources/text/translations/en.ts
@@ -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',
diff --git a/packages/happy-app/sources/text/translations/es.ts b/packages/happy-app/sources/text/translations/es.ts
index 41c17664e..22fe8710c 100644
--- a/packages/happy-app/sources/text/translations/es.ts
+++ b/packages/happy-app/sources/text/translations/es.ts
@@ -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',
diff --git a/packages/happy-app/sources/text/translations/it.ts b/packages/happy-app/sources/text/translations/it.ts
index 6dbfb52ac..66df13935 100644
--- a/packages/happy-app/sources/text/translations/it.ts
+++ b/packages/happy-app/sources/text/translations/it.ts
@@ -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',
diff --git a/packages/happy-app/sources/text/translations/ja.ts b/packages/happy-app/sources/text/translations/ja.ts
index 090480d7a..16edeb971 100644
--- a/packages/happy-app/sources/text/translations/ja.ts
+++ b/packages/happy-app/sources/text/translations/ja.ts
@@ -388,6 +388,9 @@ export const ja: TranslationStructure = {
path: 'パス',
operatingSystem: 'オペレーティングシステム',
processId: 'プロセスID',
+ startedBy: '起動元',
+ startedByDaemon: 'デーモン',
+ startedByTerminal: 'ターミナル',
happyHome: 'Happy Home',
copyMetadata: 'メタデータをコピー',
agentState: 'エージェント状態',
diff --git a/packages/happy-app/sources/text/translations/pl.ts b/packages/happy-app/sources/text/translations/pl.ts
index 55401af6a..7422b1d46 100644
--- a/packages/happy-app/sources/text/translations/pl.ts
+++ b/packages/happy-app/sources/text/translations/pl.ts
@@ -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',
diff --git a/packages/happy-app/sources/text/translations/pt.ts b/packages/happy-app/sources/text/translations/pt.ts
index 75d9afed2..f5d13bd45 100644
--- a/packages/happy-app/sources/text/translations/pt.ts
+++ b/packages/happy-app/sources/text/translations/pt.ts
@@ -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',
diff --git a/packages/happy-app/sources/text/translations/ru.ts b/packages/happy-app/sources/text/translations/ru.ts
index 930474bb7..a16ca5856 100644
--- a/packages/happy-app/sources/text/translations/ru.ts
+++ b/packages/happy-app/sources/text/translations/ru.ts
@@ -330,6 +330,9 @@ export const ru: TranslationStructure = {
path: 'Путь',
operatingSystem: 'Операционная система',
processId: 'ID процесса',
+ startedBy: 'Запущено',
+ startedByDaemon: 'Демон',
+ startedByTerminal: 'Терминал',
happyHome: 'Домашний каталог Happy',
copyMetadata: 'Копировать метаданные',
agentState: 'Состояние агента',
diff --git a/packages/happy-app/sources/text/translations/zh-Hans.ts b/packages/happy-app/sources/text/translations/zh-Hans.ts
index a9db3ead3..19d0a8afe 100644
--- a/packages/happy-app/sources/text/translations/zh-Hans.ts
+++ b/packages/happy-app/sources/text/translations/zh-Hans.ts
@@ -358,6 +358,9 @@ export const zhHans: TranslationStructure = {
path: '路径',
operatingSystem: '操作系统',
processId: '进程 ID',
+ startedBy: '启动方式',
+ startedByDaemon: '守护进程',
+ startedByTerminal: '终端',
happyHome: 'Happy 主目录',
copyMetadata: '复制元数据',
agentState: 'Agent 状态',
diff --git a/packages/happy-app/sources/text/translations/zh-Hant.ts b/packages/happy-app/sources/text/translations/zh-Hant.ts
index e09ca6f3c..4ec227f9a 100644
--- a/packages/happy-app/sources/text/translations/zh-Hant.ts
+++ b/packages/happy-app/sources/text/translations/zh-Hant.ts
@@ -357,6 +357,9 @@ export const zhHant: TranslationStructure = {
path: '路徑',
operatingSystem: '作業系統',
processId: '處理程序 ID',
+ startedBy: '啟動方式',
+ startedByDaemon: '守護程序',
+ startedByTerminal: '終端機',
happyHome: 'Happy 主目錄',
copyMetadata: '複製中繼資料',
agentState: 'Agent 狀態',
diff --git a/packages/happy-app/sources/utils/sessionUtils.test.ts b/packages/happy-app/sources/utils/sessionUtils.test.ts
new file mode 100644
index 000000000..419027b47
--- /dev/null
+++ b/packages/happy-app/sources/utils/sessionUtils.test.ts
@@ -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 = {
+ 'status.unknown': 'Unknown',
+ 'sessionInfo.startedByDaemon': 'Daemon',
+ 'sessionInfo.startedByTerminal': 'Terminal',
+ };
+ return translations[key] || key;
+ }
+}));
+
+function createSession(overrides: Partial = {}): 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);
+ 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);
+ 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);
+ 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('~');
+ });
+ });
+});
diff --git a/packages/happy-app/sources/utils/sessionUtils.ts b/packages/happy-app/sources/utils/sessionUtils.ts
index 752d2010e..398224983 100644
--- a/packages/happy-app/sources/utils/sessionUtils.ts
+++ b/packages/happy-app/sources/utils/sessionUtils.ts
@@ -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');
}