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 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
- // Spawn the process with Bun's built-in terminal support
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: options.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._subprocess?.terminal) {
282
+ if (this._closed || !this._terminal) {
140
283
  return;
141
284
  }
142
- this._subprocess.terminal.write(data);
285
+ this._terminal.write(data);
143
286
  }
144
287
  resize(columns, rows) {
145
- if (this._closed || !this._subprocess?.terminal) {
288
+ if (this._closed || !this._terminal) {
146
289
  return;
147
290
  }
148
291
  this._cols = columns;
149
292
  this._rows = rows;
150
- this._subprocess.terminal.resize(columns, rows);
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
- if (this._subprocess?.terminal) {
158
- this._subprocess.terminal.close();
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.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.2",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "3.2.2",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "3.2.2",
47
- "@kodaikabasawa/ccmanager-linux-x64": "3.2.2",
48
- "@kodaikabasawa/ccmanager-win32-x64": "3.2.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",