ccmanager 3.2.3 → 3.2.5
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/Session.js +21 -3
- package/dist/services/__tests__/stateDetector.codex.test.js +49 -0
- package/dist/services/__tests__/stateDetector.gemini.test.js +58 -0
- package/dist/services/__tests__/stateDetector.github-copilot.test.js +30 -0
- package/dist/services/bunTerminal.js +202 -22
- package/dist/services/stateDetector.js +19 -6
- package/package.json +6 -6
|
@@ -68,6 +68,21 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
68
68
|
// These sequences leak as literal text when replaying buffered output
|
|
69
69
|
return input.replace(/\x1B\](?:10|11);[^\x07\x1B]*(?:\x07|\x1B\\)/g, '');
|
|
70
70
|
};
|
|
71
|
+
const normalizeLineEndings = (input) => {
|
|
72
|
+
// Ensure LF moves to column 0 to prevent cursor drift when ONLCR is disabled.
|
|
73
|
+
let normalized = '';
|
|
74
|
+
for (let i = 0; i < input.length; i++) {
|
|
75
|
+
const char = input[i];
|
|
76
|
+
if (char === '\n') {
|
|
77
|
+
const prev = i > 0 ? input[i - 1] : '';
|
|
78
|
+
if (prev !== '\r') {
|
|
79
|
+
normalized += '\r';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
normalized += char;
|
|
83
|
+
}
|
|
84
|
+
return normalized;
|
|
85
|
+
};
|
|
71
86
|
useEffect(() => {
|
|
72
87
|
if (!stdout)
|
|
73
88
|
return;
|
|
@@ -79,6 +94,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
79
94
|
stdout.write('\x1b[>4m'); // Disable xterm modifyOtherKeys extensions
|
|
80
95
|
stdout.write('\x1b[?1004l'); // Disable focus reporting
|
|
81
96
|
stdout.write('\x1b[?2004l'); // Disable bracketed paste (can interfere with shortcuts)
|
|
97
|
+
stdout.write('\x1b[?7h'); // Re-enable auto-wrap
|
|
82
98
|
};
|
|
83
99
|
const sanitizeReplayBuffer = (input) => {
|
|
84
100
|
// Remove terminal mode toggles emitted by Codex so replay doesn't re-enable them
|
|
@@ -91,6 +107,8 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
91
107
|
};
|
|
92
108
|
// Reset modes immediately on entry in case a previous session left them on
|
|
93
109
|
resetTerminalInputModes();
|
|
110
|
+
// Prevent line wrapping from drifting redraws in TUIs that rely on cursor-up clears.
|
|
111
|
+
stdout.write('\x1b[?7l');
|
|
94
112
|
// Clear screen when entering session
|
|
95
113
|
stdout.write('\x1B[2J\x1B[H');
|
|
96
114
|
// Handle session restoration
|
|
@@ -101,7 +119,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
101
119
|
const buffer = restoredSession.outputHistory[i];
|
|
102
120
|
if (!buffer)
|
|
103
121
|
continue;
|
|
104
|
-
const str = sanitizeReplayBuffer(buffer.toString('utf8'));
|
|
122
|
+
const str = normalizeLineEndings(sanitizeReplayBuffer(buffer.toString('utf8')));
|
|
105
123
|
// Skip clear screen sequences at the beginning
|
|
106
124
|
if (i === 0 && (str.includes('\x1B[2J') || str.includes('\x1B[H'))) {
|
|
107
125
|
// Skip this buffer or remove the clear sequence
|
|
@@ -109,7 +127,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
109
127
|
.replace(/\x1B\[2J/g, '')
|
|
110
128
|
.replace(/\x1B\[H/g, '');
|
|
111
129
|
if (cleaned.length > 0) {
|
|
112
|
-
stdout.write(cleaned);
|
|
130
|
+
stdout.write(normalizeLineEndings(cleaned));
|
|
113
131
|
}
|
|
114
132
|
}
|
|
115
133
|
else {
|
|
@@ -144,7 +162,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
144
162
|
const handleSessionData = (activeSession, data) => {
|
|
145
163
|
// Only handle data for our session
|
|
146
164
|
if (activeSession.id === session.id && !isExiting) {
|
|
147
|
-
stdout.write(data);
|
|
165
|
+
stdout.write(normalizeLineEndings(data));
|
|
148
166
|
}
|
|
149
167
|
};
|
|
150
168
|
const handleSessionExit = (exitedSession) => {
|
|
@@ -54,6 +54,28 @@ describe('CodexStateDetector', () => {
|
|
|
54
54
|
// Assert
|
|
55
55
|
expect(state).toBe('waiting_input');
|
|
56
56
|
});
|
|
57
|
+
it('should detect waiting_input state for "Press enter to confirm or esc to cancel" pattern', () => {
|
|
58
|
+
// Arrange
|
|
59
|
+
terminal = createMockTerminal([
|
|
60
|
+
'Some output',
|
|
61
|
+
'Press enter to confirm or esc to cancel',
|
|
62
|
+
]);
|
|
63
|
+
// Act
|
|
64
|
+
const state = detector.detectState(terminal, 'idle');
|
|
65
|
+
// Assert
|
|
66
|
+
expect(state).toBe('waiting_input');
|
|
67
|
+
});
|
|
68
|
+
it('should prioritize "Press enter to confirm" over busy state with esc interrupt', () => {
|
|
69
|
+
// Arrange
|
|
70
|
+
terminal = createMockTerminal([
|
|
71
|
+
'esc to interrupt',
|
|
72
|
+
'Press enter to confirm or esc to cancel',
|
|
73
|
+
]);
|
|
74
|
+
// Act
|
|
75
|
+
const state = detector.detectState(terminal, 'idle');
|
|
76
|
+
// Assert
|
|
77
|
+
expect(state).toBe('waiting_input');
|
|
78
|
+
});
|
|
57
79
|
it('should detect busy state for Esc to interrupt pattern', () => {
|
|
58
80
|
// Arrange
|
|
59
81
|
terminal = createMockTerminal([
|
|
@@ -94,4 +116,31 @@ describe('CodexStateDetector', () => {
|
|
|
94
116
|
// Assert
|
|
95
117
|
expect(state).toBe('waiting_input');
|
|
96
118
|
});
|
|
119
|
+
it('should detect waiting_input state for "Confirm with ... Enter" pattern', () => {
|
|
120
|
+
// Arrange
|
|
121
|
+
terminal = createMockTerminal(['Some output', 'Confirm with Y Enter']);
|
|
122
|
+
// Act
|
|
123
|
+
const state = detector.detectState(terminal, 'idle');
|
|
124
|
+
// Assert
|
|
125
|
+
expect(state).toBe('waiting_input');
|
|
126
|
+
});
|
|
127
|
+
it('should detect waiting_input for "Confirm with" pattern with longer text', () => {
|
|
128
|
+
// Arrange
|
|
129
|
+
terminal = createMockTerminal([
|
|
130
|
+
'Some output',
|
|
131
|
+
'Confirm with Shift + Y Enter',
|
|
132
|
+
]);
|
|
133
|
+
// Act
|
|
134
|
+
const state = detector.detectState(terminal, 'idle');
|
|
135
|
+
// Assert
|
|
136
|
+
expect(state).toBe('waiting_input');
|
|
137
|
+
});
|
|
138
|
+
it('should prioritize "Confirm with ... Enter" over busy state', () => {
|
|
139
|
+
// Arrange
|
|
140
|
+
terminal = createMockTerminal(['Esc to interrupt', 'Confirm with Y Enter']);
|
|
141
|
+
// Act
|
|
142
|
+
const state = detector.detectState(terminal, 'idle');
|
|
143
|
+
// Assert
|
|
144
|
+
expect(state).toBe('waiting_input');
|
|
145
|
+
});
|
|
97
146
|
});
|
|
@@ -20,6 +20,18 @@ describe('GeminiStateDetector', () => {
|
|
|
20
20
|
// Assert
|
|
21
21
|
expect(state).toBe('waiting_input');
|
|
22
22
|
});
|
|
23
|
+
it('should detect waiting_input when "Apply this change" prompt is present (without ?)', () => {
|
|
24
|
+
// Arrange
|
|
25
|
+
terminal = createMockTerminal([
|
|
26
|
+
'Some output from Gemini',
|
|
27
|
+
'│ Apply this change',
|
|
28
|
+
'│ > ',
|
|
29
|
+
]);
|
|
30
|
+
// Act
|
|
31
|
+
const state = detector.detectState(terminal, 'idle');
|
|
32
|
+
// Assert
|
|
33
|
+
expect(state).toBe('waiting_input');
|
|
34
|
+
});
|
|
23
35
|
it('should detect waiting_input when "Allow execution?" prompt is present', () => {
|
|
24
36
|
// Arrange
|
|
25
37
|
terminal = createMockTerminal([
|
|
@@ -32,6 +44,18 @@ describe('GeminiStateDetector', () => {
|
|
|
32
44
|
// Assert
|
|
33
45
|
expect(state).toBe('waiting_input');
|
|
34
46
|
});
|
|
47
|
+
it('should detect waiting_input when "Allow execution" prompt is present (without ?)', () => {
|
|
48
|
+
// Arrange
|
|
49
|
+
terminal = createMockTerminal([
|
|
50
|
+
'Command found: npm install',
|
|
51
|
+
'│ Allow execution',
|
|
52
|
+
'│ > ',
|
|
53
|
+
]);
|
|
54
|
+
// Act
|
|
55
|
+
const state = detector.detectState(terminal, 'idle');
|
|
56
|
+
// Assert
|
|
57
|
+
expect(state).toBe('waiting_input');
|
|
58
|
+
});
|
|
35
59
|
it('should detect waiting_input when "Do you want to proceed?" prompt is present', () => {
|
|
36
60
|
// Arrange
|
|
37
61
|
terminal = createMockTerminal([
|
|
@@ -44,6 +68,40 @@ describe('GeminiStateDetector', () => {
|
|
|
44
68
|
// Assert
|
|
45
69
|
expect(state).toBe('waiting_input');
|
|
46
70
|
});
|
|
71
|
+
it('should detect waiting_input when "Do you want to proceed" prompt is present (without ?)', () => {
|
|
72
|
+
// Arrange
|
|
73
|
+
terminal = createMockTerminal([
|
|
74
|
+
'Changes detected',
|
|
75
|
+
'│ Do you want to proceed',
|
|
76
|
+
'│ > ',
|
|
77
|
+
]);
|
|
78
|
+
// Act
|
|
79
|
+
const state = detector.detectState(terminal, 'idle');
|
|
80
|
+
// Assert
|
|
81
|
+
expect(state).toBe('waiting_input');
|
|
82
|
+
});
|
|
83
|
+
it('should detect waiting_input when "Waiting for user confirmation..." is present', () => {
|
|
84
|
+
// Arrange
|
|
85
|
+
terminal = createMockTerminal([
|
|
86
|
+
'Processing...',
|
|
87
|
+
'Waiting for user confirmation...',
|
|
88
|
+
]);
|
|
89
|
+
// Act
|
|
90
|
+
const state = detector.detectState(terminal, 'idle');
|
|
91
|
+
// Assert
|
|
92
|
+
expect(state).toBe('waiting_input');
|
|
93
|
+
});
|
|
94
|
+
it('should prioritize "Waiting for user confirmation" over busy state', () => {
|
|
95
|
+
// Arrange
|
|
96
|
+
terminal = createMockTerminal([
|
|
97
|
+
'Press ESC to cancel',
|
|
98
|
+
'Waiting for user confirmation...',
|
|
99
|
+
]);
|
|
100
|
+
// Act
|
|
101
|
+
const state = detector.detectState(terminal, 'idle');
|
|
102
|
+
// Assert
|
|
103
|
+
expect(state).toBe('waiting_input');
|
|
104
|
+
});
|
|
47
105
|
it('should detect waiting_input for multiline confirmation ending with "yes"', () => {
|
|
48
106
|
// Arrange
|
|
49
107
|
terminal = createMockTerminal([
|
|
@@ -19,6 +19,36 @@ describe('GitHubCopilotStateDetector', () => {
|
|
|
19
19
|
// Assert
|
|
20
20
|
expect(state).toBe('waiting_input');
|
|
21
21
|
});
|
|
22
|
+
it('detects waiting_input when "Confirm with ... Enter" pattern is present', () => {
|
|
23
|
+
// Arrange
|
|
24
|
+
terminal = createMockTerminal(['Some output', 'Confirm with Y Enter']);
|
|
25
|
+
// Act
|
|
26
|
+
const state = detector.detectState(terminal, 'idle');
|
|
27
|
+
// Assert
|
|
28
|
+
expect(state).toBe('waiting_input');
|
|
29
|
+
});
|
|
30
|
+
it('detects waiting_input for "Confirm with" pattern with longer text', () => {
|
|
31
|
+
// Arrange
|
|
32
|
+
terminal = createMockTerminal([
|
|
33
|
+
'Some output',
|
|
34
|
+
'Confirm with Shift + Y Enter',
|
|
35
|
+
]);
|
|
36
|
+
// Act
|
|
37
|
+
const state = detector.detectState(terminal, 'idle');
|
|
38
|
+
// Assert
|
|
39
|
+
expect(state).toBe('waiting_input');
|
|
40
|
+
});
|
|
41
|
+
it('prioritizes "Confirm with ... Enter" over busy state', () => {
|
|
42
|
+
// Arrange
|
|
43
|
+
terminal = createMockTerminal([
|
|
44
|
+
'Press Esc to cancel',
|
|
45
|
+
'Confirm with Y Enter',
|
|
46
|
+
]);
|
|
47
|
+
// Act
|
|
48
|
+
const state = detector.detectState(terminal, 'idle');
|
|
49
|
+
// Assert
|
|
50
|
+
expect(state).toBe('waiting_input');
|
|
51
|
+
});
|
|
22
52
|
it('detects busy when "Esc to cancel" is present', () => {
|
|
23
53
|
// Arrange
|
|
24
54
|
terminal = createMockTerminal([
|
|
@@ -58,6 +58,37 @@ class BunTerminal {
|
|
|
58
58
|
writable: true,
|
|
59
59
|
value: null
|
|
60
60
|
});
|
|
61
|
+
Object.defineProperty(this, "_terminal", {
|
|
62
|
+
enumerable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
writable: true,
|
|
65
|
+
value: null
|
|
66
|
+
});
|
|
67
|
+
Object.defineProperty(this, "_decoder", {
|
|
68
|
+
enumerable: true,
|
|
69
|
+
configurable: true,
|
|
70
|
+
writable: true,
|
|
71
|
+
value: new globalThis.TextDecoder('utf-8')
|
|
72
|
+
});
|
|
73
|
+
// Buffering to combine fragmented data chunks from the same event loop
|
|
74
|
+
Object.defineProperty(this, "_dataBuffer", {
|
|
75
|
+
enumerable: true,
|
|
76
|
+
configurable: true,
|
|
77
|
+
writable: true,
|
|
78
|
+
value: ''
|
|
79
|
+
});
|
|
80
|
+
Object.defineProperty(this, "_flushTimer", {
|
|
81
|
+
enumerable: true,
|
|
82
|
+
configurable: true,
|
|
83
|
+
writable: true,
|
|
84
|
+
value: null
|
|
85
|
+
});
|
|
86
|
+
Object.defineProperty(this, "_syncOutputMode", {
|
|
87
|
+
enumerable: true,
|
|
88
|
+
configurable: true,
|
|
89
|
+
writable: true,
|
|
90
|
+
value: false
|
|
91
|
+
});
|
|
61
92
|
Object.defineProperty(this, "onData", {
|
|
62
93
|
enumerable: true,
|
|
63
94
|
configurable: true,
|
|
@@ -93,29 +124,61 @@ class BunTerminal {
|
|
|
93
124
|
this._cols = options.cols ?? 80;
|
|
94
125
|
this._rows = options.rows ?? 24;
|
|
95
126
|
this._process = file;
|
|
96
|
-
//
|
|
127
|
+
// Build environment with TERM variable (like node-pty does with 'name' option)
|
|
128
|
+
const env = {};
|
|
129
|
+
if (options.env) {
|
|
130
|
+
for (const [key, value] of Object.entries(options.env)) {
|
|
131
|
+
if (value !== undefined) {
|
|
132
|
+
env[key] = value;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Set TERM from the 'name' option (like node-pty does)
|
|
137
|
+
env['TERM'] = options.name || 'xterm-256color';
|
|
138
|
+
// Create a standalone Bun.Terminal instance for better control over termios settings
|
|
139
|
+
this._terminal = new Bun.Terminal({
|
|
140
|
+
cols: this._cols,
|
|
141
|
+
rows: this._rows,
|
|
142
|
+
data: (_terminal, data) => {
|
|
143
|
+
if (this._closed)
|
|
144
|
+
return;
|
|
145
|
+
const str = typeof data === 'string'
|
|
146
|
+
? data
|
|
147
|
+
: this._decoder.decode(data, { stream: true });
|
|
148
|
+
this._dataBuffer += str;
|
|
149
|
+
this._processBuffer();
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
// Match node-pty behavior by starting in raw mode (no canonical input/echo),
|
|
153
|
+
// while keeping Bun's output processing defaults intact.
|
|
154
|
+
this._terminal.setRawMode(true);
|
|
155
|
+
// Disable ONLCR in the PTY output flags to avoid double CRLF translation
|
|
156
|
+
// when forwarding PTY output to the real stdout TTY.
|
|
157
|
+
const ONLCR_FLAG = 0x0002;
|
|
158
|
+
this._terminal.outputFlags = this._terminal.outputFlags & ~ONLCR_FLAG;
|
|
159
|
+
// Keep Bun defaults for other termios flags.
|
|
160
|
+
// Spawn the process with the pre-configured terminal
|
|
97
161
|
this._subprocess = Bun.spawn([file, ...args], {
|
|
98
162
|
cwd: options.cwd ?? process.cwd(),
|
|
99
|
-
env
|
|
100
|
-
terminal:
|
|
101
|
-
cols: this._cols,
|
|
102
|
-
rows: this._rows,
|
|
103
|
-
data: (_terminal, data) => {
|
|
104
|
-
if (!this._closed) {
|
|
105
|
-
const str = typeof data === 'string'
|
|
106
|
-
? data
|
|
107
|
-
: Buffer.from(data).toString('utf8');
|
|
108
|
-
for (const listener of this._dataListeners) {
|
|
109
|
-
listener(str);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
},
|
|
163
|
+
env,
|
|
164
|
+
terminal: this._terminal,
|
|
114
165
|
});
|
|
115
166
|
this._pid = this._subprocess.pid;
|
|
116
167
|
// Handle process exit
|
|
117
168
|
this._subprocess.exited.then(exitCode => {
|
|
118
169
|
if (!this._closed) {
|
|
170
|
+
this._closed = true;
|
|
171
|
+
// Clear any pending flush timer
|
|
172
|
+
if (this._flushTimer) {
|
|
173
|
+
clearTimeout(this._flushTimer);
|
|
174
|
+
this._flushTimer = null;
|
|
175
|
+
}
|
|
176
|
+
// Flush any remaining buffered data before exit
|
|
177
|
+
this._finalizeDecoder();
|
|
178
|
+
this._syncOutputMode = false;
|
|
179
|
+
// Temporarily unset _closed to allow final flush
|
|
180
|
+
this._closed = false;
|
|
181
|
+
this._flushBuffer();
|
|
119
182
|
this._closed = true;
|
|
120
183
|
for (const listener of this._exitListeners) {
|
|
121
184
|
listener({ exitCode });
|
|
@@ -123,6 +186,86 @@ class BunTerminal {
|
|
|
123
186
|
}
|
|
124
187
|
});
|
|
125
188
|
}
|
|
189
|
+
_emitData(payload) {
|
|
190
|
+
if (payload.length === 0 || this._closed) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
for (const listener of this._dataListeners) {
|
|
194
|
+
listener(payload);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
_flushBuffer() {
|
|
198
|
+
if (this._dataBuffer.length === 0 || this._closed) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const bufferedData = this._dataBuffer;
|
|
202
|
+
this._dataBuffer = '';
|
|
203
|
+
this._emitData(bufferedData);
|
|
204
|
+
}
|
|
205
|
+
_finalizeDecoder() {
|
|
206
|
+
const remaining = this._decoder.decode(new Uint8Array(), { stream: false });
|
|
207
|
+
if (remaining.length > 0) {
|
|
208
|
+
this._dataBuffer += remaining;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
_processBuffer() {
|
|
212
|
+
if (this._closed) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
let madeProgress = true;
|
|
216
|
+
while (madeProgress) {
|
|
217
|
+
madeProgress = false;
|
|
218
|
+
if (this._syncOutputMode) {
|
|
219
|
+
const endIndex = this._dataBuffer.indexOf(BunTerminal.SYNC_OUTPUT_END);
|
|
220
|
+
if (endIndex !== -1) {
|
|
221
|
+
const endOffset = endIndex + BunTerminal.SYNC_OUTPUT_END.length;
|
|
222
|
+
const frame = this._dataBuffer.slice(0, endOffset);
|
|
223
|
+
this._dataBuffer = this._dataBuffer.slice(endOffset);
|
|
224
|
+
this._syncOutputMode = false;
|
|
225
|
+
if (this._flushTimer) {
|
|
226
|
+
clearTimeout(this._flushTimer);
|
|
227
|
+
this._flushTimer = null;
|
|
228
|
+
}
|
|
229
|
+
this._emitData(frame);
|
|
230
|
+
madeProgress = true;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (this._flushTimer) {
|
|
234
|
+
clearTimeout(this._flushTimer);
|
|
235
|
+
}
|
|
236
|
+
this._flushTimer = setTimeout(() => {
|
|
237
|
+
this._flushTimer = null;
|
|
238
|
+
this._syncOutputMode = false;
|
|
239
|
+
this._flushBuffer();
|
|
240
|
+
}, BunTerminal.SYNC_TIMEOUT_MS);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const startIndex = this._dataBuffer.indexOf(BunTerminal.SYNC_OUTPUT_START);
|
|
244
|
+
if (startIndex !== -1) {
|
|
245
|
+
if (startIndex > 0) {
|
|
246
|
+
const leading = this._dataBuffer.slice(0, startIndex);
|
|
247
|
+
this._dataBuffer = this._dataBuffer.slice(startIndex);
|
|
248
|
+
this._emitData(leading);
|
|
249
|
+
madeProgress = true;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
this._syncOutputMode = true;
|
|
253
|
+
if (this._flushTimer) {
|
|
254
|
+
clearTimeout(this._flushTimer);
|
|
255
|
+
this._flushTimer = null;
|
|
256
|
+
}
|
|
257
|
+
madeProgress = true;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (this._flushTimer) {
|
|
261
|
+
clearTimeout(this._flushTimer);
|
|
262
|
+
}
|
|
263
|
+
this._flushTimer = setTimeout(() => {
|
|
264
|
+
this._flushTimer = null;
|
|
265
|
+
this._flushBuffer();
|
|
266
|
+
}, BunTerminal.FLUSH_DELAY_MS);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
126
269
|
get pid() {
|
|
127
270
|
return this._pid;
|
|
128
271
|
}
|
|
@@ -136,32 +279,69 @@ class BunTerminal {
|
|
|
136
279
|
return this._process;
|
|
137
280
|
}
|
|
138
281
|
write(data) {
|
|
139
|
-
if (this._closed || !this.
|
|
282
|
+
if (this._closed || !this._terminal) {
|
|
140
283
|
return;
|
|
141
284
|
}
|
|
142
|
-
this.
|
|
285
|
+
this._terminal.write(data);
|
|
143
286
|
}
|
|
144
287
|
resize(columns, rows) {
|
|
145
|
-
if (this._closed || !this.
|
|
288
|
+
if (this._closed || !this._terminal) {
|
|
146
289
|
return;
|
|
147
290
|
}
|
|
148
291
|
this._cols = columns;
|
|
149
292
|
this._rows = rows;
|
|
150
|
-
this.
|
|
293
|
+
this._terminal.resize(columns, rows);
|
|
151
294
|
}
|
|
152
295
|
kill(_signal) {
|
|
153
296
|
if (this._closed) {
|
|
154
297
|
return;
|
|
155
298
|
}
|
|
156
299
|
this._closed = true;
|
|
157
|
-
|
|
158
|
-
|
|
300
|
+
// Clear any pending flush timer
|
|
301
|
+
if (this._flushTimer) {
|
|
302
|
+
clearTimeout(this._flushTimer);
|
|
303
|
+
this._flushTimer = null;
|
|
304
|
+
}
|
|
305
|
+
// Flush any remaining buffered data
|
|
306
|
+
this._finalizeDecoder();
|
|
307
|
+
this._syncOutputMode = false;
|
|
308
|
+
// Temporarily unset _closed to allow final flush
|
|
309
|
+
this._closed = false;
|
|
310
|
+
this._flushBuffer();
|
|
311
|
+
this._closed = true;
|
|
312
|
+
if (this._terminal) {
|
|
313
|
+
this._terminal.close();
|
|
159
314
|
}
|
|
160
315
|
if (this._subprocess) {
|
|
161
316
|
this._subprocess.kill();
|
|
162
317
|
}
|
|
163
318
|
}
|
|
164
319
|
}
|
|
320
|
+
// Synchronized output escape sequences (used by Ink and other TUI frameworks)
|
|
321
|
+
Object.defineProperty(BunTerminal, "SYNC_OUTPUT_START", {
|
|
322
|
+
enumerable: true,
|
|
323
|
+
configurable: true,
|
|
324
|
+
writable: true,
|
|
325
|
+
value: '\x1b[?2026h'
|
|
326
|
+
});
|
|
327
|
+
Object.defineProperty(BunTerminal, "SYNC_OUTPUT_END", {
|
|
328
|
+
enumerable: true,
|
|
329
|
+
configurable: true,
|
|
330
|
+
writable: true,
|
|
331
|
+
value: '\x1b[?2026l'
|
|
332
|
+
});
|
|
333
|
+
Object.defineProperty(BunTerminal, "FLUSH_DELAY_MS", {
|
|
334
|
+
enumerable: true,
|
|
335
|
+
configurable: true,
|
|
336
|
+
writable: true,
|
|
337
|
+
value: 8
|
|
338
|
+
}); // ~2 frames at 60fps for batching
|
|
339
|
+
Object.defineProperty(BunTerminal, "SYNC_TIMEOUT_MS", {
|
|
340
|
+
enumerable: true,
|
|
341
|
+
configurable: true,
|
|
342
|
+
writable: true,
|
|
343
|
+
value: 100
|
|
344
|
+
}); // Timeout for sync mode
|
|
165
345
|
/**
|
|
166
346
|
* Spawn a new PTY process using Bun's built-in Terminal API.
|
|
167
347
|
*
|
|
@@ -63,10 +63,14 @@ export class GeminiStateDetector extends BaseStateDetector {
|
|
|
63
63
|
detectState(terminal, _currentState) {
|
|
64
64
|
const content = this.getTerminalContent(terminal);
|
|
65
65
|
const lowerContent = content.toLowerCase();
|
|
66
|
+
// Check for explicit user confirmation message - highest priority
|
|
67
|
+
if (lowerContent.includes('waiting for user confirmation')) {
|
|
68
|
+
return 'waiting_input';
|
|
69
|
+
}
|
|
66
70
|
// Check for waiting prompts with box character
|
|
67
|
-
if (content.includes('│ Apply this change
|
|
68
|
-
content.includes('│ Allow execution
|
|
69
|
-
content.includes('│ Do you want to proceed
|
|
71
|
+
if (content.includes('│ Apply this change') ||
|
|
72
|
+
content.includes('│ Allow execution') ||
|
|
73
|
+
content.includes('│ Do you want to proceed')) {
|
|
70
74
|
return 'waiting_input';
|
|
71
75
|
}
|
|
72
76
|
// Check for multiline confirmation prompts ending with "yes"
|
|
@@ -85,6 +89,11 @@ export class CodexStateDetector extends BaseStateDetector {
|
|
|
85
89
|
detectState(terminal, _currentState) {
|
|
86
90
|
const content = this.getTerminalContent(terminal);
|
|
87
91
|
const lowerContent = content.toLowerCase();
|
|
92
|
+
// Check for confirmation prompt patterns - highest priority
|
|
93
|
+
if (lowerContent.includes('press enter to confirm or esc to cancel') ||
|
|
94
|
+
/confirm with .+ enter/i.test(content)) {
|
|
95
|
+
return 'waiting_input';
|
|
96
|
+
}
|
|
88
97
|
// Check for waiting prompts
|
|
89
98
|
if (lowerContent.includes('allow command?') ||
|
|
90
99
|
lowerContent.includes('[y/n]') ||
|
|
@@ -124,15 +133,19 @@ export class GitHubCopilotStateDetector extends BaseStateDetector {
|
|
|
124
133
|
detectState(terminal, _currentState) {
|
|
125
134
|
const content = this.getTerminalContent(terminal);
|
|
126
135
|
const lowerContent = content.toLowerCase();
|
|
127
|
-
//
|
|
136
|
+
// Check for confirmation prompt pattern - highest priority
|
|
137
|
+
if (/confirm with .+ enter/i.test(content)) {
|
|
138
|
+
return 'waiting_input';
|
|
139
|
+
}
|
|
140
|
+
// Waiting prompt has priority 2
|
|
128
141
|
if (lowerContent.includes('│ do you want')) {
|
|
129
142
|
return 'waiting_input';
|
|
130
143
|
}
|
|
131
|
-
// Busy state detection has priority
|
|
144
|
+
// Busy state detection has priority 3
|
|
132
145
|
if (lowerContent.includes('esc to cancel')) {
|
|
133
146
|
return 'busy';
|
|
134
147
|
}
|
|
135
|
-
// Otherwise idle as priority
|
|
148
|
+
// Otherwise idle as priority 4
|
|
136
149
|
return 'idle';
|
|
137
150
|
}
|
|
138
151
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.5",
|
|
4
4
|
"description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kodai Kabasawa",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"bin"
|
|
42
42
|
],
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@kodaikabasawa/ccmanager-darwin-arm64": "3.2.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "3.2.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "3.2.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "3.2.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "3.2.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "3.2.5",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.2.5",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.2.5",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.2.5",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.2.5"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|