ccmanager 2.11.6 โ 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Configuration.js +14 -0
- package/dist/components/ConfigureCustomCommand.d.ts +9 -0
- package/dist/components/ConfigureCustomCommand.js +44 -0
- package/dist/components/ConfigureOther.d.ts +6 -0
- package/dist/components/ConfigureOther.js +113 -0
- package/dist/components/ConfigureOther.test.d.ts +1 -0
- package/dist/components/ConfigureOther.test.js +80 -0
- package/dist/components/ConfigureStatusHooks.js +7 -1
- package/dist/components/ConfigureTimeout.d.ts +9 -0
- package/dist/components/ConfigureTimeout.js +42 -0
- package/dist/components/CustomCommandSummary.d.ts +6 -0
- package/dist/components/CustomCommandSummary.js +10 -0
- package/dist/components/Menu.recent-projects.test.js +2 -0
- package/dist/components/Menu.test.js +2 -0
- package/dist/components/Session.d.ts +2 -2
- package/dist/components/Session.js +67 -4
- package/dist/constants/statusIcons.d.ts +3 -1
- package/dist/constants/statusIcons.js +3 -0
- package/dist/services/autoApprovalVerifier.d.ts +25 -0
- package/dist/services/autoApprovalVerifier.js +272 -0
- package/dist/services/autoApprovalVerifier.test.d.ts +1 -0
- package/dist/services/autoApprovalVerifier.test.js +120 -0
- package/dist/services/configurationManager.d.ts +8 -0
- package/dist/services/configurationManager.js +56 -0
- package/dist/services/sessionManager.autoApproval.test.d.ts +1 -0
- package/dist/services/sessionManager.autoApproval.test.js +160 -0
- package/dist/services/sessionManager.d.ts +5 -0
- package/dist/services/sessionManager.js +149 -1
- package/dist/services/sessionManager.statePersistence.test.js +2 -0
- package/dist/services/sessionManager.test.js +6 -0
- package/dist/types/index.d.ts +15 -1
- package/dist/utils/hookExecutor.test.js +8 -0
- package/dist/utils/logger.d.ts +83 -14
- package/dist/utils/logger.js +218 -17
- package/dist/utils/worktreeUtils.test.js +1 -0
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import ConfigureStatusHooks from './ConfigureStatusHooks.js';
|
|
|
6
6
|
import ConfigureWorktreeHooks from './ConfigureWorktreeHooks.js';
|
|
7
7
|
import ConfigureWorktree from './ConfigureWorktree.js';
|
|
8
8
|
import ConfigureCommand from './ConfigureCommand.js';
|
|
9
|
+
import ConfigureOther from './ConfigureOther.js';
|
|
9
10
|
import { shortcutManager } from '../services/shortcutManager.js';
|
|
10
11
|
const Configuration = ({ onComplete }) => {
|
|
11
12
|
const [view, setView] = useState('menu');
|
|
@@ -30,6 +31,10 @@ const Configuration = ({ onComplete }) => {
|
|
|
30
31
|
label: 'C ๐ Configure Command Presets',
|
|
31
32
|
value: 'presets',
|
|
32
33
|
},
|
|
34
|
+
{
|
|
35
|
+
label: 'O ๐งช Other & Experimental',
|
|
36
|
+
value: 'other',
|
|
37
|
+
},
|
|
33
38
|
{
|
|
34
39
|
label: 'B โ Back to Main Menu',
|
|
35
40
|
value: 'back',
|
|
@@ -54,6 +59,9 @@ const Configuration = ({ onComplete }) => {
|
|
|
54
59
|
else if (item.value === 'presets') {
|
|
55
60
|
setView('presets');
|
|
56
61
|
}
|
|
62
|
+
else if (item.value === 'other') {
|
|
63
|
+
setView('other');
|
|
64
|
+
}
|
|
57
65
|
};
|
|
58
66
|
const handleSubMenuComplete = () => {
|
|
59
67
|
setView('menu');
|
|
@@ -79,6 +87,9 @@ const Configuration = ({ onComplete }) => {
|
|
|
79
87
|
case 'c':
|
|
80
88
|
setView('presets');
|
|
81
89
|
break;
|
|
90
|
+
case 'o':
|
|
91
|
+
setView('other');
|
|
92
|
+
break;
|
|
82
93
|
case 'b':
|
|
83
94
|
onComplete();
|
|
84
95
|
break;
|
|
@@ -103,6 +114,9 @@ const Configuration = ({ onComplete }) => {
|
|
|
103
114
|
if (view === 'presets') {
|
|
104
115
|
return React.createElement(ConfigureCommand, { onComplete: handleSubMenuComplete });
|
|
105
116
|
}
|
|
117
|
+
if (view === 'other') {
|
|
118
|
+
return React.createElement(ConfigureOther, { onComplete: handleSubMenuComplete });
|
|
119
|
+
}
|
|
106
120
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
107
121
|
React.createElement(Box, { marginBottom: 1 },
|
|
108
122
|
React.createElement(Text, { bold: true, color: "green" }, "Configuration")),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface ConfigureCustomCommandProps {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
onSubmit: (value: string) => void;
|
|
6
|
+
onCancel: () => void;
|
|
7
|
+
}
|
|
8
|
+
declare const ConfigureCustomCommand: React.FC<ConfigureCustomCommandProps>;
|
|
9
|
+
export default ConfigureCustomCommand;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInputWrapper from './TextInputWrapper.js';
|
|
4
|
+
import { shortcutManager } from '../services/shortcutManager.js';
|
|
5
|
+
const ConfigureCustomCommand = ({ value, onChange, onSubmit, onCancel, }) => {
|
|
6
|
+
const shouldIgnoreNextChange = React.useRef(false);
|
|
7
|
+
const handleChange = (newValue) => {
|
|
8
|
+
if (shouldIgnoreNextChange.current) {
|
|
9
|
+
shouldIgnoreNextChange.current = false;
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
onChange(newValue);
|
|
13
|
+
};
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
if (shortcutManager.matchesShortcut('cancel', input, key)) {
|
|
16
|
+
onCancel();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// Ctrl+K clears the current input
|
|
20
|
+
if (key.ctrl && input.toLowerCase() === 'k') {
|
|
21
|
+
// Ignore the TextInput change event that will fire for the same key
|
|
22
|
+
shouldIgnoreNextChange.current = true;
|
|
23
|
+
onChange('');
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
27
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
28
|
+
React.createElement(Text, { bold: true, color: "green" }, "Custom Auto-Approval Command")),
|
|
29
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
30
|
+
React.createElement(Text, null,
|
|
31
|
+
"Enter the command that returns ",
|
|
32
|
+
'{needsPermission:boolean}',
|
|
33
|
+
" JSON:")),
|
|
34
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
35
|
+
React.createElement(TextInputWrapper, { value: value, onChange: handleChange, onSubmit: () => onSubmit(value), placeholder: `e.g. jq -n '{"needsPermission":true}'`, focus: true })),
|
|
36
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
37
|
+
React.createElement(Text, { dimColor: true }, "Env provided: $DEFAULT_PROMPT, $TERMINAL_OUTPUT")),
|
|
38
|
+
React.createElement(Box, null,
|
|
39
|
+
React.createElement(Text, { dimColor: true },
|
|
40
|
+
"Press Enter to save, ",
|
|
41
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
42
|
+
" to go back, Ctrl+K to clear input"))));
|
|
43
|
+
};
|
|
44
|
+
export default ConfigureCustomCommand;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import { configurationManager } from '../services/configurationManager.js';
|
|
5
|
+
import { shortcutManager } from '../services/shortcutManager.js';
|
|
6
|
+
import ConfigureCustomCommand from './ConfigureCustomCommand.js';
|
|
7
|
+
import ConfigureTimeout from './ConfigureTimeout.js';
|
|
8
|
+
import CustomCommandSummary from './CustomCommandSummary.js';
|
|
9
|
+
const ConfigureOther = ({ onComplete }) => {
|
|
10
|
+
const autoApprovalConfig = configurationManager.getAutoApprovalConfig();
|
|
11
|
+
const [view, setView] = useState('main');
|
|
12
|
+
const [autoApprovalEnabled, setAutoApprovalEnabled] = useState(autoApprovalConfig.enabled);
|
|
13
|
+
const [customCommand, setCustomCommand] = useState(autoApprovalConfig.customCommand ?? '');
|
|
14
|
+
const [customCommandDraft, setCustomCommandDraft] = useState(customCommand);
|
|
15
|
+
const [timeout, setTimeout] = useState(autoApprovalConfig.timeout ?? 30);
|
|
16
|
+
const [timeoutDraft, setTimeoutDraft] = useState(timeout);
|
|
17
|
+
useInput((input, key) => {
|
|
18
|
+
if (shortcutManager.matchesShortcut('cancel', input, key)) {
|
|
19
|
+
if (view === 'customCommand') {
|
|
20
|
+
setCustomCommandDraft(customCommand);
|
|
21
|
+
setView('main');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (view === 'timeout') {
|
|
25
|
+
setTimeoutDraft(timeout);
|
|
26
|
+
setView('main');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
onComplete();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const menuItems = [
|
|
33
|
+
{
|
|
34
|
+
label: `Auto Approval (experimental): ${autoApprovalEnabled ? 'โ
Enabled' : 'โ Disabled'}`,
|
|
35
|
+
value: 'toggleAutoApproval',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: 'โ๏ธ Edit Custom Command',
|
|
39
|
+
value: 'customCommand',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: `โฑ๏ธ Set Timeout (${timeout}s)`,
|
|
43
|
+
value: 'timeout',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: '๐พ Save Changes',
|
|
47
|
+
value: 'save',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: 'โ Cancel',
|
|
51
|
+
value: 'cancel',
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
const handleSelect = (item) => {
|
|
55
|
+
switch (item.value) {
|
|
56
|
+
case 'toggleAutoApproval':
|
|
57
|
+
setAutoApprovalEnabled(!autoApprovalEnabled);
|
|
58
|
+
break;
|
|
59
|
+
case 'customCommand':
|
|
60
|
+
setCustomCommandDraft(customCommand);
|
|
61
|
+
setView('customCommand');
|
|
62
|
+
break;
|
|
63
|
+
case 'timeout':
|
|
64
|
+
setTimeoutDraft(timeout);
|
|
65
|
+
setView('timeout');
|
|
66
|
+
break;
|
|
67
|
+
case 'save':
|
|
68
|
+
configurationManager.setAutoApprovalConfig({
|
|
69
|
+
enabled: autoApprovalEnabled,
|
|
70
|
+
customCommand: customCommand.trim() || undefined,
|
|
71
|
+
timeout,
|
|
72
|
+
});
|
|
73
|
+
onComplete();
|
|
74
|
+
break;
|
|
75
|
+
case 'cancel':
|
|
76
|
+
onComplete();
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
if (view === 'customCommand') {
|
|
83
|
+
return (React.createElement(ConfigureCustomCommand, { value: customCommandDraft, onChange: setCustomCommandDraft, onCancel: () => {
|
|
84
|
+
setCustomCommandDraft(customCommand);
|
|
85
|
+
setView('main');
|
|
86
|
+
}, onSubmit: value => {
|
|
87
|
+
setCustomCommand(value);
|
|
88
|
+
setView('main');
|
|
89
|
+
} }));
|
|
90
|
+
}
|
|
91
|
+
if (view === 'timeout') {
|
|
92
|
+
return (React.createElement(ConfigureTimeout, { value: timeoutDraft, onChange: setTimeoutDraft, onCancel: () => {
|
|
93
|
+
setTimeoutDraft(timeout);
|
|
94
|
+
setView('main');
|
|
95
|
+
}, onSubmit: value => {
|
|
96
|
+
setTimeout(value);
|
|
97
|
+
setView('main');
|
|
98
|
+
} }));
|
|
99
|
+
}
|
|
100
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
101
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
102
|
+
React.createElement(Text, { bold: true, color: "green" }, "Other & Experimental Settings")),
|
|
103
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
104
|
+
React.createElement(Text, { dimColor: true }, "Toggle experimental capabilities and other miscellaneous options.")),
|
|
105
|
+
React.createElement(CustomCommandSummary, { command: customCommand }),
|
|
106
|
+
React.createElement(SelectInput, { items: menuItems, onSelect: handleSelect, isFocused: true }),
|
|
107
|
+
React.createElement(Box, { marginTop: 1 },
|
|
108
|
+
React.createElement(Text, { dimColor: true },
|
|
109
|
+
"Press ",
|
|
110
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
111
|
+
" to return without saving"))));
|
|
112
|
+
};
|
|
113
|
+
export default ConfigureOther;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import ConfigureOther from './ConfigureOther.js';
|
|
5
|
+
import { configurationManager } from '../services/configurationManager.js';
|
|
6
|
+
// Mock ink to avoid stdin issues during tests
|
|
7
|
+
vi.mock('ink', async () => {
|
|
8
|
+
const actual = await vi.importActual('ink');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
useInput: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
// Mock SelectInput to render labels directly
|
|
15
|
+
vi.mock('ink-select-input', async () => {
|
|
16
|
+
const React = await vi.importActual('react');
|
|
17
|
+
const { Text, Box } = await vi.importActual('ink');
|
|
18
|
+
return {
|
|
19
|
+
default: ({ items }) => React.createElement(Box, { flexDirection: 'column' }, items.map((item, index) => React.createElement(Text, { key: index }, item.label))),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
vi.mock('../services/configurationManager.js', () => ({
|
|
23
|
+
configurationManager: {
|
|
24
|
+
getAutoApprovalConfig: vi.fn(),
|
|
25
|
+
setAutoApprovalConfig: vi.fn(),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
vi.mock('../services/shortcutManager.js', () => ({
|
|
29
|
+
shortcutManager: {
|
|
30
|
+
matchesShortcut: vi.fn().mockReturnValue(false),
|
|
31
|
+
getShortcutDisplay: vi.fn().mockReturnValue('Esc'),
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
vi.mock('./TextInputWrapper.js', async () => {
|
|
35
|
+
const React = await vi.importActual('react');
|
|
36
|
+
return {
|
|
37
|
+
default: ({ value }) => React.createElement('input', { value, 'data-testid': 'text-input' }),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
vi.mock('./ConfigureCustomCommand.js', async () => {
|
|
41
|
+
const React = await vi.importActual('react');
|
|
42
|
+
return {
|
|
43
|
+
default: () => React.createElement('div', { 'data-testid': 'custom-command-editor' }),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
vi.mock('./CustomCommandSummary.js', async () => {
|
|
47
|
+
const React = await vi.importActual('react');
|
|
48
|
+
const { Text } = await vi.importActual('ink');
|
|
49
|
+
return {
|
|
50
|
+
default: ({ command }) => React.createElement(Text, null, `Custom auto-approval command: ${command || 'Empty'}`),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
const mockedConfigurationManager = configurationManager;
|
|
54
|
+
describe('ConfigureOther', () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
vi.clearAllMocks();
|
|
57
|
+
});
|
|
58
|
+
it('renders experimental settings with auto-approval status', () => {
|
|
59
|
+
mockedConfigurationManager.getAutoApprovalConfig.mockReturnValue({
|
|
60
|
+
enabled: true,
|
|
61
|
+
customCommand: '',
|
|
62
|
+
});
|
|
63
|
+
const { lastFrame } = render(React.createElement(ConfigureOther, { onComplete: vi.fn() }));
|
|
64
|
+
expect(lastFrame()).toContain('Other & Experimental Settings');
|
|
65
|
+
expect(lastFrame()).toContain('Auto Approval (experimental): โ
Enabled');
|
|
66
|
+
expect(lastFrame()).toContain('Custom auto-approval command: Empty');
|
|
67
|
+
expect(lastFrame()).toContain('Edit Custom Command');
|
|
68
|
+
expect(lastFrame()).toContain('Save Changes');
|
|
69
|
+
});
|
|
70
|
+
it('shows current custom command summary', () => {
|
|
71
|
+
mockedConfigurationManager.getAutoApprovalConfig.mockReturnValue({
|
|
72
|
+
enabled: false,
|
|
73
|
+
customCommand: 'jq -n \'{"needsPermission":true}\'',
|
|
74
|
+
});
|
|
75
|
+
const { lastFrame } = render(React.createElement(ConfigureOther, { onComplete: vi.fn() }));
|
|
76
|
+
expect(lastFrame()).toContain('Custom auto-approval command:');
|
|
77
|
+
expect(lastFrame()).toContain('jq -n');
|
|
78
|
+
expect(lastFrame()).toContain('Edit Custom Command');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -7,6 +7,7 @@ const STATUS_LABELS = {
|
|
|
7
7
|
idle: 'Idle',
|
|
8
8
|
busy: 'Busy',
|
|
9
9
|
waiting_input: 'Waiting for Input',
|
|
10
|
+
pending_auto_approval: 'Pending Auto Approval',
|
|
10
11
|
};
|
|
11
12
|
const ConfigureStatusHooks = ({ onComplete, }) => {
|
|
12
13
|
const [view, setView] = useState('menu');
|
|
@@ -31,7 +32,12 @@ const ConfigureStatusHooks = ({ onComplete, }) => {
|
|
|
31
32
|
const getMenuItems = () => {
|
|
32
33
|
const items = [];
|
|
33
34
|
// Add status hook items
|
|
34
|
-
[
|
|
35
|
+
[
|
|
36
|
+
'idle',
|
|
37
|
+
'busy',
|
|
38
|
+
'waiting_input',
|
|
39
|
+
'pending_auto_approval',
|
|
40
|
+
].forEach(status => {
|
|
35
41
|
const hook = statusHooks[status];
|
|
36
42
|
const enabled = hook?.enabled ? 'โ' : 'โ';
|
|
37
43
|
const command = hook?.command || '(not set)';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface ConfigureTimeoutProps {
|
|
3
|
+
value: number;
|
|
4
|
+
onChange: (value: number) => void;
|
|
5
|
+
onSubmit: (value: number) => void;
|
|
6
|
+
onCancel: () => void;
|
|
7
|
+
}
|
|
8
|
+
declare const ConfigureTimeout: React.FC<ConfigureTimeoutProps>;
|
|
9
|
+
export default ConfigureTimeout;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInputWrapper from './TextInputWrapper.js';
|
|
4
|
+
import { shortcutManager } from '../services/shortcutManager.js';
|
|
5
|
+
const ConfigureTimeout = ({ value, onChange, onSubmit, onCancel, }) => {
|
|
6
|
+
const [inputValue, setInputValue] = React.useState(String(value));
|
|
7
|
+
const handleChange = (newValue) => {
|
|
8
|
+
// Only allow numeric input
|
|
9
|
+
const filtered = newValue.replace(/[^0-9]/g, '');
|
|
10
|
+
setInputValue(filtered);
|
|
11
|
+
const parsed = parseInt(filtered, 10);
|
|
12
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
13
|
+
onChange(parsed);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const handleSubmit = () => {
|
|
17
|
+
const parsed = parseInt(inputValue, 10);
|
|
18
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
19
|
+
onSubmit(parsed);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
useInput((input, key) => {
|
|
23
|
+
if (shortcutManager.matchesShortcut('cancel', input, key)) {
|
|
24
|
+
onCancel();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
28
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
29
|
+
React.createElement(Text, { bold: true, color: "green" }, "Auto-Approval Timeout")),
|
|
30
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
31
|
+
React.createElement(Text, null, "Enter timeout in seconds for auto-approval verification:")),
|
|
32
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
33
|
+
React.createElement(TextInputWrapper, { value: inputValue, onChange: handleChange, onSubmit: handleSubmit, placeholder: "e.g. 30", focus: true })),
|
|
34
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
35
|
+
React.createElement(Text, { dimColor: true }, "Must be a positive integer (minimum: 1 second, default: 30 seconds)")),
|
|
36
|
+
React.createElement(Box, null,
|
|
37
|
+
React.createElement(Text, { dimColor: true },
|
|
38
|
+
"Press Enter to save, ",
|
|
39
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
40
|
+
" to go back"))));
|
|
41
|
+
};
|
|
42
|
+
export default ConfigureTimeout;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const CustomCommandSummary = ({ command, }) => {
|
|
4
|
+
const displayValue = command.trim() ? command : 'Empty';
|
|
5
|
+
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
6
|
+
React.createElement(Text, null, "Custom auto-approval command:"),
|
|
7
|
+
React.createElement(Text, { dimColor: true }, displayValue),
|
|
8
|
+
React.createElement(Text, { dimColor: true }, "Env provided: $DEFAULT_PROMPT, $TERMINAL_OUTPUT")));
|
|
9
|
+
};
|
|
10
|
+
export default CustomCommandSummary;
|
|
@@ -99,6 +99,7 @@ describe('Menu - Recent Projects', () => {
|
|
|
99
99
|
idle: 0,
|
|
100
100
|
busy: 0,
|
|
101
101
|
waiting_input: 0,
|
|
102
|
+
pending_auto_approval: 0,
|
|
102
103
|
total: 0,
|
|
103
104
|
});
|
|
104
105
|
vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
|
|
@@ -128,6 +129,7 @@ describe('Menu - Recent Projects', () => {
|
|
|
128
129
|
idle: 0,
|
|
129
130
|
busy: 0,
|
|
130
131
|
waiting_input: 0,
|
|
132
|
+
pending_auto_approval: 0,
|
|
131
133
|
total: 0,
|
|
132
134
|
});
|
|
133
135
|
vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
|
|
@@ -272,6 +272,7 @@ describe('Menu component rendering', () => {
|
|
|
272
272
|
idle: 0,
|
|
273
273
|
busy: 0,
|
|
274
274
|
waiting_input: 0,
|
|
275
|
+
pending_auto_approval: 0,
|
|
275
276
|
total: 0,
|
|
276
277
|
});
|
|
277
278
|
vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
|
|
@@ -312,6 +313,7 @@ describe('Menu component rendering', () => {
|
|
|
312
313
|
idle: 0,
|
|
313
314
|
busy: 0,
|
|
314
315
|
waiting_input: 0,
|
|
316
|
+
pending_auto_approval: 0,
|
|
315
317
|
total: 0,
|
|
316
318
|
});
|
|
317
319
|
vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Session as
|
|
2
|
+
import { Session as ISession } from '../types/index.js';
|
|
3
3
|
import { SessionManager } from '../services/sessionManager.js';
|
|
4
4
|
interface SessionProps {
|
|
5
|
-
session:
|
|
5
|
+
session: ISession;
|
|
6
6
|
sessionManager: SessionManager;
|
|
7
7
|
onReturnToMenu: () => void;
|
|
8
8
|
}
|
|
@@ -1,9 +1,67 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import { useStdout } from 'ink';
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { Box, Text, useStdout } from 'ink';
|
|
3
3
|
import { shortcutManager } from '../services/shortcutManager.js';
|
|
4
4
|
const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
5
5
|
const { stdout } = useStdout();
|
|
6
6
|
const [isExiting, setIsExiting] = useState(false);
|
|
7
|
+
const deriveStatus = (currentSession) => {
|
|
8
|
+
// Always prioritize showing the manual approval notice when verification failed
|
|
9
|
+
if (currentSession.autoApprovalFailed) {
|
|
10
|
+
const reason = currentSession.autoApprovalReason
|
|
11
|
+
? ` Reason: ${currentSession.autoApprovalReason}.`
|
|
12
|
+
: '';
|
|
13
|
+
return {
|
|
14
|
+
message: `Auto-approval failed.${reason} Manual approval requiredโrespond to the prompt.`,
|
|
15
|
+
variant: 'error',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
if (currentSession.state === 'pending_auto_approval') {
|
|
19
|
+
return {
|
|
20
|
+
message: 'Auto-approval pending... verifying permissions (press any key to cancel)',
|
|
21
|
+
variant: 'pending',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return { message: null, variant: null };
|
|
25
|
+
};
|
|
26
|
+
const initialStatus = deriveStatus(session);
|
|
27
|
+
const [statusMessage, setStatusMessage] = useState(initialStatus.message);
|
|
28
|
+
const [statusVariant, setStatusVariant] = useState(initialStatus.variant);
|
|
29
|
+
const [columns, setColumns] = useState(() => stdout?.columns ?? process.stdout.columns ?? 80);
|
|
30
|
+
const { statusLineText, backgroundColor, textColor } = useMemo(() => {
|
|
31
|
+
if (!statusMessage || !statusVariant) {
|
|
32
|
+
return {
|
|
33
|
+
statusLineText: null,
|
|
34
|
+
backgroundColor: undefined,
|
|
35
|
+
textColor: undefined,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const maxContentWidth = Math.max(columns - 4, 0);
|
|
39
|
+
const prefix = statusVariant === 'error'
|
|
40
|
+
? '[AUTO-APPROVAL REQUIRED]'
|
|
41
|
+
: '[AUTO-APPROVAL]';
|
|
42
|
+
const prefixed = `${prefix} ${statusMessage}`;
|
|
43
|
+
const trimmed = prefixed.length > maxContentWidth
|
|
44
|
+
? prefixed.slice(0, maxContentWidth)
|
|
45
|
+
: prefixed;
|
|
46
|
+
return {
|
|
47
|
+
statusLineText: ` ${trimmed}`.padEnd(columns, ' '),
|
|
48
|
+
backgroundColor: statusVariant === 'error' ? '#d90429' : '#ffd166',
|
|
49
|
+
textColor: statusVariant === 'error' ? 'white' : '#1c1c1c',
|
|
50
|
+
};
|
|
51
|
+
}, [columns, statusMessage, statusVariant]);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const handleSessionStateChange = (updatedSession) => {
|
|
54
|
+
if (updatedSession.id !== session.id)
|
|
55
|
+
return;
|
|
56
|
+
const { message, variant } = deriveStatus(updatedSession);
|
|
57
|
+
setStatusMessage(message);
|
|
58
|
+
setStatusVariant(variant);
|
|
59
|
+
};
|
|
60
|
+
sessionManager.on('sessionStateChanged', handleSessionStateChange);
|
|
61
|
+
return () => {
|
|
62
|
+
sessionManager.off('sessionStateChanged', handleSessionStateChange);
|
|
63
|
+
};
|
|
64
|
+
}, [session.id, sessionManager]);
|
|
7
65
|
const stripOscColorSequences = (input) => {
|
|
8
66
|
// Remove default foreground/background color OSC sequences that Codex emits
|
|
9
67
|
// These sequences leak as literal text when replaying buffered output
|
|
@@ -91,6 +149,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
91
149
|
const handleSessionExit = (exitedSession) => {
|
|
92
150
|
if (exitedSession.id === session.id) {
|
|
93
151
|
setIsExiting(true);
|
|
152
|
+
setStatusMessage(null);
|
|
94
153
|
// Don't call onReturnToMenu here - App component handles it
|
|
95
154
|
}
|
|
96
155
|
};
|
|
@@ -100,6 +159,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
100
159
|
const handleResize = () => {
|
|
101
160
|
const cols = process.stdout.columns || 80;
|
|
102
161
|
const rows = process.stdout.rows || 24;
|
|
162
|
+
setColumns(cols);
|
|
103
163
|
session.process.resize(cols, rows);
|
|
104
164
|
// Also resize the virtual terminal
|
|
105
165
|
if (session.terminal) {
|
|
@@ -132,6 +192,9 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
132
192
|
onReturnToMenu();
|
|
133
193
|
return;
|
|
134
194
|
}
|
|
195
|
+
if (session.state === 'pending_auto_approval') {
|
|
196
|
+
sessionManager.cancelAutoApproval(session.worktreePath, 'User input received during auto-approval');
|
|
197
|
+
}
|
|
135
198
|
// Pass all other input directly to the PTY
|
|
136
199
|
session.process.write(data);
|
|
137
200
|
};
|
|
@@ -162,7 +225,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
162
225
|
stdout.off('resize', handleResize);
|
|
163
226
|
};
|
|
164
227
|
}, [session, sessionManager, stdout, onReturnToMenu, isExiting]);
|
|
165
|
-
|
|
166
|
-
|
|
228
|
+
return statusLineText ? (React.createElement(Box, { width: "100%" },
|
|
229
|
+
React.createElement(Text, { backgroundColor: backgroundColor, color: textColor, bold: true }, statusLineText))) : null;
|
|
167
230
|
};
|
|
168
231
|
export default Session;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SessionState } from '../types/index.js';
|
|
1
2
|
export declare const STATUS_ICONS: {
|
|
2
3
|
readonly BUSY: "โ";
|
|
3
4
|
readonly WAITING: "โ";
|
|
@@ -6,6 +7,7 @@ export declare const STATUS_ICONS: {
|
|
|
6
7
|
export declare const STATUS_LABELS: {
|
|
7
8
|
readonly BUSY: "Busy";
|
|
8
9
|
readonly WAITING: "Waiting";
|
|
10
|
+
readonly PENDING_AUTO_APPROVAL: "Pending Auto Approval";
|
|
9
11
|
readonly IDLE: "Idle";
|
|
10
12
|
};
|
|
11
13
|
export declare const MENU_ICONS: {
|
|
@@ -15,4 +17,4 @@ export declare const MENU_ICONS: {
|
|
|
15
17
|
readonly CONFIGURE_SHORTCUTS: "โจ";
|
|
16
18
|
readonly EXIT: "โป";
|
|
17
19
|
};
|
|
18
|
-
export declare const getStatusDisplay: (status:
|
|
20
|
+
export declare const getStatusDisplay: (status: SessionState) => string;
|
|
@@ -6,6 +6,7 @@ export const STATUS_ICONS = {
|
|
|
6
6
|
export const STATUS_LABELS = {
|
|
7
7
|
BUSY: 'Busy',
|
|
8
8
|
WAITING: 'Waiting',
|
|
9
|
+
PENDING_AUTO_APPROVAL: 'Pending Auto Approval',
|
|
9
10
|
IDLE: 'Idle',
|
|
10
11
|
};
|
|
11
12
|
export const MENU_ICONS = {
|
|
@@ -21,6 +22,8 @@ export const getStatusDisplay = (status) => {
|
|
|
21
22
|
return `${STATUS_ICONS.BUSY} ${STATUS_LABELS.BUSY}`;
|
|
22
23
|
case 'waiting_input':
|
|
23
24
|
return `${STATUS_ICONS.WAITING} ${STATUS_LABELS.WAITING}`;
|
|
25
|
+
case 'pending_auto_approval':
|
|
26
|
+
return `${STATUS_ICONS.WAITING} ${STATUS_LABELS.PENDING_AUTO_APPROVAL}`;
|
|
24
27
|
case 'idle':
|
|
25
28
|
return `${STATUS_ICONS.IDLE} ${STATUS_LABELS.IDLE}`;
|
|
26
29
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { ProcessError } from '../types/errors.js';
|
|
3
|
+
import { AutoApprovalResponse } from '../types/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Service to verify if auto-approval should be granted for pending states
|
|
6
|
+
* Uses Claude Haiku model to analyze terminal output and determine if
|
|
7
|
+
* user permission is required before proceeding
|
|
8
|
+
*/
|
|
9
|
+
export declare class AutoApprovalVerifier {
|
|
10
|
+
private readonly model;
|
|
11
|
+
private createExecOptions;
|
|
12
|
+
private runClaudePrompt;
|
|
13
|
+
private runCustomCommand;
|
|
14
|
+
/**
|
|
15
|
+
* Verify if the current terminal output requires user permission
|
|
16
|
+
* before proceeding with auto-approval
|
|
17
|
+
*
|
|
18
|
+
* @param terminalOutput - Current terminal output to analyze
|
|
19
|
+
* @returns Effect that resolves to true if permission needed, false if can auto-approve
|
|
20
|
+
*/
|
|
21
|
+
verifyNeedsPermission(terminalOutput: string, options?: {
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}): Effect.Effect<AutoApprovalResponse, ProcessError, never>;
|
|
24
|
+
}
|
|
25
|
+
export declare const autoApprovalVerifier: AutoApprovalVerifier;
|