项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:40:49 +08:00

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"];