Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions desktop/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,10 +820,19 @@ func (a *App) ExecuteLinearTicketWithBoatmanMode(linearAPIKey, ticketID, project
return nil
}

// BoatmanModeConfig contains configuration for boatmanmode execution
type BoatmanModeConfig struct {
MaxIterations int `json:"maxIterations"`
BaseBranch string `json:"baseBranch"`
AutoPR bool `json:"autoPR"`
ReviewSkill string `json:"reviewSkill"`
Timeout int `json:"timeout"`
}

// StreamBoatmanModeExecution runs boatmanmode workflow with streaming output
// mode can be "ticket" or "prompt"
// This function returns immediately and runs the execution in the background
func (a *App) StreamBoatmanModeExecution(sessionID, input, mode, linearAPIKey, projectPath string) error {
func (a *App) StreamBoatmanModeExecution(sessionID, input, mode, linearAPIKey, projectPath string, config *BoatmanModeConfig) error {
// Get auth config using the same mechanism as regular sessions
prefs := a.config.GetPreferences()
claudeAPIKey := prefs.APIKey
Expand Down Expand Up @@ -908,8 +917,28 @@ func (a *App) StreamBoatmanModeExecution(sessionID, input, mode, linearAPIKey, p
}
}

// Use default config if not provided
if config == nil {
config = &BoatmanModeConfig{
MaxIterations: 3,
BaseBranch: "main",
AutoPR: true,
ReviewSkill: "peer-review",
Timeout: 60,
}
}

// Convert to bmintegration.Config
bmConfig := &bmintegration.Config{
MaxIterations: config.MaxIterations,
BaseBranch: config.BaseBranch,
AutoPR: config.AutoPR,
ReviewSkill: config.ReviewSkill,
Timeout: config.Timeout,
}

// Execute with streaming (use app context for Wails events)
_, err := bmIntegration.StreamExecution(a.ctx, sessionID, input, mode, outputChan, onMessage)
_, err := bmIntegration.StreamExecution(a.ctx, sessionID, input, mode, outputChan, onMessage, bmConfig)
close(outputChan)

if err != nil {
Expand Down
45 changes: 36 additions & 9 deletions desktop/boatmanmode/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,50 @@ type BoatmanEvent struct {
// MessageCallback is called for non-JSON output lines to route them as session messages
type MessageCallback func(role, content string)

// Config contains configuration for boatmanmode execution
type Config struct {
MaxIterations int
BaseBranch string
AutoPR bool
ReviewSkill string
Timeout int
}

// StreamExecution runs the workflow with live streaming output
// It parses structured JSON events for agent/task tracking and emits them via Wails runtime
// mode can be "ticket" or "prompt"
// onMessage, if non-nil, receives non-JSON output lines as messages for the session
func (i *Integration) StreamExecution(ctx context.Context, sessionID string, input string, mode string, outputChan chan<- string, onMessage MessageCallback) (map[string]interface{}, error) {
var cmd *exec.Cmd
func (i *Integration) StreamExecution(ctx context.Context, sessionID string, input string, mode string, outputChan chan<- string, onMessage MessageCallback, config *Config) (map[string]interface{}, error) {
// Build command arguments
args := []string{"work"}

if mode == "ticket" {
cmd = exec.CommandContext(ctx, i.boatmanmodePath,
"work", input,
)
args = append(args, input)
} else {
// prompt mode
cmd = exec.CommandContext(ctx, i.boatmanmodePath,
"work",
"--prompt", input,
)
args = append(args, "--prompt", input)
}

// Add configuration flags if provided
if config != nil {
if config.MaxIterations > 0 {
args = append(args, "--max-iterations", fmt.Sprintf("%d", config.MaxIterations))
}
if config.BaseBranch != "" {
args = append(args, "--base-branch", config.BaseBranch)
}
if !config.AutoPR {
args = append(args, "--auto-pr=false")
}
if config.ReviewSkill != "" {
args = append(args, "--review-skill", config.ReviewSkill)
}
if config.Timeout > 0 {
args = append(args, "--timeout", fmt.Sprintf("%d", config.Timeout))
}
}

cmd := exec.CommandContext(ctx, i.boatmanmodePath, args...)
cmd.Dir = i.repoPath

// Set environment variables for authentication
Expand Down
14 changes: 14 additions & 0 deletions desktop/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ type UserPreferences struct {

// Linear settings
LinearAPIKey string `json:"linearAPIKey,omitempty"`

// BoatmanMode settings
BoatmanMaxIterations int `json:"boatmanMaxIterations,omitempty"` // Maximum review/refactor iterations (default: 3)
BoatmanBaseBranch string `json:"boatmanBaseBranch,omitempty"` // Base branch for worktree (default: "main")
BoatmanAutoPR bool `json:"boatmanAutoPR"` // Automatically create PR on success (default: true)
BoatmanReviewSkill string `json:"boatmanReviewSkill,omitempty"` // Claude skill for code review (default: "peer-review")
BoatmanTimeout int `json:"boatmanTimeout,omitempty"` // Timeout in minutes for each agent (default: 60)
}

// ProjectPreferences stores project-specific overrides
Expand Down Expand Up @@ -125,6 +132,13 @@ func NewConfig() (*Config, error) {
AutoCleanupSessions: true,
MaxAgentsPerSession: 20,
KeepCompletedAgents: false,

// BoatmanMode defaults
BoatmanMaxIterations: 3,
BoatmanBaseBranch: "main",
BoatmanAutoPR: true,
BoatmanReviewSkill: "peer-review",
BoatmanTimeout: 60,
},
projects: make(map[string]ProjectPreferences),
}
Expand Down
12 changes: 10 additions & 2 deletions desktop/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,15 +163,15 @@ function App() {
};

// Handle boatmanmode session creation
const handleStartBoatmanMode = async (input: string, mode: 'ticket' | 'prompt') => {
const handleStartBoatmanMode = async (input: string, mode: 'ticket' | 'prompt', config: any) => {
if (activeProject) {
// Get Linear API key from preferences (only needed for ticket mode)
const linearAPIKey = preferences?.linearAPIKey || '';
if (mode === 'ticket' && !linearAPIKey) {
setError('Please configure Linear API key in settings');
return;
}
await createBoatmanModeSession(activeProject.path, input, mode, linearAPIKey);
await createBoatmanModeSession(activeProject.path, input, mode, linearAPIKey, config);
setBoatmanModeDialogOpen(false);
} else {
setError('Please open a project first');
Expand Down Expand Up @@ -363,6 +363,13 @@ function App() {
onClose={() => setBoatmanModeDialogOpen(false)}
onStart={handleStartBoatmanMode}
projectPath={activeProject?.path || ''}
defaultConfig={{
maxIterations: preferences?.boatmanMaxIterations,
baseBranch: preferences?.boatmanBaseBranch,
autoPR: preferences?.boatmanAutoPR,
reviewSkill: preferences?.boatmanReviewSkill,
timeout: preferences?.boatmanTimeout,
}}
/>

{/* Task Detail Modal - look up latest task from store to get updated metadata */}
Expand Down Expand Up @@ -504,3 +511,4 @@ function App() {
}

export default App;
git rm <path-to-snippet-file>
15 changes: 15 additions & 0 deletions desktop/frontend/src/App.tsx (update defaultConfig prop)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{/* Boatman Mode Dialog */}
<BoatmanModeDialog
isOpen={boatmanModeDialogOpen}
onClose={() => setBoatmanModeDialogOpen(false)}
onStart={handleStartBoatmanMode}
projectPath={activeProject?.path || ''}
defaultConfig={{
maxIterations: preferences?.boatmanMaxIterations,
baseBranch: preferences?.boatmanBaseBranch,
autoPR: preferences?.boatmanAutoPR,
reviewSkill: preferences?.boatmanReviewSkill,
timeout: preferences?.boatmanTimeout,
dryRun: preferences?.boatmanDryRun,
}}
/>
140 changes: 136 additions & 4 deletions desktop/frontend/src/components/boatmanmode/BoatmanModeDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
import { useState } from 'react';
import { X, PlayCircle, AlertCircle, Ticket, MessageSquare } from 'lucide-react';
import { X, PlayCircle, AlertCircle, Ticket, MessageSquare, Settings, ChevronDown, ChevronUp } from 'lucide-react';

type ExecutionMode = 'ticket' | 'prompt';

export interface BoatmanModeConfig {
maxIterations: number;
baseBranch: string;
autoPR: boolean;
reviewSkill: string;
timeout: number;
}

interface BoatmanModeDialogProps {
isOpen: boolean;
onClose: () => void;
onStart: (input: string, mode: ExecutionMode) => void;
onStart: (input: string, mode: ExecutionMode, config: BoatmanModeConfig) => void;
projectPath: string;
defaultConfig?: Partial<BoatmanModeConfig>;
}

export function BoatmanModeDialog({ isOpen, onClose, onStart, projectPath }: BoatmanModeDialogProps) {
export function BoatmanModeDialog({ isOpen, onClose, onStart, projectPath, defaultConfig }: BoatmanModeDialogProps) {
const [mode, setMode] = useState<ExecutionMode>('prompt');
const [input, setInput] = useState('');
const [isStarting, setIsStarting] = useState(false);
const [showAdvanced, setShowAdvanced] = useState(false);

// Configuration state with defaults
const [config, setConfig] = useState<BoatmanModeConfig>({
maxIterations: defaultConfig?.maxIterations ?? 3,
baseBranch: defaultConfig?.baseBranch ?? 'main',
autoPR: defaultConfig?.autoPR ?? true,
reviewSkill: defaultConfig?.reviewSkill ?? 'peer-review',
timeout: defaultConfig?.timeout ?? 60,
});

if (!isOpen) return null;

Expand All @@ -22,7 +41,7 @@ export function BoatmanModeDialog({ isOpen, onClose, onStart, projectPath }: Boa

setIsStarting(true);
try {
await onStart(input.trim(), mode);
await onStart(input.trim(), mode, config);
onClose();
setInput('');
} catch (err) {
Expand Down Expand Up @@ -161,6 +180,119 @@ export function BoatmanModeDialog({ isOpen, onClose, onStart, projectPath }: Boa
</div>
</div>

{/* Advanced Configuration */}
<div className="border border-slate-700 rounded-lg">
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className="w-full flex items-center justify-between p-3 text-sm text-slate-300 hover:bg-slate-900/50 rounded-lg transition-colors"
>
<div className="flex items-center gap-2">
<Settings className="w-4 h-4" />
<span>Advanced Configuration</span>
</div>
{showAdvanced ? (
<ChevronUp className="w-4 h-4" />
) : (
<ChevronDown className="w-4 h-4" />
)}
</button>

{showAdvanced && (
<div className="p-4 space-y-4 border-t border-slate-700">
{/* Max Iterations */}
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Max Review Iterations
</label>
<input
type="number"
min="1"
max="10"
value={config.maxIterations}
onChange={(e) => setConfig({ ...config, maxIterations: parseInt(e.target.value) || 3 })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-slate-100 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
disabled={isStarting}
/>
<p className="text-xs text-slate-500 mt-1">
Maximum number of review/refactor iterations (default: 3)
</p>
</div>

{/* Base Branch */}
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Base Branch
</label>
<input
type="text"
value={config.baseBranch}
onChange={(e) => setConfig({ ...config, baseBranch: e.target.value })}
placeholder="main"
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
disabled={isStarting}
/>
<p className="text-xs text-slate-500 mt-1">
Base branch for git worktree (default: main)
</p>
</div>

{/* Review Skill */}
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Review Skill
</label>
<input
type="text"
value={config.reviewSkill}
onChange={(e) => setConfig({ ...config, reviewSkill: e.target.value })}
placeholder="peer-review"
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-slate-100 placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
disabled={isStarting}
/>
<p className="text-xs text-slate-500 mt-1">
Claude skill/agent to use for code review (default: peer-review)
</p>
</div>

{/* Timeout */}
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Agent Timeout (minutes)
</label>
<input
type="number"
min="5"
max="180"
value={config.timeout}
onChange={(e) => setConfig({ ...config, timeout: parseInt(e.target.value) || 60 })}
className="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded-md text-slate-100 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent"
disabled={isStarting}
/>
<p className="text-xs text-slate-500 mt-1">
Timeout in minutes for each Claude agent (default: 60)
</p>
</div>

{/* Auto PR */}
<label className="flex items-center justify-between p-3 rounded-lg border border-slate-700 cursor-pointer hover:bg-slate-900/50 transition-colors">
<div>
<p className="text-sm text-slate-100">Auto-create Pull Request</p>
<p className="text-xs text-slate-400">
Automatically create PR when execution succeeds
</p>
</div>
<input
type="checkbox"
checked={config.autoPR}
onChange={(e) => setConfig({ ...config, autoPR: e.target.checked })}
className="w-4 h-4 rounded"
disabled={isStarting}
/>
</label>
</div>
)}
</div>

{/* Warning */}
{!projectPath && (
<div className="flex items-start gap-3 p-4 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
Expand Down
Loading