Skip to content

Commit bb8514d

Browse files
committed
feat(workflow): implement real-time autonomous mode toggling
add event-driven mode switching between manual and autonomous modes emit workflow:mode-change events for real-time reactivity update input and controller loops to handle mode changes mid-execution add debug logging for mode change events and state transitions
1 parent 85608a7 commit bb8514d

File tree

4 files changed

+237
-116
lines changed

4 files changed

+237
-116
lines changed

src/cli/tui/routes/workflow/hooks/use-workflow-keyboard.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { useKeyboard } from "@opentui/solid"
9+
import { debug } from "../../../../../shared/logging/logger.js"
910
import type { WorkflowState } from "../context/ui-state"
1011

1112
export interface UseWorkflowKeyboardOptions {
@@ -50,23 +51,26 @@ export interface UseWorkflowKeyboardOptions {
5051
exitPromptBoxFocus?: () => void
5152
/** Check if autonomous mode is enabled */
5253
isAutonomousMode?: () => boolean
53-
/** Disable autonomous mode */
54-
disableAutonomousMode?: () => void
54+
/** Toggle autonomous mode on/off */
55+
toggleAutonomousMode?: () => void
5556
}
5657

5758
/**
5859
* Hook for workflow keyboard navigation
5960
*/
6061
export function useWorkflowKeyboard(options: UseWorkflowKeyboardOptions) {
6162
useKeyboard((evt) => {
63+
// Log all keyboard events to debug file
64+
debug('Key event: %s', JSON.stringify({ name: evt.name, shift: evt.shift, ctrl: evt.ctrl, meta: evt.meta }))
65+
6266
// === GLOBAL SHORTCUTS (always work) ===
6367

64-
// Shift+Tab - disable autonomous mode
68+
// Shift+Tab - toggle autonomous mode
6569
if (evt.shift && evt.name === "tab") {
6670
evt.preventDefault()
67-
if (options.isAutonomousMode?.()) {
68-
options.disableAutonomousMode?.()
69-
}
71+
debug('Shift+Tab pressed - toggling autonomous mode')
72+
debug('Current isAutonomousMode: %s', options.isAutonomousMode?.())
73+
options.toggleAutonomousMode?.()
7074
return
7175
}
7276

src/cli/tui/routes/workflow/workflow-shell.tsx

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ import { useWorkflowKeyboard } from "./hooks/use-workflow-keyboard"
2323
import { calculateVisibleItems } from "./constants"
2424
import type { WorkflowEventBus } from "../../../../workflows/events/index.js"
2525
import { setAutonomousMode as persistAutonomousMode, loadControllerConfig } from "../../../../shared/workflows/index.js"
26+
import { debug } from "../../../../shared/logging/logger.js"
2627
import path from "path"
2728

29+
/** Expand ~ to home directory if present */
30+
const resolvePath = (dir: string): string =>
31+
dir.startsWith('~') ? dir.replace('~', process.env.HOME || '') : dir
32+
2833
export interface WorkflowShellProps {
2934
version: string
3035
currentDir: string
@@ -105,16 +110,28 @@ export function WorkflowShell(props: WorkflowShellProps) {
105110
setErrorMessage(null)
106111
}
107112

113+
// Mode change listener - syncs UI state when autonomousMode changes
114+
const handleModeChange = (data: { autonomousMode: boolean }) => {
115+
debug('[MODE-CHANGE] Received event: autonomousMode=%s', data.autonomousMode)
116+
ui.actions.setAutonomousMode(data.autonomousMode)
117+
}
118+
108119
onMount(async () => {
109120
;(process as NodeJS.EventEmitter).on('workflow:error', handleWorkflowError)
110121
;(process as NodeJS.EventEmitter).on('workflow:stopping', handleStopping)
111122
;(process as NodeJS.EventEmitter).on('workflow:user-stop', handleUserStop)
123+
;(process as NodeJS.EventEmitter).on('workflow:mode-change', handleModeChange)
112124

113125
// Load initial autonomous mode state
114-
const cmRoot = path.join(props.currentDir, '.codemachine')
126+
const cmRoot = path.join(resolvePath(props.currentDir), '.codemachine')
127+
debug('onMount - loading controller config from: %s', cmRoot)
115128
const controllerState = await loadControllerConfig(cmRoot)
129+
debug('onMount - controllerState: %s', JSON.stringify(controllerState))
116130
if (controllerState?.autonomousMode) {
131+
debug('onMount - setting autonomousMode to true')
117132
ui.actions.setAutonomousMode(true)
133+
} else {
134+
debug('onMount - autonomousMode not enabled in config')
118135
}
119136

120137
if (props.eventBus) {
@@ -129,6 +146,7 @@ export function WorkflowShell(props: WorkflowShellProps) {
129146
;(process as NodeJS.EventEmitter).off('workflow:error', handleWorkflowError)
130147
;(process as NodeJS.EventEmitter).off('workflow:stopping', handleStopping)
131148
;(process as NodeJS.EventEmitter).off('workflow:user-stop', handleUserStop)
149+
;(process as NodeJS.EventEmitter).off('workflow:mode-change', handleModeChange)
132150
if (adapter) {
133151
adapter.stop()
134152
adapter.disconnect()
@@ -284,15 +302,43 @@ export function WorkflowShell(props: WorkflowShellProps) {
284302
;(process as NodeJS.EventEmitter).emit("workflow:pause")
285303
}
286304

287-
// Disable autonomous mode
288-
const disableAutonomousMode = () => {
289-
const cwd = props.currentDir
290-
const cmRoot = path.join(cwd, '.codemachine')
291-
ui.actions.setAutonomousMode(false)
292-
persistAutonomousMode(cmRoot, false).catch(() => {
293-
// best-effort persistence
294-
})
295-
toast.show({ variant: "warning", message: "Autonomous mode disabled", duration: 3000 })
305+
// Toggle autonomous mode on/off
306+
const toggleAutonomousMode = async () => {
307+
const cmRoot = path.join(resolvePath(props.currentDir), '.codemachine')
308+
309+
// Read current state from file (source of truth)
310+
const controllerState = await loadControllerConfig(cmRoot)
311+
debug('[TOGGLE] controllerState: %s', JSON.stringify(controllerState))
312+
const currentMode = controllerState?.autonomousMode ?? false
313+
const newMode = !currentMode
314+
315+
debug('[TOGGLE] Current mode from file: %s, new mode: %s', currentMode, newMode)
316+
317+
// Check if controller is configured (required for autonomous mode)
318+
if (newMode && !controllerState?.controllerConfig) {
319+
debug('[TOGGLE] Cannot enable autonomous mode - no controller configured')
320+
toast.show({ variant: "error", message: "Cannot enable: No controller configured", duration: 3000 })
321+
return
322+
}
323+
324+
// Update UI state
325+
ui.actions.setAutonomousMode(newMode)
326+
327+
// Persist to file (this also emits workflow:mode-change event)
328+
try {
329+
await persistAutonomousMode(cmRoot, newMode)
330+
debug('[TOGGLE] Successfully persisted autonomousMode=%s', newMode)
331+
toast.show({
332+
variant: newMode ? "success" : "warning",
333+
message: newMode ? "Autonomous mode enabled" : "Autonomous mode disabled",
334+
duration: 3000
335+
})
336+
} catch (err) {
337+
debug('[TOGGLE] Failed to persist autonomousMode: %s', err)
338+
// Revert UI state on error
339+
ui.actions.setAutonomousMode(currentMode)
340+
toast.show({ variant: "error", message: "Failed to toggle autonomous mode", duration: 3000 })
341+
}
296342
}
297343

298344
const getMonitoringId = (uiAgentId: string): number | undefined => {
@@ -329,7 +375,7 @@ export function WorkflowShell(props: WorkflowShellProps) {
329375
focusPromptBox: () => setIsPromptBoxFocused(true),
330376
exitPromptBoxFocus: () => setIsPromptBoxFocused(false),
331377
isAutonomousMode: () => state().autonomousMode,
332-
disableAutonomousMode,
378+
toggleAutonomousMode,
333379
})
334380

335381
return (

src/shared/workflows/controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export async function saveControllerConfig(
120120

121121
/**
122122
* Set autonomous mode on/off
123+
* Emits workflow:mode-change event for real-time reactivity
123124
*/
124125
export async function setAutonomousMode(cmRoot: string, enabled: boolean): Promise<void> {
125126
const trackingPath = path.join(cmRoot, TEMPLATE_TRACKING_FILE);
@@ -139,6 +140,12 @@ export async function setAutonomousMode(cmRoot: string, enabled: boolean): Promi
139140
data.lastUpdated = new Date().toISOString();
140141

141142
await writeFile(trackingPath, JSON.stringify(data, null, 2), 'utf8');
143+
144+
// Emit mode change event for real-time reactivity
145+
const { debug } = await import('../logging/logger.js');
146+
debug('[MODE-CHANGE] Emitting workflow:mode-change event with autonomousMode=%s', enabled);
147+
(process as NodeJS.EventEmitter).emit('workflow:mode-change', { autonomousMode: enabled });
148+
debug('[MODE-CHANGE] Event emitted');
142149
}
143150

144151
/**

0 commit comments

Comments
 (0)