Skip to content

Commit 3b0e412

Browse files
authored
Merge pull request #127 from sudocode-ai/acp
properly support copilot in ACP mode and default all manual executions to be persistent executions
2 parents 53c0e5e + 2a95a04 commit 3b0e412

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+425
-725
lines changed

frontend/src/components/chat-widget/ExecutionSelector.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ const statusConfig: Record<
2929
pending: { icon: Clock, color: 'text-yellow-500', label: 'Pending' },
3030
running: { icon: PlayCircle, color: 'text-green-500', label: 'Running' },
3131
paused: { icon: PauseCircle, color: 'text-orange-500', label: 'Paused' },
32-
waiting: { icon: Clock, color: 'text-blue-500', label: 'Waiting' },
3332
completed: { icon: CheckCircle2, color: 'text-blue-500', label: 'Completed' },
3433
failed: { icon: XCircle, color: 'text-red-500', label: 'Failed' },
3534
cancelled: { icon: XCircle, color: 'text-muted-foreground', label: 'Cancelled' },
@@ -69,7 +68,7 @@ export function ExecutionSelector({
6968
}: ExecutionSelectorProps) {
7069
// Group executions by status
7170
const grouped = useMemo(() => {
72-
const activeStatuses: ExecutionStatus[] = ['running', 'pending', 'preparing', 'paused', 'waiting']
71+
const activeStatuses: ExecutionStatus[] = ['running', 'pending', 'preparing', 'paused']
7372
const active = executions.filter((e) => activeStatuses.includes(e.status))
7473
const recent = executions
7574
.filter((e) => !activeStatuses.includes(e.status))

frontend/src/components/entities/IssueHoverContent.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ const executionStatusConfig: Record<ExecutionStatus, { label: string; color: str
4848
label: 'Paused',
4949
color: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-300',
5050
},
51-
waiting: {
52-
label: 'Waiting',
53-
color: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
54-
},
5551
completed: {
5652
label: 'Completed',
5753
color: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',

frontend/src/components/executions/AgentConfigPanel.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ export function AgentConfigPanel({
395395
const defaults: ExecutionConfig = {
396396
mode: 'worktree',
397397
cleanupMode: 'manual',
398+
sessionMode: 'persistent',
398399
}
399400

400401
// Try to load from localStorage

frontend/src/components/executions/AgentSettingsDialog.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -373,9 +373,8 @@ export function AgentSettingsDialog({
373373
</>
374374
)}
375375

376-
{/* Persistent Session Toggle (Claude Code only) */}
377-
{agentType === 'claude-code' && (
378-
<div className="space-y-4">
376+
{/* Persistent Session Toggle */}
377+
<div className="space-y-4">
379378
<div className="flex items-center justify-between rounded-lg border p-4">
380379
<div className="space-y-0.5">
381380
<Label htmlFor="persistent-session" className="text-base">
@@ -387,7 +386,7 @@ export function AgentSettingsDialog({
387386
</div>
388387
<Switch
389388
id="persistent-session"
390-
checked={config.sessionMode === 'persistent'}
389+
checked={config.sessionMode !== 'discrete'}
391390
onCheckedChange={(checked) =>
392391
onConfigChange({
393392
sessionMode: checked ? 'persistent' : 'discrete',
@@ -397,7 +396,7 @@ export function AgentSettingsDialog({
397396
</div>
398397

399398
{/* Additional options when persistent mode is enabled */}
400-
{config.sessionMode === 'persistent' && (
399+
{config.sessionMode !== 'discrete' && (
401400
<div className="ml-4 space-y-4 border-l-2 border-muted pl-4">
402401
<div className="flex items-center justify-between">
403402
<div className="space-y-0.5">
@@ -456,7 +455,6 @@ export function AgentSettingsDialog({
456455
</div>
457456
)}
458457
</div>
459-
)}
460458

461459
<div className="space-y-2">
462460
<Label htmlFor="timeout">Timeout (ms)</Label>

frontend/src/components/executions/ExecutionChainTile.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,6 @@ function renderStatusIcon(status: Execution['status']) {
6161
icon: <PauseCircle className="h-4 w-4 text-muted-foreground" />,
6262
label: 'Paused',
6363
}
64-
case 'waiting':
65-
return {
66-
icon: <Clock className="h-4 w-4 text-blue-600" />,
67-
label: 'Waiting',
68-
}
6964
case 'completed':
7065
return {
7166
icon: <CheckCircle2 className="h-4 w-4 text-green-600" />,

frontend/src/components/executions/ExecutionHistory.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,6 @@ const STATUS_CONFIG: Record<
3939
variant: 'outline',
4040
icon: <PauseCircle className="h-3 w-3" />,
4141
},
42-
waiting: {
43-
label: 'Waiting',
44-
variant: 'default',
45-
icon: <Clock className="h-3 w-3" />,
46-
},
4742
completed: {
4843
label: 'Completed',
4944
variant: 'default',

frontend/src/components/executions/ExecutionMonitor.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export function ExecutionMonitor({
205205
// Completed: completed, failed, cancelled, stopped
206206
const isActive = useMemo(() => {
207207
if (!executionProp) return true // Default to active if no execution prop
208-
const activeStatuses = ['preparing', 'pending', 'running', 'paused', 'waiting']
208+
const activeStatuses = ['preparing', 'pending', 'running', 'paused']
209209
return activeStatuses.includes(executionProp.status)
210210
}, [executionProp])
211211

@@ -246,12 +246,12 @@ export function ExecutionMonitor({
246246
// For waiting/paused executions with no WebSocket data, use logs
247247
// This handles page reload where the agent isn't actively streaming
248248
const isWaitingOrPaused =
249-
executionProp?.status === 'waiting' || executionProp?.status === 'paused'
249+
executionProp?.status === 'pending' || executionProp?.status === 'paused'
250250
if (isWaitingOrPaused && !hasWsData && hasLogsData) {
251251
return {
252252
connectionStatus: 'connected',
253253
execution: {
254-
status: (executionProp?.status || 'waiting') as ExecutionState['status'],
254+
status: (executionProp?.status || 'pending') as ExecutionState['status'],
255255
runId: executionId,
256256
error: logsResult.error?.message || null,
257257
startTime: null,
@@ -302,7 +302,7 @@ export function ExecutionMonitor({
302302

303303
// For PERSISTENT sessions with no WebSocket data yet but logs available,
304304
// show logs to prevent flash of empty content during transitions
305-
// (e.g., when transitioning from 'waiting' to 'running')
305+
// (e.g., when transitioning from 'pending' to 'running')
306306
// Non-persistent sessions skip this - they wait for WebSocket to avoid duplicate content
307307
if (isPersistentSession && !hasWsData && hasLogsData) {
308308
return {
@@ -478,9 +478,8 @@ export function ExecutionMonitor({
478478
if (!onCancel) return
479479

480480
const handleKeyDown = (event: KeyboardEvent) => {
481-
// Only cancel if execution is active (not completed/failed/cancelled)
482-
const activeStatuses = ['preparing', 'pending', 'running', 'paused', 'waiting']
483-
if (event.key === 'Escape' && activeStatuses.includes(execution.status)) {
481+
// Only cancel if execution is actively running
482+
if (event.key === 'Escape' && execution.status === 'running') {
484483
event.preventDefault()
485484
onCancel()
486485
}
@@ -672,7 +671,7 @@ export function ExecutionMonitor({
672671
)
673672
}
674673

675-
if (execution.status === 'waiting') {
674+
if (execution.status === 'pending') {
676675
return (
677676
<Badge variant="default" className="flex items-center gap-1 bg-blue-600">
678677
<Clock className="h-3 w-3" />

frontend/src/components/executions/ExecutionPreview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ export function ExecutionPreview({
169169
// Determine if execution is active or completed
170170
const isActive = useMemo(() => {
171171
if (!executionProp) return true
172-
const activeStatuses = ['preparing', 'pending', 'running', 'paused', 'waiting']
172+
const activeStatuses = ['preparing', 'pending', 'running', 'paused']
173173
return activeStatuses.includes(executionProp.status)
174174
}, [executionProp])
175175

frontend/src/components/executions/ExecutionView.tsx

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -458,15 +458,35 @@ export function ExecutionView({ executionId, onFollowUpCreated, onStatusChange,
458458
}, [])
459459

460460
// Handle cancel action for a specific execution
461+
// For persistent sessions, this interrupts (stops current work, keeps session alive)
462+
// For non-persistent sessions, this cancels (terminates execution)
461463
const handleCancel = async (execId: string) => {
462464
setCancelling(true)
463465
try {
464-
await executionsApi.cancel(execId)
466+
// Find the execution to check if it's a persistent session
467+
const execution = chainData?.executions.find((e) => e.id === execId)
468+
let isPersistentSession = false
469+
if (execution?.config) {
470+
try {
471+
const config = JSON.parse(execution.config)
472+
isPersistentSession = config.sessionMode === 'persistent'
473+
} catch {
474+
// Invalid config JSON, default to non-persistent
475+
}
476+
}
477+
478+
if (isPersistentSession) {
479+
// Interrupt: stops current work but keeps session alive
480+
await executionsApi.interrupt(execId)
481+
} else {
482+
// Cancel: fully terminates the execution
483+
await executionsApi.cancel(execId)
484+
}
465485
// Reload chain to get updated status
466486
const data = await executionsApi.getChain(executionId)
467487
setChainData(data)
468488
} catch (err) {
469-
setError(err instanceof Error ? err.message : 'Failed to cancel execution')
489+
setError(err instanceof Error ? err.message : 'Failed to stop execution')
470490
} finally {
471491
setCancelling(false)
472492
}
@@ -506,7 +526,7 @@ export function ExecutionView({ executionId, onFollowUpCreated, onStatusChange,
506526

507527
// Check if this is a persistent session that's ready for another prompt
508528
const isPersistentSessionReady =
509-
lastExecution.status === 'waiting' || lastExecution.status === 'paused'
529+
lastExecution.status === 'pending' || lastExecution.status === 'paused'
510530

511531
setSubmittingFollowUp(true)
512532
try {
@@ -528,7 +548,7 @@ export function ExecutionView({ executionId, onFollowUpCreated, onStatusChange,
528548
})
529549

530550
// Send prompt to existing persistent session
531-
// WebSocket will update the status back to 'waiting' when complete
551+
// WebSocket will update the status back to 'pending' when complete
532552
await executionsApi.sendPrompt(lastExecution.id, prompt)
533553
} else {
534554
// Create a new follow-up execution
@@ -575,7 +595,7 @@ export function ExecutionView({ executionId, onFollowUpCreated, onStatusChange,
575595
const lastExecution = chainData.executions[chainData.executions.length - 1]
576596

577597
// Only end if the session is in a state that can be ended
578-
if (lastExecution.status !== 'waiting' && lastExecution.status !== 'paused') {
598+
if (lastExecution.status !== 'pending' && lastExecution.status !== 'paused') {
579599
return
580600
}
581601

@@ -866,8 +886,8 @@ export function ExecutionView({ executionId, onFollowUpCreated, onStatusChange,
866886
if (!chainData || chainData.executions.length === 0) return
867887
const rootExec = chainData.executions[0]
868888
const lastExec = chainData.executions[chainData.executions.length - 1]
869-
// Only allow cancel for actively running executions (not waiting/paused persistent sessions)
870-
const canCancel = ['preparing', 'pending', 'running'].includes(lastExec.status)
889+
// Only allow cancel for actively running executions
890+
const canCancel = lastExec.status === 'running'
871891

872892
onHeaderDataChange?.(
873893
{
@@ -940,7 +960,7 @@ export function ExecutionView({ executionId, onFollowUpCreated, onStatusChange,
940960
lastExecution.status === 'stopped' ||
941961
lastExecution.status === 'cancelled'
942962
const isPersistentSessionReady =
943-
lastExecution.status === 'waiting' || lastExecution.status === 'paused'
963+
lastExecution.status === 'pending' || lastExecution.status === 'paused'
944964
const canEnableFollowUp = lastExecutionTerminal || isPersistentSessionReady
945965

946966
// Determine if the execution is actively running (not waiting/paused)
@@ -990,8 +1010,7 @@ export function ExecutionView({ executionId, onFollowUpCreated, onStatusChange,
9901010
onTodosUpdate={handleTodosUpdate}
9911011
onAvailableCommandsUpdate={isLast ? handleAvailableCommandsUpdate : undefined}
9921012
onCancel={
993-
isLast &&
994-
['preparing', 'pending', 'running'].includes(execution.status)
1013+
isLast && execution.status === 'running'
9951014
? () => handleCancel(execution.id)
9961015
: undefined
9971016
}
@@ -1050,7 +1069,7 @@ export function ExecutionView({ executionId, onFollowUpCreated, onStatusChange,
10501069
{endingSession ? 'Ending...' : 'End Session'}
10511070
</Button>
10521071
<span className="text-xs text-muted-foreground">
1053-
Session {lastExecution.status === 'paused' ? 'paused' : 'waiting'} for input
1072+
Session {lastExecution.status === 'paused' ? 'paused' : 'pending'} for input
10541073
</span>
10551074
</div>
10561075
)}

frontend/src/contexts/ChatWidgetContext.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export function ChatWidgetProvider({ children }: ChatWidgetProviderProps) {
122122

123123
// Find the latest active execution (running, pending, or preparing)
124124
const latestActiveExecution = useMemo(() => {
125-
const activeStatuses = ['running', 'pending', 'preparing', 'paused', 'waiting']
125+
const activeStatuses = ['running', 'pending', 'preparing', 'paused']
126126
const activeExecutions = executions.filter((e) => activeStatuses.includes(e.status))
127127
if (activeExecutions.length === 0) return null
128128
// Sort by created_at descending

0 commit comments

Comments
 (0)