Add task specifications for Phase 5 advanced input features
- 015-keyboard-shortcuts.md (completed) - 020-command-palette.md (completed) - 021-file-attachments.md (next: @mentions, drag-drop, chips) - 022-long-paste-handling.md (summarize long pastes) - 023-symbol-attachments.md (LSP integration) - 024-agent-attachments.md (agent context) - 025-image-clipboard-support.md (image handling)
This commit is contained in:
849
tasks/todo/015-keyboard-shortcuts.md
Normal file
849
tasks/todo/015-keyboard-shortcuts.md
Normal file
@@ -0,0 +1,849 @@
|
|||||||
|
# Task 015: Keyboard Shortcuts
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement comprehensive keyboard shortcuts for efficient keyboard-first navigation, inspired by the TUI's keyboard system but adapted for desktop multi-instance/multi-session workflow.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- ✅ 001-013 completed
|
||||||
|
- ✅ All core UI components built
|
||||||
|
- ✅ Message stream, prompt input, tabs working
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
1. **Tab Navigation**: Use `Cmd/Ctrl+[/]` for instances, `Cmd/Ctrl+Shift+[/]` for sessions
|
||||||
|
2. **Clear Input**: Use `Cmd/Ctrl+K` (common in Slack, Discord, VS Code)
|
||||||
|
3. **Escape Behavior**: Context-dependent (blur when idle, interrupt when busy)
|
||||||
|
4. **Message History**: Per-instance, stored in IndexedDB (embedded local database)
|
||||||
|
5. **Agent Cycling**: Include Tab/Shift+Tab for agent cycling, add model selector focus shortcut
|
||||||
|
6. **Leader Key**: Skip it - use standard Cmd/Ctrl patterns
|
||||||
|
7. **Platform**: Cmd on macOS, Ctrl elsewhere (standard cross-platform pattern)
|
||||||
|
8. **View Controls**: Not needed for MVP
|
||||||
|
9. **Help Dialog**: Not needed - inline hints instead
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
### Smart Inline Hints
|
||||||
|
|
||||||
|
Instead of a help dialog, show shortcuts contextually:
|
||||||
|
|
||||||
|
- Display hints next to actions they affect
|
||||||
|
- Keep hints subtle (small text, muted color)
|
||||||
|
- Use platform-specific symbols (⌘ on Mac, Ctrl elsewhere)
|
||||||
|
- Examples already in app: "Enter to send • Shift+Enter for new line"
|
||||||
|
|
||||||
|
### Modular Architecture
|
||||||
|
|
||||||
|
Build shortcuts in a centralized, configurable system:
|
||||||
|
|
||||||
|
- Single source of truth for all shortcuts
|
||||||
|
- Easy to extend for future customization
|
||||||
|
- Clear separation between shortcut definition and handler logic
|
||||||
|
- Registry pattern for discoverability
|
||||||
|
|
||||||
|
## Shortcuts to Implement
|
||||||
|
|
||||||
|
### Navigation (Tabs)
|
||||||
|
|
||||||
|
**Already Implemented:**
|
||||||
|
|
||||||
|
- [x] `Cmd/Ctrl+1-9` - Switch to instance tab by index
|
||||||
|
- [x] `Cmd/Ctrl+N` - New instance (select folder)
|
||||||
|
- [x] `Cmd/Ctrl+T` - New session in active instance
|
||||||
|
- [x] `Cmd/Ctrl+W` - Close active **parent** session (only)
|
||||||
|
|
||||||
|
**To Implement:**
|
||||||
|
|
||||||
|
- [ ] `Cmd/Ctrl+[` - Previous instance tab
|
||||||
|
- [ ] `Cmd/Ctrl+]` - Next instance tab
|
||||||
|
- [ ] `Cmd/Ctrl+Shift+[` - Previous session tab
|
||||||
|
- [ ] `Cmd/Ctrl+Shift+]` - Next session tab
|
||||||
|
- [ ] `Cmd/Ctrl+Shift+L` - Switch to Logs tab
|
||||||
|
|
||||||
|
### Input Management
|
||||||
|
|
||||||
|
**Already Implemented:**
|
||||||
|
|
||||||
|
- [x] `Enter` - Send message
|
||||||
|
- [x] `Shift+Enter` - New line
|
||||||
|
|
||||||
|
**To Implement:**
|
||||||
|
|
||||||
|
- [ ] `Cmd/Ctrl+K` - Clear input
|
||||||
|
- [ ] `Cmd/Ctrl+L` - Focus prompt input
|
||||||
|
- [ ] `Up Arrow` - Previous message in history (when at start of input)
|
||||||
|
- [ ] `Down Arrow` - Next message in history (when in history mode)
|
||||||
|
- [ ] `Escape` - Context-dependent:
|
||||||
|
- When idle: Blur input / close modals
|
||||||
|
- When busy: Interrupt session (requires confirmation)
|
||||||
|
|
||||||
|
### Agent/Model Selection
|
||||||
|
|
||||||
|
**To Implement:**
|
||||||
|
|
||||||
|
- [ ] `Tab` - Cycle to next agent (when input empty or not focused)
|
||||||
|
- [ ] `Shift+Tab` - Cycle to previous agent
|
||||||
|
- [ ] `Cmd/Ctrl+M` - Focus model selector dropdown
|
||||||
|
|
||||||
|
### Message Navigation
|
||||||
|
|
||||||
|
**To Implement:**
|
||||||
|
|
||||||
|
- [ ] `PgUp` - Scroll messages up
|
||||||
|
- [ ] `PgDown` - Scroll messages down
|
||||||
|
- [ ] `Home` - Jump to first message
|
||||||
|
- [ ] `End` - Jump to last message
|
||||||
|
|
||||||
|
## Architecture Design
|
||||||
|
|
||||||
|
### 1. Centralized Keyboard Registry
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/keyboard-registry.ts
|
||||||
|
|
||||||
|
export interface KeyboardShortcut {
|
||||||
|
id: string
|
||||||
|
key: string
|
||||||
|
modifiers: {
|
||||||
|
ctrl?: boolean
|
||||||
|
meta?: boolean
|
||||||
|
shift?: boolean
|
||||||
|
alt?: boolean
|
||||||
|
}
|
||||||
|
handler: () => void
|
||||||
|
description: string
|
||||||
|
context?: "global" | "input" | "messages" // Where it works
|
||||||
|
condition?: () => boolean // Runtime condition check
|
||||||
|
}
|
||||||
|
|
||||||
|
class KeyboardRegistry {
|
||||||
|
private shortcuts = new Map<string, KeyboardShortcut>()
|
||||||
|
|
||||||
|
register(shortcut: KeyboardShortcut) {
|
||||||
|
this.shortcuts.set(shortcut.id, shortcut)
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister(id: string) {
|
||||||
|
this.shortcuts.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
findMatch(event: KeyboardEvent): KeyboardShortcut | null {
|
||||||
|
for (const shortcut of this.shortcuts.values()) {
|
||||||
|
if (this.matches(event, shortcut)) {
|
||||||
|
// Check context
|
||||||
|
if (shortcut.context === "input" && !this.isInputFocused()) continue
|
||||||
|
if (shortcut.context === "messages" && this.isInputFocused()) continue
|
||||||
|
|
||||||
|
// Check runtime condition
|
||||||
|
if (shortcut.condition && !shortcut.condition()) continue
|
||||||
|
|
||||||
|
return shortcut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private matches(event: KeyboardEvent, shortcut: KeyboardShortcut): boolean {
|
||||||
|
const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase()
|
||||||
|
const ctrlMatch = event.ctrlKey === !!shortcut.modifiers.ctrl
|
||||||
|
const metaMatch = event.metaKey === !!shortcut.modifiers.meta
|
||||||
|
const shiftMatch = event.shiftKey === !!shortcut.modifiers.shift
|
||||||
|
const altMatch = event.altKey === !!shortcut.modifiers.alt
|
||||||
|
|
||||||
|
return keyMatch && ctrlMatch && metaMatch && shiftMatch && altMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
private isInputFocused(): boolean {
|
||||||
|
const active = document.activeElement
|
||||||
|
return active?.tagName === "TEXTAREA" || active?.tagName === "INPUT" || active?.hasAttribute("contenteditable")
|
||||||
|
}
|
||||||
|
|
||||||
|
getByContext(context: string): KeyboardShortcut[] {
|
||||||
|
return Array.from(this.shortcuts.values()).filter((s) => !s.context || s.context === context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const keyboardRegistry = new KeyboardRegistry()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Cross-Platform Key Helper
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/keyboard-utils.ts
|
||||||
|
|
||||||
|
export const isMac = () => navigator.platform.includes("Mac")
|
||||||
|
|
||||||
|
export const modKey = (event?: KeyboardEvent) => {
|
||||||
|
if (!event) return isMac() ? "metaKey" : "ctrlKey"
|
||||||
|
return isMac() ? event.metaKey : event.ctrlKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modKeyPressed = (event: KeyboardEvent) => {
|
||||||
|
return isMac() ? event.metaKey : event.ctrlKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatShortcut = (shortcut: KeyboardShortcut): string => {
|
||||||
|
const parts: string[] = []
|
||||||
|
|
||||||
|
if (shortcut.modifiers.ctrl || shortcut.modifiers.meta) {
|
||||||
|
parts.push(isMac() ? "⌘" : "Ctrl")
|
||||||
|
}
|
||||||
|
if (shortcut.modifiers.shift) {
|
||||||
|
parts.push(isMac() ? "⇧" : "Shift")
|
||||||
|
}
|
||||||
|
if (shortcut.modifiers.alt) {
|
||||||
|
parts.push(isMac() ? "⌥" : "Alt")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts.push(shortcut.key.toUpperCase())
|
||||||
|
|
||||||
|
return parts.join(isMac() ? "" : "+")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. IndexedDB Storage Layer
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/db.ts
|
||||||
|
|
||||||
|
const DB_NAME = "opencode-client"
|
||||||
|
const DB_VERSION = 1
|
||||||
|
const HISTORY_STORE = "message-history"
|
||||||
|
|
||||||
|
let db: IDBDatabase | null = null
|
||||||
|
|
||||||
|
async function getDB(): Promise<IDBDatabase> {
|
||||||
|
if (db) return db
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
request.onsuccess = () => {
|
||||||
|
db = request.result
|
||||||
|
resolve(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.onupgradeneeded = (event) => {
|
||||||
|
const db = (event.target as IDBOpenDBRequest).result
|
||||||
|
|
||||||
|
// Create object stores
|
||||||
|
if (!db.objectStoreNames.contains(HISTORY_STORE)) {
|
||||||
|
db.createObjectStore(HISTORY_STORE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveHistory(instanceId: string, history: string[]): Promise<void> {
|
||||||
|
const database = await getDB()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = database.transaction(HISTORY_STORE, "readwrite")
|
||||||
|
const store = tx.objectStore(HISTORY_STORE)
|
||||||
|
const request = store.put(history, instanceId)
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
request.onsuccess = () => resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadHistory(instanceId: string): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const database = await getDB()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = database.transaction(HISTORY_STORE, "readonly")
|
||||||
|
const store = tx.objectStore(HISTORY_STORE)
|
||||||
|
const request = store.get(instanceId)
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
request.onsuccess = () => resolve(request.result || [])
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to load history from IndexedDB:", error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteHistory(instanceId: string): Promise<void> {
|
||||||
|
const database = await getDB()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = database.transaction(HISTORY_STORE, "readwrite")
|
||||||
|
const store = tx.objectStore(HISTORY_STORE)
|
||||||
|
const request = store.delete(instanceId)
|
||||||
|
|
||||||
|
request.onerror = () => reject(request.error)
|
||||||
|
request.onsuccess = () => resolve()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Message History Management
|
||||||
|
|
||||||
|
Per-instance storage using IndexedDB (persists across app restarts):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/stores/message-history.ts
|
||||||
|
|
||||||
|
import { saveHistory, loadHistory, deleteHistory } from "../lib/db"
|
||||||
|
|
||||||
|
const MAX_HISTORY = 100
|
||||||
|
|
||||||
|
// In-memory cache
|
||||||
|
const instanceHistories = new Map<string, string[]>()
|
||||||
|
const historyLoaded = new Set<string>()
|
||||||
|
|
||||||
|
export async function addToHistory(instanceId: string, text: string): Promise<void> {
|
||||||
|
// Ensure history is loaded
|
||||||
|
await ensureHistoryLoaded(instanceId)
|
||||||
|
|
||||||
|
const history = instanceHistories.get(instanceId) || []
|
||||||
|
|
||||||
|
// Add to front (newest first)
|
||||||
|
history.unshift(text)
|
||||||
|
|
||||||
|
// Limit to MAX_HISTORY
|
||||||
|
if (history.length > MAX_HISTORY) {
|
||||||
|
history.length = MAX_HISTORY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cache and persist
|
||||||
|
instanceHistories.set(instanceId, history)
|
||||||
|
|
||||||
|
// Persist to IndexedDB (async, don't wait)
|
||||||
|
saveHistory(instanceId, history).catch((err) => {
|
||||||
|
console.warn("Failed to persist message history:", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHistory(instanceId: string): Promise<string[]> {
|
||||||
|
await ensureHistoryLoaded(instanceId)
|
||||||
|
return instanceHistories.get(instanceId) || []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearHistory(instanceId: string): Promise<void> {
|
||||||
|
// Manually clear history (not called on instance stop)
|
||||||
|
instanceHistories.delete(instanceId)
|
||||||
|
historyLoaded.delete(instanceId)
|
||||||
|
await deleteHistory(instanceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureHistoryLoaded(instanceId: string): Promise<void> {
|
||||||
|
if (historyLoaded.has(instanceId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const history = await loadHistory(instanceId)
|
||||||
|
instanceHistories.set(instanceId, history)
|
||||||
|
historyLoaded.add(instanceId)
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to load history:", error)
|
||||||
|
instanceHistories.set(instanceId, [])
|
||||||
|
historyLoaded.add(instanceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Inline Hint Component
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/components/keyboard-hint.tsx
|
||||||
|
|
||||||
|
import { Component } from 'solid-js'
|
||||||
|
import { formatShortcut, type KeyboardShortcut } from '../lib/keyboard-utils'
|
||||||
|
|
||||||
|
const KeyboardHint: Component<{
|
||||||
|
shortcuts: KeyboardShortcut[]
|
||||||
|
separator?: string
|
||||||
|
}> = (props) => {
|
||||||
|
return (
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{props.shortcuts.map((shortcut, i) => (
|
||||||
|
<>
|
||||||
|
{i > 0 && <span class="mx-1">{props.separator || '•'}</span>}
|
||||||
|
<kbd class="font-mono">{formatShortcut(shortcut)}</kbd>
|
||||||
|
<span class="ml-1">{shortcut.description}</span>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyboardHint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### Step 1: Create Keyboard Infrastructure
|
||||||
|
|
||||||
|
1. Create `src/lib/keyboard-registry.ts` - Central registry
|
||||||
|
2. Create `src/lib/keyboard-utils.ts` - Platform helpers
|
||||||
|
3. Create `src/lib/db.ts` - IndexedDB storage layer
|
||||||
|
4. Create `src/stores/message-history.ts` - History management
|
||||||
|
5. Create `src/components/keyboard-hint.tsx` - Inline hints component
|
||||||
|
|
||||||
|
### Step 2: Register Navigation Shortcuts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/shortcuts/navigation.ts
|
||||||
|
|
||||||
|
import { keyboardRegistry } from "../keyboard-registry"
|
||||||
|
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
|
||||||
|
import { getSessions, activeSessionId, setActiveSession } from "../../stores/sessions"
|
||||||
|
|
||||||
|
export function registerNavigationShortcuts() {
|
||||||
|
// Instance navigation
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "instance-prev",
|
||||||
|
key: "[",
|
||||||
|
modifiers: { ctrl: true, meta: true },
|
||||||
|
handler: () => {
|
||||||
|
const ids = Array.from(instances().keys())
|
||||||
|
const current = ids.indexOf(activeInstanceId() || "")
|
||||||
|
const prev = current === 0 ? ids.length - 1 : current - 1
|
||||||
|
if (ids[prev]) setActiveInstanceId(ids[prev])
|
||||||
|
},
|
||||||
|
description: "previous instance",
|
||||||
|
context: "global",
|
||||||
|
})
|
||||||
|
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "instance-next",
|
||||||
|
key: "]",
|
||||||
|
modifiers: { ctrl: true, meta: true },
|
||||||
|
handler: () => {
|
||||||
|
const ids = Array.from(instances().keys())
|
||||||
|
const current = ids.indexOf(activeInstanceId() || "")
|
||||||
|
const next = (current + 1) % ids.length
|
||||||
|
if (ids[next]) setActiveInstanceId(ids[next])
|
||||||
|
},
|
||||||
|
description: "next instance",
|
||||||
|
context: "global",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Session navigation
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "session-prev",
|
||||||
|
key: "[",
|
||||||
|
modifiers: { ctrl: true, meta: true, shift: true },
|
||||||
|
handler: () => {
|
||||||
|
const instanceId = activeInstanceId()
|
||||||
|
if (!instanceId) return
|
||||||
|
|
||||||
|
const sessions = getSessions(instanceId)
|
||||||
|
const ids = sessions.map((s) => s.id).concat(["logs"])
|
||||||
|
const current = ids.indexOf(activeSessionId().get(instanceId) || "")
|
||||||
|
const prev = current === 0 ? ids.length - 1 : current - 1
|
||||||
|
if (ids[prev]) setActiveSession(instanceId, ids[prev])
|
||||||
|
},
|
||||||
|
description: "previous session",
|
||||||
|
context: "global",
|
||||||
|
})
|
||||||
|
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "session-next",
|
||||||
|
key: "]",
|
||||||
|
modifiers: { ctrl: true, meta: true, shift: true },
|
||||||
|
handler: () => {
|
||||||
|
const instanceId = activeInstanceId()
|
||||||
|
if (!instanceId) return
|
||||||
|
|
||||||
|
const sessions = getSessions(instanceId)
|
||||||
|
const ids = sessions.map((s) => s.id).concat(["logs"])
|
||||||
|
const current = ids.indexOf(activeSessionId().get(instanceId) || "")
|
||||||
|
const next = (current + 1) % ids.length
|
||||||
|
if (ids[next]) setActiveSession(instanceId, ids[next])
|
||||||
|
},
|
||||||
|
description: "next session",
|
||||||
|
context: "global",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Logs tab
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "switch-to-logs",
|
||||||
|
key: "l",
|
||||||
|
modifiers: { ctrl: true, meta: true, shift: true },
|
||||||
|
handler: () => {
|
||||||
|
const instanceId = activeInstanceId()
|
||||||
|
if (instanceId) setActiveSession(instanceId, "logs")
|
||||||
|
},
|
||||||
|
description: "logs tab",
|
||||||
|
context: "global",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Register Input Shortcuts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/shortcuts/input.ts
|
||||||
|
|
||||||
|
export function registerInputShortcuts(clearInput: () => void, focusInput: () => void) {
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "clear-input",
|
||||||
|
key: "k",
|
||||||
|
modifiers: { ctrl: true, meta: true },
|
||||||
|
handler: clearInput,
|
||||||
|
description: "clear input",
|
||||||
|
context: "global",
|
||||||
|
})
|
||||||
|
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "focus-input",
|
||||||
|
key: "l",
|
||||||
|
modifiers: { ctrl: true, meta: true },
|
||||||
|
handler: focusInput,
|
||||||
|
description: "focus input",
|
||||||
|
context: "global",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Update PromptInput with History Navigation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/components/prompt-input.tsx
|
||||||
|
|
||||||
|
import { createSignal, onMount } from 'solid-js'
|
||||||
|
import { addToHistory, getHistory } from '../stores/message-history'
|
||||||
|
|
||||||
|
const PromptInput: Component<Props> = (props) => {
|
||||||
|
const [input, setInput] = createSignal('')
|
||||||
|
const [historyIndex, setHistoryIndex] = createSignal(-1)
|
||||||
|
const [history, setHistory] = createSignal<string[]>([])
|
||||||
|
|
||||||
|
let textareaRef: HTMLTextAreaElement | undefined
|
||||||
|
|
||||||
|
// Load history on mount
|
||||||
|
onMount(async () => {
|
||||||
|
const loaded = await getHistory(props.instanceId)
|
||||||
|
setHistory(loaded)
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
const textarea = textareaRef
|
||||||
|
if (!textarea) return
|
||||||
|
|
||||||
|
const atStart = textarea.selectionStart === 0
|
||||||
|
const currentHistory = history()
|
||||||
|
|
||||||
|
// Up arrow - navigate to older message
|
||||||
|
if (e.key === 'ArrowUp' && atStart && currentHistory.length > 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
const newIndex = Math.min(historyIndex() + 1, currentHistory.length - 1)
|
||||||
|
setHistoryIndex(newIndex)
|
||||||
|
setInput(currentHistory[newIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Down arrow - navigate to newer message
|
||||||
|
if (e.key === 'ArrowDown' && historyIndex() >= 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
const newIndex = historyIndex() - 1
|
||||||
|
if (newIndex >= 0) {
|
||||||
|
setHistoryIndex(newIndex)
|
||||||
|
setInput(currentHistory[newIndex])
|
||||||
|
} else {
|
||||||
|
setHistoryIndex(-1)
|
||||||
|
setInput('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSend() {
|
||||||
|
const text = input().trim()
|
||||||
|
if (!text) return
|
||||||
|
|
||||||
|
// Add to history (async, per instance)
|
||||||
|
await addToHistory(props.instanceId, text)
|
||||||
|
|
||||||
|
// Reload history for next navigation
|
||||||
|
const updated = await getHistory(props.instanceId)
|
||||||
|
setHistory(updated)
|
||||||
|
setHistoryIndex(-1)
|
||||||
|
|
||||||
|
await props.onSend(text)
|
||||||
|
setInput('')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="prompt-input">
|
||||||
|
<textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={input()}
|
||||||
|
onInput={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<KeyboardHint shortcuts={[
|
||||||
|
{ description: 'to send', key: 'Enter', modifiers: {} },
|
||||||
|
{ description: 'for new line', key: 'Enter', modifiers: { shift: true } }
|
||||||
|
]} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Agent Cycling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/shortcuts/agent.ts
|
||||||
|
|
||||||
|
export function registerAgentShortcuts(
|
||||||
|
cycleAgent: () => void,
|
||||||
|
cycleAgentReverse: () => void,
|
||||||
|
focusModelSelector: () => void,
|
||||||
|
) {
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "agent-next",
|
||||||
|
key: "Tab",
|
||||||
|
modifiers: {},
|
||||||
|
handler: cycleAgent,
|
||||||
|
description: "next agent",
|
||||||
|
context: "global",
|
||||||
|
condition: () => !isInputFocused(), // Only when not typing
|
||||||
|
})
|
||||||
|
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "agent-prev",
|
||||||
|
key: "Tab",
|
||||||
|
modifiers: { shift: true },
|
||||||
|
handler: cycleAgentReverse,
|
||||||
|
description: "previous agent",
|
||||||
|
context: "global",
|
||||||
|
condition: () => !isInputFocused(),
|
||||||
|
})
|
||||||
|
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "focus-model",
|
||||||
|
key: "m",
|
||||||
|
modifiers: { ctrl: true, meta: true },
|
||||||
|
handler: focusModelSelector,
|
||||||
|
description: "focus model",
|
||||||
|
context: "global",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Escape Key Context Handling
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/shortcuts/escape.ts
|
||||||
|
|
||||||
|
export function registerEscapeShortcut(
|
||||||
|
isSessionBusy: () => boolean,
|
||||||
|
interruptSession: () => void,
|
||||||
|
blurInput: () => void,
|
||||||
|
closeModal: () => void,
|
||||||
|
) {
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "escape",
|
||||||
|
key: "Escape",
|
||||||
|
modifiers: {},
|
||||||
|
handler: () => {
|
||||||
|
// Priority 1: Close modal if open
|
||||||
|
if (hasOpenModal()) {
|
||||||
|
closeModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Interrupt if session is busy
|
||||||
|
if (isSessionBusy()) {
|
||||||
|
interruptSession()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Blur input
|
||||||
|
blurInput()
|
||||||
|
},
|
||||||
|
description: "cancel/close",
|
||||||
|
context: "global",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Setup Global Listener in App
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/App.tsx
|
||||||
|
|
||||||
|
import { registerNavigationShortcuts } from "./lib/shortcuts/navigation"
|
||||||
|
import { registerInputShortcuts } from "./lib/shortcuts/input"
|
||||||
|
import { registerAgentShortcuts } from "./lib/shortcuts/agent"
|
||||||
|
import { registerEscapeShortcut } from "./lib/shortcuts/escape"
|
||||||
|
import { keyboardRegistry } from "./lib/keyboard-registry"
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Register all shortcuts
|
||||||
|
registerNavigationShortcuts()
|
||||||
|
registerInputShortcuts(
|
||||||
|
() => setInput(""),
|
||||||
|
() => document.querySelector("textarea")?.focus(),
|
||||||
|
)
|
||||||
|
registerAgentShortcuts(handleCycleAgent, handleCycleAgentReverse, () =>
|
||||||
|
document.querySelector("[data-model-selector]")?.focus(),
|
||||||
|
)
|
||||||
|
registerEscapeShortcut(
|
||||||
|
() => activeInstance()?.status === "streaming",
|
||||||
|
handleInterrupt,
|
||||||
|
() => document.activeElement?.blur(),
|
||||||
|
hideModal,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Global keydown handler
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
const shortcut = keyboardRegistry.findMatch(e)
|
||||||
|
if (shortcut) {
|
||||||
|
e.preventDefault()
|
||||||
|
shortcut.handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handleKeyDown)
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
window.removeEventListener("keydown", handleKeyDown)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 8: Add Inline Hints Throughout UI
|
||||||
|
|
||||||
|
**In PromptInput:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<KeyboardHint
|
||||||
|
shortcuts={[getShortcut("enter-to-send"), getShortcut("shift-enter-newline"), getShortcut("cmd-k-clear")]}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
**In Instance Tabs:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<KeyboardHint shortcuts={[getShortcut("cmd-1-9"), getShortcut("cmd-brackets")]} />
|
||||||
|
```
|
||||||
|
|
||||||
|
**In Agent Selector:**
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<KeyboardHint shortcuts={[getShortcut("tab-cycle")]} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Where to Show Hints
|
||||||
|
|
||||||
|
1. **Prompt Input Area** (bottom)
|
||||||
|
- Enter/Shift+Enter (already shown)
|
||||||
|
- Add: Cmd+K to clear, ↑↓ for history
|
||||||
|
|
||||||
|
2. **Instance Tabs** (subtle tooltip or header)
|
||||||
|
- Cmd+1-9, Cmd+[/]
|
||||||
|
|
||||||
|
3. **Session Tabs** (same as instance)
|
||||||
|
- Cmd+Shift+[/]
|
||||||
|
|
||||||
|
4. **Agent/Model Selectors** (placeholder or label)
|
||||||
|
- Tab/Shift+Tab, Cmd+M
|
||||||
|
|
||||||
|
5. **Empty State** (when no messages)
|
||||||
|
- Common shortcuts overview
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
|
||||||
|
- [ ] Cmd/Ctrl+[ / ] cycles instance tabs
|
||||||
|
- [ ] Cmd/Ctrl+Shift+[ / ] cycles session tabs
|
||||||
|
- [ ] Cmd/Ctrl+1-9 jumps to instance
|
||||||
|
- [ ] Cmd/Ctrl+T creates new session
|
||||||
|
- [ ] Cmd/Ctrl+W closes parent session only
|
||||||
|
- [ ] Cmd/Ctrl+Shift+L switches to logs
|
||||||
|
|
||||||
|
### Input
|
||||||
|
|
||||||
|
- [ ] Cmd/Ctrl+K clears input
|
||||||
|
- [ ] Cmd/Ctrl+L focuses input
|
||||||
|
- [ ] Up arrow loads previous message (when at start)
|
||||||
|
- [ ] Down arrow navigates forward in history
|
||||||
|
- [ ] History is per-instance
|
||||||
|
- [ ] History persists in IndexedDB across app restarts
|
||||||
|
- [ ] History limited to 100 entries (not 50)
|
||||||
|
- [ ] History loads on component mount
|
||||||
|
- [ ] History NOT cleared when instance stops
|
||||||
|
|
||||||
|
### Agent/Model
|
||||||
|
|
||||||
|
- [ ] Tab cycles agents (when not in input)
|
||||||
|
- [ ] Shift+Tab cycles agents backward
|
||||||
|
- [ ] Cmd/Ctrl+M focuses model selector
|
||||||
|
|
||||||
|
### Context Behavior
|
||||||
|
|
||||||
|
- [ ] Escape closes modals first
|
||||||
|
- [ ] Escape interrupts when busy
|
||||||
|
- [ ] Escape blurs input when idle
|
||||||
|
- [ ] Shortcuts don't fire in wrong context
|
||||||
|
|
||||||
|
### Cross-Platform
|
||||||
|
|
||||||
|
- [ ] Works with Cmd on macOS
|
||||||
|
- [ ] Works with Ctrl on Windows
|
||||||
|
- [ ] Works with Ctrl on Linux
|
||||||
|
- [ ] Hints show correct keys per platform
|
||||||
|
|
||||||
|
### Inline Hints
|
||||||
|
|
||||||
|
- [ ] Hints visible but not intrusive
|
||||||
|
- [ ] Correct platform symbols shown
|
||||||
|
- [ ] Hints appear in relevant locations
|
||||||
|
- [ ] No excessive screen space used
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
**Requires:**
|
||||||
|
|
||||||
|
- Tasks 001-013 completed
|
||||||
|
|
||||||
|
**Blocks:**
|
||||||
|
|
||||||
|
- None (final MVP task)
|
||||||
|
|
||||||
|
## Estimated Time
|
||||||
|
|
||||||
|
4-5 hours
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
✅ Task complete when:
|
||||||
|
|
||||||
|
- All shortcuts implemented and working
|
||||||
|
- Message history per-instance, persisted in IndexedDB
|
||||||
|
- History stores 100 most recent prompts
|
||||||
|
- History persists across app restarts and instance stops
|
||||||
|
- Agent cycling with Tab/Shift+Tab
|
||||||
|
- Context-aware Escape behavior
|
||||||
|
- Inline hints shown throughout UI
|
||||||
|
- Cross-platform (Cmd/Ctrl) working
|
||||||
|
- Modular registry system for future customization
|
||||||
|
- Can navigate entire app efficiently with keyboard
|
||||||
|
|
||||||
|
## Notes on History Storage
|
||||||
|
|
||||||
|
**Why per-instance (folder path)?**
|
||||||
|
|
||||||
|
- User opens same project folder multiple times → same history
|
||||||
|
- More intuitive: history tied to project, not ephemeral instance
|
||||||
|
- Survives instance restarts without losing context
|
||||||
|
|
||||||
|
**Why 100 entries?**
|
||||||
|
|
||||||
|
- More generous than TUI's 50
|
||||||
|
- ~20KB per instance (100 × ~200 chars)
|
||||||
|
- Plenty for typical usage patterns
|
||||||
|
- Can increase later if needed
|
||||||
|
|
||||||
|
**Cleanup Strategy:**
|
||||||
|
|
||||||
|
- No automatic cleanup (history persists indefinitely)
|
||||||
|
- Could add manual "Clear History" option in future
|
||||||
|
- IndexedDB handles storage efficiently
|
||||||
217
tasks/todo/020-command-palette.md
Normal file
217
tasks/todo/020-command-palette.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
---
|
||||||
|
title: Command Palette
|
||||||
|
description: Implement VSCode-style command palette with Cmd+Shift+P
|
||||||
|
---
|
||||||
|
|
||||||
|
# Implement Command Palette
|
||||||
|
|
||||||
|
Build a VSCode-style command palette that opens as a centered modal dialog.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Visual Design
|
||||||
|
|
||||||
|
- **Trigger**: Keyboard shortcut `Cmd+Shift+P` (Mac) or `Ctrl+Shift+P` (Windows/Linux)
|
||||||
|
- **Appearance**: Modal dialog centered on screen with backdrop overlay
|
||||||
|
- **Size**: ~600px wide, auto height with max height
|
||||||
|
- **Components**:
|
||||||
|
- Search/filter input at top
|
||||||
|
- Scrollable list of commands below
|
||||||
|
- Each command shows: name, description, keyboard shortcut (if any)
|
||||||
|
|
||||||
|
### Behavior
|
||||||
|
|
||||||
|
- Opens on `Cmd+Shift+P`
|
||||||
|
- Closes on `Escape` or clicking outside
|
||||||
|
- Search input is auto-focused when opened
|
||||||
|
- Filter commands as user types (fuzzy search preferred)
|
||||||
|
- Arrow keys navigate through filtered list
|
||||||
|
- Enter executes selected command
|
||||||
|
- Mouse click on command also executes it
|
||||||
|
- Closes automatically after command execution
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands to Include
|
||||||
|
|
||||||
|
### Essential Commands (MVP)
|
||||||
|
|
||||||
|
1. **Initialize AGENTS.md** (`/init`)
|
||||||
|
- Description: "Create or update AGENTS.md file"
|
||||||
|
- Action: Call `client.session.init()`
|
||||||
|
|
||||||
|
2. **Compact Session** (`/compact`)
|
||||||
|
- Description: "Summarize and compact the current session"
|
||||||
|
- Action: Call `client.session.summarize()`
|
||||||
|
|
||||||
|
3. **Undo Last Message** (`/undo`)
|
||||||
|
- Description: "Revert the last message"
|
||||||
|
- Action: Call `client.session.revert()`
|
||||||
|
|
||||||
|
4. **Toggle Thinking Blocks** (`/thinking`)
|
||||||
|
- Description: "Show/hide AI thinking process"
|
||||||
|
- Action: Toggle UI state (placeholder for now)
|
||||||
|
|
||||||
|
5. **Show Help** (`/help`)
|
||||||
|
- Description: "Display keyboard shortcuts and help"
|
||||||
|
- Action: Open help modal (placeholder for now)
|
||||||
|
|
||||||
|
### Navigation Commands (Trigger Existing Shortcuts)
|
||||||
|
|
||||||
|
6. **New Session**
|
||||||
|
- Description: "Create a new session"
|
||||||
|
- Shortcut: `Cmd+Shift+N`
|
||||||
|
- Action: Trigger existing `new-session` keyboard shortcut
|
||||||
|
|
||||||
|
7. **Open Model Selector**
|
||||||
|
- Description: "Choose a different model"
|
||||||
|
- Shortcut: `Cmd+P`
|
||||||
|
- Action: Focus model selector input
|
||||||
|
|
||||||
|
8. **Open Agent Selector**
|
||||||
|
- Description: "Choose a different agent"
|
||||||
|
- Action: Click agent selector to open dropdown
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
components/
|
||||||
|
command-palette.tsx # Main command palette component
|
||||||
|
lib/
|
||||||
|
commands.ts # Command registry and definitions
|
||||||
|
stores/
|
||||||
|
command-palette.ts # State for showing/hiding palette
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Registry Structure
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Command {
|
||||||
|
id: string
|
||||||
|
label: string
|
||||||
|
description: string
|
||||||
|
keywords?: string[] // For fuzzy search
|
||||||
|
shortcut?: KeyboardShortcut
|
||||||
|
action: () => void | Promise<void>
|
||||||
|
category?: string // Group commands by category
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integration Points
|
||||||
|
|
||||||
|
1. **Register global keyboard shortcut** in App.tsx:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
keyboardRegistry.register({
|
||||||
|
id: "command-palette",
|
||||||
|
key: "p",
|
||||||
|
modifiers: { meta: true, shift: true },
|
||||||
|
handler: () => setShowCommandPalette(true),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Pass necessary props** to command palette:
|
||||||
|
- Current instance ID
|
||||||
|
- Current session ID
|
||||||
|
- SDK client reference
|
||||||
|
- Handler functions for UI actions
|
||||||
|
|
||||||
|
3. **Execute commands** based on type:
|
||||||
|
- API calls: Use SDK client
|
||||||
|
- UI actions: Call selector focus/click
|
||||||
|
- Shortcuts: Trigger registered keyboard shortcuts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI Component Details
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────┐
|
||||||
|
│ Command Palette │
|
||||||
|
├──────────────────────────────────────────────────────┤
|
||||||
|
│ 🔍 Type a command or search... │
|
||||||
|
├──────────────────────────────────────────────────────┤
|
||||||
|
│ › Initialize AGENTS.md │
|
||||||
|
│ Create or update AGENTS.md file │
|
||||||
|
│ │
|
||||||
|
│ Compact Session │
|
||||||
|
│ Summarize and compact the current session │
|
||||||
|
│ │
|
||||||
|
│ New Session ⌘⇧N │
|
||||||
|
│ Create a new session │
|
||||||
|
│ │
|
||||||
|
│ Open Model Selector ⌘P │
|
||||||
|
│ Choose a different model │
|
||||||
|
└──────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
|
||||||
|
- Use Kobalte Dialog for modal foundation
|
||||||
|
- Dark/light mode support matching app theme
|
||||||
|
- Highlight selected command with blue background
|
||||||
|
- Show keyboard shortcuts right-aligned in gray
|
||||||
|
- Smooth animations for open/close
|
||||||
|
|
||||||
|
### Keyboard Navigation
|
||||||
|
|
||||||
|
- `Cmd+Shift+P`: Open palette
|
||||||
|
- `Escape`: Close palette
|
||||||
|
- `ArrowUp`: Previous command
|
||||||
|
- `ArrowDown`: Next command
|
||||||
|
- `Enter`: Execute selected command
|
||||||
|
- Type to filter
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Palette opens with `Cmd+Shift+P`
|
||||||
|
- [ ] Search input is auto-focused
|
||||||
|
- [ ] All 8 commands are listed
|
||||||
|
- [ ] Typing filters commands (case-insensitive substring match)
|
||||||
|
- [ ] Arrow keys navigate through list
|
||||||
|
- [ ] Enter executes selected command
|
||||||
|
- [ ] Click executes command
|
||||||
|
- [ ] Escape or click outside closes palette
|
||||||
|
- [ ] Palette closes after command execution
|
||||||
|
- [ ] Keyboard shortcuts display correctly (⌘⇧N, ⌘P, etc.)
|
||||||
|
- [ ] Commands execute their intended actions:
|
||||||
|
- `/init` calls API
|
||||||
|
- `/compact` calls API
|
||||||
|
- `/undo` calls API
|
||||||
|
- New Session creates a session
|
||||||
|
- Model/Agent selectors open
|
||||||
|
- [ ] Works in both light and dark mode
|
||||||
|
- [ ] Smooth open/close animations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Future Enhancements (Post-MVP)
|
||||||
|
|
||||||
|
- Fuzzy search algorithm (not just substring)
|
||||||
|
- Command history (recently used commands first)
|
||||||
|
- Command categories/grouping
|
||||||
|
- Custom user-defined commands
|
||||||
|
- Command arguments/parameters
|
||||||
|
- Command aliases
|
||||||
|
- Search by keyboard shortcut
|
||||||
|
- Quick switch between sessions/instances
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This replaces the slash command (`/command`) approach
|
||||||
|
- Command palette is more discoverable and flexible
|
||||||
|
- Provides a foundation for adding more commands in the future
|
||||||
|
- Similar to VSCode Cmd+Shift+P, Sublime Text Cmd+Shift+P, etc.
|
||||||
40
tasks/todo/021-file-attachments.md
Normal file
40
tasks/todo/021-file-attachments.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: File Attachments
|
||||||
|
description: Add @mentions, drag & drop, and chips for files.
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement File Attachments
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Implement @ Mentions
|
||||||
|
|
||||||
|
When a user types `@` in the input field, display a file picker with search functionality.
|
||||||
|
|
||||||
|
Allow users to select files to attach to their prompt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Visual Attachment Chips
|
||||||
|
|
||||||
|
Display attached files as interactive chips above the input area.
|
||||||
|
|
||||||
|
Chips should include a filename and a removable "x" button.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Drag and Drop Files
|
||||||
|
|
||||||
|
Enable dragging files from the operating system directly onto the input area.
|
||||||
|
|
||||||
|
Automatically create an attachment chip for dropped files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Acceptance Criteria
|
||||||
|
|
||||||
|
- Typing `@` brings up a file selection autocomplete.
|
||||||
|
- Files can be selected and appear as chips.
|
||||||
|
- Users can drag and drop files onto the input, creating chips.
|
||||||
|
- Attached files are included in the prompt submission.
|
||||||
|
- Attachment chips can be removed by clicking "x".
|
||||||
29
tasks/todo/022-long-paste-handling.md
Normal file
29
tasks/todo/022-long-paste-handling.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
title: Long Paste Handling
|
||||||
|
description: Summarize large pasted text into attachments.
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement Long Paste Handling
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Detect Long Pastes
|
||||||
|
|
||||||
|
Monitor clipboard paste events for text content. Identify if the pasted text exceeds a defined length (e.g., >150 characters or >3 lines).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Create Summarized Attachments
|
||||||
|
|
||||||
|
If a paste is identified as "long", prevent direct insertion into the input field. Instead, create a new text attachment containing the full content.
|
||||||
|
|
||||||
|
Display a summarized chip for the attachment, such as `[pasted #1 10+ lines]`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Acceptance Criteria
|
||||||
|
|
||||||
|
- Pasting short text directly inserts it into the input.
|
||||||
|
- Pasting long text creates a summarized attachment chip.
|
||||||
|
- The full content of the long paste is retained within the attachment for submission.
|
||||||
|
- Multiple long pastes create distinct numbered chips.
|
||||||
37
tasks/todo/023-symbol-attachments.md
Normal file
37
tasks/todo/023-symbol-attachments.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: Symbol Attachments
|
||||||
|
description: Attach code symbols with LSP integration.
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement Symbol Attachments
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LSP Integration
|
||||||
|
|
||||||
|
Integrate with the Language Server Protocol (LSP) to get a list of symbols in the current project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### @ Symbol Autocomplete
|
||||||
|
|
||||||
|
When a user types `@` followed by a symbol-like pattern, trigger an autocomplete with relevant code symbols.
|
||||||
|
|
||||||
|
Include symbols from various file types supported by LSP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Attach and Navigate Symbols
|
||||||
|
|
||||||
|
Allow users to select a symbol from the autocomplete list to attach it to the prompt.
|
||||||
|
|
||||||
|
Display attached symbols as interactive chips. Optionally, implement functionality to jump to the symbol definition in an editor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Acceptance Criteria
|
||||||
|
|
||||||
|
- Typing `@` followed by a partial symbol name displays matching symbol suggestions.
|
||||||
|
- Selecting a symbol creates an attachment chip.
|
||||||
|
- Attached symbols are correctly formatted for submission.
|
||||||
|
- (Optional) Clicking a symbol chip navigates to its definition.
|
||||||
31
tasks/todo/024-agent-attachments.md
Normal file
31
tasks/todo/024-agent-attachments.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: Agent Attachments
|
||||||
|
description: Allow @agent mentions for multi-agent conversations.
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement Agent Attachments
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### @ Agent Autocomplete
|
||||||
|
|
||||||
|
When a user types `@` followed by an agent name, display an autocomplete list of available agents.
|
||||||
|
|
||||||
|
Filter agent suggestions as the user types.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Attach Agents
|
||||||
|
|
||||||
|
Enable users to select an agent from the autocomplete list to attach to their prompt.
|
||||||
|
|
||||||
|
Display attached agents as interactive chips.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Acceptance Criteria
|
||||||
|
|
||||||
|
- Typing `@` followed by a partial agent name displays matching agent suggestions.
|
||||||
|
- Selecting an agent creates an attachment chip.
|
||||||
|
- Attached agents are included in the prompt submission.
|
||||||
|
- Agent chips can be removed.
|
||||||
31
tasks/todo/025-image-clipboard-support.md
Normal file
31
tasks/todo/025-image-clipboard-support.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: Image Clipboard
|
||||||
|
description: Support pasting images from the clipboard.
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement Image Clipboard Support
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Detect Image Paste
|
||||||
|
|
||||||
|
Detect when image data is present in the system clipboard during a paste event.
|
||||||
|
|
||||||
|
Prioritize image data over text data if both are present.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Create Image Attachment
|
||||||
|
|
||||||
|
Automatically create an image attachment from the pasted image data. Convert the image to a base64 encoded format for internal handling and submission.
|
||||||
|
|
||||||
|
Display the image attachment as a chip in the input area.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Acceptance Criteria
|
||||||
|
|
||||||
|
- Pasting an image from the clipboard creates an image attachment chip.
|
||||||
|
- The image data is base64 encoded and associated with the attachment.
|
||||||
|
- The attachment chip has a suitable display name (e.g., `[Image #1]`).
|
||||||
|
- Users can clear the image attachment.
|
||||||
Reference in New Issue
Block a user