ccmanager 4.0.4 → 4.1.1
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/Confirmation.d.ts +2 -3
- package/dist/components/PresetSelector.test.js +4 -0
- package/dist/components/Session.js +7 -18
- package/dist/components/Session.test.d.ts +1 -0
- package/dist/components/Session.test.js +101 -0
- package/dist/services/sessionManager.autoApproval.test.js +15 -0
- package/dist/services/sessionManager.d.ts +1 -0
- package/dist/services/sessionManager.effect.test.js +15 -0
- package/dist/services/sessionManager.js +14 -22
- package/dist/services/sessionManager.test.js +36 -43
- package/dist/types/index.d.ts +2 -1
- package/dist/utils/hookExecutor.test.js +4 -4
- package/dist/utils/worktreeUtils.test.js +1 -1
- package/package.json +7 -6
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import type { Key } from 'ink';
|
|
2
3
|
export interface ConfirmationOption {
|
|
3
4
|
label: string;
|
|
4
5
|
value: string;
|
|
@@ -14,9 +15,7 @@ interface ConfirmationProps {
|
|
|
14
15
|
hint?: React.ReactNode;
|
|
15
16
|
onCancel?: () => void;
|
|
16
17
|
onEscape?: () => void;
|
|
17
|
-
onCustomInput?: (input: string, key:
|
|
18
|
-
[key: string]: boolean;
|
|
19
|
-
}) => boolean;
|
|
18
|
+
onCustomInput?: (input: string, key: Key) => boolean;
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
21
|
* Reusable confirmation component with SelectInput UI pattern
|
|
@@ -65,23 +65,11 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
65
65
|
stdout.write('\x1b[?7l');
|
|
66
66
|
// Clear screen when entering session
|
|
67
67
|
stdout.write('\x1B[2J\x1B[H');
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
const handleSessionRestore = (restoredSession) => {
|
|
68
|
+
// Restore the current terminal state from the headless xterm snapshot.
|
|
69
|
+
const handleSessionRestore = (restoredSession, restoreSnapshot) => {
|
|
71
70
|
if (restoredSession.id === session.id) {
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
// Concatenate all history buffers and write at once for better performance
|
|
75
|
-
const allHistory = Buffer.concat(restoredSession.outputHistory);
|
|
76
|
-
const historyStr = allHistory.toString('utf8');
|
|
77
|
-
// Normalize the output
|
|
78
|
-
const normalized = normalizeLineEndings(historyStr);
|
|
79
|
-
// Remove leading clear screen sequences to avoid double-clear
|
|
80
|
-
const cleaned = normalized
|
|
81
|
-
.replace(/^\x1B\[2J/g, '')
|
|
82
|
-
.replace(/^\x1B\[H/g, '');
|
|
83
|
-
if (cleaned.length > 0) {
|
|
84
|
-
stdout.write(cleaned);
|
|
71
|
+
if (restoreSnapshot.length > 0) {
|
|
72
|
+
stdout.write(restoreSnapshot);
|
|
85
73
|
}
|
|
86
74
|
}
|
|
87
75
|
};
|
|
@@ -102,8 +90,6 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
102
90
|
};
|
|
103
91
|
sessionManager.on('sessionData', handleSessionData);
|
|
104
92
|
sessionManager.on('sessionExit', handleSessionExit);
|
|
105
|
-
// Mark session as active (this will trigger the restore event)
|
|
106
|
-
sessionManager.setSessionActive(session.id, true);
|
|
107
93
|
// Immediately resize the PTY and terminal to current dimensions
|
|
108
94
|
// This fixes rendering issues when terminal width changed while in menu
|
|
109
95
|
// https://github.com/kbwo/ccmanager/issues/2
|
|
@@ -120,6 +106,9 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
120
106
|
catch {
|
|
121
107
|
/* empty */
|
|
122
108
|
}
|
|
109
|
+
// Mark session as active after resizing so the restore snapshot matches
|
|
110
|
+
// the current terminal dimensions.
|
|
111
|
+
sessionManager.setSessionActive(session.id, true);
|
|
123
112
|
// Handle terminal resize
|
|
124
113
|
const handleResize = () => {
|
|
125
114
|
const cols = process.stdout.columns || 80;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { EventEmitter } from 'events';
|
|
5
|
+
const testState = vi.hoisted(() => ({
|
|
6
|
+
stdout: null,
|
|
7
|
+
}));
|
|
8
|
+
class MockStdout extends EventEmitter {
|
|
9
|
+
write = vi.fn();
|
|
10
|
+
}
|
|
11
|
+
vi.mock('ink', async () => {
|
|
12
|
+
const actual = await vi.importActual('ink');
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
useStdout: vi.fn(() => ({ stdout: testState.stdout })),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
import Session from './Session.js';
|
|
19
|
+
describe('Session', () => {
|
|
20
|
+
const originalColumns = process.stdout.columns;
|
|
21
|
+
const originalRows = process.stdout.rows;
|
|
22
|
+
const originalIsTTY = process.stdin.isTTY;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
testState.stdout = new MockStdout();
|
|
25
|
+
Object.defineProperty(process.stdout, 'columns', {
|
|
26
|
+
value: 120,
|
|
27
|
+
configurable: true,
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(process.stdout, 'rows', {
|
|
30
|
+
value: 40,
|
|
31
|
+
configurable: true,
|
|
32
|
+
});
|
|
33
|
+
Object.defineProperty(process.stdin, 'isTTY', {
|
|
34
|
+
value: false,
|
|
35
|
+
configurable: true,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
testState.stdout = null;
|
|
40
|
+
Object.defineProperty(process.stdout, 'columns', {
|
|
41
|
+
value: originalColumns,
|
|
42
|
+
configurable: true,
|
|
43
|
+
});
|
|
44
|
+
Object.defineProperty(process.stdout, 'rows', {
|
|
45
|
+
value: originalRows,
|
|
46
|
+
configurable: true,
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(process.stdin, 'isTTY', {
|
|
49
|
+
value: originalIsTTY,
|
|
50
|
+
configurable: true,
|
|
51
|
+
});
|
|
52
|
+
vi.restoreAllMocks();
|
|
53
|
+
});
|
|
54
|
+
it('resizes before activating and writes restore snapshots verbatim', async () => {
|
|
55
|
+
const listeners = new Map();
|
|
56
|
+
const processResize = vi.fn();
|
|
57
|
+
const processWrite = vi.fn();
|
|
58
|
+
const terminalResize = vi.fn();
|
|
59
|
+
const setSessionActive = vi.fn((sessionId, active) => {
|
|
60
|
+
if (sessionId === session.id && active) {
|
|
61
|
+
for (const handler of listeners.get('sessionRestore') ?? []) {
|
|
62
|
+
handler(session, '\nrestored');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const session = {
|
|
67
|
+
id: 'session-1',
|
|
68
|
+
process: {
|
|
69
|
+
write: processWrite,
|
|
70
|
+
resize: processResize,
|
|
71
|
+
},
|
|
72
|
+
terminal: {
|
|
73
|
+
resize: terminalResize,
|
|
74
|
+
},
|
|
75
|
+
stateMutex: {
|
|
76
|
+
getSnapshot: () => ({ state: 'idle' }),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const sessionManager = {
|
|
80
|
+
on: vi.fn((event, handler) => {
|
|
81
|
+
const handlers = listeners.get(event) ?? new Set();
|
|
82
|
+
handlers.add(handler);
|
|
83
|
+
listeners.set(event, handlers);
|
|
84
|
+
return sessionManager;
|
|
85
|
+
}),
|
|
86
|
+
off: vi.fn((event, handler) => {
|
|
87
|
+
listeners.get(event)?.delete(handler);
|
|
88
|
+
return sessionManager;
|
|
89
|
+
}),
|
|
90
|
+
setSessionActive,
|
|
91
|
+
cancelAutoApproval: vi.fn(),
|
|
92
|
+
};
|
|
93
|
+
render(_jsx(Session, { session: session, sessionManager: sessionManager, onReturnToMenu: vi.fn() }));
|
|
94
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
95
|
+
expect(processResize).toHaveBeenCalledWith(120, 40);
|
|
96
|
+
expect(terminalResize).toHaveBeenCalledWith(120, 40);
|
|
97
|
+
expect(processResize.mock.invocationCallOrder[0] ?? 0).toBeLessThan(setSessionActive.mock.invocationCallOrder[0] ?? 0);
|
|
98
|
+
expect(terminalResize.mock.invocationCallOrder[0] ?? 0).toBeLessThan(setSessionActive.mock.invocationCallOrder[0] ?? 0);
|
|
99
|
+
expect(testState.stdout?.write).toHaveBeenNthCalledWith(3, '\nrestored');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -52,16 +52,31 @@ vi.mock('./config/configReader.js', () => ({
|
|
|
52
52
|
setAutoApprovalEnabled: vi.fn(),
|
|
53
53
|
},
|
|
54
54
|
}));
|
|
55
|
+
vi.mock('@xterm/addon-serialize', () => ({
|
|
56
|
+
SerializeAddon: vi.fn().mockImplementation(function () {
|
|
57
|
+
return {
|
|
58
|
+
serialize: vi.fn(() => ''),
|
|
59
|
+
activate: vi.fn(),
|
|
60
|
+
dispose: vi.fn(),
|
|
61
|
+
};
|
|
62
|
+
}),
|
|
63
|
+
}));
|
|
55
64
|
vi.mock('@xterm/headless', () => ({
|
|
56
65
|
default: {
|
|
57
66
|
Terminal: vi.fn().mockImplementation(function () {
|
|
58
67
|
return {
|
|
68
|
+
rows: 24,
|
|
69
|
+
cols: 80,
|
|
59
70
|
buffer: {
|
|
60
71
|
active: {
|
|
72
|
+
type: 'normal',
|
|
73
|
+
baseY: 0,
|
|
61
74
|
length: 0,
|
|
62
75
|
getLine: vi.fn(),
|
|
63
76
|
},
|
|
64
77
|
},
|
|
78
|
+
loadAddon: vi.fn(),
|
|
79
|
+
resize: vi.fn(),
|
|
65
80
|
write: vi.fn(),
|
|
66
81
|
};
|
|
67
82
|
}),
|
|
@@ -35,6 +35,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
|
|
|
35
35
|
private updateSessionState;
|
|
36
36
|
constructor();
|
|
37
37
|
private createTerminal;
|
|
38
|
+
private getRestoreSnapshot;
|
|
38
39
|
private createSessionInternal;
|
|
39
40
|
/**
|
|
40
41
|
* Create session with command preset using Effect-based error handling
|
|
@@ -33,17 +33,32 @@ vi.mock('./config/configReader.js', () => ({
|
|
|
33
33
|
getStatusHooks: vi.fn(() => ({})),
|
|
34
34
|
},
|
|
35
35
|
}));
|
|
36
|
+
vi.mock('@xterm/addon-serialize', () => ({
|
|
37
|
+
SerializeAddon: vi.fn().mockImplementation(function () {
|
|
38
|
+
return {
|
|
39
|
+
serialize: vi.fn(() => ''),
|
|
40
|
+
activate: vi.fn(),
|
|
41
|
+
dispose: vi.fn(),
|
|
42
|
+
};
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
36
45
|
// Mock Terminal
|
|
37
46
|
vi.mock('@xterm/headless', () => ({
|
|
38
47
|
default: {
|
|
39
48
|
Terminal: vi.fn().mockImplementation(function () {
|
|
40
49
|
return {
|
|
50
|
+
rows: 24,
|
|
51
|
+
cols: 80,
|
|
41
52
|
buffer: {
|
|
42
53
|
active: {
|
|
54
|
+
type: 'normal',
|
|
55
|
+
baseY: 0,
|
|
43
56
|
length: 0,
|
|
44
57
|
getLine: vi.fn(),
|
|
45
58
|
},
|
|
46
59
|
},
|
|
60
|
+
loadAddon: vi.fn(),
|
|
61
|
+
resize: vi.fn(),
|
|
47
62
|
write: vi.fn(),
|
|
48
63
|
};
|
|
49
64
|
}),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from './bunTerminal.js';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
3
|
import pkg from '@xterm/headless';
|
|
4
|
+
import { SerializeAddon } from '@xterm/addon-serialize';
|
|
4
5
|
import { spawn as childSpawn } from 'child_process';
|
|
5
6
|
import { configReader } from './config/configReader.js';
|
|
6
7
|
import { executeStatusHook } from '../utils/hookExecutor.js';
|
|
@@ -17,6 +18,7 @@ import { injectTeammateMode } from '../utils/commandArgs.js';
|
|
|
17
18
|
import { preparePresetLaunch } from '../utils/presetPrompt.js';
|
|
18
19
|
const { Terminal } = pkg;
|
|
19
20
|
const TERMINAL_CONTENT_MAX_LINES = 300;
|
|
21
|
+
const TERMINAL_SCROLLBACK_LINES = 5000;
|
|
20
22
|
export class SessionManager extends EventEmitter {
|
|
21
23
|
sessions;
|
|
22
24
|
waitingWithBottomBorder = new Map();
|
|
@@ -192,14 +194,22 @@ export class SessionManager extends EventEmitter {
|
|
|
192
194
|
return new Terminal({
|
|
193
195
|
cols: process.stdout.columns || 80,
|
|
194
196
|
rows: process.stdout.rows || 24,
|
|
197
|
+
scrollback: TERMINAL_SCROLLBACK_LINES,
|
|
195
198
|
allowProposedApi: true,
|
|
196
199
|
logLevel: 'off',
|
|
197
200
|
});
|
|
198
201
|
}
|
|
202
|
+
getRestoreSnapshot(session) {
|
|
203
|
+
return session.serializer.serialize({
|
|
204
|
+
scrollback: 0,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
199
207
|
async createSessionInternal(worktreePath, ptyProcess, options = {}) {
|
|
200
208
|
const existingSessions = this.getSessionsForWorktree(worktreePath);
|
|
201
209
|
const maxNumber = existingSessions.reduce((max, s) => Math.max(max, s.sessionNumber), 0);
|
|
202
210
|
const terminal = this.createTerminal();
|
|
211
|
+
const serializer = new SerializeAddon();
|
|
212
|
+
terminal.loadAddon(serializer);
|
|
203
213
|
const detectionStrategy = options.detectionStrategy ?? 'claude';
|
|
204
214
|
const stateDetector = createStateDetector(detectionStrategy);
|
|
205
215
|
const session = {
|
|
@@ -212,10 +222,10 @@ export class SessionManager extends EventEmitter {
|
|
|
212
222
|
lastAccessedAt: Date.now(),
|
|
213
223
|
process: ptyProcess,
|
|
214
224
|
output: [],
|
|
215
|
-
outputHistory: [],
|
|
216
225
|
lastActivity: new Date(),
|
|
217
226
|
isActive: false,
|
|
218
227
|
terminal,
|
|
228
|
+
serializer,
|
|
219
229
|
stateCheckInterval: undefined, // Will be set in setupBackgroundHandler
|
|
220
230
|
isPrimaryCommand: options.isPrimaryCommand ?? true,
|
|
221
231
|
presetName: options.presetName,
|
|
@@ -291,24 +301,6 @@ export class SessionManager extends EventEmitter {
|
|
|
291
301
|
session.process.onData((data) => {
|
|
292
302
|
// Write data to virtual terminal
|
|
293
303
|
session.terminal.write(data);
|
|
294
|
-
// Check for screen clear escape sequence (e.g., from /clear command)
|
|
295
|
-
// When detected, clear the output history to prevent replaying old content on restore
|
|
296
|
-
// This helps avoid excessive scrolling when restoring sessions with large output history
|
|
297
|
-
if (data.includes('\x1B[2J')) {
|
|
298
|
-
session.outputHistory = [];
|
|
299
|
-
}
|
|
300
|
-
// Store in output history as Buffer
|
|
301
|
-
const buffer = Buffer.from(data, 'utf8');
|
|
302
|
-
session.outputHistory.push(buffer);
|
|
303
|
-
// Limit memory usage - keep max 10MB of output history
|
|
304
|
-
const MAX_HISTORY_SIZE = 10 * 1024 * 1024; // 10MB
|
|
305
|
-
let totalSize = session.outputHistory.reduce((sum, buf) => sum + buf.length, 0);
|
|
306
|
-
while (totalSize > MAX_HISTORY_SIZE && session.outputHistory.length > 0) {
|
|
307
|
-
const removed = session.outputHistory.shift();
|
|
308
|
-
if (removed) {
|
|
309
|
-
totalSize -= removed.length;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
304
|
session.lastActivity = new Date();
|
|
313
305
|
// Only emit data events when session is active
|
|
314
306
|
if (session.isActive) {
|
|
@@ -479,9 +471,9 @@ export class SessionManager extends EventEmitter {
|
|
|
479
471
|
session.isActive = active;
|
|
480
472
|
if (active) {
|
|
481
473
|
session.lastAccessedAt = Date.now();
|
|
482
|
-
|
|
483
|
-
if (
|
|
484
|
-
this.emit('sessionRestore', session);
|
|
474
|
+
const restoreSnapshot = this.getRestoreSnapshot(session);
|
|
475
|
+
if (restoreSnapshot.length > 0) {
|
|
476
|
+
this.emit('sessionRestore', session, restoreSnapshot);
|
|
485
477
|
}
|
|
486
478
|
}
|
|
487
479
|
}
|
|
@@ -37,19 +37,38 @@ vi.mock('./config/configReader.js', () => ({
|
|
|
37
37
|
setAutoApprovalEnabled: vi.fn(),
|
|
38
38
|
},
|
|
39
39
|
}));
|
|
40
|
+
vi.mock('@xterm/addon-serialize', () => ({
|
|
41
|
+
SerializeAddon: vi.fn().mockImplementation(function () {
|
|
42
|
+
return {
|
|
43
|
+
serialize: vi.fn(() => ''),
|
|
44
|
+
activate: vi.fn(),
|
|
45
|
+
dispose: vi.fn(),
|
|
46
|
+
};
|
|
47
|
+
}),
|
|
48
|
+
}));
|
|
40
49
|
// Mock Terminal
|
|
41
50
|
vi.mock('@xterm/headless', () => ({
|
|
42
51
|
default: {
|
|
43
52
|
Terminal: vi.fn(function () {
|
|
44
53
|
return {
|
|
54
|
+
rows: 24,
|
|
55
|
+
cols: 80,
|
|
45
56
|
buffer: {
|
|
46
57
|
active: {
|
|
58
|
+
type: 'normal',
|
|
59
|
+
baseY: 0,
|
|
47
60
|
length: 0,
|
|
48
61
|
getLine: vi.fn(function () {
|
|
49
62
|
return null;
|
|
50
63
|
}),
|
|
51
64
|
},
|
|
52
65
|
},
|
|
66
|
+
loadAddon: vi.fn(function () {
|
|
67
|
+
return undefined;
|
|
68
|
+
}),
|
|
69
|
+
resize: vi.fn(function () {
|
|
70
|
+
return undefined;
|
|
71
|
+
}),
|
|
53
72
|
write: vi.fn(function () {
|
|
54
73
|
return undefined;
|
|
55
74
|
}),
|
|
@@ -740,63 +759,37 @@ describe('SessionManager', () => {
|
|
|
740
759
|
expect(session.isPrimaryCommand).toBe(false);
|
|
741
760
|
});
|
|
742
761
|
});
|
|
743
|
-
describe('
|
|
744
|
-
it('should
|
|
745
|
-
// Setup
|
|
762
|
+
describe('session restore snapshots', () => {
|
|
763
|
+
it('should emit serialized terminal output when activating a session', async () => {
|
|
746
764
|
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
747
765
|
id: '1',
|
|
748
766
|
name: 'Main',
|
|
749
767
|
command: 'claude',
|
|
750
768
|
});
|
|
751
769
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
752
|
-
// Create session
|
|
753
770
|
const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
});
|
|
765
|
-
it('should not clear output history for normal data', async () => {
|
|
766
|
-
// Setup
|
|
771
|
+
const serializeMock = vi
|
|
772
|
+
.spyOn(session.serializer, 'serialize')
|
|
773
|
+
.mockReturnValue('\u001b[31mrestored\u001b[0m');
|
|
774
|
+
const restoreHandler = vi.fn();
|
|
775
|
+
sessionManager.on('sessionRestore', restoreHandler);
|
|
776
|
+
sessionManager.setSessionActive(session.id, true);
|
|
777
|
+
expect(serializeMock).toHaveBeenCalledWith({ scrollback: 0 });
|
|
778
|
+
expect(restoreHandler).toHaveBeenCalledWith(session, '\u001b[31mrestored\u001b[0m');
|
|
779
|
+
});
|
|
780
|
+
it('should skip restore event when serialized output is empty', async () => {
|
|
767
781
|
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
768
782
|
id: '1',
|
|
769
783
|
name: 'Main',
|
|
770
784
|
command: 'claude',
|
|
771
785
|
});
|
|
772
786
|
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
773
|
-
// Create session
|
|
774
|
-
const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
|
|
775
|
-
// Simulate normal data output without screen clear
|
|
776
|
-
mockPty.emit('data', 'Hello World');
|
|
777
|
-
mockPty.emit('data', 'More data');
|
|
778
|
-
mockPty.emit('data', 'Even more data');
|
|
779
|
-
// Verify output history contains all data
|
|
780
|
-
expect(session.outputHistory.length).toBe(3);
|
|
781
|
-
});
|
|
782
|
-
it('should clear history when screen clear is part of larger data chunk', async () => {
|
|
783
|
-
// Setup
|
|
784
|
-
vi.mocked(configReader.getDefaultPreset).mockReturnValue({
|
|
785
|
-
id: '1',
|
|
786
|
-
name: 'Main',
|
|
787
|
-
command: 'claude',
|
|
788
|
-
});
|
|
789
|
-
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
790
|
-
// Create session
|
|
791
787
|
const session = await Effect.runPromise(sessionManager.createSessionWithPresetEffect('/test/worktree'));
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
// Verify output history was cleared and only contains the new chunk
|
|
798
|
-
expect(session.outputHistory.length).toBe(1);
|
|
799
|
-
expect(session.outputHistory[0]?.toString()).toBe('prefix\x1B[2Jsuffix');
|
|
788
|
+
vi.spyOn(session.serializer, 'serialize').mockReturnValue('');
|
|
789
|
+
const restoreHandler = vi.fn();
|
|
790
|
+
sessionManager.on('sessionRestore', restoreHandler);
|
|
791
|
+
sessionManager.setSessionActive(session.id, true);
|
|
792
|
+
expect(restoreHandler).not.toHaveBeenCalled();
|
|
800
793
|
});
|
|
801
794
|
});
|
|
802
795
|
describe('static methods', () => {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IPty } from '../services/bunTerminal.js';
|
|
2
2
|
import type pkg from '@xterm/headless';
|
|
3
|
+
import type { SerializeAddon } from '@xterm/addon-serialize';
|
|
3
4
|
import { GitStatus } from '../utils/gitStatus.js';
|
|
4
5
|
import { Mutex, SessionStateData } from '../utils/mutex.js';
|
|
5
6
|
import type { StateDetector } from '../services/stateDetector/types.js';
|
|
@@ -25,10 +26,10 @@ export interface Session {
|
|
|
25
26
|
lastAccessedAt: number;
|
|
26
27
|
process: IPty;
|
|
27
28
|
output: string[];
|
|
28
|
-
outputHistory: Buffer[];
|
|
29
29
|
lastActivity: Date;
|
|
30
30
|
isActive: boolean;
|
|
31
31
|
terminal: Terminal;
|
|
32
|
+
serializer: SerializeAddon;
|
|
32
33
|
stateCheckInterval: NodeJS.Timeout | undefined;
|
|
33
34
|
isPrimaryCommand: boolean;
|
|
34
35
|
presetName: string | undefined;
|
|
@@ -382,8 +382,8 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
382
382
|
lastAccessedAt: Date.now(),
|
|
383
383
|
process: {},
|
|
384
384
|
terminal: {},
|
|
385
|
+
serializer: {},
|
|
385
386
|
output: [],
|
|
386
|
-
outputHistory: [],
|
|
387
387
|
stateCheckInterval: undefined,
|
|
388
388
|
isPrimaryCommand: true,
|
|
389
389
|
presetName: undefined,
|
|
@@ -441,8 +441,8 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
441
441
|
lastAccessedAt: Date.now(),
|
|
442
442
|
process: {},
|
|
443
443
|
terminal: {},
|
|
444
|
+
serializer: {},
|
|
444
445
|
output: [],
|
|
445
|
-
outputHistory: [],
|
|
446
446
|
stateCheckInterval: undefined,
|
|
447
447
|
isPrimaryCommand: true,
|
|
448
448
|
presetName: undefined,
|
|
@@ -498,8 +498,8 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
498
498
|
lastAccessedAt: Date.now(),
|
|
499
499
|
process: {},
|
|
500
500
|
terminal: {},
|
|
501
|
+
serializer: {},
|
|
501
502
|
output: [],
|
|
502
|
-
outputHistory: [],
|
|
503
503
|
stateCheckInterval: undefined,
|
|
504
504
|
isPrimaryCommand: true,
|
|
505
505
|
presetName: undefined,
|
|
@@ -557,8 +557,8 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
557
557
|
lastAccessedAt: Date.now(),
|
|
558
558
|
process: {},
|
|
559
559
|
terminal: {},
|
|
560
|
+
serializer: {},
|
|
560
561
|
output: [],
|
|
561
|
-
outputHistory: [],
|
|
562
562
|
stateCheckInterval: undefined,
|
|
563
563
|
isPrimaryCommand: true,
|
|
564
564
|
presetName: undefined,
|
|
@@ -129,10 +129,10 @@ describe('prepareSessionItems', () => {
|
|
|
129
129
|
lastAccessedAt: Date.now(),
|
|
130
130
|
process: {},
|
|
131
131
|
output: [],
|
|
132
|
-
outputHistory: [],
|
|
133
132
|
lastActivity: new Date(),
|
|
134
133
|
isActive: true,
|
|
135
134
|
terminal: {},
|
|
135
|
+
serializer: {},
|
|
136
136
|
stateCheckInterval: undefined,
|
|
137
137
|
isPrimaryCommand: true,
|
|
138
138
|
presetName: undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
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": "4.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "4.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "4.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "4.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "4.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "4.1.1",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.1",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.1",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "4.1.1",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "4.1.1"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
},
|
|
69
69
|
"prettier": "@vdemedes/prettier-config",
|
|
70
70
|
"dependencies": {
|
|
71
|
+
"@xterm/addon-serialize": "^0.14.0",
|
|
71
72
|
"@xterm/headless": "^6.0.0",
|
|
72
73
|
"effect": "^3.18.2",
|
|
73
74
|
"ink": "^6.6.0",
|