ccmanager 0.1.0 → 0.1.2
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/README.md +33 -6
- package/dist/components/App.js +6 -6
- package/dist/components/Configuration.d.ts +6 -0
- package/dist/components/Configuration.js +49 -0
- package/dist/components/ConfigureHooks.d.ts +6 -0
- package/dist/components/ConfigureHooks.js +133 -0
- package/dist/components/Menu.js +4 -4
- package/dist/components/Session.js +19 -18
- package/dist/services/configurationManager.d.ts +17 -0
- package/dist/services/configurationManager.js +115 -0
- package/dist/services/sessionManager.colorRestore.test.d.ts +1 -0
- package/dist/services/sessionManager.colorRestore.test.js +142 -0
- package/dist/services/sessionManager.d.ts +1 -0
- package/dist/services/sessionManager.integration.test.d.ts +1 -0
- package/dist/services/sessionManager.integration.test.js +178 -0
- package/dist/services/sessionManager.js +41 -16
- package/dist/services/shortcutManager.d.ts +0 -3
- package/dist/services/shortcutManager.js +11 -59
- package/dist/services/worktreeService.js +6 -1
- package/dist/types/index.d.ts +16 -1
- package/dist/utils/terminalSerializer.d.ts +119 -0
- package/dist/utils/terminalSerializer.js +376 -0
- package/dist/utils/terminalSerializer.test.d.ts +1 -0
- package/dist/utils/terminalSerializer.test.js +137 -0
- package/package.json +1 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { SessionManager } from './sessionManager.js';
|
|
3
|
+
import { spawn } from 'node-pty';
|
|
4
|
+
// Create mock pty process
|
|
5
|
+
const createMockPtyProcess = () => {
|
|
6
|
+
const handlers = {
|
|
7
|
+
data: [],
|
|
8
|
+
exit: [],
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
write: vi.fn(),
|
|
12
|
+
resize: vi.fn(),
|
|
13
|
+
onData: vi.fn((handler) => {
|
|
14
|
+
handlers.data.push(handler);
|
|
15
|
+
}),
|
|
16
|
+
onExit: vi.fn((handler) => {
|
|
17
|
+
handlers.exit.push(handler);
|
|
18
|
+
}),
|
|
19
|
+
kill: vi.fn(),
|
|
20
|
+
_emit: (event, ...args) => {
|
|
21
|
+
if (event === 'data' && handlers.data.length > 0) {
|
|
22
|
+
handlers.data.forEach(h => h(args[0]));
|
|
23
|
+
}
|
|
24
|
+
else if (event === 'exit' && handlers.exit.length > 0) {
|
|
25
|
+
handlers.exit.forEach(h => h(args[0]));
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
// Mock node-pty
|
|
31
|
+
vi.mock('node-pty', () => ({
|
|
32
|
+
spawn: vi.fn(),
|
|
33
|
+
}));
|
|
34
|
+
// Mock @xterm/headless
|
|
35
|
+
vi.mock('@xterm/headless', () => ({
|
|
36
|
+
default: {
|
|
37
|
+
Terminal: vi.fn().mockImplementation(() => ({
|
|
38
|
+
buffer: {
|
|
39
|
+
active: {
|
|
40
|
+
length: 10,
|
|
41
|
+
cursorY: 0,
|
|
42
|
+
cursorX: 0,
|
|
43
|
+
getLine: vi.fn(),
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
write: vi.fn(),
|
|
47
|
+
resize: vi.fn(),
|
|
48
|
+
clear: vi.fn(),
|
|
49
|
+
onData: vi.fn(),
|
|
50
|
+
})),
|
|
51
|
+
},
|
|
52
|
+
}));
|
|
53
|
+
describe('SessionManager - Partial TUI Update Integration', () => {
|
|
54
|
+
let sessionManager;
|
|
55
|
+
const mockWorktreePath = '/test/worktree';
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
sessionManager = new SessionManager();
|
|
58
|
+
vi.clearAllMocks();
|
|
59
|
+
});
|
|
60
|
+
it('should not accumulate duplicate content in output history', () => {
|
|
61
|
+
// Create a mock PTY process
|
|
62
|
+
const mockProcess = createMockPtyProcess();
|
|
63
|
+
vi.mocked(spawn).mockReturnValue(mockProcess);
|
|
64
|
+
// Create a session
|
|
65
|
+
sessionManager.createSession(mockWorktreePath);
|
|
66
|
+
const session = sessionManager.sessions.get(mockWorktreePath);
|
|
67
|
+
expect(session).toBeDefined();
|
|
68
|
+
// Simulate multiple partial updates from Claude Code
|
|
69
|
+
const updates = [
|
|
70
|
+
'+ Exploring... (10s ・ 53 tokens ・ esc to interrupt)\r',
|
|
71
|
+
'\x1B[1A\x1B[K+ Exploring... (10s ・ 57 tokens ・ esc to interrupt)\r',
|
|
72
|
+
'\x1B[1A\x1B[K+ Exploring... (10s ・ 76 tokens ・ esc to interrupt)\r',
|
|
73
|
+
'\x1B[1A\x1B[K+ Exploring... (10s ・ 89 tokens ・ esc to interrupt)\r',
|
|
74
|
+
'\x1B[1A\x1B[K+ Exploring... (10s ・ 102 tokens ・ esc to interrupt)\r',
|
|
75
|
+
];
|
|
76
|
+
// Process each update
|
|
77
|
+
updates.forEach(update => {
|
|
78
|
+
// Simulate PTY data event
|
|
79
|
+
mockProcess._emit('data', update);
|
|
80
|
+
});
|
|
81
|
+
// Check that the virtual terminal received all updates
|
|
82
|
+
expect(session.terminal.write).toHaveBeenCalledTimes(5);
|
|
83
|
+
// The outputHistory should be empty since we removed that functionality
|
|
84
|
+
expect(session.outputHistory).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
it('should use virtual terminal buffer for session restoration', () => {
|
|
87
|
+
// Create a mock PTY process
|
|
88
|
+
const mockProcess = createMockPtyProcess();
|
|
89
|
+
vi.mocked(spawn).mockReturnValue(mockProcess);
|
|
90
|
+
sessionManager.createSession(mockWorktreePath);
|
|
91
|
+
const session = sessionManager.sessions.get(mockWorktreePath);
|
|
92
|
+
// Mock the terminal buffer to contain the final state
|
|
93
|
+
// Type cast is acceptable for test mocks
|
|
94
|
+
const mockTerminal = session.terminal;
|
|
95
|
+
mockTerminal.buffer.active.getLine = vi.fn((index) => {
|
|
96
|
+
const lines = [
|
|
97
|
+
'Welcome to Claude Code',
|
|
98
|
+
'+ Exploring... (10s ・ 218 tokens ・ esc to interrupt)',
|
|
99
|
+
'',
|
|
100
|
+
'Task completed successfully',
|
|
101
|
+
'> ',
|
|
102
|
+
];
|
|
103
|
+
if (index < lines.length) {
|
|
104
|
+
return {
|
|
105
|
+
translateToString: () => lines[index],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
});
|
|
110
|
+
mockTerminal.buffer.active.length = 5;
|
|
111
|
+
mockTerminal.buffer.active.cursorY = 4;
|
|
112
|
+
mockTerminal.buffer.active.cursorX = 2;
|
|
113
|
+
// Emit restore event
|
|
114
|
+
sessionManager.emit('sessionRestore', session);
|
|
115
|
+
// The terminal buffer should be used for restoration, not output history
|
|
116
|
+
// This prevents duplicate content issues
|
|
117
|
+
expect(session.outputHistory).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
it('should handle ANSI escape sequences correctly in virtual terminal', () => {
|
|
120
|
+
// Create a mock PTY process
|
|
121
|
+
const mockProcess = createMockPtyProcess();
|
|
122
|
+
vi.mocked(spawn).mockReturnValue(mockProcess);
|
|
123
|
+
sessionManager.createSession(mockWorktreePath);
|
|
124
|
+
const session = sessionManager.sessions.get(mockWorktreePath);
|
|
125
|
+
// Simulate data with ANSI escape sequences
|
|
126
|
+
const dataWithEscapes = [
|
|
127
|
+
'Line 1\n',
|
|
128
|
+
'Line 2\n',
|
|
129
|
+
'\x1B[1A\x1B[KReplaced Line 2\n', // Move up one line, clear line, write new text
|
|
130
|
+
'\x1B[2J\x1B[H', // Clear screen and move to home
|
|
131
|
+
'Fresh start\n',
|
|
132
|
+
];
|
|
133
|
+
dataWithEscapes.forEach(data => {
|
|
134
|
+
mockProcess._emit('data', data);
|
|
135
|
+
});
|
|
136
|
+
// Virtual terminal should handle all the escape sequences
|
|
137
|
+
expect(session.terminal.write).toHaveBeenCalledTimes(5);
|
|
138
|
+
// No raw output should be stored
|
|
139
|
+
expect(session.outputHistory).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
it('should emit sessionData events for active sessions only', () => {
|
|
142
|
+
// Create a mock PTY process
|
|
143
|
+
const mockProcess = createMockPtyProcess();
|
|
144
|
+
vi.mocked(spawn).mockReturnValue(mockProcess);
|
|
145
|
+
const dataHandler = vi.fn();
|
|
146
|
+
sessionManager.on('sessionData', dataHandler);
|
|
147
|
+
sessionManager.createSession(mockWorktreePath);
|
|
148
|
+
const session = sessionManager.sessions.get(mockWorktreePath);
|
|
149
|
+
// Session is not active by default
|
|
150
|
+
mockProcess._emit('data', 'Test data 1');
|
|
151
|
+
// Should not emit data when inactive
|
|
152
|
+
expect(dataHandler).not.toHaveBeenCalled();
|
|
153
|
+
// Activate session
|
|
154
|
+
sessionManager.setSessionActive(mockWorktreePath, true);
|
|
155
|
+
// Now data should be emitted
|
|
156
|
+
mockProcess._emit('data', 'Test data 2');
|
|
157
|
+
expect(dataHandler).toHaveBeenCalledWith(session, 'Test data 2');
|
|
158
|
+
});
|
|
159
|
+
it('should restore session without replaying output history', () => {
|
|
160
|
+
// Create a mock PTY process
|
|
161
|
+
const mockProcess = createMockPtyProcess();
|
|
162
|
+
vi.mocked(spawn).mockReturnValue(mockProcess);
|
|
163
|
+
const restoreHandler = vi.fn();
|
|
164
|
+
sessionManager.on('sessionRestore', restoreHandler);
|
|
165
|
+
sessionManager.createSession(mockWorktreePath);
|
|
166
|
+
const session = sessionManager.sessions.get(mockWorktreePath);
|
|
167
|
+
// Add some data to the session
|
|
168
|
+
mockProcess._emit('data', 'Old output that should not be replayed\n');
|
|
169
|
+
mockProcess._emit('data', 'More old output\n');
|
|
170
|
+
// Deactivate then reactivate session
|
|
171
|
+
sessionManager.setSessionActive(mockWorktreePath, false);
|
|
172
|
+
sessionManager.setSessionActive(mockWorktreePath, true);
|
|
173
|
+
// Should emit restore event
|
|
174
|
+
expect(restoreHandler).toHaveBeenCalledWith(session);
|
|
175
|
+
// But should not have any output history to replay
|
|
176
|
+
expect(session.outputHistory).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'node-pty';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
3
|
import pkg from '@xterm/headless';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { configurationManager } from './configurationManager.js';
|
|
6
|
+
import { WorktreeService } from './worktreeService.js';
|
|
4
7
|
const { Terminal } = pkg;
|
|
5
8
|
export class SessionManager extends EventEmitter {
|
|
6
9
|
stripAnsi(str) {
|
|
@@ -101,7 +104,7 @@ export class SessionManager extends EventEmitter {
|
|
|
101
104
|
process: ptyProcess,
|
|
102
105
|
state: 'busy', // Session starts as busy when created
|
|
103
106
|
output: [],
|
|
104
|
-
outputHistory: [],
|
|
107
|
+
outputHistory: [], // Kept for backward compatibility but no longer used
|
|
105
108
|
lastActivity: new Date(),
|
|
106
109
|
isActive: false,
|
|
107
110
|
terminal,
|
|
@@ -115,20 +118,10 @@ export class SessionManager extends EventEmitter {
|
|
|
115
118
|
setupBackgroundHandler(session) {
|
|
116
119
|
// This handler always runs for all data
|
|
117
120
|
session.process.onData((data) => {
|
|
118
|
-
// Write data to virtual terminal
|
|
121
|
+
// Write data to virtual terminal - this maintains the proper rendered state
|
|
119
122
|
session.terminal.write(data);
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
session.outputHistory.push(buffer);
|
|
123
|
-
// Limit memory usage - keep max 10MB of output history
|
|
124
|
-
const MAX_HISTORY_SIZE = 10 * 1024 * 1024; // 10MB
|
|
125
|
-
let totalSize = session.outputHistory.reduce((sum, buf) => sum + buf.length, 0);
|
|
126
|
-
while (totalSize > MAX_HISTORY_SIZE && session.outputHistory.length > 0) {
|
|
127
|
-
const removed = session.outputHistory.shift();
|
|
128
|
-
if (removed) {
|
|
129
|
-
totalSize -= removed.length;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
123
|
+
// We no longer need to maintain outputHistory since we use the virtual terminal buffer
|
|
124
|
+
// This prevents duplicate content issues and reduces memory usage
|
|
132
125
|
session.lastActivity = new Date();
|
|
133
126
|
// Only emit data events when session is active
|
|
134
127
|
if (session.isActive) {
|
|
@@ -141,6 +134,7 @@ export class SessionManager extends EventEmitter {
|
|
|
141
134
|
const newState = this.detectTerminalState(session.terminal);
|
|
142
135
|
if (newState !== oldState) {
|
|
143
136
|
session.state = newState;
|
|
137
|
+
this.executeStatusHook(oldState, newState, session);
|
|
144
138
|
this.emit('sessionStateChanged', session);
|
|
145
139
|
}
|
|
146
140
|
}, 100); // Check every 100ms
|
|
@@ -163,8 +157,9 @@ export class SessionManager extends EventEmitter {
|
|
|
163
157
|
const session = this.sessions.get(worktreePath);
|
|
164
158
|
if (session) {
|
|
165
159
|
session.isActive = active;
|
|
166
|
-
// If becoming active, emit a restore event
|
|
167
|
-
|
|
160
|
+
// If becoming active, emit a restore event
|
|
161
|
+
// The Session component will use the virtual terminal buffer instead of outputHistory
|
|
162
|
+
if (active) {
|
|
168
163
|
this.emit('sessionRestore', session);
|
|
169
164
|
}
|
|
170
165
|
}
|
|
@@ -196,6 +191,36 @@ export class SessionManager extends EventEmitter {
|
|
|
196
191
|
getAllSessions() {
|
|
197
192
|
return Array.from(this.sessions.values());
|
|
198
193
|
}
|
|
194
|
+
executeStatusHook(oldState, newState, session) {
|
|
195
|
+
const statusHooks = configurationManager.getStatusHooks();
|
|
196
|
+
const hook = statusHooks[newState];
|
|
197
|
+
if (hook && hook.enabled && hook.command) {
|
|
198
|
+
// Get branch information
|
|
199
|
+
const worktreeService = new WorktreeService();
|
|
200
|
+
const worktrees = worktreeService.getWorktrees();
|
|
201
|
+
const worktree = worktrees.find(wt => wt.path === session.worktreePath);
|
|
202
|
+
const branch = worktree?.branch || 'unknown';
|
|
203
|
+
// Execute the hook command in the session's worktree directory
|
|
204
|
+
exec(hook.command, {
|
|
205
|
+
cwd: session.worktreePath,
|
|
206
|
+
env: {
|
|
207
|
+
...process.env,
|
|
208
|
+
CCMANAGER_OLD_STATE: oldState,
|
|
209
|
+
CCMANAGER_NEW_STATE: newState,
|
|
210
|
+
CCMANAGER_WORKTREE: session.worktreePath,
|
|
211
|
+
CCMANAGER_WORKTREE_BRANCH: branch,
|
|
212
|
+
CCMANAGER_SESSION_ID: session.id,
|
|
213
|
+
},
|
|
214
|
+
}, (error, _stdout, stderr) => {
|
|
215
|
+
if (error) {
|
|
216
|
+
console.error(`Failed to execute ${newState} hook: ${error.message}`);
|
|
217
|
+
}
|
|
218
|
+
if (stderr) {
|
|
219
|
+
console.error(`Hook stderr: ${stderr}`);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
199
224
|
destroy() {
|
|
200
225
|
// Clean up all sessions
|
|
201
226
|
for (const worktreePath of this.sessions.keys()) {
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { ShortcutKey, ShortcutConfig } from '../types/index.js';
|
|
2
2
|
import { Key } from 'ink';
|
|
3
3
|
export declare class ShortcutManager {
|
|
4
|
-
private shortcuts;
|
|
5
|
-
private configPath;
|
|
6
4
|
private reservedKeys;
|
|
7
5
|
constructor();
|
|
8
|
-
private loadShortcuts;
|
|
9
6
|
private validateShortcut;
|
|
10
7
|
private isReservedKey;
|
|
11
8
|
saveShortcuts(shortcuts: ShortcutConfig): boolean;
|
|
@@ -1,21 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import * as os from 'os';
|
|
1
|
+
import { configurationManager } from './configurationManager.js';
|
|
5
2
|
export class ShortcutManager {
|
|
6
3
|
constructor() {
|
|
7
|
-
Object.defineProperty(this, "shortcuts", {
|
|
8
|
-
enumerable: true,
|
|
9
|
-
configurable: true,
|
|
10
|
-
writable: true,
|
|
11
|
-
value: void 0
|
|
12
|
-
});
|
|
13
|
-
Object.defineProperty(this, "configPath", {
|
|
14
|
-
enumerable: true,
|
|
15
|
-
configurable: true,
|
|
16
|
-
writable: true,
|
|
17
|
-
value: void 0
|
|
18
|
-
});
|
|
19
4
|
Object.defineProperty(this, "reservedKeys", {
|
|
20
5
|
enumerable: true,
|
|
21
6
|
configurable: true,
|
|
@@ -27,31 +12,6 @@ export class ShortcutManager {
|
|
|
27
12
|
{ ctrl: true, key: '[' },
|
|
28
13
|
]
|
|
29
14
|
});
|
|
30
|
-
// Use platform-specific config directory
|
|
31
|
-
const configDir = process.platform === 'win32'
|
|
32
|
-
? path.join(process.env['APPDATA'] || os.homedir(), 'ccmanager')
|
|
33
|
-
: path.join(os.homedir(), '.config', 'ccmanager');
|
|
34
|
-
this.configPath = path.join(configDir, 'shortcuts.json');
|
|
35
|
-
this.shortcuts = this.loadShortcuts();
|
|
36
|
-
}
|
|
37
|
-
loadShortcuts() {
|
|
38
|
-
try {
|
|
39
|
-
if (fs.existsSync(this.configPath)) {
|
|
40
|
-
const data = fs.readFileSync(this.configPath, 'utf8');
|
|
41
|
-
const loaded = JSON.parse(data);
|
|
42
|
-
// Validate loaded shortcuts
|
|
43
|
-
const validated = {
|
|
44
|
-
returnToMenu: this.validateShortcut(loaded.returnToMenu) ||
|
|
45
|
-
DEFAULT_SHORTCUTS.returnToMenu,
|
|
46
|
-
cancel: this.validateShortcut(loaded.cancel) || DEFAULT_SHORTCUTS.cancel,
|
|
47
|
-
};
|
|
48
|
-
return validated;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
console.error('Failed to load shortcuts:', error);
|
|
53
|
-
}
|
|
54
|
-
return { ...DEFAULT_SHORTCUTS };
|
|
55
15
|
}
|
|
56
16
|
validateShortcut(shortcut) {
|
|
57
17
|
if (!shortcut || typeof shortcut !== 'object') {
|
|
@@ -88,30 +48,21 @@ export class ShortcutManager {
|
|
|
88
48
|
}
|
|
89
49
|
saveShortcuts(shortcuts) {
|
|
90
50
|
// Validate all shortcuts
|
|
51
|
+
const currentShortcuts = configurationManager.getShortcuts();
|
|
91
52
|
const validated = {
|
|
92
53
|
returnToMenu: this.validateShortcut(shortcuts.returnToMenu) ||
|
|
93
|
-
|
|
94
|
-
cancel: this.validateShortcut(shortcuts.cancel) ||
|
|
54
|
+
currentShortcuts.returnToMenu,
|
|
55
|
+
cancel: this.validateShortcut(shortcuts.cancel) || currentShortcuts.cancel,
|
|
95
56
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!fs.existsSync(dir)) {
|
|
99
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
100
|
-
}
|
|
101
|
-
fs.writeFileSync(this.configPath, JSON.stringify(validated, null, 2));
|
|
102
|
-
this.shortcuts = validated;
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
console.error('Failed to save shortcuts:', error);
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
57
|
+
configurationManager.setShortcuts(validated);
|
|
58
|
+
return true;
|
|
109
59
|
}
|
|
110
60
|
getShortcuts() {
|
|
111
|
-
return
|
|
61
|
+
return configurationManager.getShortcuts();
|
|
112
62
|
}
|
|
113
63
|
matchesShortcut(shortcutName, input, key) {
|
|
114
|
-
const
|
|
64
|
+
const shortcuts = configurationManager.getShortcuts();
|
|
65
|
+
const shortcut = shortcuts[shortcutName];
|
|
115
66
|
if (!shortcut)
|
|
116
67
|
return false;
|
|
117
68
|
// Handle escape key specially
|
|
@@ -129,7 +80,8 @@ export class ShortcutManager {
|
|
|
129
80
|
return input.toLowerCase() === shortcut.key.toLowerCase();
|
|
130
81
|
}
|
|
131
82
|
getShortcutDisplay(shortcutName) {
|
|
132
|
-
const
|
|
83
|
+
const shortcuts = configurationManager.getShortcuts();
|
|
84
|
+
const shortcut = shortcuts[shortcutName];
|
|
133
85
|
if (!shortcut)
|
|
134
86
|
return '';
|
|
135
87
|
const parts = [];
|
|
@@ -32,7 +32,12 @@ export class WorktreeService {
|
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
else if (line.startsWith('branch ')) {
|
|
35
|
-
|
|
35
|
+
let branch = line.substring(7);
|
|
36
|
+
// Remove refs/heads/ prefix if present
|
|
37
|
+
if (branch.startsWith('refs/heads/')) {
|
|
38
|
+
branch = branch.substring(11);
|
|
39
|
+
}
|
|
40
|
+
currentWorktree.branch = branch;
|
|
36
41
|
}
|
|
37
42
|
else if (line === 'bare') {
|
|
38
43
|
currentWorktree.isMainWorktree = true;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { IPty } from 'node-pty';
|
|
2
|
+
import type pkg from '@xterm/headless';
|
|
3
|
+
export type Terminal = InstanceType<typeof pkg.Terminal>;
|
|
2
4
|
export type SessionState = 'idle' | 'busy' | 'waiting_input';
|
|
3
5
|
export interface Worktree {
|
|
4
6
|
path: string;
|
|
@@ -15,7 +17,7 @@ export interface Session {
|
|
|
15
17
|
outputHistory: Buffer[];
|
|
16
18
|
lastActivity: Date;
|
|
17
19
|
isActive: boolean;
|
|
18
|
-
terminal:
|
|
20
|
+
terminal: Terminal;
|
|
19
21
|
stateCheckInterval?: NodeJS.Timeout;
|
|
20
22
|
}
|
|
21
23
|
export interface SessionManager {
|
|
@@ -36,3 +38,16 @@ export interface ShortcutConfig {
|
|
|
36
38
|
cancel: ShortcutKey;
|
|
37
39
|
}
|
|
38
40
|
export declare const DEFAULT_SHORTCUTS: ShortcutConfig;
|
|
41
|
+
export interface StatusHook {
|
|
42
|
+
command: string;
|
|
43
|
+
enabled: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface StatusHookConfig {
|
|
46
|
+
idle?: StatusHook;
|
|
47
|
+
busy?: StatusHook;
|
|
48
|
+
waiting_input?: StatusHook;
|
|
49
|
+
}
|
|
50
|
+
export interface ConfigurationData {
|
|
51
|
+
shortcuts?: ShortcutConfig;
|
|
52
|
+
statusHooks?: StatusHookConfig;
|
|
53
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Terminal } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* TerminalSerializer: Converts terminal screen content to text while preserving colors and formatting.
|
|
4
|
+
*
|
|
5
|
+
* Imagine taking a screenshot of your terminal, but instead of an image, you get text that
|
|
6
|
+
* can recreate the exact same appearance with all colors and styles when displayed again.
|
|
7
|
+
*/
|
|
8
|
+
export declare class TerminalSerializer {
|
|
9
|
+
/**
|
|
10
|
+
* ESC: The "magic" prefix that tells the terminal "the next characters are instructions, not text"
|
|
11
|
+
* '\x1b[' is like saying "Hey terminal, listen up for special commands!"
|
|
12
|
+
*/
|
|
13
|
+
private static readonly ESC;
|
|
14
|
+
/**
|
|
15
|
+
* RESET: The command that tells terminal "go back to normal text" (no colors, no bold, etc.)
|
|
16
|
+
* Like clicking "clear formatting" in a word processor
|
|
17
|
+
*/
|
|
18
|
+
private static readonly RESET;
|
|
19
|
+
/**
|
|
20
|
+
* Determines which color system is being used for a given color value.
|
|
21
|
+
*
|
|
22
|
+
* Terminal supports different color systems:
|
|
23
|
+
* - Mode 0 (DEFAULT): Basic terminal colors (like white text on black background)
|
|
24
|
+
* - Mode 1 (P16): 16 basic colors (8 normal + 8 bright versions)
|
|
25
|
+
* - Mode 2 (P256): 256 colors (used for more variety)
|
|
26
|
+
* - Mode 3 (RGB): True color with Red, Green, Blue values (16.7 million colors)
|
|
27
|
+
*
|
|
28
|
+
* @param colorValue - The packed color information from the terminal
|
|
29
|
+
* @returns 0, 1, 2, or 3 indicating which color system to use
|
|
30
|
+
*/
|
|
31
|
+
private static getColorMode;
|
|
32
|
+
/**
|
|
33
|
+
* Extracts the actual color value from the packed color information.
|
|
34
|
+
*
|
|
35
|
+
* The extraction method depends on the color mode:
|
|
36
|
+
* - For RGB mode: Extracts all three color components (R, G, B)
|
|
37
|
+
* - For palette modes: Extracts just the color index number
|
|
38
|
+
*
|
|
39
|
+
* Think of it like unpacking a suitcase - different items are packed differently
|
|
40
|
+
*
|
|
41
|
+
* @param colorValue - The packed color information from the terminal
|
|
42
|
+
* @returns The actual color value (either RGB values or palette index)
|
|
43
|
+
*/
|
|
44
|
+
private static extractColor;
|
|
45
|
+
/**
|
|
46
|
+
* Converts a color value into the special text codes that terminals understand.
|
|
47
|
+
*
|
|
48
|
+
* Terminals use "ANSI escape sequences" - special character combinations that control
|
|
49
|
+
* how text appears. It's like HTML tags, but for terminals.
|
|
50
|
+
*
|
|
51
|
+
* Examples of what this function produces:
|
|
52
|
+
* - Red text: "\x1b[31m" (tells terminal "make the following text red")
|
|
53
|
+
* - Blue background: "\x1b[44m" (tells terminal "make the background blue")
|
|
54
|
+
* - RGB color: "\x1b[38;2;255;128;0m" (orange text using RGB values)
|
|
55
|
+
*
|
|
56
|
+
* @param colorValue - The color to convert (packed number with mode and color data)
|
|
57
|
+
* @param isBackground - true for background color, false for text color
|
|
58
|
+
* @returns ANSI escape sequence string that terminals can interpret
|
|
59
|
+
*/
|
|
60
|
+
private static colorToAnsi;
|
|
61
|
+
/**
|
|
62
|
+
* Converts a single line of terminal content into text with color/style codes.
|
|
63
|
+
*
|
|
64
|
+
* This function processes each character in a line and:
|
|
65
|
+
* 1. Extracts the character itself
|
|
66
|
+
* 2. Checks its color (text and background)
|
|
67
|
+
* 3. Checks its style (bold, italic, underline, etc.)
|
|
68
|
+
* 4. Adds the necessary codes to recreate that appearance
|
|
69
|
+
*
|
|
70
|
+
* It's like going through a line character by character and noting:
|
|
71
|
+
* "This letter is red, this one is bold, this one has blue background..."
|
|
72
|
+
*
|
|
73
|
+
* @param line - One line from the terminal screen
|
|
74
|
+
* @param cols - Number of columns (width) of the terminal
|
|
75
|
+
* @param trimRight - Whether to remove trailing spaces (like right-trim in text editors)
|
|
76
|
+
* @returns The line as text with embedded color/style codes
|
|
77
|
+
*/
|
|
78
|
+
private static serializeLine;
|
|
79
|
+
/**
|
|
80
|
+
* Converts the entire terminal screen (or part of it) into text with colors preserved.
|
|
81
|
+
*
|
|
82
|
+
* This is the main function that processes multiple lines of terminal content.
|
|
83
|
+
* It's like taking a "text screenshot" of your terminal that can be replayed later
|
|
84
|
+
* with all the colors and formatting intact.
|
|
85
|
+
*
|
|
86
|
+
* @param terminal - The terminal object containing the screen buffer
|
|
87
|
+
* @param options - Configuration options:
|
|
88
|
+
* - startLine: First line to include (default: 0, the top)
|
|
89
|
+
* - endLine: Last line to include (default: bottom of screen)
|
|
90
|
+
* - trimRight: Remove trailing spaces from each line (default: true)
|
|
91
|
+
* - includeEmptyLines: Keep blank lines in output (default: true)
|
|
92
|
+
* @returns Multi-line string with embedded ANSI codes for colors/styles
|
|
93
|
+
*/
|
|
94
|
+
static serialize(terminal: Terminal, options?: {
|
|
95
|
+
startLine?: number;
|
|
96
|
+
endLine?: number;
|
|
97
|
+
trimRight?: boolean;
|
|
98
|
+
includeEmptyLines?: boolean;
|
|
99
|
+
}): string;
|
|
100
|
+
/**
|
|
101
|
+
* Convenience function to get just the last few lines from the terminal.
|
|
102
|
+
*
|
|
103
|
+
* Useful when you only need recent output, like:
|
|
104
|
+
* - Getting the last error message
|
|
105
|
+
* - Showing recent command output
|
|
106
|
+
* - Displaying the current prompt
|
|
107
|
+
*
|
|
108
|
+
* Example: getLastLines(terminal, 10) gets the last 10 lines
|
|
109
|
+
*
|
|
110
|
+
* @param terminal - The terminal object containing the screen buffer
|
|
111
|
+
* @param lineCount - How many lines from the bottom to include
|
|
112
|
+
* @param options - Same options as serialize() for controlling output format
|
|
113
|
+
* @returns The requested lines as text with color/style codes
|
|
114
|
+
*/
|
|
115
|
+
static getLastLines(terminal: Terminal, lineCount: number, options?: {
|
|
116
|
+
trimRight?: boolean;
|
|
117
|
+
includeEmptyLines?: boolean;
|
|
118
|
+
}): string;
|
|
119
|
+
}
|