slopus--happy
98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
227 行
8.2 KiB
TypeScript
227 行
8.2 KiB
TypeScript
import * as React from 'react';
|
|
import { Session } from '@/sync/storageTypes';
|
|
import { t } from '@/text';
|
|
import { buildResumeCommand, buildResumeCommandBlock, ResumeCommandBlock } from './resumeCommand';
|
|
|
|
export type SessionState = 'disconnected' | 'thinking' | 'waiting' | 'permission_required';
|
|
|
|
export interface SessionStatus {
|
|
state: SessionState;
|
|
isConnected: boolean;
|
|
statusText: string;
|
|
shouldShowStatus: boolean;
|
|
statusColor: string;
|
|
statusDotColor: string;
|
|
isPulsing?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Get the current state of a session based on presence and thinking status.
|
|
* Uses centralized session state from storage.ts
|
|
*/
|
|
export function useSessionStatus(session: Session): SessionStatus {
|
|
const isOnline = session.presence === "online";
|
|
const hasPermissions = (session.agentState?.requests && Object.keys(session.agentState.requests).length > 0 ? true : false);
|
|
|
|
const vibingMessage = React.useMemo(() => {
|
|
return vibingMessages[Math.floor(Math.random() * vibingMessages.length)].toLowerCase() + '…';
|
|
}, [isOnline, hasPermissions, session.thinking]);
|
|
|
|
if (!isOnline) {
|
|
return {
|
|
state: 'disconnected',
|
|
isConnected: false,
|
|
statusText: t('status.lastSeen', { time: formatLastSeen(session.activeAt, false) }),
|
|
shouldShowStatus: true,
|
|
statusColor: '#999',
|
|
statusDotColor: '#999'
|
|
};
|
|
}
|
|
|
|
// Check if permission is required
|
|
if (hasPermissions) {
|
|
return {
|
|
state: 'permission_required',
|
|
isConnected: true,
|
|
statusText: t('status.permissionRequired'),
|
|
shouldShowStatus: true,
|
|
statusColor: '#FF9500',
|
|
statusDotColor: '#FF9500',
|
|
isPulsing: true
|
|
};
|
|
}
|
|
|
|
if (session.thinking === true) {
|
|
return {
|
|
state: 'thinking',
|
|
isConnected: true,
|
|
statusText: vibingMessage,
|
|
shouldShowStatus: true,
|
|
statusColor: '#007AFF',
|
|
statusDotColor: '#007AFF',
|
|
isPulsing: true
|
|
};
|
|
}
|
|
|
|
return {
|
|
state: 'waiting',
|
|
isConnected: true,
|
|
statusText: t('status.online'),
|
|
shouldShowStatus: false,
|
|
statusColor: '#34C759',
|
|
statusDotColor: '#34C759'
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extracts a display name from a session's metadata path.
|
|
* Returns the last segment of the path, or 'unknown' if no path is available.
|
|
*/
|
|
export function getSessionName(session: Session): string {
|
|
if (session.metadata?.summary) {
|
|
return session.metadata.summary.text;
|
|
}
|
|
return t('session.newChat');
|
|
}
|
|
|
|
/**
|
|
* Generates a deterministic avatar ID from machine ID and path.
|
|
* This ensures the same machine + path combination always gets the same avatar.
|
|
*/
|
|
export function getSessionAvatarId(session: Session): string {
|
|
if (session.metadata?.machineId && session.metadata?.path) {
|
|
// Combine machine ID and path for a unique, deterministic avatar
|
|
return `${session.metadata.machineId}:${session.metadata.path}`;
|
|
}
|
|
// Fallback to session ID if metadata is missing
|
|
return session.id;
|
|
}
|
|
|
|
/**
|
|
* Returns the CLI command to resume a disconnected session, or null if not resumable.
|
|
* Uses flavor-specific commands which work without happy-agent auth.
|
|
*/
|
|
export function getResumeCommand(session: Session): string | null {
|
|
return buildResumeCommand(session.metadata ?? {});
|
|
}
|
|
|
|
export function getResumeCommandBlock(session: Session): ResumeCommandBlock | null {
|
|
return buildResumeCommandBlock(session.metadata ?? {});
|
|
}
|
|
|
|
/**
|
|
* Formats a path relative to home directory if possible.
|
|
* If the path starts with the home directory, replaces it with ~
|
|
* Otherwise returns the full path.
|
|
*/
|
|
export function formatPathRelativeToHome(path: string, homeDir?: string): string {
|
|
if (!homeDir) return path;
|
|
|
|
// Normalize paths to handle trailing slashes
|
|
const normalizedHome = homeDir.endsWith('/') ? homeDir.slice(0, -1) : homeDir;
|
|
const normalizedPath = path;
|
|
|
|
// Check if path starts with home directory
|
|
if (normalizedPath.startsWith(normalizedHome)) {
|
|
// Replace home directory with ~
|
|
const relativePath = normalizedPath.slice(normalizedHome.length);
|
|
// Add ~ and ensure there's a / after it if needed
|
|
if (relativePath.startsWith('/')) {
|
|
return '~' + relativePath;
|
|
} else if (relativePath === '') {
|
|
return '~';
|
|
} else {
|
|
return '~/' + relativePath;
|
|
}
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* Returns the session path for the subtitle.
|
|
*/
|
|
export function getSessionSubtitle(session: Session): string {
|
|
if (session.metadata) {
|
|
return formatPathRelativeToHome(session.metadata.path, session.metadata.homeDir);
|
|
}
|
|
return t('status.unknown');
|
|
}
|
|
|
|
/**
|
|
* Checks if a session is currently online based on the active flag.
|
|
* A session is considered online if the active flag is true.
|
|
*/
|
|
export function isSessionOnline(session: Session): boolean {
|
|
return session.active;
|
|
}
|
|
|
|
/**
|
|
* Checks if a session should be shown in the active sessions group.
|
|
* Uses the active flag directly.
|
|
*/
|
|
export function isSessionActive(session: Session): boolean {
|
|
return session.active;
|
|
}
|
|
|
|
/**
|
|
* Formats OS platform string into a more readable format
|
|
*/
|
|
export function formatOSPlatform(platform?: string): string {
|
|
if (!platform) return '';
|
|
|
|
const osMap: Record<string, string> = {
|
|
'darwin': 'macOS',
|
|
'win32': 'Windows',
|
|
'linux': 'Linux',
|
|
'android': 'Android',
|
|
'ios': 'iOS',
|
|
'aix': 'AIX',
|
|
'freebsd': 'FreeBSD',
|
|
'openbsd': 'OpenBSD',
|
|
'sunos': 'SunOS'
|
|
};
|
|
|
|
return osMap[platform.toLowerCase()] || platform;
|
|
}
|
|
|
|
/**
|
|
* Formats the last seen time of a session into a human-readable relative time.
|
|
* @param activeAt - Timestamp when the session was last active
|
|
* @param isActive - Whether the session is currently active
|
|
* @returns Formatted string like "Active now", "5 minutes ago", "2 hours ago", or a date
|
|
*/
|
|
export function formatLastSeen(activeAt: number, isActive: boolean = false): string {
|
|
if (isActive) {
|
|
return t('status.activeNow');
|
|
}
|
|
|
|
const now = Date.now();
|
|
const diffMs = now - activeAt;
|
|
const diffSeconds = Math.floor(diffMs / 1000);
|
|
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
const diffHours = Math.floor(diffMinutes / 60);
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
|
|
if (diffSeconds < 60) {
|
|
return t('time.justNow');
|
|
} else if (diffMinutes < 60) {
|
|
return t('time.minutesAgo', { count: diffMinutes });
|
|
} else if (diffHours < 24) {
|
|
return t('time.hoursAgo', { count: diffHours });
|
|
} else if (diffDays < 7) {
|
|
return t('sessionHistory.daysAgo', { count: diffDays });
|
|
} else {
|
|
// Format as date
|
|
const date = new Date(activeAt);
|
|
const options: Intl.DateTimeFormatOptions = {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
|
|
};
|
|
return date.toLocaleDateString(undefined, options);
|
|
}
|
|
}
|
|
|
|
export const vibingMessages = ["Accomplishing", "Actioning", "Actualizing", "Baking", "Booping", "Brewing", "Calculating", "Cerebrating", "Channelling", "Churning", "Clauding", "Coalescing", "Cogitating", "Computing", "Combobulating", "Concocting", "Conjuring", "Considering", "Contemplating", "Cooking", "Crafting", "Creating", "Crunching", "Deciphering", "Deliberating", "Determining", "Discombobulating", "Divining", "Doing", "Effecting", "Elucidating", "Enchanting", "Envisioning", "Finagling", "Flibbertigibbeting", "Forging", "Forming", "Frolicking", "Generating", "Germinating", "Hatching", "Herding", "Honking", "Ideating", "Imagining", "Incubating", "Inferring", "Manifesting", "Marinating", "Meandering", "Moseying", "Mulling", "Mustering", "Musing", "Noodling", "Percolating", "Perusing", "Philosophising", "Pontificating", "Pondering", "Processing", "Puttering", "Puzzling", "Reticulating", "Ruminating", "Scheming", "Schlepping", "Shimmying", "Simmering", "Smooshing", "Spelunking", "Spinning", "Stewing", "Sussing", "Synthesizing", "Thinking", "Tinkering", "Transmuting", "Unfurling", "Unravelling", "Vibing", "Wandering", "Whirring", "Wibbling", "Wizarding", "Working", "Wrangling"];
|