ccmanager 0.1.3 → 0.1.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.
@@ -3,6 +3,7 @@ import { Box, Text } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
4
  import ConfigureShortcuts from './ConfigureShortcuts.js';
5
5
  import ConfigureHooks from './ConfigureHooks.js';
6
+ import ConfigureWorktree from './ConfigureWorktree.js';
6
7
  const Configuration = ({ onComplete }) => {
7
8
  const [view, setView] = useState('menu');
8
9
  const menuItems = [
@@ -14,6 +15,10 @@ const Configuration = ({ onComplete }) => {
14
15
  label: '🔧 Configure Status Hooks',
15
16
  value: 'hooks',
16
17
  },
18
+ {
19
+ label: '📁 Configure Worktree Settings',
20
+ value: 'worktree',
21
+ },
17
22
  {
18
23
  label: '← Back to Main Menu',
19
24
  value: 'back',
@@ -29,6 +34,9 @@ const Configuration = ({ onComplete }) => {
29
34
  else if (item.value === 'hooks') {
30
35
  setView('hooks');
31
36
  }
37
+ else if (item.value === 'worktree') {
38
+ setView('worktree');
39
+ }
32
40
  };
33
41
  const handleSubMenuComplete = () => {
34
42
  setView('menu');
@@ -39,6 +47,9 @@ const Configuration = ({ onComplete }) => {
39
47
  if (view === 'hooks') {
40
48
  return React.createElement(ConfigureHooks, { onComplete: handleSubMenuComplete });
41
49
  }
50
+ if (view === 'worktree') {
51
+ return React.createElement(ConfigureWorktree, { onComplete: handleSubMenuComplete });
52
+ }
42
53
  return (React.createElement(Box, { flexDirection: "column" },
43
54
  React.createElement(Box, { marginBottom: 1 },
44
55
  React.createElement(Text, { bold: true, color: "green" }, "Configuration")),
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface ConfigureWorktreeProps {
3
+ onComplete: () => void;
4
+ }
5
+ declare const ConfigureWorktree: React.FC<ConfigureWorktreeProps>;
6
+ export default ConfigureWorktree;
@@ -0,0 +1,99 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ import TextInput from 'ink-text-input';
5
+ import { configurationManager } from '../services/configurationManager.js';
6
+ import { shortcutManager } from '../services/shortcutManager.js';
7
+ const ConfigureWorktree = ({ onComplete }) => {
8
+ const worktreeConfig = configurationManager.getWorktreeConfig();
9
+ const [autoDirectory, setAutoDirectory] = useState(worktreeConfig.autoDirectory);
10
+ const [pattern, setPattern] = useState(worktreeConfig.autoDirectoryPattern || '../{branch}');
11
+ const [editMode, setEditMode] = useState('menu');
12
+ const [tempPattern, setTempPattern] = useState(pattern);
13
+ useInput((input, key) => {
14
+ if (editMode === 'menu' &&
15
+ shortcutManager.matchesShortcut('cancel', input, key)) {
16
+ onComplete();
17
+ }
18
+ });
19
+ const menuItems = [
20
+ {
21
+ label: `Auto Directory: ${autoDirectory ? '✅ Enabled' : '❌ Disabled'}`,
22
+ value: 'toggle',
23
+ },
24
+ {
25
+ label: `Pattern: ${pattern}`,
26
+ value: 'pattern',
27
+ },
28
+ {
29
+ label: '💾 Save Changes',
30
+ value: 'save',
31
+ },
32
+ {
33
+ label: '← Cancel',
34
+ value: 'cancel',
35
+ },
36
+ ];
37
+ const handleMenuSelect = (item) => {
38
+ switch (item.value) {
39
+ case 'toggle':
40
+ setAutoDirectory(!autoDirectory);
41
+ break;
42
+ case 'pattern':
43
+ setTempPattern(pattern);
44
+ setEditMode('pattern');
45
+ break;
46
+ case 'save':
47
+ // Save the configuration
48
+ configurationManager.setWorktreeConfig({
49
+ autoDirectory,
50
+ autoDirectoryPattern: pattern,
51
+ });
52
+ onComplete();
53
+ break;
54
+ case 'cancel':
55
+ onComplete();
56
+ break;
57
+ }
58
+ };
59
+ const handlePatternSubmit = (value) => {
60
+ if (value.trim()) {
61
+ setPattern(value.trim());
62
+ }
63
+ setEditMode('menu');
64
+ };
65
+ if (editMode === 'pattern') {
66
+ return (React.createElement(Box, { flexDirection: "column" },
67
+ React.createElement(Box, { marginBottom: 1 },
68
+ React.createElement(Text, { bold: true, color: "green" }, "Configure Directory Pattern")),
69
+ React.createElement(Box, { marginBottom: 1 },
70
+ React.createElement(Text, null, "Enter the pattern for automatic directory generation:")),
71
+ React.createElement(Box, { marginBottom: 1 },
72
+ React.createElement(Text, { dimColor: true },
73
+ "Available placeholders: ",
74
+ '{branch}',
75
+ " - full branch name")),
76
+ React.createElement(Box, null,
77
+ React.createElement(Text, { color: "cyan" }, '> '),
78
+ React.createElement(TextInput, { value: tempPattern, onChange: setTempPattern, onSubmit: handlePatternSubmit, placeholder: "../{branch}" })),
79
+ React.createElement(Box, { marginTop: 1 },
80
+ React.createElement(Text, { dimColor: true }, "Press Enter to save or Escape to cancel"))));
81
+ }
82
+ return (React.createElement(Box, { flexDirection: "column" },
83
+ React.createElement(Box, { marginBottom: 1 },
84
+ React.createElement(Text, { bold: true, color: "green" }, "Configure Worktree Settings")),
85
+ React.createElement(Box, { marginBottom: 1 },
86
+ React.createElement(Text, { dimColor: true }, "Configure automatic worktree directory generation")),
87
+ autoDirectory && (React.createElement(Box, { marginBottom: 1 },
88
+ React.createElement(Text, null,
89
+ "Example: branch \"feature/my-feature\" \u2192 directory \"",
90
+ pattern.replace('{branch}', 'feature-my-feature'),
91
+ "\""))),
92
+ React.createElement(SelectInput, { items: menuItems, onSelect: handleMenuSelect, isFocused: true }),
93
+ React.createElement(Box, { marginTop: 1 },
94
+ React.createElement(Text, { dimColor: true },
95
+ "Press ",
96
+ shortcutManager.getShortcutDisplay('cancel'),
97
+ " to cancel without saving"))));
98
+ };
99
+ export default ConfigureWorktree;
@@ -1,11 +1,17 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import TextInput from 'ink-text-input';
4
4
  import { shortcutManager } from '../services/shortcutManager.js';
5
+ import { configurationManager } from '../services/configurationManager.js';
6
+ import { generateWorktreeDirectory } from '../utils/worktreeUtils.js';
5
7
  const NewWorktree = ({ onComplete, onCancel }) => {
6
- const [step, setStep] = useState('path');
8
+ const worktreeConfig = configurationManager.getWorktreeConfig();
9
+ const isAutoDirectory = worktreeConfig.autoDirectory;
10
+ // Adjust initial step based on auto directory mode
11
+ const [step, setStep] = useState(isAutoDirectory ? 'branch' : 'path');
7
12
  const [path, setPath] = useState('');
8
13
  const [branch, setBranch] = useState('');
14
+ const [generatedPath, setGeneratedPath] = useState('');
9
15
  useInput((input, key) => {
10
16
  if (shortcutManager.matchesShortcut('cancel', input, key)) {
11
17
  onCancel();
@@ -20,18 +26,32 @@ const NewWorktree = ({ onComplete, onCancel }) => {
20
26
  const handleBranchSubmit = (value) => {
21
27
  if (value.trim()) {
22
28
  setBranch(value.trim());
23
- onComplete(path, value.trim());
29
+ if (isAutoDirectory) {
30
+ // Generate path from branch name
31
+ const autoPath = generateWorktreeDirectory(value.trim(), worktreeConfig.autoDirectoryPattern);
32
+ onComplete(autoPath, value.trim());
33
+ }
34
+ else {
35
+ onComplete(path, value.trim());
36
+ }
24
37
  }
25
38
  };
39
+ // Update generated path preview when branch changes in auto mode
40
+ useEffect(() => {
41
+ if (isAutoDirectory && branch) {
42
+ const autoPath = generateWorktreeDirectory(branch, worktreeConfig.autoDirectoryPattern);
43
+ setGeneratedPath(autoPath);
44
+ }
45
+ }, [branch, isAutoDirectory, worktreeConfig.autoDirectoryPattern]);
26
46
  return (React.createElement(Box, { flexDirection: "column" },
27
47
  React.createElement(Box, { marginBottom: 1 },
28
48
  React.createElement(Text, { bold: true, color: "green" }, "Create New Worktree")),
29
- step === 'path' ? (React.createElement(Box, { flexDirection: "column" },
49
+ step === 'path' && !isAutoDirectory ? (React.createElement(Box, { flexDirection: "column" },
30
50
  React.createElement(Box, { marginBottom: 1 },
31
51
  React.createElement(Text, null, "Enter worktree path (relative to repository root):")),
32
52
  React.createElement(Box, null,
33
53
  React.createElement(Text, { color: "cyan" }, '> '),
34
- React.createElement(TextInput, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })))) : (React.createElement(Box, { flexDirection: "column" },
54
+ React.createElement(TextInput, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })))) : step === 'branch' && !isAutoDirectory ? (React.createElement(Box, { flexDirection: "column" },
35
55
  React.createElement(Box, { marginBottom: 1 },
36
56
  React.createElement(Text, null,
37
57
  "Enter branch name for worktree at ",
@@ -39,7 +59,17 @@ const NewWorktree = ({ onComplete, onCancel }) => {
39
59
  ":")),
40
60
  React.createElement(Box, null,
41
61
  React.createElement(Text, { color: "cyan" }, '> '),
42
- React.createElement(TextInput, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })))),
62
+ React.createElement(TextInput, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })))) : (React.createElement(Box, { flexDirection: "column" },
63
+ React.createElement(Box, { marginBottom: 1 },
64
+ React.createElement(Text, null, "Enter branch name (directory will be auto-generated):")),
65
+ React.createElement(Box, null,
66
+ React.createElement(Text, { color: "cyan" }, '> '),
67
+ React.createElement(TextInput, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })),
68
+ generatedPath && (React.createElement(Box, { marginTop: 1 },
69
+ React.createElement(Text, { dimColor: true },
70
+ "Worktree will be created at:",
71
+ ' ',
72
+ React.createElement(Text, { color: "green" }, generatedPath)))))),
43
73
  React.createElement(Box, { marginTop: 1 },
44
74
  React.createElement(Text, { dimColor: true },
45
75
  "Press ",
@@ -1,7 +1,6 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useStdout } from 'ink';
3
3
  import { shortcutManager } from '../services/shortcutManager.js';
4
- import { TerminalSerializer } from '../utils/terminalSerializer.js';
5
4
  const Session = ({ session, sessionManager, onReturnToMenu, }) => {
6
5
  const { stdout } = useStdout();
7
6
  const [isExiting, setIsExiting] = useState(false);
@@ -13,24 +12,24 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
13
12
  // Handle session restoration
14
13
  const handleSessionRestore = (restoredSession) => {
15
14
  if (restoredSession.id === session.id) {
16
- // Instead of replaying all history, use the virtual terminal's current buffer
17
- // This avoids duplicate content issues
18
- const terminal = restoredSession.terminal;
19
- if (terminal) {
20
- // Use the TerminalSerializer to preserve ANSI escape sequences (colors, styles)
21
- const serializedOutput = TerminalSerializer.serialize(terminal, {
22
- trimRight: true,
23
- includeEmptyLines: true,
24
- });
25
- // Write the serialized terminal state with preserved formatting
26
- if (serializedOutput) {
27
- stdout.write(serializedOutput);
28
- // Position cursor at the correct location
29
- const buffer = terminal.buffer.active;
30
- const cursorY = buffer.cursorY;
31
- const cursorX = buffer.cursorX;
32
- // Move cursor to the saved position
33
- stdout.write(`\x1B[${cursorY + 1};${cursorX + 1}H`);
15
+ // Replay all buffered output, but skip the initial clear if present
16
+ for (let i = 0; i < restoredSession.outputHistory.length; i++) {
17
+ const buffer = restoredSession.outputHistory[i];
18
+ if (!buffer)
19
+ continue;
20
+ const str = buffer.toString('utf8');
21
+ // Skip clear screen sequences at the beginning
22
+ if (i === 0 && (str.includes('\x1B[2J') || str.includes('\x1B[H'))) {
23
+ // Skip this buffer or remove the clear sequence
24
+ const cleaned = str
25
+ .replace(/\x1B\[2J/g, '')
26
+ .replace(/\x1B\[H/g, '');
27
+ if (cleaned.length > 0) {
28
+ stdout.write(Buffer.from(cleaned, 'utf8'));
29
+ }
30
+ }
31
+ else {
32
+ stdout.write(buffer);
34
33
  }
35
34
  }
36
35
  }
@@ -1,4 +1,4 @@
1
- import { ConfigurationData, StatusHookConfig, ShortcutConfig } from '../types/index.js';
1
+ import { ConfigurationData, StatusHookConfig, ShortcutConfig, WorktreeConfig } from '../types/index.js';
2
2
  export declare class ConfigurationManager {
3
3
  private configPath;
4
4
  private legacyShortcutsPath;
@@ -13,5 +13,7 @@ export declare class ConfigurationManager {
13
13
  setStatusHooks(hooks: StatusHookConfig): void;
14
14
  getConfiguration(): ConfigurationData;
15
15
  setConfiguration(config: ConfigurationData): void;
16
+ getWorktreeConfig(): WorktreeConfig;
17
+ setWorktreeConfig(worktreeConfig: WorktreeConfig): void;
16
18
  }
17
19
  export declare const configurationManager: ConfigurationManager;
@@ -63,6 +63,11 @@ export class ConfigurationManager {
63
63
  if (!this.config.statusHooks) {
64
64
  this.config.statusHooks = {};
65
65
  }
66
+ if (!this.config.worktree) {
67
+ this.config.worktree = {
68
+ autoDirectory: false,
69
+ };
70
+ }
66
71
  }
67
72
  migrateLegacyShortcuts() {
68
73
  if (existsSync(this.legacyShortcutsPath)) {
@@ -111,5 +116,14 @@ export class ConfigurationManager {
111
116
  this.config = config;
112
117
  this.saveConfig();
113
118
  }
119
+ getWorktreeConfig() {
120
+ return (this.config.worktree || {
121
+ autoDirectory: false,
122
+ });
123
+ }
124
+ setWorktreeConfig(worktreeConfig) {
125
+ this.config.worktree = worktreeConfig;
126
+ this.saveConfig();
127
+ }
114
128
  }
115
129
  export const configurationManager = new ConfigurationManager();
@@ -104,7 +104,7 @@ export class SessionManager extends EventEmitter {
104
104
  process: ptyProcess,
105
105
  state: 'busy', // Session starts as busy when created
106
106
  output: [],
107
- outputHistory: [], // Kept for backward compatibility but no longer used
107
+ outputHistory: [],
108
108
  lastActivity: new Date(),
109
109
  isActive: false,
110
110
  terminal,
@@ -118,10 +118,20 @@ export class SessionManager extends EventEmitter {
118
118
  setupBackgroundHandler(session) {
119
119
  // This handler always runs for all data
120
120
  session.process.onData((data) => {
121
- // Write data to virtual terminal - this maintains the proper rendered state
121
+ // Write data to virtual terminal
122
122
  session.terminal.write(data);
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
123
+ // Store in output history as Buffer
124
+ const buffer = Buffer.from(data, 'utf8');
125
+ session.outputHistory.push(buffer);
126
+ // Limit memory usage - keep max 10MB of output history
127
+ const MAX_HISTORY_SIZE = 10 * 1024 * 1024; // 10MB
128
+ let totalSize = session.outputHistory.reduce((sum, buf) => sum + buf.length, 0);
129
+ while (totalSize > MAX_HISTORY_SIZE && session.outputHistory.length > 0) {
130
+ const removed = session.outputHistory.shift();
131
+ if (removed) {
132
+ totalSize -= removed.length;
133
+ }
134
+ }
125
135
  session.lastActivity = new Date();
126
136
  // Only emit data events when session is active
127
137
  if (session.isActive) {
@@ -157,9 +167,8 @@ export class SessionManager extends EventEmitter {
157
167
  const session = this.sessions.get(worktreePath);
158
168
  if (session) {
159
169
  session.isActive = active;
160
- // If becoming active, emit a restore event
161
- // The Session component will use the virtual terminal buffer instead of outputHistory
162
- if (active) {
170
+ // If becoming active, emit a restore event with the output history
171
+ if (active && session.outputHistory.length > 0) {
163
172
  this.emit('sessionRestore', session);
164
173
  }
165
174
  }
@@ -179,6 +179,11 @@ export class WorktreeService {
179
179
  cwd: sourceWorktree.path,
180
180
  encoding: 'utf8',
181
181
  });
182
+ // After rebase, merge the rebased source branch into target branch
183
+ execSync(`git merge --ff-only "${sourceBranch}"`, {
184
+ cwd: targetWorktree.path,
185
+ encoding: 'utf8',
186
+ });
182
187
  }
183
188
  else {
184
189
  // Regular merge
@@ -47,7 +47,12 @@ export interface StatusHookConfig {
47
47
  busy?: StatusHook;
48
48
  waiting_input?: StatusHook;
49
49
  }
50
+ export interface WorktreeConfig {
51
+ autoDirectory: boolean;
52
+ autoDirectoryPattern?: string;
53
+ }
50
54
  export interface ConfigurationData {
51
55
  shortcuts?: ShortcutConfig;
52
56
  statusHooks?: StatusHookConfig;
57
+ worktree?: WorktreeConfig;
53
58
  }
@@ -0,0 +1,5 @@
1
+ export declare function generateWorktreeDirectory(branchName: string, pattern?: string): string;
2
+ export declare function extractBranchParts(branchName: string): {
3
+ prefix?: string;
4
+ name: string;
5
+ };
@@ -0,0 +1,29 @@
1
+ import path from 'path';
2
+ export function generateWorktreeDirectory(branchName, pattern) {
3
+ // Default pattern if not specified
4
+ const defaultPattern = '../{branch}';
5
+ const activePattern = pattern || defaultPattern;
6
+ // Sanitize branch name for filesystem
7
+ // Replace slashes with dashes, remove special characters
8
+ const sanitizedBranch = branchName
9
+ .replace(/\//g, '-') // Replace forward slashes with dashes
10
+ .replace(/[^a-zA-Z0-9-_.]/g, '') // Remove special characters except dash, dot, underscore
11
+ .replace(/^-+|-+$/g, '') // Remove leading/trailing dashes
12
+ .toLowerCase(); // Convert to lowercase for consistency
13
+ // Replace placeholders in pattern
14
+ const directory = activePattern
15
+ .replace('{branch}', sanitizedBranch)
16
+ .replace('{branch-name}', sanitizedBranch);
17
+ // Ensure the path is relative to the repository root
18
+ return path.normalize(directory);
19
+ }
20
+ export function extractBranchParts(branchName) {
21
+ const parts = branchName.split('/');
22
+ if (parts.length > 1) {
23
+ return {
24
+ prefix: parts[0],
25
+ name: parts.slice(1).join('/'),
26
+ };
27
+ }
28
+ return { name: branchName };
29
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateWorktreeDirectory, extractBranchParts, } from './worktreeUtils.js';
3
+ describe('generateWorktreeDirectory', () => {
4
+ describe('with default pattern', () => {
5
+ it('should generate directory with sanitized branch name', () => {
6
+ expect(generateWorktreeDirectory('feature/my-feature')).toBe('../feature-my-feature');
7
+ expect(generateWorktreeDirectory('bugfix/fix-123')).toBe('../bugfix-fix-123');
8
+ expect(generateWorktreeDirectory('release/v1.0.0')).toBe('../release-v1.0.0');
9
+ });
10
+ it('should handle branch names without slashes', () => {
11
+ expect(generateWorktreeDirectory('main')).toBe('../main');
12
+ expect(generateWorktreeDirectory('develop')).toBe('../develop');
13
+ expect(generateWorktreeDirectory('my-feature')).toBe('../my-feature');
14
+ });
15
+ it('should remove special characters', () => {
16
+ expect(generateWorktreeDirectory('feature/my@feature!')).toBe('../feature-myfeature');
17
+ expect(generateWorktreeDirectory('bugfix/#123')).toBe('../bugfix-123');
18
+ expect(generateWorktreeDirectory('release/v1.0.0-beta')).toBe('../release-v1.0.0-beta');
19
+ });
20
+ it('should handle edge cases', () => {
21
+ expect(generateWorktreeDirectory('//feature//')).toBe('../feature');
22
+ expect(generateWorktreeDirectory('-feature-')).toBe('../feature');
23
+ expect(generateWorktreeDirectory('FEATURE/UPPERCASE')).toBe('../feature-uppercase');
24
+ });
25
+ });
26
+ describe('with custom patterns', () => {
27
+ it('should use custom pattern with {branch} placeholder', () => {
28
+ expect(generateWorktreeDirectory('feature/my-feature', '../worktrees/{branch}')).toBe('../worktrees/feature-my-feature');
29
+ expect(generateWorktreeDirectory('bugfix/123', '/tmp/{branch}-wt')).toBe('/tmp/bugfix-123-wt');
30
+ });
31
+ it('should handle patterns without placeholders', () => {
32
+ expect(generateWorktreeDirectory('feature/test', '../fixed-directory')).toBe('../fixed-directory');
33
+ });
34
+ it('should normalize paths', () => {
35
+ expect(generateWorktreeDirectory('feature/test', '../foo/../bar/{branch}')).toBe('../bar/feature-test');
36
+ expect(generateWorktreeDirectory('feature/test', './worktrees/{branch}')).toBe('worktrees/feature-test');
37
+ });
38
+ });
39
+ });
40
+ describe('extractBranchParts', () => {
41
+ it('should extract prefix and name from branch with slash', () => {
42
+ expect(extractBranchParts('feature/my-feature')).toEqual({
43
+ prefix: 'feature',
44
+ name: 'my-feature',
45
+ });
46
+ expect(extractBranchParts('bugfix/fix-123')).toEqual({
47
+ prefix: 'bugfix',
48
+ name: 'fix-123',
49
+ });
50
+ });
51
+ it('should handle branches with multiple slashes', () => {
52
+ expect(extractBranchParts('feature/user/profile-page')).toEqual({
53
+ prefix: 'feature',
54
+ name: 'user/profile-page',
55
+ });
56
+ expect(extractBranchParts('release/v1.0/final')).toEqual({
57
+ prefix: 'release',
58
+ name: 'v1.0/final',
59
+ });
60
+ });
61
+ it('should handle branches without slashes', () => {
62
+ expect(extractBranchParts('main')).toEqual({
63
+ name: 'main',
64
+ });
65
+ expect(extractBranchParts('develop')).toEqual({
66
+ name: 'develop',
67
+ });
68
+ });
69
+ it('should handle empty branch name', () => {
70
+ expect(extractBranchParts('')).toEqual({
71
+ name: '',
72
+ });
73
+ });
74
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",
@@ -1,142 +0,0 @@
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
- // Don't mock @xterm/headless - let it use the real implementation
35
- // since we need actual terminal functionality for color testing
36
- describe('SessionManager - Color Restoration', () => {
37
- let sessionManager;
38
- const mockWorktreePath = '/test/worktree';
39
- beforeEach(() => {
40
- sessionManager = new SessionManager();
41
- vi.clearAllMocks();
42
- });
43
- it('should preserve ANSI colors when switching between sessions', async () => {
44
- // Create a mock PTY process
45
- const mockProcess = createMockPtyProcess();
46
- vi.mocked(spawn).mockReturnValue(mockProcess);
47
- sessionManager.createSession(mockWorktreePath);
48
- const session = sessionManager.sessions.get(mockWorktreePath);
49
- expect(session).toBeDefined();
50
- // Simulate colorful output from Claude Code
51
- const colorfulData = [
52
- '\x1b[32m✓\x1b[0m File created successfully\n',
53
- '\x1b[1;34mRunning tests...\x1b[0m\n',
54
- '\x1b[38;5;196mError:\x1b[0m Test failed\n',
55
- '\x1b[38;2;255;165;0mWarning:\x1b[0m Deprecated API\n',
56
- ];
57
- // Activate session first
58
- sessionManager.setSessionActive(mockWorktreePath, true);
59
- // Send colored data to the terminal
60
- for (const data of colorfulData) {
61
- mockProcess._emit('data', data);
62
- // Wait for terminal to process the data
63
- await new Promise(resolve => setTimeout(resolve, 10));
64
- }
65
- // Deactivate session
66
- sessionManager.setSessionActive(mockWorktreePath, false);
67
- // Set up listener to capture restore event
68
- let restoredContent = null;
69
- sessionManager.on('sessionRestore', restoredSession => {
70
- // In real usage, the Session component would use TerminalSerializer here
71
- // For this test, we'll verify the terminal buffer contains the data
72
- const terminal = restoredSession.terminal;
73
- if (terminal) {
74
- // Access the terminal buffer to verify colors are preserved
75
- const buffer = terminal.buffer.active;
76
- restoredContent = '';
77
- // Simple check: verify buffer has content
78
- for (let i = 0; i < buffer.length; i++) {
79
- const line = buffer.getLine(i);
80
- if (line) {
81
- // Check if line has colored cells
82
- for (let x = 0; x < terminal.cols; x++) {
83
- const cell = line.getCell(x);
84
- if (cell && cell.getChars()) {
85
- const fgColorMode = cell.getFgColorMode();
86
- const bgColorMode = cell.getBgColorMode();
87
- // If any cell has non-default color, we know colors are preserved
88
- if (fgColorMode !== 0 || bgColorMode !== 0) {
89
- restoredContent = 'has-colors';
90
- break;
91
- }
92
- }
93
- }
94
- }
95
- }
96
- }
97
- });
98
- // Reactivate session (simulating switching back)
99
- sessionManager.setSessionActive(mockWorktreePath, true);
100
- // Verify that colors were preserved in the terminal buffer
101
- expect(restoredContent).toBe('has-colors');
102
- });
103
- it('should handle complex color sequences during restoration', async () => {
104
- // Create a mock PTY process
105
- const mockProcess = createMockPtyProcess();
106
- vi.mocked(spawn).mockReturnValue(mockProcess);
107
- sessionManager.createSession(mockWorktreePath);
108
- const session = sessionManager.sessions.get(mockWorktreePath);
109
- // Activate session
110
- sessionManager.setSessionActive(mockWorktreePath, true);
111
- // Send a complex sequence with cursor movements and color changes
112
- const complexSequence = [
113
- 'Line 1: Normal text\n',
114
- '\x1b[32mLine 2: Green text\x1b[0m\n',
115
- '\x1b[1A\x1b[K\x1b[31mLine 2: Now red text\x1b[0m\n', // Move up, clear line, write red
116
- '\x1b[1;33mLine 3: Bold yellow\x1b[0m\n',
117
- '\x1b[48;5;17m\x1b[38;5;231mWhite on dark blue background\x1b[0m\n',
118
- ];
119
- for (const data of complexSequence) {
120
- mockProcess._emit('data', data);
121
- await new Promise(resolve => setTimeout(resolve, 10));
122
- }
123
- // Check terminal has processed the sequences correctly
124
- const terminal = session.terminal;
125
- expect(terminal).toBeDefined();
126
- // Verify buffer contains content (actual color verification would require
127
- // checking individual cells, which is done in terminalSerializer.test.ts)
128
- const buffer = terminal.buffer.active;
129
- let hasContent = false;
130
- for (let i = 0; i < buffer.length; i++) {
131
- const line = buffer.getLine(i);
132
- if (line) {
133
- const text = line.translateToString(true);
134
- if (text.trim()) {
135
- hasContent = true;
136
- break;
137
- }
138
- }
139
- }
140
- expect(hasContent).toBe(true);
141
- });
142
- });
@@ -1 +0,0 @@
1
- export {};