Add new mobile chat controls (#247)

* chore: upgrade @opencode-ai/sdk to 1.1.40

* chore(ui): bump @opencode-ai/sdk to 1.1.40

* feat(chat): add mobile controls drawer and panel switching

Add mobile-only controls drawer with open/close and panel switching
Introduce state and handlers for mobile controls and panels
Reset mobile UI state when switching to non-mobile or returning to unified controls

* feat: add mobile chat controls utilities

Add utilities to compute and display the selected agent and model names in mobile chat controls.
Add effort variant formatting, serialization, parsing, and ranking helpers for quick options.
Expose helpers to build quick effort option lists for the UI

* feat(ModelControls): allow external mobile panel control

Initialize mobile panel state from external props when provided
Fall back to internal mobile panel state when external control is absent
Notify parent on panel changes via onMobilePanelChange callback

* feat(chat): add StatusChip component

Add StatusChip button that displays agent, model, and effort
Show marquee animation when the label is truncated
Bind to config and session stores to reflect current context

* feat: add UnifiedControlsDrawer chat controls

Add a side panel to switch agent, model, and effort quickly
Show recent agents and models for faster reselect
Persist agent/model/variant selections in session

* fix(openchamber): correct base64 to Uint8Array typing

* feat: add reduced-motion support for marquee animations

Add marquee-text--auto to enable continuous scrolling
Introduce prefers-reduced-motion media query to disable animations
Apply reduced-motion rules to active marquee and hover states

* feat[worktrees]: enhance sdk worktree removal with fallbacks

Add dynamic resolution of remove/delete/archive methods for worktrees
Fallback to delete or archive when remove is not available
Throw clear error when SDK version does not support worktree removal

* feat(ui): track recent agents and efforts in UI store

Add recentAgents array to UI state for quick access
Track up to 5 variants per provider/model in recentEfforts
Expose addRecentAgent and addRecentEffort actions to update history

* fix(deps): upgrade @opencode-ai/sdk to 1.1.42

Upgrade the OpenCode AI SDK to 1.1.42 across packages
Refresh lockfile entries to reflect the new SDK version and integrity hash
Ensure downstream packages consume the latest SDK and remain compatible

* refactor: simplify agent overflow logic in UnifiedControlsDrawer
This commit is contained in:
Nelson Pires
2026-01-30 11:13:37 +02:00
committed by GitHub
parent ab431a04a6
commit 2ed1a73d3c
14 changed files with 788 additions and 35 deletions
+82 -9
View File
@@ -22,6 +22,8 @@ import { SkillAutocomplete, type SkillAutocompleteHandle } from './SkillAutocomp
import { cn } from '@/lib/utils';
import { ServerFilePicker } from './ServerFilePicker';
import { ModelControls } from './ModelControls';
import { StatusChip } from './StatusChip';
import { UnifiedControlsDrawer } from './UnifiedControlsDrawer';
import { parseAgentMentions } from '@/lib/messages/agentMentions';
import { StatusRow } from './StatusRow';
import { useAssistantStatus } from '@/hooks/useAssistantStatus';
@@ -31,6 +33,7 @@ import { useFileStore } from '@/stores/fileStore';
import { isVSCodeRuntime } from '@/lib/desktop';
import { isIMECompositionEvent } from '@/lib/ime';
import { StopIcon } from '@/components/icons/StopIcon';
import type { MobileControlsPanel } from './mobileControlsUtils';
import {
DropdownMenu,
DropdownMenuContent,
@@ -108,6 +111,8 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
const [showSkillAutocomplete, setShowSkillAutocomplete] = React.useState(false);
const [skillQuery, setSkillQuery] = React.useState('');
const [textareaSize, setTextareaSize] = React.useState<{ height: number; maxHeight: number } | null>(null);
const [mobileControlsOpen, setMobileControlsOpen] = React.useState(false);
const [mobileControlsPanel, setMobileControlsPanel] = React.useState<MobileControlsPanel>(null);
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const dropZoneRef = React.useRef<HTMLDivElement>(null);
const mentionRef = React.useRef<FileMentionHandle>(null);
@@ -203,6 +208,54 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
}
}, [isMobile]);
const handleOpenMobileControls = React.useCallback(() => {
if (!isMobile) {
return;
}
if (mobileControlsOpen) {
setMobileControlsOpen(false);
return;
}
setMobileControlsPanel(null);
if (isKeyboardOpen) {
textareaRef.current?.blur();
requestAnimationFrame(() => {
setMobileControlsOpen(true);
});
return;
}
setMobileControlsOpen(true);
}, [isMobile, isKeyboardOpen, mobileControlsOpen]);
const handleCloseMobileControls = React.useCallback(() => {
setMobileControlsOpen(false);
}, []);
const handleOpenMobilePanel = React.useCallback((panel: MobileControlsPanel) => {
if (!isMobile) {
return;
}
setMobileControlsOpen(false);
textareaRef.current?.blur();
requestAnimationFrame(() => {
setMobileControlsPanel(panel);
});
}, [isMobile]);
const handleReturnToUnifiedControls = React.useCallback(() => {
if (!isMobile) {
return;
}
setMobileControlsPanel(null);
requestAnimationFrame(() => {
setMobileControlsOpen(true);
});
}, [isMobile]);
// Consume pending input text (e.g., from revert action)
React.useEffect(() => {
if (pendingInputText !== null) {
@@ -974,6 +1027,13 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
}
}, [currentSessionId, isMobile]);
React.useEffect(() => {
if (!isMobile) {
setMobileControlsOpen(false);
setMobileControlsPanel(null);
}
}, [isMobile]);
React.useEffect(() => {
if (abortPromptSessionId && abortPromptSessionId !== currentSessionId) {
clearAbortPrompt();
@@ -1462,17 +1522,30 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
data-chat-input-footer="true"
>
{isMobile ? (
<div className="flex w-full items-center gap-x-1.5">
<div className="flex items-center flex-shrink-0 gap-x-1">
{attachmentsControls}
</div>
<div className="flex flex-1 items-center justify-end gap-x-1 min-w-0">
<div className="flex flex-1 min-w-0 justify-end overflow-hidden">
<ModelControls className={cn('w-full flex items-center justify-end min-w-0')} />
<>
<div className="flex w-full items-center gap-x-1.5">
<div className="flex items-center flex-shrink-0 gap-x-1">
{attachmentsControls}
</div>
<div className="flex flex-1 items-center gap-x-1 min-w-0">
<StatusChip onClick={handleOpenMobileControls} className="min-w-0" />
{actionButton}
</div>
{actionButton}
</div>
</div>
<ModelControls
className="hidden"
mobilePanel={mobileControlsPanel}
onMobilePanelChange={setMobileControlsPanel}
onMobilePanelSelection={handleReturnToUnifiedControls}
/>
<UnifiedControlsDrawer
open={mobileControlsOpen}
onClose={handleCloseMobileControls}
onOpenAgent={() => handleOpenMobilePanel('agent')}
onOpenModel={() => handleOpenMobilePanel('model')}
onOpenEffort={() => handleOpenMobilePanel('variant')}
/>
</>
) : (
<>
<div className={cn("flex items-center flex-shrink-0", footerGapClass)}>