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:
dexhorthy
2025-06-17 11:44:06 -07:00
parent bab19aba22
commit 5f0dddf9cc
11 changed files with 193 additions and 151 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>
)
}
}

View File

@@ -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" />

View File

@@ -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 (

View File

@@ -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' ||

View File

@@ -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
View File

@@ -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.