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
@@ -3,6 +3,7 @@ import { SIDEBAR_SECTION_CONFIG_MAP, SIDEBAR_SECTION_DESCRIPTIONS } from '@/cons
|
||||
import type { SidebarSection } from '@/constants/sidebar';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { McpIcon } from '@/components/icons/McpIcon';
|
||||
|
||||
interface SectionPlaceholderProps {
|
||||
sectionId: SidebarSection;
|
||||
@@ -18,7 +19,7 @@ export const SectionPlaceholder: React.FC<SectionPlaceholderProps> = ({ sectionI
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center">
|
||||
<div className="rounded-full bg-accent/40 p-3 text-muted-foreground">
|
||||
<Icon name={icon} className="h-5 w-5" />
|
||||
{icon === 'mcp-custom' ? <McpIcon className="h-5 w-5" /> : <Icon name={icon} className="h-5 w-5" />}
|
||||
</div>
|
||||
<h3 className="typography-ui-label font-semibold text-foreground">{config.label}</h3>
|
||||
<p className="typography-meta max-w-xs text-muted-foreground">
|
||||
@@ -31,7 +32,7 @@ export const SectionPlaceholder: React.FC<SectionPlaceholderProps> = ({ sectionI
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4 px-6 text-center">
|
||||
<div className="rounded-full bg-accent/40 p-4 text-muted-foreground">
|
||||
<Icon name={icon} className="h-8 w-8" />
|
||||
{icon === 'mcp-custom' ? <McpIcon className="h-8 w-8" /> : <Icon name={icon} className="h-8 w-8" />}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h2 className="typography-h2 font-semibold text-foreground">{config.label}</h2>
|
||||
|
||||
@@ -4,20 +4,27 @@ import { useShallow } from 'zustand/react/shallow';
|
||||
import { UpdateDialog } from '@/components/ui/UpdateDialog';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { toast } from '@/components/ui';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
|
||||
const GITHUB_URL = 'https://github.com/btriapitsyn/openchamber';
|
||||
const GITHUB_URL = 'https://github.com/openchamber/openchamber';
|
||||
const DISCORD_URL = 'https://discord.gg/ZYRSdnwwKA';
|
||||
const X_URL = 'https://x.com/openchamber_dev';
|
||||
|
||||
const MIN_CHECKING_DURATION = 800; // ms
|
||||
|
||||
export const AboutSettings: React.FC = () => {
|
||||
type AboutSettingsProps = {
|
||||
initialUpdateDialogOpen?: boolean;
|
||||
};
|
||||
|
||||
export const AboutSettings: React.FC<AboutSettingsProps> = ({ initialUpdateDialogOpen = false }) => {
|
||||
const { t } = useI18n();
|
||||
const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false);
|
||||
const [updateDialogOpen, setUpdateDialogOpen] = React.useState(initialUpdateDialogOpen);
|
||||
const [showChecking, setShowChecking] = React.useState(false);
|
||||
const [openChamberVersion, setOpenChamberVersion] = React.useState<string | null>(null);
|
||||
const [openCodeVersion, setOpenCodeVersion] = React.useState<string | null>(null);
|
||||
const updateStore = useUpdateStore(useShallow((s) => ({
|
||||
info: s.info,
|
||||
@@ -34,21 +41,48 @@ export const AboutSettings: React.FC = () => {
|
||||
})));
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
const currentVersion = updateStore.info?.currentVersion || 'unknown';
|
||||
const currentVersion = openChamberVersion || updateStore.info?.currentVersion || 'unknown';
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadOpenChamberVersion = async () => {
|
||||
try {
|
||||
const response = await runtimeFetch('/api/system/info', {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const data = await response.json().catch(() => null) as { openchamberVersion?: unknown } | null;
|
||||
const version = typeof data?.openchamberVersion === 'string' && data.openchamberVersion.trim().length > 0
|
||||
? data.openchamberVersion.trim()
|
||||
: null;
|
||||
if (!cancelled) setOpenChamberVersion(version);
|
||||
} catch {
|
||||
if (!cancelled) setOpenChamberVersion(null);
|
||||
}
|
||||
};
|
||||
|
||||
void loadOpenChamberVersion();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const loadOpenCodeVersion = async () => {
|
||||
try {
|
||||
const response = await runtimeFetch('/api/opencode/version', {
|
||||
const response = await runtimeFetch('/api/opencode/upgrade-status', {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const data = await response.json().catch(() => null) as { version?: unknown } | null;
|
||||
const version = typeof data?.version === 'string' && data.version.trim().length > 0
|
||||
? data.version.trim()
|
||||
const data = await response.json().catch(() => null) as { currentVersion?: unknown } | null;
|
||||
const version = typeof data?.currentVersion === 'string' && data.currentVersion.trim().length > 0
|
||||
? data.currentVersion.trim()
|
||||
: null;
|
||||
if (!cancelled) setOpenCodeVersion(version);
|
||||
} catch {
|
||||
@@ -86,82 +120,91 @@ export const AboutSettings: React.FC = () => {
|
||||
|
||||
const isChecking = updateStore.checking || showChecking;
|
||||
|
||||
// Compact mobile layout for sidebar footer
|
||||
if (isMobile) {
|
||||
return (
|
||||
<div className="w-full space-y-2">
|
||||
{/* Version row with update status */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground">
|
||||
v{currentVersion}
|
||||
</span>
|
||||
<div className="w-full space-y-6 pb-2">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<OpenChamberLogo width={72} height={72} />
|
||||
<h2 className="mt-4 typography-ui-header font-semibold text-foreground">OpenChamber</h2>
|
||||
<div className="mt-2 space-y-1 typography-ui text-muted-foreground">
|
||||
<p>{t('aboutDialog.openChamberVersionLabel', { version: currentVersion })}</p>
|
||||
<p>{t('aboutDialog.openCodeVersionLabel', { version: openCodeVersion || t('settings.openchamber.about.state.unknown') })}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
{!updateStore.available && !updateStore.error && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => updateStore.checkForUpdates()}
|
||||
disabled={isChecking}
|
||||
className={cn(
|
||||
'typography-meta text-muted-foreground/60 hover:text-muted-foreground disabled:cursor-default',
|
||||
isChecking && 'animate-pulse [animation-duration:1s]'
|
||||
)}
|
||||
className="h-10 w-auto justify-center gap-2 rounded-xl px-4"
|
||||
>
|
||||
{t('settings.openchamber.about.actions.checkUpdates')}
|
||||
</button>
|
||||
{isChecking ? <Icon name="loader" className="size-4 animate-spin" /> : <Icon name="refresh" className="size-4" />}
|
||||
{isChecking ? t('settings.openchamber.about.state.checking') : t('settings.openchamber.about.actions.checkForUpdates')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!isChecking && updateStore.available && (
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => setUpdateDialogOpen(true)}
|
||||
className="flex items-center gap-1 typography-meta text-[var(--primary-base)] hover:underline"
|
||||
className="h-10 w-auto justify-center gap-2 rounded-xl px-4"
|
||||
>
|
||||
<Icon name="download" className="h-3.5 w-3.5" />
|
||||
{t('settings.openchamber.about.actions.update')}
|
||||
</button>
|
||||
<Icon name="download" className="size-4" />
|
||||
{t('settings.openchamber.about.actions.updateToVersion', { version: updateStore.info?.version || '' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="typography-meta text-muted-foreground">{t('settings.openchamber.about.field.openCodeVersion')}</span>
|
||||
<span className="typography-meta text-muted-foreground font-mono">{openCodeVersion || t('settings.openchamber.about.state.unknown')}</span>
|
||||
</div>
|
||||
|
||||
{updateStore.error && (
|
||||
<p className="typography-micro text-[var(--status-error)] truncate">{updateStore.error}</p>
|
||||
<p className="rounded-xl border border-[var(--status-error-border)] bg-[var(--status-error-background)] px-3 py-2 typography-meta text-[var(--status-error)]">
|
||||
{updateStore.error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Links row */}
|
||||
<div className="flex items-center gap-3">
|
||||
<a
|
||||
href={GITHUB_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 typography-meta text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Icon name="github-fill" className="h-3.5 w-3.5" />
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<div className="flex items-center justify-center gap-5">
|
||||
<a
|
||||
href={GITHUB_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 typography-ui-label text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
<Icon name="github-fill" className="size-5" />
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={DISCORD_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 typography-ui-label text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
<Icon name="discord-fill" className="size-5" />
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://discord.gg/ZYRSdnwwKA"
|
||||
href={X_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 typography-meta text-muted-foreground hover:text-foreground transition-colors"
|
||||
className="flex items-center gap-1.5 typography-ui-label text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
<Icon name="discord-fill" className="h-3.5 w-3.5" />
|
||||
<span>Discord</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://x.com/btriapitsyn"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 typography-meta text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<Icon name="twitter-xfill" className="h-3.5 w-3.5" />
|
||||
<span>@btriapitsyn</span>
|
||||
<Icon name="twitter-xfill" className="size-5" />
|
||||
<span>@openchamber_dev</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p className="text-center typography-ui text-muted-foreground/60">
|
||||
{t('aboutDialog.footerNote')}
|
||||
</p>
|
||||
|
||||
<UpdateDialog
|
||||
open={updateDialogOpen}
|
||||
onOpenChange={setUpdateDialogOpen}
|
||||
@@ -247,15 +290,15 @@ export const AboutSettings: React.FC = () => {
|
||||
<span>GitHub</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://x.com/btriapitsyn"
|
||||
<a
|
||||
href={X_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground typography-meta transition-colors"
|
||||
>
|
||||
<Icon name="twitter-xfill" className="h-4 w-4" />
|
||||
<span>@btriapitsyn</span>
|
||||
</a>
|
||||
<span>@openchamber_dev</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ const VisualSectionContent: React.FC = () => {
|
||||
|
||||
// Chat section: User message rendering, Diff layout, Mobile status bar, Show reasoning traces, Queue mode, Persist draft
|
||||
const ChatSectionContent: React.FC = () => {
|
||||
return <OpenChamberVisualSettings visibleSettings={['chatRenderMode', 'messageTransport', 'activityRenderMode', 'userMessageRendering', 'mermaidRendering', 'reasoning', 'showToolFileIcons', 'showTurnChangedFiles', 'expandedTools', 'stickyUserHeader', 'wideChatLayout', 'splitAssistantMessageActions', 'diffLayout', 'mobileStatusBar', 'dotfiles', 'fileViewerPreview', 'queueMode', 'persistDraft', 'inputSpellcheck']} />;
|
||||
return <OpenChamberVisualSettings visibleSettings={['chatRenderMode', 'messageTransport', 'activityRenderMode', 'userMessageRendering', 'mermaidRendering', 'reasoning', 'showToolFileIcons', 'showTurnChangedFiles', 'expandedTools', 'stickyUserHeader', 'wideChatLayout', 'splitAssistantMessageActions', 'diffLayout', 'dotfiles', 'fileViewerPreview', 'queueMode', 'persistDraft', 'inputSpellcheck']} />;
|
||||
};
|
||||
|
||||
// Sessions section: Default model & agent, Session retention
|
||||
|
||||
@@ -319,8 +319,6 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
const setWeekStartPreference = useUIStore(state => state.setWeekStartPreference);
|
||||
const showSplitAssistantMessageActions = useUIStore(state => state.showSplitAssistantMessageActions);
|
||||
const setShowSplitAssistantMessageActions = useUIStore(state => state.setShowSplitAssistantMessageActions);
|
||||
const showMobileSessionStatusBar = useUIStore(state => state.showMobileSessionStatusBar);
|
||||
const setShowMobileSessionStatusBar = useUIStore(state => state.setShowMobileSessionStatusBar);
|
||||
const messageStreamTransport = useConfigStore((state) => state.settingsMessageStreamTransport);
|
||||
const setMessageStreamTransport = useConfigStore((state) => state.setSettingsMessageStreamTransport);
|
||||
const settingsDefaultFileViewerPreview = useConfigStore((state) => state.settingsDefaultFileViewerPreview);
|
||||
@@ -538,7 +536,6 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|| shouldShow('wideChatLayout')
|
||||
|| shouldShow('splitAssistantMessageActions')
|
||||
|| shouldShow('diffLayout')
|
||||
|| (shouldShow('mobileStatusBar') && isMobile)
|
||||
|| shouldShow('dotfiles')
|
||||
|| shouldShow('fileViewerPreview')
|
||||
|| shouldShow('reasoning')
|
||||
@@ -1698,7 +1695,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') || shouldShow('splitAssistantMessageActions') || (shouldShow('mobileStatusBar') && isMobile) || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('queueMode') || shouldShow('persistDraft') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning')) && (
|
||||
{(shouldShow('stickyUserHeader') || shouldShow('wideChatLayout') || shouldShow('splitAssistantMessageActions') || shouldShow('dotfiles') || shouldShow('fileViewerPreview') || shouldShow('queueMode') || shouldShow('persistDraft') || shouldShow('showToolFileIcons') || shouldShow('showTurnChangedFiles') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning')) && (
|
||||
<section className="p-2 space-y-0.5">
|
||||
{shouldShow('reasoning') && (
|
||||
<div
|
||||
@@ -1871,29 +1868,6 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShow('mobileStatusBar') && isMobile && (
|
||||
<div
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={showMobileSessionStatusBar}
|
||||
onClick={() => setShowMobileSessionStatusBar(!showMobileSessionStatusBar)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
setShowMobileSessionStatusBar(!showMobileSessionStatusBar);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={showMobileSessionStatusBar}
|
||||
onChange={setShowMobileSessionStatusBar}
|
||||
ariaLabel={t('settings.openchamber.visual.field.showMobileStatusBarAria')}
|
||||
/>
|
||||
<span className="typography-ui-label text-foreground">{t('settings.openchamber.visual.field.showMobileStatusBar')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShow('dotfiles') && !isVSCodeRuntime() && (
|
||||
<div
|
||||
className="group flex cursor-pointer items-center gap-2 py-0.5"
|
||||
|
||||
Reference in New Issue
Block a user