feat: implement platform-specific expand chat input with mobile optimizations
- Add platform detection (Electron vs Web) for expand behavior - Electron: 3-state (normal → 50% → 80%) with double-click - Web/Mobile: 2-state (normal → expanded) with instant single tap - Implement fixed 15-line height for web/mobile (360px, capped) - Add orientation-aware height calculation (landscape vs portrait) - Remove tooltip on web/mobile, keep for Electron desktop - Add responsive placeholder text to prevent overlap on mobile - Desktop: "Type your message, @file, @agent, or paste images and text..." - Mobile (≤640px): "Type message, @file, @agent..." - Delete dev-docs/expand-chat-input.md per upstream feedback Addresses PR feedback to simplify from 3-state to 2-state for web/mobile while maintaining rich desktop experience in Electron app.
This commit is contained in:
@@ -1,521 +0,0 @@
|
|||||||
# Expand Chat Input Implementation Plan
|
|
||||||
|
|
||||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
||||||
|
|
||||||
**Goal:** Add an expand/minimize button to the chat input section that allows users to expand the textarea to 50% or 80% of the chat window height with single/double-click, maintaining Gemini-like UX.
|
|
||||||
|
|
||||||
**Architecture:** Add a new state signal `expandState` to track 3 states (normal/50%/80%), create an `ExpandButton` component positioned alongside history buttons, calculate dynamic heights based on parent container, apply smooth CSS transitions, and add comprehensive hover tooltips explaining both click behaviors.
|
|
||||||
|
|
||||||
**Tech Stack:** SolidJS (signals/effects), lucide-solid icons (Maximize2/Minimize2), CSS transitions, existing button styling patterns from prompt-input.css
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 1: Add expand state signal and types
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `packages/ui/src/components/prompt-input.tsx:33-52`
|
|
||||||
|
|
||||||
**Step 1: Understand current state structure**
|
|
||||||
|
|
||||||
Read lines 33-52 to see how signals like `prompt`, `mode`, `showPicker` are created.
|
|
||||||
|
|
||||||
Expected: See `createSignal` patterns for boolean and string states.
|
|
||||||
|
|
||||||
**Step 2: Add expand state signal after line 47**
|
|
||||||
|
|
||||||
In `packages/ui/src/components/prompt-input.tsx`, after line 47 (`const [mode, setMode] = createSignal<"normal" | "shell">("normal")`), add:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const [expandState, setExpandState] = createSignal<"normal" | "fifty" | "eighty">("normal")
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 3: Add height calculation memos after line 58**
|
|
||||||
|
|
||||||
Add these utility functions right after the signal definitions (before the createEffect on line 59):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const calculateContainerHeight = () => {
|
|
||||||
if (!containerRef) return 0
|
|
||||||
const rect = containerRef.getBoundingClientRect()
|
|
||||||
const root = containerRef.closest(".session-view")
|
|
||||||
if (!root) return 0
|
|
||||||
const rootRect = root.getBoundingClientRect()
|
|
||||||
return rootRect.height - rect.top
|
|
||||||
}
|
|
||||||
|
|
||||||
const getExpandedHeight = (): string => {
|
|
||||||
const state = expandState()
|
|
||||||
if (state === "normal") return "auto"
|
|
||||||
const containerHeight = calculateContainerHeight()
|
|
||||||
if (state === "fifty") return `${containerHeight * 0.5}px`
|
|
||||||
return `${containerHeight * 0.8}px`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 4: Verify no syntax errors**
|
|
||||||
|
|
||||||
Run in terminal:
|
|
||||||
```bash
|
|
||||||
cd packages/ui && npm run type-check
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: No TypeScript errors in prompt-input.tsx.
|
|
||||||
|
|
||||||
**Step 5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add packages/ui/src/components/prompt-input.tsx
|
|
||||||
git commit -m "feat: add expand state signal and height calculation helpers"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 2: Create ExpandButton component
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `packages/ui/src/components/expand-button.tsx`
|
|
||||||
|
|
||||||
**Step 1: Create the component file**
|
|
||||||
|
|
||||||
Create `packages/ui/src/components/expand-button.tsx` with this content:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { createSignal, Show } from "solid-js"
|
|
||||||
import { Maximize2, Minimize2 } from "lucide-solid"
|
|
||||||
|
|
||||||
interface ExpandButtonProps {
|
|
||||||
expandState: () => "normal" | "fifty" | "eighty"
|
|
||||||
onToggleExpand: (nextState: "normal" | "fifty" | "eighty") => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ExpandButton(props: ExpandButtonProps) {
|
|
||||||
const [clickTime, setClickTime] = createSignal<number>(0)
|
|
||||||
const DOUBLE_CLICK_THRESHOLD = 300
|
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
const now = Date.now()
|
|
||||||
const lastClick = clickTime()
|
|
||||||
const isDoubleClick = now - lastClick < DOUBLE_CLICK_THRESHOLD
|
|
||||||
|
|
||||||
setClickTime(now)
|
|
||||||
|
|
||||||
const current = props.expandState()
|
|
||||||
|
|
||||||
if (isDoubleClick) {
|
|
||||||
// Double click behavior
|
|
||||||
if (current === "normal") {
|
|
||||||
props.onToggleExpand("fifty")
|
|
||||||
} else if (current === "fifty") {
|
|
||||||
props.onToggleExpand("eighty")
|
|
||||||
} else {
|
|
||||||
props.onToggleExpand("normal")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Single click behavior
|
|
||||||
if (current === "normal") {
|
|
||||||
props.onToggleExpand("fifty")
|
|
||||||
} else {
|
|
||||||
props.onToggleExpand("normal")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset click timer after threshold
|
|
||||||
setTimeout(() => setClickTime(0), DOUBLE_CLICK_THRESHOLD)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTooltip = () => {
|
|
||||||
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 expand to 50%"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="prompt-expand-button"
|
|
||||||
onClick={handleClick}
|
|
||||||
disabled={false}
|
|
||||||
aria-label="Toggle chat input height"
|
|
||||||
title={getTooltip()}
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={props.expandState() === "normal"}
|
|
||||||
fallback={<Minimize2 class="h-5 w-5" aria-hidden="true" />}
|
|
||||||
>
|
|
||||||
<Maximize2 class="h-5 w-5" aria-hidden="true" />
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 2: Verify component syntax**
|
|
||||||
|
|
||||||
Run in terminal:
|
|
||||||
```bash
|
|
||||||
cd packages/ui && npm run type-check
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: No TypeScript errors in expand-button.tsx.
|
|
||||||
|
|
||||||
**Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add packages/ui/src/components/expand-button.tsx
|
|
||||||
git commit -m "feat: create ExpandButton component with click/double-click logic"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 3: Integrate ExpandButton into PromptInput
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `packages/ui/src/components/prompt-input.tsx:1-3, 1040-1250`
|
|
||||||
|
|
||||||
**Step 1: Add import for ExpandButton**
|
|
||||||
|
|
||||||
In `packages/ui/src/components/prompt-input.tsx`, at line 1 after the existing imports, add:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import ExpandButton from "./expand-button"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 2: Add expand handler function**
|
|
||||||
|
|
||||||
After the `handleAbort` function (around line 703), add:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
function handleExpandToggle(nextState: "normal" | "fifty" | "eighty") {
|
|
||||||
setExpandState(nextState)
|
|
||||||
// Keep focus on textarea
|
|
||||||
textareaRef?.focus()
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 3: Render ExpandButton in JSX**
|
|
||||||
|
|
||||||
Find the section where history buttons are rendered (around line 1188-1211). Right before the closing `</Show>` tag of the history buttons, add the ExpandButton:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
<Show when={hasHistory()}>
|
|
||||||
<div class="prompt-expand-top">
|
|
||||||
<ExpandButton
|
|
||||||
expandState={expandState}
|
|
||||||
onToggleExpand={handleExpandToggle}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
```
|
|
||||||
|
|
||||||
Wait - actually, the expand button should always show (not conditionally with history). Let me correct:
|
|
||||||
|
|
||||||
Replace the above with - add this RIGHT AFTER the `</Show>` closing tag for history buttons (after line 1211):
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
<div class="prompt-expand-top">
|
|
||||||
<ExpandButton
|
|
||||||
expandState={expandState}
|
|
||||||
onToggleExpand={handleExpandToggle}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 4: Apply dynamic height to textarea**
|
|
||||||
|
|
||||||
Find the textarea element (around line 1166-1187). Modify the style binding on the textarea to include dynamic height:
|
|
||||||
|
|
||||||
Change from:
|
|
||||||
```typescript
|
|
||||||
style={attachments().length > 0 ? { "padding-top": "8px" } : {}}
|
|
||||||
```
|
|
||||||
|
|
||||||
To:
|
|
||||||
```typescript
|
|
||||||
style={{
|
|
||||||
"padding-top": attachments().length > 0 ? "8px" : "0",
|
|
||||||
"height": getExpandedHeight(),
|
|
||||||
"overflow-y": expandState() !== "normal" ? "auto" : "visible",
|
|
||||||
"transition": "height 0.25s ease",
|
|
||||||
}}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 5: Verify no errors**
|
|
||||||
|
|
||||||
Run in terminal:
|
|
||||||
```bash
|
|
||||||
cd packages/ui && npm run type-check
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: No TypeScript errors.
|
|
||||||
|
|
||||||
**Step 6: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add packages/ui/src/components/prompt-input.tsx
|
|
||||||
git commit -m "feat: integrate ExpandButton and apply dynamic height to textarea"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 4: Add CSS styles for expand button positioning
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `packages/ui/src/styles/messaging/prompt-input.css:72-107`
|
|
||||||
|
|
||||||
**Step 1: Add prompt-expand-top styles**
|
|
||||||
|
|
||||||
After the `.prompt-history-bottom` rule (around line 88), add:
|
|
||||||
|
|
||||||
```css
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
|
||||||
opacity: 0.4;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 2: Adjust prompt-input-field-container for expanded states**
|
|
||||||
|
|
||||||
After the `.prompt-input-field` rule (around line 37), add:
|
|
||||||
|
|
||||||
```css
|
|
||||||
.prompt-input-field-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 56px;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
height: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
transition: height 0.25s ease;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Actually, the min-height should remain. Let me provide the corrected version - modify the existing `.prompt-input-field-container` rule (lines 23-30) to add the transition:
|
|
||||||
|
|
||||||
Change line 24 from:
|
|
||||||
```css
|
|
||||||
.prompt-input-field-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 56px;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
height: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
To:
|
|
||||||
```css
|
|
||||||
.prompt-input-field-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 56px;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
height: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
transition: height 0.25s ease;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 3: Verify CSS syntax**
|
|
||||||
|
|
||||||
Run in terminal:
|
|
||||||
```bash
|
|
||||||
npm run build --workspace @neuralnomads/codenomad-ui
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Build succeeds with no CSS errors.
|
|
||||||
|
|
||||||
**Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add packages/ui/src/styles/messaging/prompt-input.css
|
|
||||||
git commit -m "style: add expand button positioning and styles"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 5: Test expand/minimize functionality in dev
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- No new files
|
|
||||||
|
|
||||||
**Step 1: Start dev server**
|
|
||||||
|
|
||||||
Run in terminal:
|
|
||||||
```bash
|
|
||||||
npm run dev --workspace @neuralnomads/codenomad-ui
|
|
||||||
```
|
|
||||||
|
|
||||||
Wait for the server to start and print the local URL.
|
|
||||||
|
|
||||||
**Step 2: Test expand button visibility**
|
|
||||||
|
|
||||||
- Open the dev app in browser
|
|
||||||
- Create/open a session
|
|
||||||
- Verify expand button appears in top-right corner of input area, left of arrow buttons
|
|
||||||
- Verify button shows Maximize2 icon
|
|
||||||
|
|
||||||
**Step 3: Test single-click to 50% expand**
|
|
||||||
|
|
||||||
- Click the expand button once
|
|
||||||
- Verify textarea height increases to ~50% of chat window
|
|
||||||
- Verify icon changes to Minimize2
|
|
||||||
- Verify tooltip text updates
|
|
||||||
|
|
||||||
**Step 4: Test double-click from normal to 80% expand**
|
|
||||||
|
|
||||||
- Click expand button twice rapidly (within 300ms)
|
|
||||||
- Verify textarea height jumps to ~80% of chat window
|
|
||||||
- Verify scrollbar appears if content is long
|
|
||||||
- Type some text to verify scrolling works
|
|
||||||
|
|
||||||
**Step 5: Test minimize from 80%**
|
|
||||||
|
|
||||||
- Click expand button once
|
|
||||||
- Verify textarea collapses back to normal height (56px min-height)
|
|
||||||
- Verify icon changes back to Maximize2
|
|
||||||
|
|
||||||
**Step 6: Test expand button with 50% state**
|
|
||||||
|
|
||||||
- Single-click expand button → should go to 50%
|
|
||||||
- Double-click expand button → should go to 80%
|
|
||||||
- Verify tooltip updates to "Double-click to expand to 80% • Click to minimize"
|
|
||||||
|
|
||||||
**Step 7: Verify arrow buttons unchanged**
|
|
||||||
|
|
||||||
- Verify up/down arrow buttons still visible and functional
|
|
||||||
- Verify Stop/Start buttons position unchanged
|
|
||||||
- Verify all buttons maintain original sizes
|
|
||||||
|
|
||||||
**Step 8: Verify smooth transitions**
|
|
||||||
|
|
||||||
- Watch height changes - should see smooth 250ms transition
|
|
||||||
- No jarring jumps or layout shifts
|
|
||||||
|
|
||||||
**Expected Results:**
|
|
||||||
- All expand/minimize transitions work as specified
|
|
||||||
- No overlapping buttons
|
|
||||||
- Scrollbar appears correctly when needed
|
|
||||||
- Tooltips are comprehensive and helpful
|
|
||||||
|
|
||||||
If any test fails, return to the task that needs fixing before continuing.
|
|
||||||
|
|
||||||
**Step 9: Manual testing complete**
|
|
||||||
|
|
||||||
Once all tests pass, stop the dev server:
|
|
||||||
```bash
|
|
||||||
Ctrl+C
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 10: Commit test verification**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add -A
|
|
||||||
git commit -m "test: verify expand button functionality and UX"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 6: Final cleanup and verification
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Review: `packages/ui/src/components/prompt-input.tsx`
|
|
||||||
- Review: `packages/ui/src/components/expand-button.tsx`
|
|
||||||
- Review: `packages/ui/src/styles/messaging/prompt-input.css`
|
|
||||||
|
|
||||||
**Step 1: Verify no console errors**
|
|
||||||
|
|
||||||
Restart dev server and check browser console:
|
|
||||||
```bash
|
|
||||||
npm run dev --workspace @neuralnomads/codenomad-ui
|
|
||||||
```
|
|
||||||
|
|
||||||
Open DevTools console - should show no warnings or errors related to expand button.
|
|
||||||
|
|
||||||
**Step 2: Check responsive behavior**
|
|
||||||
|
|
||||||
Resize browser window to different sizes - verify button positioning remains correct at all sizes.
|
|
||||||
|
|
||||||
**Step 3: Verify with attachments**
|
|
||||||
|
|
||||||
- Add an attachment (paste an image or large text)
|
|
||||||
- Expand the input
|
|
||||||
- Verify attachment chips display correctly above expanded textarea
|
|
||||||
|
|
||||||
**Step 4: Test with history buttons**
|
|
||||||
|
|
||||||
- Navigate through prompt history with arrow buttons while expanded
|
|
||||||
- Verify history navigation still works
|
|
||||||
- Verify expand button doesn't interfere with history buttons
|
|
||||||
|
|
||||||
**Step 5: Build for production**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build --workspace @neuralnomads/codenomad-ui
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: Build succeeds with no errors.
|
|
||||||
|
|
||||||
**Step 6: Final commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add -A
|
|
||||||
git commit -m "feat: complete expand chat input feature with full UX"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
This plan implements the expand chat input feature across 6 tasks:
|
|
||||||
|
|
||||||
1. ✅ Add expand state signal and height helpers
|
|
||||||
2. ✅ Create ExpandButton component with click/double-click logic
|
|
||||||
3. ✅ Integrate ExpandButton and apply dynamic heights
|
|
||||||
4. ✅ Add CSS styles for button and positioning
|
|
||||||
5. ✅ Test all functionality in dev
|
|
||||||
6. ✅ Final verification and build
|
|
||||||
|
|
||||||
**Estimated time:** 45-60 minutes total
|
|
||||||
|
|
||||||
**Key points:**
|
|
||||||
- Expand button positioned top-right, left of arrow buttons
|
|
||||||
- 3 states: normal (56px min) → 50% height → 80% height
|
|
||||||
- Single-click advances one state, double-click skips to 80%
|
|
||||||
- Smooth 250ms transitions with scrollbar support
|
|
||||||
- Comprehensive hover tooltips explain all behaviors
|
|
||||||
- Maintains all existing button positions and sizes
|
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/plugin": "1.1.10"
|
"@opencode-ai/plugin": "1.1.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { createSignal, Show } from "solid-js"
|
import { createSignal, Show } from "solid-js"
|
||||||
import { Maximize2, Minimize2 } from "lucide-solid"
|
import { Maximize2, Minimize2 } from "lucide-solid"
|
||||||
|
import { isElectronHost } from "../lib/runtime-env"
|
||||||
|
|
||||||
interface ExpandButtonProps {
|
interface ExpandButtonProps {
|
||||||
expandState: () => "normal" | "fifty" | "eighty"
|
expandState: () => "normal" | "fifty" | "eighty" | "expanded"
|
||||||
onToggleExpand: (nextState: "normal" | "fifty" | "eighty") => void
|
onToggleExpand: (nextState: "normal" | "fifty" | "eighty" | "expanded") => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExpandButton(props: ExpandButtonProps) {
|
export default function ExpandButton(props: ExpandButtonProps) {
|
||||||
@@ -11,7 +12,23 @@ export default function ExpandButton(props: ExpandButtonProps) {
|
|||||||
const [clickTimer, setClickTimer] = createSignal<number | null>(null)
|
const [clickTimer, setClickTimer] = createSignal<number | null>(null)
|
||||||
const DOUBLE_CLICK_THRESHOLD = 300
|
const DOUBLE_CLICK_THRESHOLD = 300
|
||||||
|
|
||||||
|
// Check if we're in Electron (desktop app with 3-state support)
|
||||||
|
const isDesktopApp = isElectronHost()
|
||||||
|
|
||||||
function handleClick() {
|
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 now = Date.now()
|
||||||
const lastClick = clickTime()
|
const lastClick = clickTime()
|
||||||
const isDoubleClick = now - lastClick < DOUBLE_CLICK_THRESHOLD
|
const isDoubleClick = now - lastClick < DOUBLE_CLICK_THRESHOLD
|
||||||
@@ -23,8 +40,6 @@ export default function ExpandButton(props: ExpandButtonProps) {
|
|||||||
setClickTimer(null)
|
setClickTimer(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const current = props.expandState()
|
|
||||||
|
|
||||||
if (isDoubleClick) {
|
if (isDoubleClick) {
|
||||||
// Double click behavior - execute immediately
|
// Double click behavior - execute immediately
|
||||||
if (current === "normal") {
|
if (current === "normal") {
|
||||||
@@ -55,6 +70,11 @@ export default function ExpandButton(props: ExpandButtonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getTooltip = () => {
|
const getTooltip = () => {
|
||||||
|
// No tooltip for web/mobile - only Electron gets tooltips
|
||||||
|
if (!isDesktopApp) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
const current = props.expandState()
|
const current = props.expandState()
|
||||||
if (current === "normal") {
|
if (current === "normal") {
|
||||||
return "Click to expand (50%) • Double-click to expand further (80%)"
|
return "Click to expand (50%) • Double-click to expand further (80%)"
|
||||||
@@ -65,17 +85,22 @@ export default function ExpandButton(props: ExpandButtonProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isExpanded = () => {
|
||||||
|
const state = props.expandState()
|
||||||
|
return state !== "normal"
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="prompt-expand-button"
|
class={`prompt-expand-button ${isDesktopApp ? "desktop-mode" : "web-mode"}`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
aria-label="Toggle chat input height"
|
aria-label="Toggle chat input height"
|
||||||
data-tooltip={getTooltip()}
|
data-tooltip={getTooltip()}
|
||||||
>
|
>
|
||||||
<Show
|
<Show
|
||||||
when={props.expandState() === "normal"}
|
when={!isExpanded()}
|
||||||
fallback={<Minimize2 class="h-5 w-5" aria-hidden="true" />}
|
fallback={<Minimize2 class="h-5 w-5" aria-hidden="true" />}
|
||||||
>
|
>
|
||||||
<Maximize2 class="h-5 w-5" aria-hidden="true" />
|
<Maximize2 class="h-5 w-5" aria-hidden="true" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createSignal, Show, onMount, For, onCleanup, createEffect, on, untrack,
|
|||||||
import { ArrowBigUp, ArrowBigDown } from "lucide-solid"
|
import { ArrowBigUp, ArrowBigDown } from "lucide-solid"
|
||||||
import UnifiedPicker from "./unified-picker"
|
import UnifiedPicker from "./unified-picker"
|
||||||
import ExpandButton from "./expand-button"
|
import ExpandButton from "./expand-button"
|
||||||
|
import { isElectronHost } from "../lib/runtime-env"
|
||||||
import { addToHistory, getHistory } from "../stores/message-history"
|
import { addToHistory, getHistory } from "../stores/message-history"
|
||||||
import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments"
|
import { getAttachments, addAttachment, clearAttachments, removeAttachment } from "../stores/attachments"
|
||||||
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
|
import { resolvePastedPlaceholders } from "../lib/prompt-placeholders"
|
||||||
@@ -47,40 +48,87 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const [pasteCount, setPasteCount] = createSignal(0)
|
const [pasteCount, setPasteCount] = createSignal(0)
|
||||||
const [imageCount, setImageCount] = createSignal(0)
|
const [imageCount, setImageCount] = createSignal(0)
|
||||||
const [mode, setMode] = createSignal<"normal" | "shell">("normal")
|
const [mode, setMode] = createSignal<"normal" | "shell">("normal")
|
||||||
const [expandState, setExpandState] = createSignal<"normal" | "fifty" | "eighty">("normal")
|
const [expandState, setExpandState] = createSignal<"normal" | "fifty" | "eighty" | "expanded">("normal")
|
||||||
const SELECTION_INSERT_MAX_LENGTH = 2000
|
const SELECTION_INSERT_MAX_LENGTH = 2000
|
||||||
let textareaRef: HTMLTextAreaElement | undefined
|
let textareaRef: HTMLTextAreaElement | undefined
|
||||||
let containerRef: HTMLDivElement | undefined
|
let containerRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
const calculateExpandedHeight = () => {
|
// Check if we're in Electron (desktop app with 3-state support)
|
||||||
if (!containerRef) {
|
const isDesktopApp = isElectronHost()
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = containerRef.closest(".session-view")
|
// Fixed line height for web/mobile expanded state (15 lines as suggested)
|
||||||
if (!root) {
|
const EXPANDED_LINES = 15
|
||||||
return 0
|
const LINE_HEIGHT = 24
|
||||||
}
|
const FIXED_EXPANDED_HEIGHT = EXPANDED_LINES * LINE_HEIGHT // 360px
|
||||||
const rootRect = root.getBoundingClientRect()
|
|
||||||
|
|
||||||
// Reserve minimum space for message section (200px minimum)
|
const calculateExpandedHeight = () => {
|
||||||
const MIN_MESSAGE_SPACE = 200
|
if (!containerRef) {
|
||||||
const availableForInput = rootRect.height - MIN_MESSAGE_SPACE
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
return availableForInput
|
const root = containerRef.closest(".session-view")
|
||||||
}
|
if (!root) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const rootRect = root.getBoundingClientRect()
|
||||||
|
|
||||||
const expandedHeight = createMemo(() => {
|
// Reserve minimum space for message section
|
||||||
const state = expandState()
|
// Use larger reserve for landscape orientation (less vertical space)
|
||||||
if (state === "normal") return "auto"
|
const isLandscape = typeof window !== "undefined" && window.innerWidth > window.innerHeight
|
||||||
|
const MIN_MESSAGE_SPACE = isLandscape ? 150 : 200
|
||||||
|
const availableForInput = rootRect.height - MIN_MESSAGE_SPACE
|
||||||
|
|
||||||
const availableHeight = calculateExpandedHeight()
|
return availableForInput
|
||||||
|
}
|
||||||
|
|
||||||
if (state === "fifty") {
|
const expandedHeight = createMemo(() => {
|
||||||
return `${availableHeight * 0.5}px`
|
const state = expandState()
|
||||||
}
|
if (state === "normal") return "auto"
|
||||||
return `${availableHeight * 0.8}px`
|
|
||||||
})
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Responsive placeholder text - shorter on mobile to avoid overlap with expand button
|
||||||
|
const [isMobileWidth, setIsMobileWidth] = createSignal(false)
|
||||||
|
|
||||||
|
const updateMobileWidth = () => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
setIsMobileWidth(window.innerWidth <= 640)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateMobileWidth()
|
||||||
|
window.addEventListener("resize", updateMobileWidth)
|
||||||
|
onCleanup(() => {
|
||||||
|
window.removeEventListener("resize", updateMobileWidth)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const getPlaceholder = () => {
|
||||||
|
if (mode() === "shell") {
|
||||||
|
return "Run a shell command (Esc to exit)..."
|
||||||
|
}
|
||||||
|
// Use shorter placeholder on mobile to prevent overlap with expand button
|
||||||
|
if (isMobileWidth()) {
|
||||||
|
return "Type message, @file, @agent..."
|
||||||
|
}
|
||||||
|
return "Type your message, @file, @agent, or paste images and text..."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -647,7 +695,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
// Record attempted slash commands even if execution fails.
|
// Record attempted slash commands even if execution fails.
|
||||||
void refreshHistory()
|
void refreshHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isShellMode) {
|
if (isShellMode) {
|
||||||
if (props.onRunShell) {
|
if (props.onRunShell) {
|
||||||
@@ -674,7 +722,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
textareaRef?.focus()
|
textareaRef?.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusTextareaEnd() {
|
function focusTextareaEnd() {
|
||||||
if (!textareaRef) return
|
if (!textareaRef) return
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -684,7 +732,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
textareaRef.focus()
|
textareaRef.focus()
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
function canUseHistory(force = false) {
|
function canUseHistory(force = false) {
|
||||||
if (force) return true
|
if (force) return true
|
||||||
if (showPicker()) return false
|
if (showPicker()) return false
|
||||||
@@ -692,29 +740,29 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
if (!textarea) return false
|
if (!textarea) return false
|
||||||
return textarea.selectionStart === 0 && textarea.selectionEnd === 0
|
return textarea.selectionStart === 0 && textarea.selectionEnd === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPreviousHistory(force = false) {
|
function selectPreviousHistory(force = false) {
|
||||||
const entries = history()
|
const entries = history()
|
||||||
if (entries.length === 0) return false
|
if (entries.length === 0) return false
|
||||||
if (!canUseHistory(force)) return false
|
if (!canUseHistory(force)) return false
|
||||||
|
|
||||||
if (historyIndex() === -1) {
|
if (historyIndex() === -1) {
|
||||||
setHistoryDraft(prompt())
|
setHistoryDraft(prompt())
|
||||||
}
|
}
|
||||||
|
|
||||||
const newIndex = historyIndex() === -1 ? 0 : Math.min(historyIndex() + 1, entries.length - 1)
|
const newIndex = historyIndex() === -1 ? 0 : Math.min(historyIndex() + 1, entries.length - 1)
|
||||||
setHistoryIndex(newIndex)
|
setHistoryIndex(newIndex)
|
||||||
setPrompt(entries[newIndex])
|
setPrompt(entries[newIndex])
|
||||||
focusTextareaEnd()
|
focusTextareaEnd()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectNextHistory(force = false) {
|
function selectNextHistory(force = false) {
|
||||||
const entries = history()
|
const entries = history()
|
||||||
if (entries.length === 0) return false
|
if (entries.length === 0) return false
|
||||||
if (!canUseHistory(force)) return false
|
if (!canUseHistory(force)) return false
|
||||||
if (historyIndex() === -1) return false
|
if (historyIndex() === -1) return false
|
||||||
|
|
||||||
const newIndex = historyIndex() - 1
|
const newIndex = historyIndex() - 1
|
||||||
if (newIndex >= 0) {
|
if (newIndex >= 0) {
|
||||||
setHistoryIndex(newIndex)
|
setHistoryIndex(newIndex)
|
||||||
@@ -728,18 +776,18 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
focusTextareaEnd()
|
focusTextareaEnd()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAbort() {
|
function handleAbort() {
|
||||||
if (!props.onAbortSession || !props.isSessionBusy) return
|
if (!props.onAbortSession || !props.isSessionBusy) return
|
||||||
void props.onAbortSession()
|
void props.onAbortSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleExpandToggle(nextState: "normal" | "fifty" | "eighty") {
|
function handleExpandToggle(nextState: "normal" | "fifty" | "eighty" | "expanded") {
|
||||||
setExpandState(nextState)
|
setExpandState(nextState)
|
||||||
// Keep focus on textarea
|
// Keep focus on textarea
|
||||||
textareaRef?.focus()
|
textareaRef?.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInput(e: Event) {
|
function handleInput(e: Event) {
|
||||||
|
|
||||||
const target = e.target as HTMLTextAreaElement
|
const target = e.target as HTMLTextAreaElement
|
||||||
@@ -803,9 +851,9 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
item:
|
item:
|
||||||
| { type: "agent"; agent: Agent }
|
| { type: "agent"; agent: Agent }
|
||||||
| {
|
| {
|
||||||
type: "file"
|
type: "file"
|
||||||
file: { path: string; relativePath?: string; isGitFile: boolean; isDirectory?: boolean }
|
file: { path: string; relativePath?: string; isGitFile: boolean; isDirectory?: boolean }
|
||||||
}
|
}
|
||||||
| { type: "command"; command: SDKCommand },
|
| { type: "command"; command: SDKCommand },
|
||||||
) {
|
) {
|
||||||
if (item.type === "command") {
|
if (item.type === "command") {
|
||||||
@@ -1056,18 +1104,18 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const canStop = () => Boolean(props.isSessionBusy && props.onAbortSession)
|
const canStop = () => Boolean(props.isSessionBusy && props.onAbortSession)
|
||||||
|
|
||||||
const hasHistory = () => history().length > 0
|
const hasHistory = () => history().length > 0
|
||||||
const canHistoryGoPrevious = () => hasHistory() && (historyIndex() === -1 || historyIndex() < history().length - 1)
|
const canHistoryGoPrevious = () => hasHistory() && (historyIndex() === -1 || historyIndex() < history().length - 1)
|
||||||
const canHistoryGoNext = () => historyIndex() >= 0
|
const canHistoryGoNext = () => historyIndex() >= 0
|
||||||
|
|
||||||
const canSend = () => {
|
const canSend = () => {
|
||||||
if (props.disabled) return false
|
if (props.disabled) return false
|
||||||
const hasText = prompt().trim().length > 0
|
const hasText = prompt().trim().length > 0
|
||||||
if (mode() === "shell") return hasText
|
if (mode() === "shell") return hasText
|
||||||
return hasText || attachments().length > 0
|
return hasText || attachments().length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const shellHint = () => (mode() === "shell" ? { key: "Esc", text: "to exit shell mode" } : { key: "!", text: "Shell mode" })
|
const shellHint = () => (mode() === "shell" ? { key: "Esc", text: "to exit shell mode" } : { key: "!", text: "Shell mode" })
|
||||||
const commandHint = () => ({ key: "/", text: "Commands" })
|
const commandHint = () => ({ key: "/", text: "Commands" })
|
||||||
|
|
||||||
@@ -1199,7 +1247,7 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div
|
<div
|
||||||
class="prompt-input-field-container"
|
class="prompt-input-field-container"
|
||||||
style={{
|
style={{
|
||||||
"height": expandedHeight(),
|
"height": expandedHeight(),
|
||||||
@@ -1208,100 +1256,96 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
>
|
>
|
||||||
<div class="prompt-input-field">
|
<div class="prompt-input-field">
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
class={`prompt-input ${mode() === "shell" ? "shell-mode" : ""}`}
|
class={`prompt-input ${mode() === "shell" ? "shell-mode" : ""}`}
|
||||||
placeholder={
|
placeholder={getPlaceholder()}
|
||||||
mode() === "shell"
|
value={prompt()}
|
||||||
? "Run a shell command (Esc to exit)..."
|
onInput={handleInput}
|
||||||
: "Type your message, @file, @agent, or paste images and text..."
|
onKeyDown={handleKeyDown}
|
||||||
}
|
onPaste={handlePaste}
|
||||||
value={prompt()}
|
onFocus={() => setIsFocused(true)}
|
||||||
onInput={handleInput}
|
onBlur={() => setIsFocused(false)}
|
||||||
onKeyDown={handleKeyDown}
|
disabled={props.disabled}
|
||||||
onPaste={handlePaste}
|
rows={4}
|
||||||
onFocus={() => setIsFocused(true)}
|
style={{
|
||||||
onBlur={() => setIsFocused(false)}
|
"padding-top": attachments().length > 0 ? "8px" : "0",
|
||||||
disabled={props.disabled}
|
"overflow-y": expandState() !== "normal" ? "auto" : "visible",
|
||||||
rows={4}
|
}}
|
||||||
style={{
|
spellcheck={false}
|
||||||
"padding-top": attachments().length > 0 ? "8px" : "0",
|
autocorrect="off"
|
||||||
"overflow-y": expandState() !== "normal" ? "auto" : "visible",
|
autoCapitalize="off"
|
||||||
}}
|
autocomplete="off"
|
||||||
spellcheck={false}
|
|
||||||
autocorrect="off"
|
|
||||||
autoCapitalize="off"
|
|
||||||
autocomplete="off"
|
|
||||||
/>
|
|
||||||
<Show when={hasHistory()}>
|
|
||||||
<div class="prompt-history-top">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="prompt-history-button"
|
|
||||||
onClick={() => selectPreviousHistory(true)}
|
|
||||||
disabled={!canHistoryGoPrevious()}
|
|
||||||
aria-label="Previous prompt"
|
|
||||||
>
|
|
||||||
<ArrowBigUp class="h-5 w-5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="prompt-history-bottom">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="prompt-history-button"
|
|
||||||
onClick={() => selectNextHistory(true)}
|
|
||||||
disabled={!canHistoryGoNext()}
|
|
||||||
aria-label="Next prompt"
|
|
||||||
>
|
|
||||||
<ArrowBigDown class="h-5 w-5" aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="prompt-expand-top">
|
|
||||||
<ExpandButton
|
|
||||||
expandState={expandState}
|
|
||||||
onToggleExpand={handleExpandToggle}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<Show when={hasHistory()}>
|
||||||
<Show when={shouldShowOverlay()}>
|
<div class="prompt-history-top">
|
||||||
<div class={`prompt-input-overlay ${mode() === "shell" ? "shell-mode" : ""}`}>
|
<button
|
||||||
<Show
|
type="button"
|
||||||
when={props.escapeInDebounce}
|
class="prompt-history-button"
|
||||||
fallback={
|
onClick={() => selectPreviousHistory(true)}
|
||||||
<>
|
disabled={!canHistoryGoPrevious()}
|
||||||
<span class="prompt-overlay-text">
|
aria-label="Previous prompt"
|
||||||
<Kbd>Enter</Kbd> New line • <Kbd shortcut="cmd+enter" /> Send • <Kbd>@</Kbd> Files/agents • <Kbd>↑↓</Kbd> History
|
>
|
||||||
</span>
|
<ArrowBigUp class="h-5 w-5" aria-hidden="true" />
|
||||||
<Show when={attachments().length > 0}>
|
</button>
|
||||||
<span class="prompt-overlay-text prompt-overlay-muted">• {attachments().length} file(s) attached</span>
|
</div>
|
||||||
</Show>
|
<div class="prompt-history-bottom">
|
||||||
<span class="prompt-overlay-text">
|
<button
|
||||||
• <Kbd>{shellHint().key}</Kbd> {shellHint().text}
|
type="button"
|
||||||
</span>
|
class="prompt-history-button"
|
||||||
<Show when={mode() !== "shell"}>
|
onClick={() => selectNextHistory(true)}
|
||||||
|
disabled={!canHistoryGoNext()}
|
||||||
|
aria-label="Next prompt"
|
||||||
|
>
|
||||||
|
<ArrowBigDown class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
<div class="prompt-expand-top">
|
||||||
|
<ExpandButton
|
||||||
|
expandState={expandState}
|
||||||
|
onToggleExpand={handleExpandToggle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Show when={shouldShowOverlay()}>
|
||||||
|
<div class={`prompt-input-overlay ${mode() === "shell" ? "shell-mode" : ""}`}>
|
||||||
|
<Show
|
||||||
|
when={props.escapeInDebounce}
|
||||||
|
fallback={
|
||||||
|
<>
|
||||||
<span class="prompt-overlay-text">
|
<span class="prompt-overlay-text">
|
||||||
• <Kbd>{commandHint().key}</Kbd> {commandHint().text}
|
<Kbd>Enter</Kbd> New line • <Kbd shortcut="cmd+enter" /> Send • <Kbd>@</Kbd> Files/agents • <Kbd>↑↓</Kbd> History
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
<Show when={attachments().length > 0}>
|
||||||
|
<span class="prompt-overlay-text prompt-overlay-muted">• {attachments().length} file(s) attached</span>
|
||||||
|
</Show>
|
||||||
|
<span class="prompt-overlay-text">
|
||||||
|
• <Kbd>{shellHint().key}</Kbd> {shellHint().text}
|
||||||
|
</span>
|
||||||
|
<Show when={mode() !== "shell"}>
|
||||||
|
<span class="prompt-overlay-text">
|
||||||
|
• <Kbd>{commandHint().key}</Kbd> {commandHint().text}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={mode() === "shell"}>
|
||||||
|
<span class="prompt-overlay-shell-active">Shell mode active</span>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<span class="prompt-overlay-text prompt-overlay-warning">
|
||||||
|
Press <Kbd>Esc</Kbd> again to abort session
|
||||||
|
</span>
|
||||||
<Show when={mode() === "shell"}>
|
<Show when={mode() === "shell"}>
|
||||||
<span class="prompt-overlay-shell-active">Shell mode active</span>
|
<span class="prompt-overlay-shell-active">Shell mode active</span>
|
||||||
</Show>
|
</Show>
|
||||||
</>
|
</>
|
||||||
}
|
</Show>
|
||||||
>
|
</div>
|
||||||
<>
|
</Show>
|
||||||
<span class="prompt-overlay-text prompt-overlay-warning">
|
</div>
|
||||||
Press <Kbd>Esc</Kbd> again to abort session
|
|
||||||
</span>
|
|
||||||
<Show when={mode() === "shell"}>
|
|
||||||
<span class="prompt-overlay-shell-active">Shell mode active</span>
|
|
||||||
</Show>
|
|
||||||
</>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="prompt-input-actions">
|
<div class="prompt-input-actions">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -163,11 +163,17 @@
|
|||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-expand-button:hover::after {
|
/* Only show tooltip on hover for Electron (desktop-mode) */
|
||||||
|
.prompt-expand-button.desktop-mode:hover::after {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Web/Mobile: No tooltip (data-tooltip will be undefined anyway) */
|
||||||
|
.prompt-expand-button.web-mode::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.prompt-overlay-text {
|
.prompt-overlay-text {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user