feat: refine mobile session status bar header
Remove the per-row agent color dot (and its now-unused helpers), keeping only the status indicator. Add a "+" button to the panel header that opens a new session draft targeting the filtered project (or the most recently active session's project under "All", falling back to the default target), and restyle the close button to match it. Show the context token usage next to the percentage (e.g. 5.1% 20.4K/400.0K) at a larger, consistent size, and compute the percentage with a fraction instead of the store's integer-rounded value.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useSelectionStore } from '@/sync/selection-store';
|
||||
import { useAllSessionStatuses, useAllLiveSessions } from '@/sync/sync-context';
|
||||
import { useGlobalSessionsStore, ensureGlobalSessionsLoaded, refreshGlobalSessions } from '@/stores/useGlobalSessionsStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
@@ -9,7 +8,6 @@ import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import type { ProjectEntry } from '@/lib/api/types';
|
||||
import { cn, formatDirectoryName } from '@/lib/utils';
|
||||
import { getAgentColor } from '@/lib/agentColors';
|
||||
import type { SessionContextUsage } from '@/stores/types/sessionTypes';
|
||||
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, ProjectIconImage } from '@/lib/projectMeta';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
@@ -172,17 +170,7 @@ function useSessionGrouping(
|
||||
return { sessions: processedSessions, totalRunning, totalUnread, totalCount: processedSessions.length };
|
||||
}
|
||||
|
||||
function useSessionHelpers(agents: Array<{ name: string }>) {
|
||||
const getSessionAgentName = React.useCallback((session: Session): string => {
|
||||
const agent = (session as { agent?: string }).agent;
|
||||
if (agent) return agent;
|
||||
|
||||
const sessionAgentSelection = useSelectionStore.getState().getSessionAgentSelection(session.id);
|
||||
if (sessionAgentSelection) return sessionAgentSelection;
|
||||
|
||||
return agents[0]?.name ?? 'agent';
|
||||
}, [agents]);
|
||||
|
||||
function useSessionHelpers() {
|
||||
const getSessionTitle = React.useCallback((session: Session): string => {
|
||||
const title = session.title;
|
||||
if (title && title.trim()) return title;
|
||||
@@ -194,7 +182,7 @@ function useSessionHelpers(agents: Array<{ name: string }>) {
|
||||
return (unseenCounts[sessionId] ?? 0) > 0;
|
||||
}, [unseenCounts]);
|
||||
|
||||
return { getSessionAgentName, getSessionTitle, needsAttention };
|
||||
return { getSessionTitle, needsAttention };
|
||||
}
|
||||
|
||||
// Per-project status indicators (running / unread) for the filter chips.
|
||||
@@ -300,14 +288,29 @@ function UnreadIndicator({ count }: { count: number }) {
|
||||
function TokenUsageIndicator({ contextUsage }: { contextUsage: SessionContextUsage | null }) {
|
||||
if (!contextUsage || contextUsage.totalTokens === 0) return null;
|
||||
|
||||
const percentage = Math.min(contextUsage.percentage, 999);
|
||||
// Recompute with a fraction: contextUsage.percentage is rounded to an integer
|
||||
// in the store, which would always render as "X.0%".
|
||||
const percentage = contextUsage.contextLimit > 0
|
||||
? Math.min((contextUsage.totalTokens / contextUsage.contextLimit) * 100, 999)
|
||||
: 0;
|
||||
const colorClass =
|
||||
percentage >= 90 ? 'text-[var(--status-error)]' :
|
||||
percentage >= 75 ? 'text-[var(--status-warning)]' : 'text-[var(--status-success)]';
|
||||
|
||||
const formatTokens = (value: number): string => {
|
||||
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
|
||||
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const tokens = contextUsage.contextLimit > 0
|
||||
? `${formatTokens(contextUsage.totalTokens)}/${formatTokens(contextUsage.contextLimit)}`
|
||||
: formatTokens(contextUsage.totalTokens);
|
||||
|
||||
return (
|
||||
<span className={cn("text-[12px] tabular-nums font-medium", colorClass)}>
|
||||
{percentage.toFixed(1)}%
|
||||
<span className="flex items-baseline gap-1.5 text-[15px] tabular-nums">
|
||||
<span className={cn("font-medium", colorClass)}>{percentage.toFixed(1)}%</span>
|
||||
<span className="text-[var(--surface-mutedForeground)]">{tokens}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -316,20 +319,16 @@ function TokenUsageIndicator({ contextUsage }: { contextUsage: SessionContextUsa
|
||||
function SessionItem({
|
||||
session,
|
||||
isCurrent,
|
||||
getSessionAgentName,
|
||||
getSessionTitle,
|
||||
onClick,
|
||||
needsAttention,
|
||||
}: {
|
||||
session: SessionWithStatus;
|
||||
isCurrent: boolean;
|
||||
getSessionAgentName: (s: Session) => string;
|
||||
getSessionTitle: (s: Session) => string;
|
||||
onClick: () => void;
|
||||
needsAttention: (sessionId: string) => boolean;
|
||||
}) {
|
||||
const agentName = getSessionAgentName(session);
|
||||
const agentColor = getAgentColor(agentName);
|
||||
const attention = needsAttention(session.id);
|
||||
|
||||
return (
|
||||
@@ -346,11 +345,6 @@ function SessionItem({
|
||||
<StatusIndicator isRunning={session._statusType !== 'idle'} needsAttention={attention} />
|
||||
</span>
|
||||
|
||||
<span
|
||||
className="h-2.5 w-2.5 flex-shrink-0 rounded-full"
|
||||
style={{ backgroundColor: `var(${agentColor.var})` }}
|
||||
/>
|
||||
|
||||
<span className={cn(
|
||||
"flex-1 truncate text-[15px] leading-tight",
|
||||
isCurrent ? "font-semibold text-[var(--surface-foreground)]" : "text-[var(--surface-foreground)]"
|
||||
@@ -500,8 +494,8 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
|
||||
const sessionStatus = useAllSessionStatuses();
|
||||
const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession);
|
||||
const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft);
|
||||
const getContextUsage = useSessionUIStore((state) => state.getContextUsage);
|
||||
const agents = useConfigStore((state) => state.agents);
|
||||
const getCurrentModel = useConfigStore((state) => state.getCurrentModel);
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
const showMobileSessionStatusBar = useUIStore((state) => state.showMobileSessionStatusBar);
|
||||
@@ -512,7 +506,7 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
const homeDirectory = useDirectoryStore((state) => state.homeDirectory);
|
||||
|
||||
const { sessions: sortedSessions, totalRunning, totalUnread } = useSessionGrouping(sessions, sessionStatus);
|
||||
const { getSessionAgentName, getSessionTitle, needsAttention } = useSessionHelpers(agents);
|
||||
const { getSessionTitle, needsAttention } = useSessionHelpers();
|
||||
const getProjectStatus = useProjectStatus(sessions, sessionStatus, currentSessionId);
|
||||
const resolveProjectRoots = useProjectRootsResolver();
|
||||
|
||||
@@ -572,7 +566,28 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const renderHeader = React.useCallback((closeButton: React.ReactNode) => (
|
||||
// "+" — start a new session draft. Target the project selected in the filter;
|
||||
// for "All", use the most recently active session's directory, falling back to
|
||||
// the store's own default target when there are no sessions.
|
||||
const handleNewChat = React.useCallback(() => {
|
||||
setOpen(false);
|
||||
if (filterProjectId) {
|
||||
const project = projects.find((p) => p.id === filterProjectId);
|
||||
if (project) {
|
||||
openNewSessionDraft({ selectedProjectId: project.id, directoryOverride: project.path });
|
||||
return;
|
||||
}
|
||||
}
|
||||
const mostRecent = [...sessions].sort((a, b) => {
|
||||
const aTime = (a as { time?: { updated?: number } }).time?.updated ?? 0;
|
||||
const bTime = (b as { time?: { updated?: number } }).time?.updated ?? 0;
|
||||
return bTime - aTime;
|
||||
})[0];
|
||||
const directory = mostRecent ? sessionDirectory(mostRecent) : '';
|
||||
openNewSessionDraft(directory ? { directoryOverride: directory } : undefined);
|
||||
}, [filterProjectId, projects, sessions, openNewSessionDraft, setOpen]);
|
||||
|
||||
const renderHeader = React.useCallback(() => (
|
||||
<div className="shrink-0">
|
||||
<div className="flex justify-center pt-2.5 pb-1">
|
||||
<div className="h-1 w-9 rounded-full bg-[color-mix(in_srgb,var(--surface-mutedForeground)_40%,transparent)]" />
|
||||
@@ -586,7 +601,23 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
<RunningIndicator count={totalRunning} />
|
||||
<UnreadIndicator count={totalUnread} />
|
||||
<TokenUsageIndicator contextUsage={contextUsage} />
|
||||
{closeButton}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNewChat}
|
||||
aria-label={t('mobile.sessions.newChat')}
|
||||
className="flex size-8 items-center justify-center rounded-full text-[var(--surface-mutedForeground)] transition-colors hover:bg-[var(--interactive-hover)] hover:text-[var(--surface-foreground)]"
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<Icon name="add" className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex size-8 items-center justify-center rounded-full text-[var(--surface-mutedForeground)] transition-colors hover:bg-[var(--interactive-hover)] hover:text-[var(--surface-foreground)]"
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<Icon name="close" className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -620,7 +651,7 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
), [t, totalRunning, totalUnread, contextUsage, projects, filterProjectId, setFilterProjectId, formatProjectLabel, currentTheme, getProjectStatus]);
|
||||
), [t, totalRunning, totalUnread, contextUsage, projects, filterProjectId, setFilterProjectId, formatProjectLabel, currentTheme, getProjectStatus, handleNewChat, setOpen]);
|
||||
|
||||
if (!isMobile || !showMobileSessionStatusBar) {
|
||||
return null;
|
||||
@@ -646,7 +677,6 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
key={session.id}
|
||||
session={session}
|
||||
isCurrent={session.id === currentSessionId}
|
||||
getSessionAgentName={getSessionAgentName}
|
||||
getSessionTitle={getSessionTitle}
|
||||
onClick={() => handleSessionClick(session)}
|
||||
needsAttention={needsAttention}
|
||||
|
||||
Reference in New Issue
Block a user