Add terminal quick keys option and desktop toggle (#249)
* feat: add terminal quick keys toggle in OpenChamber UI * feat: enhance mobile input handling in Terminal for Android devices
This commit is contained in:
committed by
GitHub
parent
1442e44e16
commit
b74ac777f5
@@ -83,7 +83,7 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
|||||||
|
|
||||||
// Visual section: Theme Mode, Font Size, Spacing, Corner Radius, Input Bar Offset (mobile)
|
// Visual section: Theme Mode, Font Size, Spacing, Corner Radius, Input Bar Offset (mobile)
|
||||||
const VisualSectionContent: React.FC = () => {
|
const VisualSectionContent: React.FC = () => {
|
||||||
return <OpenChamberVisualSettings visibleSettings={['theme', 'fontSize', 'spacing', 'cornerRadius', 'inputBarOffset']} />;
|
return <OpenChamberVisualSettings visibleSettings={['theme', 'fontSize', 'spacing', 'cornerRadius', 'inputBarOffset', 'terminalQuickKeys']} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Chat section: Default Tool Output, Diff layout, Show reasoning traces, Queue mode
|
// Chat section: Default Tool Output, Diff layout, Show reasoning traces, Queue mode
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useMessageQueueStore } from '@/stores/messageQueueStore';
|
|||||||
import { cn, getModifierLabel } from '@/lib/utils';
|
import { cn, getModifierLabel } from '@/lib/utils';
|
||||||
import { ButtonSmall } from '@/components/ui/button-small';
|
import { ButtonSmall } from '@/components/ui/button-small';
|
||||||
import { NumberInput } from '@/components/ui/number-input';
|
import { NumberInput } from '@/components/ui/number-input';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import {
|
import {
|
||||||
@@ -73,7 +74,7 @@ const DIFF_VIEW_MODE_OPTIONS: Option<'single' | 'stacked'>[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export type VisibleSetting = 'theme' | 'fontSize' | 'spacing' | 'cornerRadius' | 'inputBarOffset' | 'toolOutput' | 'diffLayout' | 'dotfiles' | 'reasoning' | 'queueMode' | 'textJustificationActivity';
|
export type VisibleSetting = 'theme' | 'fontSize' | 'spacing' | 'cornerRadius' | 'inputBarOffset' | 'toolOutput' | 'diffLayout' | 'dotfiles' | 'reasoning' | 'queueMode' | 'textJustificationActivity' | 'terminalQuickKeys';
|
||||||
|
|
||||||
interface OpenChamberVisualSettingsProps {
|
interface OpenChamberVisualSettingsProps {
|
||||||
/** Which settings to show. If undefined, shows all. */
|
/** Which settings to show. If undefined, shows all. */
|
||||||
@@ -101,6 +102,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
const setDiffLayoutPreference = useUIStore(state => state.setDiffLayoutPreference);
|
const setDiffLayoutPreference = useUIStore(state => state.setDiffLayoutPreference);
|
||||||
const diffViewMode = useUIStore(state => state.diffViewMode);
|
const diffViewMode = useUIStore(state => state.diffViewMode);
|
||||||
const setDiffViewMode = useUIStore(state => state.setDiffViewMode);
|
const setDiffViewMode = useUIStore(state => state.setDiffViewMode);
|
||||||
|
const showTerminalQuickKeysOnDesktop = useUIStore(state => state.showTerminalQuickKeysOnDesktop);
|
||||||
|
const setShowTerminalQuickKeysOnDesktop = useUIStore(state => state.setShowTerminalQuickKeysOnDesktop);
|
||||||
const queueModeEnabled = useMessageQueueStore(state => state.queueModeEnabled);
|
const queueModeEnabled = useMessageQueueStore(state => state.queueModeEnabled);
|
||||||
const setQueueMode = useMessageQueueStore(state => state.setQueueMode);
|
const setQueueMode = useMessageQueueStore(state => state.setQueueMode);
|
||||||
const {
|
const {
|
||||||
@@ -252,6 +255,25 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{shouldShow('terminalQuickKeys') && !isMobile && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="typography-ui-header font-semibold text-foreground">
|
||||||
|
Show terminal optional key bar
|
||||||
|
</h3>
|
||||||
|
<p className="typography-ui text-muted-foreground">
|
||||||
|
Esc, Ctrl, arrows, Enter.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Switch
|
||||||
|
checked={showTerminalQuickKeysOnDesktop}
|
||||||
|
onCheckedChange={setShowTerminalQuickKeysOnDesktop}
|
||||||
|
className="data-[state=checked]:bg-status-info"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{shouldShow('cornerRadius') && (
|
{shouldShow('cornerRadius') && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|||||||
@@ -824,6 +824,12 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={cn('relative h-full w-full terminal-viewport-container', className)}
|
className={cn('relative h-full w-full terminal-viewport-container', className)}
|
||||||
style={{ backgroundColor: theme.background }}
|
style={{ backgroundColor: theme.background }}
|
||||||
|
onTouchStart={(event) => {
|
||||||
|
if (enableTouchScroll) {
|
||||||
|
const touch = event.touches?.[0];
|
||||||
|
focusHiddenInput(touch?.clientX, touch?.clientY);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
if (enableTouchScroll) {
|
if (enableTouchScroll) {
|
||||||
focusHiddenInput(event.clientX, event.clientY);
|
focusHiddenInput(event.clientX, event.clientY);
|
||||||
@@ -867,13 +873,37 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
|||||||
margin: 0,
|
margin: 0,
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
}}
|
}}
|
||||||
|
onBeforeInput={(event) => {
|
||||||
|
const nativeEvent = event.nativeEvent as unknown as InputEvent | undefined;
|
||||||
|
const inputType = nativeEvent?.inputType ?? '';
|
||||||
|
const data = typeof nativeEvent?.data === 'string' ? nativeEvent.data : '';
|
||||||
|
|
||||||
|
// Android Chrome sometimes never commits text into the DOM value
|
||||||
|
// for invisible inputs. Use beforeinput as the primary path.
|
||||||
|
if (inputType === 'insertText' && data) {
|
||||||
|
event.preventDefault();
|
||||||
|
inputHandlerRef.current(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputType === 'insertLineBreak') {
|
||||||
|
event.preventDefault();
|
||||||
|
inputHandlerRef.current('\r');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputType === 'deleteContentBackward') {
|
||||||
|
event.preventDefault();
|
||||||
|
inputHandlerRef.current('\x7f');
|
||||||
|
}
|
||||||
|
}}
|
||||||
onInput={(event) => {
|
onInput={(event) => {
|
||||||
const raw = String(event.currentTarget.value || '');
|
const raw = String(event.currentTarget.value || '');
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// iOS often inserts `\n` for Enter; the PTY expects CR.
|
// Some mobile keyboards (iOS esp.) insert `\n` for Enter; PTY expects CR.
|
||||||
const value = raw.replace(/\r\n|\r|\n/g, '\r');
|
const value = raw.replace(/\r\n|\r|\n/g, '\r');
|
||||||
inputHandlerRef.current(value);
|
inputHandlerRef.current(value);
|
||||||
event.currentTarget.value = '';
|
event.currentTarget.value = '';
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ export const TerminalView: React.FC = () => {
|
|||||||
const { currentTheme } = useThemeSystem();
|
const { currentTheme } = useThemeSystem();
|
||||||
const { monoFont } = useFontPreferences();
|
const { monoFont } = useFontPreferences();
|
||||||
const { isMobile, hasTouchInput } = useDeviceInfo();
|
const { isMobile, hasTouchInput } = useDeviceInfo();
|
||||||
|
const showTerminalQuickKeysOnDesktop = useUIStore((state) => state.showTerminalQuickKeysOnDesktop);
|
||||||
|
const showQuickKeys = isMobile || showTerminalQuickKeysOnDesktop;
|
||||||
|
|
||||||
const { currentSessionId, newSessionDraft } = useSessionStore();
|
const { currentSessionId, newSessionDraft } = useSessionStore();
|
||||||
const hasActiveContext = currentSessionId !== null || newSessionDraft?.open === true;
|
const hasActiveContext = currentSessionId !== null || newSessionDraft?.open === true;
|
||||||
@@ -138,10 +140,10 @@ export const TerminalView: React.FC = () => {
|
|||||||
}, [effectiveDirectory]);
|
}, [effectiveDirectory]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isMobile && activeModifier !== null) {
|
if (!showQuickKeys && activeModifier !== null) {
|
||||||
setActiveModifier(null);
|
setActiveModifier(null);
|
||||||
}
|
}
|
||||||
}, [isMobile, activeModifier, setActiveModifier]);
|
}, [showQuickKeys, activeModifier, setActiveModifier]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!terminalSessionId && activeModifier !== null) {
|
if (!terminalSessionId && activeModifier !== null) {
|
||||||
@@ -492,7 +494,7 @@ export const TerminalView: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!isMobile || !activeModifier || !terminalSessionId) {
|
if (!showQuickKeys || !activeModifier || !terminalSessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +567,7 @@ export const TerminalView: React.FC = () => {
|
|||||||
activeModifier,
|
activeModifier,
|
||||||
handleMobileKeyPress,
|
handleMobileKeyPress,
|
||||||
handleViewportInput,
|
handleViewportInput,
|
||||||
isMobile,
|
showQuickKeys,
|
||||||
setActiveModifier,
|
setActiveModifier,
|
||||||
terminalSessionId,
|
terminalSessionId,
|
||||||
]);
|
]);
|
||||||
@@ -693,7 +695,7 @@ export const TerminalView: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isMobile ? (
|
{showQuickKeys ? (
|
||||||
<div className="mt-2 flex flex-wrap items-center gap-1">
|
<div className="mt-2 flex flex-wrap items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ interface UIStore {
|
|||||||
notificationMode: 'always' | 'hidden-only';
|
notificationMode: 'always' | 'hidden-only';
|
||||||
notifyOnSubtasks: boolean;
|
notifyOnSubtasks: boolean;
|
||||||
|
|
||||||
|
showTerminalQuickKeysOnDesktop: boolean;
|
||||||
|
|
||||||
setTheme: (theme: 'light' | 'dark' | 'system') => void;
|
setTheme: (theme: 'light' | 'dark' | 'system') => void;
|
||||||
toggleSidebar: () => void;
|
toggleSidebar: () => void;
|
||||||
setSidebarOpen: (open: boolean) => void;
|
setSidebarOpen: (open: boolean) => void;
|
||||||
@@ -122,6 +124,7 @@ interface UIStore {
|
|||||||
setImagePreviewOpen: (open: boolean) => void;
|
setImagePreviewOpen: (open: boolean) => void;
|
||||||
setNativeNotificationsEnabled: (value: boolean) => void;
|
setNativeNotificationsEnabled: (value: boolean) => void;
|
||||||
setNotificationMode: (mode: 'always' | 'hidden-only') => void;
|
setNotificationMode: (mode: 'always' | 'hidden-only') => void;
|
||||||
|
setShowTerminalQuickKeysOnDesktop: (value: boolean) => void;
|
||||||
setNotifyOnSubtasks: (value: boolean) => void;
|
setNotifyOnSubtasks: (value: boolean) => void;
|
||||||
openMultiRunLauncher: () => void;
|
openMultiRunLauncher: () => void;
|
||||||
openMultiRunLauncherWithPrompt: (prompt: string) => void;
|
openMultiRunLauncherWithPrompt: (prompt: string) => void;
|
||||||
@@ -180,6 +183,8 @@ export const useUIStore = create<UIStore>()(
|
|||||||
notificationMode: 'hidden-only',
|
notificationMode: 'hidden-only',
|
||||||
notifyOnSubtasks: true,
|
notifyOnSubtasks: true,
|
||||||
|
|
||||||
|
showTerminalQuickKeysOnDesktop: false,
|
||||||
|
|
||||||
setTheme: (theme) => {
|
setTheme: (theme) => {
|
||||||
set({ theme });
|
set({ theme });
|
||||||
get().applyTheme();
|
get().applyTheme();
|
||||||
@@ -597,6 +602,10 @@ export const useUIStore = create<UIStore>()(
|
|||||||
set({ notificationMode: mode });
|
set({ notificationMode: mode });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setShowTerminalQuickKeysOnDesktop: (value) => {
|
||||||
|
set({ showTerminalQuickKeysOnDesktop: value });
|
||||||
|
},
|
||||||
|
|
||||||
setNotifyOnSubtasks: (value) => {
|
setNotifyOnSubtasks: (value) => {
|
||||||
set({ notifyOnSubtasks: value });
|
set({ notifyOnSubtasks: value });
|
||||||
},
|
},
|
||||||
@@ -634,6 +643,7 @@ export const useUIStore = create<UIStore>()(
|
|||||||
diffViewMode: state.diffViewMode,
|
diffViewMode: state.diffViewMode,
|
||||||
nativeNotificationsEnabled: state.nativeNotificationsEnabled,
|
nativeNotificationsEnabled: state.nativeNotificationsEnabled,
|
||||||
notificationMode: state.notificationMode,
|
notificationMode: state.notificationMode,
|
||||||
|
showTerminalQuickKeysOnDesktop: state.showTerminalQuickKeysOnDesktop,
|
||||||
notifyOnSubtasks: state.notifyOnSubtasks,
|
notifyOnSubtasks: state.notifyOnSubtasks,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run build:watch",
|
"dev": "bun run build:watch",
|
||||||
"dev:server": "node server/index.js --port 3001",
|
"dev:server": "bun server/index.js --port 3001",
|
||||||
"dev:server:watch": "nodemon --watch server --ext js --exec \"node server/index.js --port 3001\"",
|
"dev:server:watch": "nodemon --watch server --ext js --exec \"bun server/index.js --port 3001\"",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"build:watch": "vite build --watch",
|
"build:watch": "vite build --watch",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
|
|||||||
Reference in New Issue
Block a user