ccmanager 2.11.5 โ 3.0.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 +87 -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/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 +91 -8
- 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 +265 -0
- package/dist/services/autoApprovalVerifier.test.d.ts +1 -0
- package/dist/services/autoApprovalVerifier.test.js +120 -0
- package/dist/services/configurationManager.d.ts +7 -0
- package/dist/services/configurationManager.js +35 -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 +14 -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,87 @@
|
|
|
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 CustomCommandSummary from './CustomCommandSummary.js';
|
|
8
|
+
const ConfigureOther = ({ onComplete }) => {
|
|
9
|
+
const autoApprovalConfig = configurationManager.getAutoApprovalConfig();
|
|
10
|
+
const [view, setView] = useState('main');
|
|
11
|
+
const [autoApprovalEnabled, setAutoApprovalEnabled] = useState(autoApprovalConfig.enabled);
|
|
12
|
+
const [customCommand, setCustomCommand] = useState(autoApprovalConfig.customCommand ?? '');
|
|
13
|
+
const [customCommandDraft, setCustomCommandDraft] = useState(customCommand);
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
if (shortcutManager.matchesShortcut('cancel', input, key)) {
|
|
16
|
+
if (view === 'customCommand') {
|
|
17
|
+
setCustomCommandDraft(customCommand);
|
|
18
|
+
setView('main');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
onComplete();
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const menuItems = [
|
|
25
|
+
{
|
|
26
|
+
label: `Auto Approval (experimental): ${autoApprovalEnabled ? 'โ
Enabled' : 'โ Disabled'}`,
|
|
27
|
+
value: 'toggleAutoApproval',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: 'โ๏ธ Edit Custom Command',
|
|
31
|
+
value: 'customCommand',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: '๐พ Save Changes',
|
|
35
|
+
value: 'save',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
label: 'โ Cancel',
|
|
39
|
+
value: 'cancel',
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
const handleSelect = (item) => {
|
|
43
|
+
switch (item.value) {
|
|
44
|
+
case 'toggleAutoApproval':
|
|
45
|
+
setAutoApprovalEnabled(!autoApprovalEnabled);
|
|
46
|
+
break;
|
|
47
|
+
case 'customCommand':
|
|
48
|
+
setCustomCommandDraft(customCommand);
|
|
49
|
+
setView('customCommand');
|
|
50
|
+
break;
|
|
51
|
+
case 'save':
|
|
52
|
+
configurationManager.setAutoApprovalConfig({
|
|
53
|
+
enabled: autoApprovalEnabled,
|
|
54
|
+
customCommand: customCommand.trim() || undefined,
|
|
55
|
+
});
|
|
56
|
+
onComplete();
|
|
57
|
+
break;
|
|
58
|
+
case 'cancel':
|
|
59
|
+
onComplete();
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
if (view === 'customCommand') {
|
|
66
|
+
return (React.createElement(ConfigureCustomCommand, { value: customCommandDraft, onChange: setCustomCommandDraft, onCancel: () => {
|
|
67
|
+
setCustomCommandDraft(customCommand);
|
|
68
|
+
setView('main');
|
|
69
|
+
}, onSubmit: value => {
|
|
70
|
+
setCustomCommand(value);
|
|
71
|
+
setView('main');
|
|
72
|
+
} }));
|
|
73
|
+
}
|
|
74
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
75
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
76
|
+
React.createElement(Text, { bold: true, color: "green" }, "Other & Experimental Settings")),
|
|
77
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
78
|
+
React.createElement(Text, { dimColor: true }, "Toggle experimental capabilities and other miscellaneous options.")),
|
|
79
|
+
React.createElement(CustomCommandSummary, { command: customCommand }),
|
|
80
|
+
React.createElement(SelectInput, { items: menuItems, onSelect: handleSelect, isFocused: true }),
|
|
81
|
+
React.createElement(Box, { marginTop: 1 },
|
|
82
|
+
React.createElement(Text, { dimColor: true },
|
|
83
|
+
"Press ",
|
|
84
|
+
shortcutManager.getShortcutDisplay('cancel'),
|
|
85
|
+
" to return without saving"))));
|
|
86
|
+
};
|
|
87
|
+
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,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
|
|
@@ -12,6 +70,26 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
12
70
|
useEffect(() => {
|
|
13
71
|
if (!stdout)
|
|
14
72
|
return;
|
|
73
|
+
const resetTerminalInputModes = () => {
|
|
74
|
+
// Reset terminal modes that interactive tools like Codex enable (kitty keyboard
|
|
75
|
+
// protocol / modifyOtherKeys / focus tracking) so they don't leak into other
|
|
76
|
+
// sessions after we detach.
|
|
77
|
+
stdout.write('\x1b[>0u'); // Disable kitty keyboard protocol (CSI u sequences)
|
|
78
|
+
stdout.write('\x1b[>4m'); // Disable xterm modifyOtherKeys extensions
|
|
79
|
+
stdout.write('\x1b[?1004l'); // Disable focus reporting
|
|
80
|
+
stdout.write('\x1b[?2004l'); // Disable bracketed paste (can interfere with shortcuts)
|
|
81
|
+
};
|
|
82
|
+
const sanitizeReplayBuffer = (input) => {
|
|
83
|
+
// Remove terminal mode toggles emitted by Codex so replay doesn't re-enable them
|
|
84
|
+
// on our own TTY when restoring the session view.
|
|
85
|
+
return stripOscColorSequences(input)
|
|
86
|
+
.replace(/\x1B\[>4;?\d*m/g, '') // modifyOtherKeys set/reset
|
|
87
|
+
.replace(/\x1B\[>[0-9;]*u/g, '') // kitty keyboard protocol enables
|
|
88
|
+
.replace(/\x1B\[\?1004[hl]/g, '') // focus tracking
|
|
89
|
+
.replace(/\x1B\[\?2004[hl]/g, ''); // bracketed paste
|
|
90
|
+
};
|
|
91
|
+
// Reset modes immediately on entry in case a previous session left them on
|
|
92
|
+
resetTerminalInputModes();
|
|
15
93
|
// Clear screen when entering session
|
|
16
94
|
stdout.write('\x1B[2J\x1B[H');
|
|
17
95
|
// Handle session restoration
|
|
@@ -22,7 +100,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
22
100
|
const buffer = restoredSession.outputHistory[i];
|
|
23
101
|
if (!buffer)
|
|
24
102
|
continue;
|
|
25
|
-
const str =
|
|
103
|
+
const str = sanitizeReplayBuffer(buffer.toString('utf8'));
|
|
26
104
|
// Skip clear screen sequences at the beginning
|
|
27
105
|
if (i === 0 && (str.includes('\x1B[2J') || str.includes('\x1B[H'))) {
|
|
28
106
|
// Skip this buffer or remove the clear sequence
|
|
@@ -71,6 +149,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
71
149
|
const handleSessionExit = (exitedSession) => {
|
|
72
150
|
if (exitedSession.id === session.id) {
|
|
73
151
|
setIsExiting(true);
|
|
152
|
+
setStatusMessage(null);
|
|
74
153
|
// Don't call onReturnToMenu here - App component handles it
|
|
75
154
|
}
|
|
76
155
|
};
|
|
@@ -80,6 +159,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
80
159
|
const handleResize = () => {
|
|
81
160
|
const cols = process.stdout.columns || 80;
|
|
82
161
|
const rows = process.stdout.rows || 24;
|
|
162
|
+
setColumns(cols);
|
|
83
163
|
session.process.resize(cols, rows);
|
|
84
164
|
// Also resize the virtual terminal
|
|
85
165
|
if (session.terminal) {
|
|
@@ -101,9 +181,9 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
101
181
|
return;
|
|
102
182
|
// Check for return to menu shortcut
|
|
103
183
|
if (shortcutManager.matchesRawInput('returnToMenu', data)) {
|
|
104
|
-
// Disable
|
|
184
|
+
// Disable any extended input modes that might have been enabled by the PTY
|
|
105
185
|
if (stdout) {
|
|
106
|
-
|
|
186
|
+
resetTerminalInputModes();
|
|
107
187
|
}
|
|
108
188
|
// Restore stdin state before returning to menu
|
|
109
189
|
stdin.removeListener('data', handleStdinData);
|
|
@@ -112,6 +192,9 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
112
192
|
onReturnToMenu();
|
|
113
193
|
return;
|
|
114
194
|
}
|
|
195
|
+
if (session.state === 'pending_auto_approval') {
|
|
196
|
+
sessionManager.cancelAutoApproval(session.worktreePath, 'User input received during auto-approval');
|
|
197
|
+
}
|
|
115
198
|
// Pass all other input directly to the PTY
|
|
116
199
|
session.process.write(data);
|
|
117
200
|
};
|
|
@@ -121,7 +204,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
121
204
|
stdin.removeListener('data', handleStdinData);
|
|
122
205
|
// Disable focus reporting mode that might have been enabled by the PTY
|
|
123
206
|
if (stdout) {
|
|
124
|
-
|
|
207
|
+
resetTerminalInputModes();
|
|
125
208
|
}
|
|
126
209
|
// Restore stdin to its original state
|
|
127
210
|
if (stdin.isTTY) {
|
|
@@ -142,7 +225,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
142
225
|
stdout.off('resize', handleResize);
|
|
143
226
|
};
|
|
144
227
|
}, [session, sessionManager, stdout, onReturnToMenu, isExiting]);
|
|
145
|
-
|
|
146
|
-
|
|
228
|
+
return statusLineText ? (React.createElement(Box, { width: "100%" },
|
|
229
|
+
React.createElement(Text, { backgroundColor: backgroundColor, color: textColor, bold: true }, statusLineText))) : null;
|
|
147
230
|
};
|
|
148
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;
|