ccmanager 3.2.2 → 3.2.4
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/cli.js +2 -0
- package/dist/components/Session.js +21 -3
- package/dist/services/bunTerminal.js +202 -22
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import meow from 'meow';
|
|
|
5
5
|
import App from './components/App.js';
|
|
6
6
|
import { worktreeConfigManager } from './services/worktreeConfigManager.js';
|
|
7
7
|
import { globalSessionOrchestrator } from './services/globalSessionOrchestrator.js';
|
|
8
|
+
const version = typeof CCMANAGER_VERSION !== 'undefined' ? CCMANAGER_VERSION : 'dev';
|
|
8
9
|
const cli = meow(`
|
|
9
10
|
Usage
|
|
10
11
|
$ ccmanager
|
|
@@ -22,6 +23,7 @@ const cli = meow(`
|
|
|
22
23
|
$ ccmanager --devc-up-command "devcontainer up --workspace-folder ." --devc-exec-command "devcontainer exec --workspace-folder ."
|
|
23
24
|
`, {
|
|
24
25
|
importMeta: import.meta,
|
|
26
|
+
version: version,
|
|
25
27
|
flags: {
|
|
26
28
|
multiProject: {
|
|
27
29
|
type: 'boolean',
|
|
@@ -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) => {
|
|
@@ -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
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.4",
|
|
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.4",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.2.4",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.2.4",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.2.4",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.2.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|