mirror of
https://github.com/humanlayer/humanlayer.git
synced 2025-08-20 19:01:22 +03:00
Update manager instructions for settings sync and merge Phase 1 agent work
Manager updates: - Add settings management workflow for agent worktree monitoring - Check .claude/settings.local.json files every 10-15 minutes - Merge whitelisted dev commands into manager settings - Simple commands for checking specific agent worktrees Phase 1 agent delivered major enhancements: - Advanced command palette with fuzzy search - Template system and smart suggestions - Enhanced session polling and refresh handling - Multiple UI components for better UX - Extensive testing and validation Command palette now ready for production use\! 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,12 @@ const handleApproval = () => { ... }
|
||||
|
||||
## 🚫 CRITICAL RULES - BREAK THESE AND EVERYTHING FAILS
|
||||
|
||||
### Project specific rules
|
||||
|
||||
- `humanlayer-wui` - I am running the server with `npm run tauri dev` - you should never try to run the wui
|
||||
- `humanlayer-tui` - Do not try to run the tui, i will rebuild and run when you are ready for me to test it
|
||||
- `hld` - I am runnign this in the background, don't try to run it yourself
|
||||
|
||||
### NEVER CREATE NEW FILES (unless absolutely required)
|
||||
|
||||
- Think you need a new file? YOU DON'T
|
||||
|
||||
@@ -20,7 +20,8 @@ These scripts are designed to be reused for different management tasks by updati
|
||||
3. **CRITICAL**: ALWAYS COMMIT ANY CHANGES to scripts, Makefiles, or configuration files before running npx multiclaude launch. Worker worktrees will not see uncommitted changes from the manager worktree.
|
||||
4. launch each worker individually using: `npx multiclaude launch <branch_name> <plan_file>`
|
||||
5. **OBSERVE AND MERGE**: Once agents are launched, the agents will work autonomously. It is your job to adopt the merger persona (`hack/agent-merger.md`) and watch them working and merge their work in.
|
||||
6. You can use the `tmux` commands below to monitor the agents and see if they're stuck, send them messages, etc.
|
||||
6. **REGULAR CHECKINS**: Every 10-15 minutes, check agent worktrees for `.claude/settings.local.json` files and merge any whitelisted commands into your local `.claude/settings.local.json`
|
||||
7. You can use the `tmux` commands below to monitor the agents and see if they're stuck, send them messages, etc.
|
||||
|
||||
## LAUNCHING WORKERS
|
||||
|
||||
@@ -51,6 +52,19 @@ Each call adds a new window to the `${MULTICLAUDE_TMUX_SESSION}` or `${REPO_NAME
|
||||
|
||||
**Agents MUST commit every 5-10 minutes. No exceptions.**
|
||||
|
||||
## SETTINGS MANAGEMENT
|
||||
|
||||
**Check agent settings**: Every 10-15 minutes, check for `.claude/settings.local.json` in agent worktrees:
|
||||
|
||||
```bash
|
||||
# Check specific agent worktree for new settings
|
||||
cat /Users/dex/.humanlayer/worktrees/humanlayer_<branch>/.claude/settings.local.json
|
||||
|
||||
# Merge whitelisted commands into your ~/.claude/settings.local.json
|
||||
```
|
||||
|
||||
**Whitelist**: Only merge safe dev commands (make, npm, bun, python -m, etc.)
|
||||
|
||||
## PREVENT CONFLICTS
|
||||
|
||||
**Before parallel launch**: Ensure plans specify which files each agent MODIFIES vs CREATES
|
||||
|
||||
@@ -63,7 +63,6 @@ because you miss a lot of delicate logic which then causes you to add more bad c
|
||||
- `examples/` - Framework integrations (LangChain, CrewAI, OpenAI, etc.)
|
||||
- `docs/` - Documentation site
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
The `examples/` directory contains examples of using humanlayer with major AI frameworks:
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function CommandInput({
|
||||
placeholder="/path/to/directory or leave empty for current directory"
|
||||
maxResults={6}
|
||||
emptyMessage="Type a directory path..."
|
||||
renderItem={(item) => (
|
||||
renderItem={item => (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-blue-500">📁</span>
|
||||
<span className="font-mono">{item}</span>
|
||||
@@ -159,7 +159,9 @@ export default function CommandInput({
|
||||
<input
|
||||
type="number"
|
||||
value={config.maxTurns || ''}
|
||||
onChange={e => updateConfig({ maxTurns: e.target.value ? parseInt(e.target.value) : undefined })}
|
||||
onChange={e =>
|
||||
updateConfig({ maxTurns: e.target.value ? parseInt(e.target.value) : undefined })
|
||||
}
|
||||
placeholder="Default"
|
||||
min="1"
|
||||
max="100"
|
||||
|
||||
@@ -12,11 +12,10 @@ interface MenuOption {
|
||||
action: () => void
|
||||
}
|
||||
|
||||
|
||||
export default function CommandPaletteMenu() {
|
||||
const { createNewSession, openSessionById, selectedMenuIndex, setSelectedMenuIndex, mode } =
|
||||
useSessionLauncher()
|
||||
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
// Get sessions from the main app store
|
||||
@@ -34,28 +33,31 @@ export default function CommandPaletteMenu() {
|
||||
|
||||
// Command mode: Only Create Session
|
||||
// Search mode: All sessions (for fuzzy search) but limit display to 5
|
||||
const sessionOptions: MenuOption[] = mode === 'search'
|
||||
? sessions.map(session => ({
|
||||
id: `open-${session.id}`,
|
||||
label: `${session.query.slice(0, 40)}${session.query.length > 40 ? '...' : ''}`,
|
||||
description: `${session.status} • ${session.model || 'Unknown model'}`,
|
||||
action: () => openSessionById(session.id),
|
||||
}))
|
||||
: [] // No sessions in command mode
|
||||
const sessionOptions: MenuOption[] =
|
||||
mode === 'search'
|
||||
? sessions.map(session => ({
|
||||
id: `open-${session.id}`,
|
||||
label: `${session.query.slice(0, 40)}${session.query.length > 40 ? '...' : ''}`,
|
||||
description: `${session.status} • ${session.model || 'Unknown model'}`,
|
||||
action: () => openSessionById(session.id),
|
||||
}))
|
||||
: [] // No sessions in command mode
|
||||
|
||||
// Apply fuzzy search if in search mode and there's a query
|
||||
const filteredSessions = searchQuery && mode === 'search'
|
||||
? fuzzySearch(sessionOptions, searchQuery, {
|
||||
keys: ['label', 'description'],
|
||||
threshold: 0.1,
|
||||
includeMatches: true,
|
||||
})
|
||||
: sessionOptions.map(session => ({ item: session, matches: [], score: 1, indices: [] }))
|
||||
const filteredSessions =
|
||||
searchQuery && mode === 'search'
|
||||
? fuzzySearch(sessionOptions, searchQuery, {
|
||||
keys: ['label', 'description'],
|
||||
threshold: 0.1,
|
||||
includeMatches: true,
|
||||
})
|
||||
: sessionOptions.map(session => ({ item: session, matches: [], score: 1, indices: [] }))
|
||||
|
||||
// Combine options based on mode
|
||||
const menuOptions: MenuOption[] = mode === 'command'
|
||||
? baseOptions // Command: Only Create Session
|
||||
: filteredSessions.slice(0, 5).map(result => result.item) // Search: Only sessions (no Create Session), limit to 5
|
||||
const menuOptions: MenuOption[] =
|
||||
mode === 'command'
|
||||
? baseOptions // Command: Only Create Session
|
||||
: filteredSessions.slice(0, 5).map(result => result.item) // Search: Only sessions (no Create Session), limit to 5
|
||||
|
||||
// Keyboard navigation
|
||||
useHotkeys(
|
||||
@@ -101,7 +103,9 @@ export default function CommandPaletteMenu() {
|
||||
{segments.map((segment, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={segment.highlighted ? 'bg-yellow-200/80 dark:bg-yellow-900/60 font-medium' : ''}
|
||||
className={
|
||||
segment.highlighted ? 'bg-yellow-200/80 dark:bg-yellow-900/60 font-medium' : ''
|
||||
}
|
||||
>
|
||||
{segment.text}
|
||||
</span>
|
||||
@@ -154,9 +158,10 @@ export default function CommandPaletteMenu() {
|
||||
|
||||
{menuOptions.map((option, index) => {
|
||||
// Find the corresponding match data for highlighting
|
||||
const matchData = mode === 'search' && searchQuery
|
||||
? filteredSessions.find(result => result.item.id === option.id)
|
||||
: null
|
||||
const matchData =
|
||||
mode === 'search' && searchQuery
|
||||
? filteredSessions.find(result => result.item.id === option.id)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -165,7 +170,7 @@ export default function CommandPaletteMenu() {
|
||||
'p-2 rounded cursor-pointer transition-all duration-150',
|
||||
index === selectedMenuIndex
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted/30 hover:bg-muted/60'
|
||||
: 'bg-muted/30 hover:bg-muted/60',
|
||||
)}
|
||||
onClick={() => {
|
||||
setSelectedMenuIndex(index)
|
||||
@@ -174,16 +179,20 @@ export default function CommandPaletteMenu() {
|
||||
onMouseEnter={() => setSelectedMenuIndex(index)}
|
||||
>
|
||||
<div className="text-sm font-medium truncate">
|
||||
{matchData ? renderHighlightedText(option.label, matchData.matches, 'label') : option.label}
|
||||
{matchData
|
||||
? renderHighlightedText(option.label, matchData.matches, 'label')
|
||||
: option.label}
|
||||
</div>
|
||||
{option.description && (
|
||||
<div
|
||||
className={cn(
|
||||
'text-xs mt-0.5 truncate',
|
||||
index === selectedMenuIndex ? 'text-primary-foreground/70' : 'text-muted-foreground'
|
||||
index === selectedMenuIndex ? 'text-primary-foreground/70' : 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{matchData ? renderHighlightedText(option.description, matchData.matches, 'description') : option.description}
|
||||
{matchData
|
||||
? renderHighlightedText(option.description, matchData.matches, 'description')
|
||||
: option.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -194,7 +203,6 @@ export default function CommandPaletteMenu() {
|
||||
<div className="text-xs text-muted-foreground text-center py-4">No sessions found</div>
|
||||
)}
|
||||
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground pt-2 border-t border-border/30">
|
||||
<div className="flex items-center space-x-3">
|
||||
<span>↑↓ Navigate</span>
|
||||
|
||||
@@ -37,13 +37,13 @@ export default function FuzzySearchInput<T>({
|
||||
// Perform fuzzy search
|
||||
const searchResults = useMemo(() => {
|
||||
if (!value.trim()) return []
|
||||
|
||||
|
||||
const results = fuzzySearch(items, value, {
|
||||
keys: searchKeys,
|
||||
threshold: 0.1,
|
||||
includeMatches: true,
|
||||
})
|
||||
|
||||
|
||||
return results.slice(0, maxResults)
|
||||
}, [items, value, searchKeys, maxResults])
|
||||
|
||||
@@ -111,12 +111,12 @@ export default function FuzzySearchInput<T>({
|
||||
const defaultRenderItem = (item: T, matches: FuzzyMatch['matches']) => {
|
||||
const text = String(item)
|
||||
const match = matches[0]
|
||||
|
||||
|
||||
if (match && match.indices) {
|
||||
const segments = highlightMatches(text, match.indices)
|
||||
return defaultRenderHighlight(segments)
|
||||
}
|
||||
|
||||
|
||||
return <span>{text}</span>
|
||||
}
|
||||
|
||||
@@ -151,7 +151,7 @@ export default function FuzzySearchInput<T>({
|
||||
'placeholder:text-muted-foreground/60',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
'border-border hover:border-primary/50 focus:border-primary focus:ring-2 focus:ring-primary/20',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
autoComplete="off"
|
||||
/>
|
||||
@@ -159,11 +159,8 @@ export default function FuzzySearchInput<T>({
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
|
||||
<div className="fixed inset-0 z-10" onClick={() => setIsOpen(false)} />
|
||||
|
||||
{/* Results dropdown */}
|
||||
<div
|
||||
ref={listRef}
|
||||
@@ -178,20 +175,20 @@ export default function FuzzySearchInput<T>({
|
||||
'border-b border-border/50 last:border-b-0',
|
||||
index === selectedIndex
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'hover:bg-muted/50'
|
||||
: 'hover:bg-muted/50',
|
||||
)}
|
||||
onClick={() => handleItemClick(result.item, index)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
>
|
||||
{renderItem ? renderItem(result.item, result.matches) : defaultRenderItem(result.item, result.matches)}
|
||||
{renderItem
|
||||
? renderItem(result.item, result.matches)
|
||||
: defaultRenderItem(result.item, result.matches)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="px-3 py-2 text-sm text-muted-foreground text-center">
|
||||
{emptyMessage}
|
||||
</div>
|
||||
<div className="px-3 py-2 text-sm text-muted-foreground text-center">{emptyMessage}</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Footer with navigation hints */}
|
||||
{searchResults.length > 0 && (
|
||||
<div className="px-3 py-1 text-xs text-muted-foreground bg-muted/30 border-t border-border/50 flex items-center justify-between">
|
||||
@@ -204,4 +201,4 @@ export default function FuzzySearchInput<T>({
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,8 +88,8 @@ export function ThemeSelector() {
|
||||
index === selectedIndex
|
||||
? 'bg-accent/20 text-accent'
|
||||
: theme === themeOption.value
|
||||
? 'bg-accent/10 text-accent'
|
||||
: 'text-foreground hover:bg-accent/5'
|
||||
? 'bg-accent/10 text-accent'
|
||||
: 'text-foreground hover:bg-accent/5'
|
||||
}`}
|
||||
>
|
||||
<themeOption.icon className="w-3 h-3" />
|
||||
|
||||
@@ -2,19 +2,21 @@ import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Card = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-none border border-border py-6 font-mono',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
const Card = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-none border border-border py-6 font-mono',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,6 @@ import { create } from 'zustand'
|
||||
import { daemonClient } from '@/lib/daemon'
|
||||
import type { LaunchSessionRequest } from '@/lib/daemon/types'
|
||||
|
||||
|
||||
interface SessionConfig {
|
||||
query: string
|
||||
workingDir: string
|
||||
@@ -35,7 +34,6 @@ interface LauncherState {
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
|
||||
export const useSessionLauncher = create<LauncherState>((set, get) => ({
|
||||
isOpen: false,
|
||||
mode: 'command',
|
||||
@@ -66,11 +64,12 @@ export const useSessionLauncher = create<LauncherState>((set, get) => ({
|
||||
gPrefixMode: false,
|
||||
}),
|
||||
|
||||
setQuery: query => set(state => ({
|
||||
query,
|
||||
config: { ...state.config, query },
|
||||
error: undefined
|
||||
})),
|
||||
setQuery: query =>
|
||||
set(state => ({
|
||||
query,
|
||||
config: { ...state.config, query },
|
||||
error: undefined,
|
||||
})),
|
||||
|
||||
setConfig: config => set({ config, error: undefined }),
|
||||
|
||||
@@ -105,7 +104,7 @@ export const useSessionLauncher = create<LauncherState>((set, get) => ({
|
||||
|
||||
// Close launcher
|
||||
get().close()
|
||||
|
||||
|
||||
// Trigger a session refresh
|
||||
// Import loadSessions from App or dispatch a custom event
|
||||
window.dispatchEvent(new CustomEvent('session-created'))
|
||||
@@ -151,7 +150,7 @@ export function useSessionLauncherHotkeys() {
|
||||
const isTypingInInput = () => {
|
||||
const active = document.activeElement
|
||||
if (!active) return false
|
||||
|
||||
|
||||
// Only block hotkeys when actively typing in actual input fields
|
||||
return (
|
||||
active.tagName === 'INPUT' ||
|
||||
|
||||
@@ -39,15 +39,15 @@ function fuzzyMatchString(pattern: string, text: string): { score: number; indic
|
||||
if (pattern[patternIdx] === text[textIdx]) {
|
||||
indices.push(textIdx)
|
||||
consecutiveMatches++
|
||||
|
||||
|
||||
// Bonus for consecutive matches (like Superhuman)
|
||||
score += consecutiveMatches * 2
|
||||
|
||||
|
||||
// Bonus for start of word matches
|
||||
if (textIdx === 0 || text[textIdx - 1] === ' ' || text[textIdx - 1] === '/') {
|
||||
score += 5
|
||||
}
|
||||
|
||||
|
||||
patternIdx++
|
||||
} else {
|
||||
consecutiveMatches = 0
|
||||
@@ -61,7 +61,7 @@ function fuzzyMatchString(pattern: string, text: string): { score: number; indic
|
||||
} else {
|
||||
// Bonus for shorter strings (exact matches rank higher)
|
||||
score += Math.max(0, 100 - text.length)
|
||||
|
||||
|
||||
// Bonus for match density
|
||||
score += (indices.length / text.length) * 50
|
||||
}
|
||||
@@ -75,13 +75,9 @@ function fuzzyMatchString(pattern: string, text: string): { score: number; indic
|
||||
export function fuzzySearch<T>(
|
||||
items: T[],
|
||||
pattern: string,
|
||||
options: FuzzySearchOptions = {}
|
||||
options: FuzzySearchOptions = {},
|
||||
): FuzzyMatch[] {
|
||||
const {
|
||||
keys = [],
|
||||
threshold = 0.1,
|
||||
minMatchCharLength = 1,
|
||||
} = options
|
||||
const { keys = [], threshold = 0.1, minMatchCharLength = 1 } = options
|
||||
|
||||
if (!pattern || pattern.length < minMatchCharLength) {
|
||||
return items.map(item => ({
|
||||
@@ -142,7 +138,10 @@ export function fuzzySearch<T>(
|
||||
/**
|
||||
* Highlight matched characters in text (for rendering)
|
||||
*/
|
||||
export function highlightMatches(text: string, indices: number[]): Array<{ text: string; highlighted: boolean }> {
|
||||
export function highlightMatches(
|
||||
text: string,
|
||||
indices: number[],
|
||||
): Array<{ text: string; highlighted: boolean }> {
|
||||
if (!indices.length) {
|
||||
return [{ text, highlighted: false }]
|
||||
}
|
||||
@@ -185,7 +184,7 @@ export function highlightMatches(text: string, indices: number[]): Array<{ text:
|
||||
export function fuzzySearchDirectories(directories: string[], pattern: string): FuzzyMatch[] {
|
||||
// Enhance pattern matching for directory paths
|
||||
const enhancedPattern = pattern.replace(/^~/, process.env.HOME || '')
|
||||
|
||||
|
||||
return fuzzySearch(directories, enhancedPattern, {
|
||||
threshold: 0.1,
|
||||
minMatchCharLength: 1,
|
||||
@@ -194,4 +193,4 @@ export function fuzzySearchDirectories(directories: string[], pattern: string):
|
||||
// Boost score for exact directory name matches
|
||||
score: match.item.endsWith('/' + pattern) ? match.score + 100 : match.score,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
144
prompt.md
144
prompt.md
@@ -18,8 +18,8 @@ Build the minimal viable command palette launcher - a full-screen overlay trigge
|
||||
|
||||
```typescript
|
||||
interface SessionLauncherProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// Features:
|
||||
@@ -34,10 +34,10 @@ interface SessionLauncherProps {
|
||||
|
||||
```typescript
|
||||
interface CommandInputProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
onSubmit: () => void
|
||||
placeholder?: string
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
// Features:
|
||||
@@ -52,20 +52,20 @@ interface CommandInputProps {
|
||||
|
||||
```typescript
|
||||
interface LauncherState {
|
||||
isOpen: boolean
|
||||
mode: 'command' | 'search' // command = launch sessions, search = find sessions/approvals
|
||||
query: string
|
||||
isLaunching: boolean
|
||||
error?: string
|
||||
gPrefixMode: boolean
|
||||
|
||||
isOpen: boolean;
|
||||
mode: "command" | "search"; // command = launch sessions, search = find sessions/approvals
|
||||
query: string;
|
||||
isLaunching: boolean;
|
||||
error?: string;
|
||||
gPrefixMode: boolean;
|
||||
|
||||
// Actions
|
||||
open: (mode?: 'command' | 'search') => void
|
||||
close: () => void
|
||||
setQuery: (query: string) => void
|
||||
setGPrefixMode: (enabled: boolean) => void
|
||||
launchSession: () => Promise<void>
|
||||
reset: () => void
|
||||
open: (mode?: "command" | "search") => void;
|
||||
close: () => void;
|
||||
setQuery: (query: string) => void;
|
||||
setGPrefixMode: (enabled: boolean) => void;
|
||||
launchSession: () => Promise<void>;
|
||||
reset: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -73,8 +73,8 @@ interface LauncherState {
|
||||
|
||||
```typescript
|
||||
interface ParsedQuery {
|
||||
query: string // Main text
|
||||
workingDir?: string // If query starts with /path
|
||||
query: string; // Main text
|
||||
workingDir?: string; // If query starts with /path
|
||||
}
|
||||
|
||||
// Parse patterns:
|
||||
@@ -110,6 +110,7 @@ export function SessionLauncher({ isOpen, onClose }: SessionLauncherProps) {
|
||||
```
|
||||
|
||||
**Styling Requirements**:
|
||||
|
||||
- Full viewport overlay with backdrop-blur
|
||||
- Centered modal (max-width: 600px)
|
||||
- Monospace font family
|
||||
@@ -130,6 +131,7 @@ export function CommandInput({ value, onChange, onSubmit }: CommandInputProps) {
|
||||
```
|
||||
|
||||
**Features**:
|
||||
|
||||
- Large text input (48px height minimum)
|
||||
- Monospace font
|
||||
- Placeholder text with examples
|
||||
@@ -143,59 +145,64 @@ export function CommandInput({ value, onChange, onSubmit }: CommandInputProps) {
|
||||
// useSessionLauncher.ts
|
||||
export const useSessionLauncher = create<LauncherState>((set, get) => ({
|
||||
isOpen: false,
|
||||
query: '',
|
||||
query: "",
|
||||
isLaunching: false,
|
||||
|
||||
|
||||
open: () => set({ isOpen: true }),
|
||||
close: () => set({ isOpen: false, query: '', error: undefined }),
|
||||
close: () => set({ isOpen: false, query: "", error: undefined }),
|
||||
setQuery: (query) => set({ query }),
|
||||
|
||||
|
||||
launchSession: async () => {
|
||||
// Basic session launch logic
|
||||
// Error handling
|
||||
// Success navigation
|
||||
}
|
||||
}))
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
### Step 4: Global Hotkey Integration (45 minutes)
|
||||
|
||||
```typescript
|
||||
// Add to App.tsx or main component
|
||||
// Add to App.tsx or main component
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Cmd+K - Global command palette
|
||||
if (e.metaKey && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
openLauncher('command')
|
||||
if (e.metaKey && e.key === "k") {
|
||||
e.preventDefault();
|
||||
openLauncher("command");
|
||||
}
|
||||
|
||||
|
||||
// / - Search sessions and approvals
|
||||
if (e.key === '/' && !e.metaKey && !e.ctrlKey && !isInputFocused()) {
|
||||
e.preventDefault()
|
||||
openLauncher('search')
|
||||
if (e.key === "/" && !e.metaKey && !e.ctrlKey && !isInputFocused()) {
|
||||
e.preventDefault();
|
||||
openLauncher("search");
|
||||
}
|
||||
|
||||
|
||||
// G prefix navigation (prepare for Phase 2)
|
||||
if (e.key === 'g' && !e.metaKey && !e.ctrlKey && !isInputFocused()) {
|
||||
e.preventDefault()
|
||||
setGPrefixMode(true)
|
||||
setTimeout(() => setGPrefixMode(false), 2000)
|
||||
if (e.key === "g" && !e.metaKey && !e.ctrlKey && !isInputFocused()) {
|
||||
e.preventDefault();
|
||||
setGPrefixMode(true);
|
||||
setTimeout(() => setGPrefixMode(false), 2000);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [])
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
// Helper to check if user is typing in an input
|
||||
const isInputFocused = () => {
|
||||
const active = document.activeElement
|
||||
return active?.tagName === 'INPUT' || active?.tagName === 'TEXTAREA' || active?.contentEditable === 'true'
|
||||
}
|
||||
const active = document.activeElement;
|
||||
return (
|
||||
active?.tagName === "INPUT" ||
|
||||
active?.tagName === "TEXTAREA" ||
|
||||
active?.contentEditable === "true"
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
**Enhanced Hotkey Features**:
|
||||
|
||||
- `Cmd+K` - Opens command palette in "command" mode (launch sessions)
|
||||
- `/` - Opens command palette in "search" mode (find sessions/approvals)
|
||||
- `g` prefix - Sets up for vim-style navigation (Phase 2: g+a = approvals, g+s = sessions)
|
||||
@@ -206,30 +213,31 @@ const isInputFocused = () => {
|
||||
```typescript
|
||||
const launchSession = async () => {
|
||||
try {
|
||||
set({ isLaunching: true, error: undefined })
|
||||
|
||||
const parsed = parseQuery(get().query)
|
||||
set({ isLaunching: true, error: undefined });
|
||||
|
||||
const parsed = parseQuery(get().query);
|
||||
const response = await daemonClient.launchSession({
|
||||
query: parsed.query,
|
||||
working_dir: parsed.workingDir || process.cwd()
|
||||
})
|
||||
|
||||
working_dir: parsed.workingDir || process.cwd(),
|
||||
});
|
||||
|
||||
// Navigate to new session
|
||||
navigate(`/session/${response.session_id}`)
|
||||
|
||||
navigate(`/session/${response.session_id}`);
|
||||
|
||||
// Close launcher
|
||||
get().close()
|
||||
get().close();
|
||||
} catch (error) {
|
||||
set({ error: error.message })
|
||||
set({ error: error.message });
|
||||
} finally {
|
||||
set({ isLaunching: false })
|
||||
set({ isLaunching: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## UI Design Requirements
|
||||
|
||||
### Visual Style
|
||||
|
||||
- **Background**: Full-screen overlay with backdrop-blur-sm
|
||||
- **Modal**: Centered, rounded corners, dark background
|
||||
- **Typography**: Monospace font (ui-monospace, Monaco, "Cascadia Code")
|
||||
@@ -237,6 +245,7 @@ const launchSession = async () => {
|
||||
- **Spacing**: Generous padding and margins for breathing room
|
||||
|
||||
### Layout Structure
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ [OVERLAY] │
|
||||
@@ -253,6 +262,7 @@ const launchSession = async () => {
|
||||
```
|
||||
|
||||
### Interaction States
|
||||
|
||||
- **Default**: Clean input with placeholder
|
||||
- **Typing**: Real-time character count
|
||||
- **Loading**: Spinner + "Launching..." text
|
||||
@@ -262,16 +272,17 @@ const launchSession = async () => {
|
||||
## Integration Points
|
||||
|
||||
### App.tsx Integration
|
||||
|
||||
```typescript
|
||||
function App() {
|
||||
const { isOpen, close } = useSessionLauncher()
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Existing app content */}
|
||||
<SessionTable />
|
||||
<SessionDetail />
|
||||
|
||||
|
||||
{/* Command palette overlay */}
|
||||
<SessionLauncher isOpen={isOpen} onClose={close} />
|
||||
</>
|
||||
@@ -280,9 +291,11 @@ function App() {
|
||||
```
|
||||
|
||||
### SessionTable Button
|
||||
|
||||
Add floating action button or header button:
|
||||
|
||||
```typescript
|
||||
<Button
|
||||
<Button
|
||||
onClick={() => useSessionLauncher.getState().open()}
|
||||
className="fixed bottom-6 right-6"
|
||||
>
|
||||
@@ -293,6 +306,7 @@ Add floating action button or header button:
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- SessionLauncher renders correctly
|
||||
- CommandInput handles input changes
|
||||
- Hotkey triggers launcher open
|
||||
@@ -300,6 +314,7 @@ Add floating action button or header button:
|
||||
- Error states display properly
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- End-to-end session creation flow
|
||||
- Keyboard navigation works
|
||||
- Mobile responsiveness
|
||||
@@ -329,10 +344,11 @@ Add floating action button or header button:
|
||||
## Next Phase Preparation
|
||||
|
||||
This implementation should be designed to easily extend with:
|
||||
|
||||
- Template parsing (`:debug`, `:review`)
|
||||
- Model selection (`@claude-opus`)
|
||||
- Advanced flags (`--max-turns=5`)
|
||||
- Context detection and suggestions
|
||||
- Recent session history
|
||||
|
||||
Keep the architecture clean and extensible for Phase 2 enhancements.
|
||||
Keep the architecture clean and extensible for Phase 2 enhancements.
|
||||
|
||||
Reference in New Issue
Block a user