refactor: simplify expand chat input to 2-state with optimized button layout

- Remove 3-state logic (normal/50%/80%) - now only normal/expanded
- Remove double-click detection and tooltips for simplicity
- Remove platform-specific behavior (same UX for Electron and web)
- Optimize button layout: reduce from 36px to 28px to fit 3 buttons
- Position expand button above history buttons in vertical stack
- Keep 15-line expanded height (360px, capped to available space)

Per upstream dev feedback to keep it simple with one approach
This commit is contained in:
bizzkoot
2026-01-13 06:45:56 +08:00
parent 2e56a5e9f4
commit 71f58e7c5f
3 changed files with 49 additions and 208 deletions

View File

@@ -1,109 +1,29 @@
import { createSignal, Show } from "solid-js"
import { Show } from "solid-js"
import { Maximize2, Minimize2 } from "lucide-solid"
import { isElectronHost } from "../lib/runtime-env"
interface ExpandButtonProps {
expandState: () => "normal" | "fifty" | "eighty" | "expanded"
onToggleExpand: (nextState: "normal" | "fifty" | "eighty" | "expanded") => void
expandState: () => "normal" | "expanded"
onToggleExpand: (nextState: "normal" | "expanded") => void
}
export default function ExpandButton(props: ExpandButtonProps) {
const [clickTime, setClickTime] = createSignal<number>(0)
const [clickTimer, setClickTimer] = createSignal<number | null>(null)
const DOUBLE_CLICK_THRESHOLD = 300
// Check if we're in Electron (desktop app with 3-state support)
const isDesktopApp = isElectronHost()
function handleClick() {
const current = props.expandState()
if (!isDesktopApp) {
// Web/Mobile: Simple 2-state toggle (instant, no delay)
if (current === "normal") {
props.onToggleExpand("expanded")
} else {
props.onToggleExpand("normal")
}
return
}
// Electron: 3-state with double-click detection
const now = Date.now()
const lastClick = clickTime()
const isDoubleClick = now - lastClick < DOUBLE_CLICK_THRESHOLD
// Clear any pending single-click timer
const timer = clickTimer()
if (timer !== null) {
clearTimeout(timer)
setClickTimer(null)
}
if (isDoubleClick) {
// Double click behavior - execute immediately
if (current === "normal") {
props.onToggleExpand("eighty")
} else if (current === "fifty") {
props.onToggleExpand("eighty")
} else {
props.onToggleExpand("fifty")
}
// Reset click time to prevent triple-click issues
setClickTime(0)
} else {
// Single click behavior - delay to wait for potential double-click
setClickTime(now)
const newTimer = window.setTimeout(() => {
const currentState = props.expandState()
if (currentState === "normal") {
props.onToggleExpand("fifty")
} else {
props.onToggleExpand("normal")
}
setClickTimer(null)
}, DOUBLE_CLICK_THRESHOLD)
setClickTimer(newTimer)
}
}
const getTooltip = () => {
// No tooltip for web/mobile - only Electron gets tooltips
if (!isDesktopApp) {
return undefined
}
const current = props.expandState()
if (current === "normal") {
return "Click to expand (50%) • Double-click to expand further (80%)"
} else if (current === "fifty") {
return "Double-click to expand to 80% • Click to minimize"
} else {
return "Click to minimize • Double-click to reduce to 50%"
}
}
const isExpanded = () => {
const state = props.expandState()
return state !== "normal"
props.onToggleExpand(current === "normal" ? "expanded" : "normal")
}
return (
<button
type="button"
class={`prompt-expand-button ${isDesktopApp ? "desktop-mode" : "web-mode"}`}
class="prompt-expand-button"
onClick={handleClick}
disabled={false}
aria-label="Toggle chat input height"
data-tooltip={getTooltip()}
>
<Show
when={!isExpanded()}
fallback={<Minimize2 class="h-5 w-5" aria-hidden="true" />}
when={props.expandState() === "normal"}
fallback={<Minimize2 class="h-4 w-4" aria-hidden="true" />}
>
<Maximize2 class="h-5 w-5" aria-hidden="true" />
<Maximize2 class="h-4 w-4" aria-hidden="true" />
</Show>
</button>
)

View File

@@ -2,7 +2,6 @@ import { createSignal, Show, onMount, For, onCleanup, createEffect, on, untrack,
import { ArrowBigUp, ArrowBigDown } from "lucide-solid"
import UnifiedPicker from "./unified-picker"
import ExpandButton from "./expand-button"
import { isElectronHost } from "../lib/runtime-env"
import { addToHistory, getHistory } from "../stores/message-history"
import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments"
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
@@ -48,13 +47,12 @@ export default function PromptInput(props: PromptInputProps) {
const [pasteCount, setPasteCount] = createSignal(0)
const [imageCount, setImageCount] = createSignal(0)
const [mode, setMode] = createSignal<"normal" | "shell">("normal")
const [expandState, setExpandState] = createSignal<"normal" | "fifty" | "eighty" | "expanded">("normal")
const [expandState, setExpandState] = createSignal<"normal" | "expanded">("normal")
const SELECTION_INSERT_MAX_LENGTH = 2000
let textareaRef: HTMLTextAreaElement | undefined
let containerRef: HTMLDivElement | undefined
// Check if we're in Electron (desktop app with 3-state support)
const isDesktopApp = isElectronHost()
// Fixed line height for expanded state (15 lines as suggested by dev)
// Fixed line height for web/mobile expanded state (15 lines as suggested)
const EXPANDED_LINES = 15
@@ -85,21 +83,11 @@ export default function PromptInput(props: PromptInputProps) {
const state = expandState()
if (state === "normal") return "auto"
// Use fixed height, but cap at available space
// This prevents overflow in landscape or small screens
const availableHeight = calculateExpandedHeight()
if (isDesktopApp) {
// Electron: Use percentage-based heights (50% / 80%)
if (state === "fifty") {
return `${availableHeight * 0.5}px`
}
// state === "eighty"
return `${availableHeight * 0.8}px`
} else {
// Web/Mobile: Use fixed height, but cap at available space
// This prevents overflow in landscape or small screens
const maxHeight = Math.min(FIXED_EXPANDED_HEIGHT, availableHeight * 0.6)
return `${Math.max(maxHeight, 150)}px` // Minimum 150px to be useful
}
const maxHeight = Math.min(FIXED_EXPANDED_HEIGHT, availableHeight * 0.6)
return `${Math.max(maxHeight, 150)}px` // Minimum 150px to be useful
})
// Responsive placeholder text - shorter on mobile to avoid overlap with expand button
@@ -782,7 +770,7 @@ export default function PromptInput(props: PromptInputProps) {
void props.onAbortSession()
}
function handleExpandToggle(nextState: "normal" | "fifty" | "eighty" | "expanded") {
function handleExpandToggle(nextState: "normal" | "expanded") {
setExpandState(nextState)
// Keep focus on textarea
textareaRef?.focus()
@@ -1276,8 +1264,12 @@ export default function PromptInput(props: PromptInputProps) {
autoCapitalize="off"
autocomplete="off"
/>
<Show when={hasHistory()}>
<div class="prompt-history-top">
<div class="prompt-nav-buttons">
<ExpandButton
expandState={expandState}
onToggleExpand={handleExpandToggle}
/>
<Show when={hasHistory()}>
<button
type="button"
class="prompt-history-button"
@@ -1287,8 +1279,6 @@ export default function PromptInput(props: PromptInputProps) {
>
<ArrowBigUp class="h-5 w-5" aria-hidden="true" />
</button>
</div>
<div class="prompt-history-bottom">
<button
type="button"
class="prompt-history-button"
@@ -1298,13 +1288,7 @@ export default function PromptInput(props: PromptInputProps) {
>
<ArrowBigDown class="h-5 w-5" aria-hidden="true" />
</button>
</div>
</Show>
<div class="prompt-expand-top">
<ExpandButton
expandState={expandState}
onToggleExpand={handleExpandToggle}
/>
</Show>
</div>
<Show when={shouldShowOverlay()}>
<div class={`prompt-input-overlay ${mode() === "shell" ? "shell-mode" : ""}`}>

View File

@@ -37,18 +37,18 @@
height: 100%;
}
.prompt-input {
@apply w-full pl-3 pr-10 pt-2.5 border text-sm resize-none outline-none transition-colors;
font-family: inherit;
background-color: var(--surface-base);
color: inherit;
border-color: var(--border-base);
line-height: var(--line-height-normal);
border-radius: 0;
padding-bottom: 0;
height: 100%;
min-height: 100%;
}
.prompt-input {
@apply w-full pl-3 pr-10 pt-2.5 border text-sm resize-none outline-none transition-colors;
font-family: inherit;
background-color: var(--surface-base);
color: inherit;
border-color: var(--border-base);
line-height: var(--line-height-normal);
border-radius: 0;
padding-bottom: 0;
height: 100%;
min-height: 100%;
}
.prompt-input-overlay {
@@ -70,110 +70,47 @@
color: var(--text-primary);
}
.prompt-history-top,
.prompt-history-bottom {
/* Navigation buttons container (expand, prev, next) */
.prompt-nav-buttons {
position: absolute;
right: 0.35rem;
top: 0.25rem;
right: 0.25rem;
bottom: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
justify-content: flex-start;
gap: 0.125rem;
z-index: 2;
}
.prompt-history-top {
top: 0.3rem;
}
.prompt-history-bottom {
bottom: 0.6rem;
}
.prompt-expand-button,
.prompt-history-button {
@apply w-9 h-9 flex items-center justify-center rounded-md;
@apply w-7 h-7 flex items-center justify-center rounded-md;
color: var(--text-muted);
background-color: rgba(15, 23, 42, 0.04);
transition: background-color 0.15s ease, color 0.15s ease;
padding: 0;
flex-shrink: 0;
}
.prompt-expand-button:hover:not(:disabled),
.prompt-history-button:hover:not(:disabled) {
background-color: var(--surface-secondary);
color: var(--text-primary);
}
.prompt-history-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.prompt-expand-top {
position: absolute;
top: 0.3rem;
right: 3.5rem;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.prompt-expand-button {
@apply w-9 h-9 flex items-center justify-center rounded-md;
color: var(--text-muted);
background-color: rgba(15, 23, 42, 0.04);
transition: background-color 0.15s ease, color 0.15s ease;
padding: 0;
position: relative;
}
.prompt-expand-button:hover:not(:disabled) {
background-color: var(--surface-secondary);
color: var(--text-primary);
}
.prompt-expand-button:active:not(:disabled) {
background-color: var(--accent-primary);
color: var(--text-inverted);
transform: scale(0.95);
}
.prompt-expand-button:disabled {
.prompt-expand-button:disabled,
.prompt-history-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.prompt-expand-button::after {
content: attr(data-tooltip);
position: absolute;
top: calc(100% + 8px);
right: 0;
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.98) 100%);
color: var(--text-primary);
padding: 0.5rem 0.75rem;
border-radius: 6px;
font-size: 0.75rem;
line-height: 1.4;
white-space: nowrap;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(255, 255, 255, 0.1);
border: 1px solid var(--border-base);
pointer-events: none;
opacity: 0;
transform: translateY(-4px);
transition: opacity 0.2s ease, transform 0.2s ease;
z-index: 1000;
backdrop-filter: blur(8px);
}
/* Only show tooltip on hover for Electron (desktop-mode) */
.prompt-expand-button.desktop-mode:hover::after {
opacity: 1;
transform: translateY(0);
}
/* Web/Mobile: No tooltip (data-tooltip will be undefined anyway) */
.prompt-expand-button.web-mode::after {
display: none;
}
.prompt-overlay-text {
display: inline-flex;
align-items: center;
@@ -369,4 +306,4 @@
gap: 0;
padding: 0;
}
}
}