feat: improve mobile UX (#1591)
Added a mobile MCP overlay so MCP tools can be opened and managed from the mobile UI without relying on desktop-only dropdown behavior. Improved mobile session panel touch handling so tapping the status/session area opens the right panel reliably on phones and tablets. Cleaned up mobile usage provider metadata by removing duplicate rows, hiding unset providers, and showing provider logos consistently. Added eager loading for provider logos used in mobile usage views to avoid delayed or missing icons when the panel opens. Refined the mobile update and about flows in OpenChamber settings so release/update information is easier to read on small screens. Adjusted related layout, header, VS Code layout, command palette, and settings text/localization details needed for the mobile polish.
This commit is contained in:
committed by
GitHub
parent
0153f8787d
commit
eff6f46ad9
@@ -2,13 +2,11 @@ import React from 'react';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useAllSessionStatuses, useAllLiveSessions } from '@/sync/sync-context';
|
||||
import { mergeSessionDirectoryMetadata, useGlobalSessionsStore, ensureGlobalSessionsLoaded, refreshGlobalSessions } from '@/stores/useGlobalSessionsStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
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 type { SessionContextUsage } from '@/stores/types/sessionTypes';
|
||||
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, ProjectIconImage } from '@/lib/projectMeta';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
@@ -288,36 +286,6 @@ function UnreadIndicator({ count }: { count: number }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TokenUsageIndicator({ contextUsage }: { contextUsage: SessionContextUsage | null }) {
|
||||
if (!contextUsage || contextUsage.totalTokens === 0) return null;
|
||||
|
||||
// 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="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>
|
||||
);
|
||||
}
|
||||
|
||||
// A single session row sized for comfortable touch.
|
||||
function SessionItem({
|
||||
session,
|
||||
@@ -448,19 +416,19 @@ export const MobileSessionPanelTrigger: React.FC<MobileSessionPanelTriggerProps>
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
const showMobileSessionStatusBar = useUIStore((state) => state.showMobileSessionStatusBar);
|
||||
const open = useUIStore((state) => state.mobileSessionPanelOpen);
|
||||
const setOpen = useUIStore((state) => state.setMobileSessionPanelOpen);
|
||||
const handledTouchRef = React.useRef(false);
|
||||
|
||||
// Ensure the cross-project session list is loaded once, so the panel reflects
|
||||
// every project, not just the active directory.
|
||||
React.useEffect(() => {
|
||||
if (isMobile && showMobileSessionStatusBar) {
|
||||
if (isMobile) {
|
||||
void ensureGlobalSessionsLoaded();
|
||||
}
|
||||
}, [isMobile, showMobileSessionStatusBar]);
|
||||
}, [isMobile]);
|
||||
|
||||
if (!isMobile || !showMobileSessionStatusBar) {
|
||||
if (!isMobile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -476,9 +444,17 @@ export const MobileSessionPanelTrigger: React.FC<MobileSessionPanelTriggerProps>
|
||||
if (event.pointerType === 'touch') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handledTouchRef.current = true;
|
||||
setOpen(!open);
|
||||
}
|
||||
}}
|
||||
onClick={() => setOpen(!open)}
|
||||
onClick={() => {
|
||||
if (handledTouchRef.current) {
|
||||
handledTouchRef.current = false;
|
||||
return;
|
||||
}
|
||||
setOpen(!open);
|
||||
}}
|
||||
title={t('mobile.sessions.search.section.sessions')}
|
||||
aria-label={t('mobile.sessions.search.section.sessions')}
|
||||
aria-expanded={open}
|
||||
@@ -493,15 +469,12 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const { currentTheme } = useThemeSystem();
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
const sessions = useAllProjectSessions();
|
||||
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 getCurrentModel = useConfigStore((state) => state.getCurrentModel);
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
const showMobileSessionStatusBar = useUIStore((state) => state.showMobileSessionStatusBar);
|
||||
const open = useUIStore((state) => state.mobileSessionPanelOpen);
|
||||
const setOpen = useUIStore((state) => state.setMobileSessionPanelOpen);
|
||||
|
||||
@@ -554,15 +527,6 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
[filteredSessions],
|
||||
);
|
||||
|
||||
// Token usage for the current session.
|
||||
const currentModel = getCurrentModel();
|
||||
const limit = currentModel && typeof currentModel.limit === 'object' && currentModel.limit !== null
|
||||
? (currentModel.limit as Record<string, unknown>)
|
||||
: null;
|
||||
const contextLimit = (limit && typeof limit.context === 'number' ? limit.context : 0);
|
||||
const outputLimit = (limit && typeof limit.output === 'number' ? limit.output : 0);
|
||||
const contextUsage = getContextUsage(contextLimit, outputLimit);
|
||||
|
||||
const handleSessionClick = (session: SessionWithStatus) => {
|
||||
setCurrentSession(session.id, sessionDirectory(session) || null);
|
||||
onSessionSwitch?.(session.id);
|
||||
@@ -603,7 +567,6 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
<div className="flex items-center gap-3">
|
||||
<RunningIndicator count={totalRunning} />
|
||||
<UnreadIndicator count={totalUnread} />
|
||||
<TokenUsageIndicator contextUsage={contextUsage} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNewChat}
|
||||
@@ -654,9 +617,9 @@ export const MobileSessionStatusBar: React.FC<MobileSessionStatusBarProps> = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
), [t, totalRunning, totalUnread, contextUsage, projects, filterProjectId, setFilterProjectId, formatProjectLabel, currentTheme, getProjectStatus, handleNewChat, setOpen]);
|
||||
), [t, totalRunning, totalUnread, projects, filterProjectId, setFilterProjectId, formatProjectLabel, currentTheme, getProjectStatus, handleNewChat, setOpen]);
|
||||
|
||||
if (!isMobile || !showMobileSessionStatusBar) {
|
||||
if (!isMobile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user